1 /***************************************************************************
2               SonagramWindow.cpp  -  window for showing a sonagram
3                              -------------------
4     begin                : Fri Jul 28 2000
5     copyright            : (C) 2000 by Thomas Eschenbacher
6     email                : Thomas.Eschenbacher@gmx.de
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "config.h"
19 
20 #include <math.h>
21 #include <new>
22 
23 #include <QBitmap>
24 #include <QImage>
25 #include <QLabel>
26 #include <QMenuBar>
27 #include <QPointer>
28 #include <QLayout>
29 #include <QStatusBar>
30 #include <QTimer>
31 
32 #include "libkwave/String.h"
33 #include "libkwave/Utils.h"
34 #include "libkwave/WindowFunction.h"
35 
36 #include "libgui/FileDialog.h"
37 #include "libgui/ImageView.h"
38 #include "libgui/ScaleWidget.h"
39 
40 #include "SonagramWindow.h"
41 
42 /**
43  * delay between two screen updates [ms]
44  */
45 #define REFRESH_DELAY 100
46 
47 /**
48  * Color values below this limit are cut off when adjusting the
49  * sonagram image's brightness
50  * I found out by experiments that 0.1% seems to be reasonable
51  */
52 #define COLOR_CUTOFF_RATIO (0.1/100.0)
53 
54 static const char *background[] = {
55 /* width height num_colors chars_per_pixel */
56 "     20     20          2               1",
57 /* colors */
58 "# c #808080",
59 ". c None",
60 /* pixels */
61 "##########..........",
62 "##########..........",
63 "##########..........",
64 "##########..........",
65 "##########..........",
66 "##########..........",
67 "##########..........",
68 "##########..........",
69 "##########..........",
70 "##########..........",
71 "..........##########",
72 "..........##########",
73 "..........##########",
74 "..........##########",
75 "..........##########",
76 "..........##########",
77 "..........##########",
78 "..........##########",
79 "..........##########",
80 "..........##########"
81 };
82 
83 //****************************************************************************
SonagramWindow(QWidget * parent,const QString & name)84 Kwave::SonagramWindow::SonagramWindow(QWidget *parent, const QString &name)
85     :KMainWindow(parent),
86      m_status_time(Q_NULLPTR),
87      m_status_freq(Q_NULLPTR),
88      m_status_ampl(Q_NULLPTR),
89      m_image(),
90      m_color_mode(0),
91      m_view(Q_NULLPTR),
92      m_overview(Q_NULLPTR),
93      m_points(0),
94      m_rate(0),
95      m_xscale(Q_NULLPTR),
96      m_yscale(Q_NULLPTR),
97      m_refresh_timer()
98 {
99 
100     for (unsigned int i = 0; i < 256; ++i) { m_histogram[i] = 0; }
101 
102     QWidget *mainwidget = new(std::nothrow) QWidget(this);
103     Q_ASSERT(mainwidget);
104     if (!mainwidget) return;
105     setCentralWidget(mainwidget);
106 
107     QGridLayout *top_layout = new(std::nothrow) QGridLayout(mainwidget);
108     Q_ASSERT(top_layout);
109     if (!top_layout) return;
110 
111     QMenuBar *bar = menuBar();
112     Q_ASSERT(bar);
113     if (!bar) return ;
114 
115 //    QMenu *spectral = new QMenu();
116 //    Q_ASSERT(spectral);
117 //    if (!spectral) return ;
118 
119     QMenu *file = bar->addMenu(i18n("&Sonagram"));
120     Q_ASSERT(file);
121     if (!file) return ;
122 
123 //    bar->addAction(i18n("&Spectral Data"), spectral);
124 //    file->addAction(i18n("&Import from Bitmap..."), this, SLOT(load()));
125 
126     file->addAction(
127 	QIcon::fromTheme(_("document-export")),
128 	i18n("&Export to Bitmap..."),
129 	this, SLOT(save())
130     );
131     file->addAction(
132         QIcon::fromTheme(_("dialog-close")),
133 	i18n("&Close"),
134 	this, SLOT(close()),
135 	QKeySequence::Close
136     );
137 
138 //    spectral->addAction (i18n("&Retransform to Signal"), this, SLOT(toSignal()));
139 
140     QStatusBar *status = statusBar();
141     Q_ASSERT(status);
142     if (!status) return ;
143 
144     m_status_time = new(std::nothrow)
145 	QLabel(i18n("Time: ------ ms"), status);
146     m_status_freq = new(std::nothrow)
147 	QLabel(i18n("Frequency: ------ Hz"), status);
148     m_status_ampl = new(std::nothrow)
149 	QLabel(i18n("Amplitude: --- %"), status);
150     status->addPermanentWidget(m_status_time);
151     status->addPermanentWidget(m_status_freq);
152     status->addPermanentWidget(m_status_ampl);
153 
154     m_view = new(std::nothrow) Kwave::ImageView(mainwidget);
155     Q_ASSERT(m_view);
156     if (!m_view) return;
157     top_layout->addWidget(m_view, 0, 1);
158     QPalette palette;
159     palette.setBrush(m_view->backgroundRole(), QBrush(QImage(background)));
160     m_view->setAutoFillBackground(true);
161     m_view->setPalette(palette);
162 
163     m_xscale = new(std::nothrow)
164 	Kwave::ScaleWidget(mainwidget, 0, 100, i18n("ms"));
165     Q_ASSERT(m_xscale);
166     if (!m_xscale) return;
167     m_xscale->setFixedHeight(m_xscale->sizeHint().height());
168     top_layout->addWidget(m_xscale, 1, 1);
169 
170     m_yscale = new(std::nothrow)
171 	Kwave::ScaleWidget(mainwidget, 0, 100, i18n("Hz"));
172     Q_ASSERT(m_yscale);
173     if (!m_yscale) return ;
174     m_yscale->setFixedWidth(m_yscale->sizeHint().width());
175     m_yscale->setMinimumHeight(9*6*5);
176     top_layout->addWidget(m_yscale, 0, 0);
177 
178     m_overview = new(std::nothrow) Kwave::ImageView(mainwidget);
179     Q_ASSERT(m_overview);
180     if (!m_overview) return;
181     m_overview->setFixedHeight(SONAGRAM_OVERVIEW_HEIGHT);
182     top_layout->addWidget(m_overview, 2, 1);
183 
184     connect(m_view, SIGNAL(sigCursorPos(QPoint)),
185 	    this, SLOT(cursorPosChanged(QPoint)));
186     connect(&m_refresh_timer, SIGNAL(timeout()),
187             this, SLOT(refresh_view()));
188 
189     setName(name);
190 
191     top_layout->setRowStretch(0, 100);
192     top_layout->setRowStretch(1, 0);
193     top_layout->setRowStretch(2, 0);
194     top_layout->setColumnStretch(0, 0);
195     top_layout->setColumnStretch(1, 100);
196     top_layout->activate();
197 
198     if (m_status_time) m_status_time->setText(i18n("Time: 0 ms"));
199     if (m_status_freq) m_status_freq->setText(i18n("Frequency: 0 Hz"));
200     if (m_status_ampl) m_status_ampl->setText(i18n("Amplitude: 0 %"));
201 
202     // try to make 5:3 format (looks best)
203     int w = sizeHint().width();
204     int h = sizeHint().height();
205     if ((w * 3 / 5) < h) w = (h * 5) / 3;
206     if ((h * 5 / 3) < w) h = (w * 3) / 5;
207     resize(w, h);
208 
209     show();
210 }
211 
212 //****************************************************************************
close()213 void Kwave::SonagramWindow::close()
214 {
215     QWidget::close();
216 }
217 
218 //****************************************************************************
save()219 void Kwave::SonagramWindow::save()
220 {
221     if (m_image.isNull()) return;
222 
223     QPointer<Kwave::FileDialog> dlg = new(std::nothrow) Kwave::FileDialog(
224 	_("kfiledialog:///kwave_sonagram"),
225 	Kwave::FileDialog::SaveFile, QString(),
226 	this, QUrl(), _("*.bmp")
227     );
228     if (!dlg) return;
229     dlg->setWindowTitle(i18n("Save Sonagram"));
230     if ((dlg->exec() == QDialog::Accepted) && dlg) {
231 	QString filename = dlg->selectedUrl().toLocalFile();
232 	if (!filename.isEmpty()) m_image.save(filename, "BMP");
233     }
234     delete dlg;
235 }
236 
237 //****************************************************************************
load()238 void Kwave::SonagramWindow::load()
239 {
240 //    if (image) {
241 //	QString filename = QFileDialog::getOpenFileName(this, QString(), "", "*.bmp");
242 //	printf ("loading %s\n", filename.local8Bit().data());
243 //	if (!filename.isNull()) {
244 //	    printf ("loading %s\n", filename.local8Bit().data());
245 //	    QImage *newimage = new QImage (filename);
246 //	    Q_ASSERT(newimage);
247 //	    if (newimage) {
248 //		if ((image->height() == newimage->height())
249 //		    && (image->width() == newimage->width())) {
250 //
251 //		    for (int i = 0; i < x; i++) {
252 //			for (int j = 0; j < points / 2; j++) {
253 //			    if (data[i]) {
254 //				// data[i][j].real;
255 //			    }
256 //
257 //			}
258 //		    }
259 //
260 //		    delete image;
261 //		    image = newimage;
262 //		    view->setImage (image);
263 //		} else {
264 //		    char buf[128];
265 //		    delete newimage;
266 //		    snprintf(buf, sizeof(buf), i18n("Bitmap must be %dx%d"),
267 //			     image->width(), image->height());
268 //		    KMsgBox::message (this, "Info", buf, 2);
269 //		}
270 //	    } else
271 //		KMsgBox::message (this, i18n("Error"),
272 //				  i18n("Could not open Bitmap"), 2);
273 //	}
274 //    }
275 }
276 
277 //****************************************************************************
setImage(QImage image)278 void Kwave::SonagramWindow::setImage(QImage image)
279 {
280     Q_ASSERT(m_view);
281     if (!m_view) return;
282 
283     m_image = image;
284 
285     // re-initialize histogram over all pixels
286     for (unsigned int i = 0; i < 256; i++)
287 	m_histogram[i] = 0;
288     if (!m_image.isNull()) {
289 	for (int x = 0; x < m_image.width(); x++) {
290 	    for (int y = 0; y < m_image.height(); y++) {
291 		quint8 p = static_cast<quint8>(m_image.pixelIndex(x, y));
292 		m_histogram[p]++;
293 	    }
294 	}
295     }
296 
297     refresh_view();
298 }
299 
300 //****************************************************************************
setOverView(const QImage & overview)301 void Kwave::SonagramWindow::setOverView(const QImage &overview)
302 {
303     if (m_overview) m_overview->setImage(overview);
304 }
305 
306 //****************************************************************************
insertSlice(const unsigned int slice_nr,const QByteArray & slice)307 void Kwave::SonagramWindow::insertSlice(const unsigned int slice_nr,
308                                         const QByteArray &slice)
309 {
310     Q_ASSERT(m_view);
311     if (!m_view) return;
312     if (m_image.isNull()) return;
313 
314     unsigned int image_width  = m_image.width();
315     unsigned int image_height = m_image.height();
316 
317     // slice is out of range ?
318     if (slice_nr >= image_width) return;
319 
320     unsigned int y;
321     unsigned int size = slice.size();
322     for (y = 0; y < size; y++) {
323 	quint8 p;
324 
325 	// remove the current pixel from the histogram
326 	p = static_cast<quint8>(m_image.pixelIndex(slice_nr, y));
327 	m_histogram[p]--;
328 
329 	// set the new pixel value
330 	p = slice[(size - 1) - y];
331 	m_image.setPixel(slice_nr, y, p);
332 
333 	// insert the new pixel into the histogram
334 	m_histogram[p]++;
335     }
336     while (y < image_height) { // fill the rest with blank
337 	m_image.setPixel(slice_nr, y++, 0xFE);
338 	m_histogram[0xFE]++;
339     }
340 
341     if (!m_refresh_timer.isActive()) {
342 	m_refresh_timer.setSingleShot(true);
343 	m_refresh_timer.start(REFRESH_DELAY);
344     }
345 }
346 
347 //****************************************************************************
adjustBrightness()348 void Kwave::SonagramWindow::adjustBrightness()
349 {
350     if (m_image.isNull()) return;
351 
352     // get the sum of pixels != 0
353     unsigned long int sum = 0;
354     for (unsigned int i = 1; i <= 254; i++)
355 	sum += m_histogram[i];
356 
357     // cut off all parts below the cutoff ratio (e.g. 0.1%)
358     unsigned int cutoff = Kwave::toUint(
359 	static_cast<double>(sum) * COLOR_CUTOFF_RATIO);
360 
361     // get the last used color from the histogram
362     int last = 254;
363     while ((last >= 0) && (m_histogram[last] <= cutoff))
364 	last--;
365 
366     QColor c;
367     for (int i = 0; i < 255; i++) {
368 	int v;
369 
370 	if (i >= last) {
371 	    v = 254;
372 	} else {
373 	    // map [0...last] to [254...0]
374 	    v = ((last - i) * 254) / last;
375 	}
376 
377 	if (m_color_mode == 1) {
378 	    // rainbow effect
379 	    c.setHsv( (v * 255) / 255, 255, 255, 255);
380 	} else {
381 	    // greyscale palette
382 	    c.setRgb(v, v, v, 255);
383 	}
384 
385 	m_image.setColor(i, c.rgba());
386 // 	qDebug("color[%3d] = 0x%08X",i, c.rgba());
387     }
388 
389     // use color 0xFF for transparency !
390     m_image.setColor(0xFF, QColor(0, 0, 0, 0).rgba());
391 }
392 
393 //****************************************************************************
refresh_view()394 void Kwave::SonagramWindow::refresh_view()
395 {
396     Q_ASSERT(m_view);
397     if (!m_view) return;
398     adjustBrightness();
399     m_view->setImage(m_image);
400 }
401 
402 //****************************************************************************
toSignal()403 void Kwave::SonagramWindow::toSignal()
404 {
405 /** @todo needs to be ported to fftw and re-activated */
406 //    gsl_fft_complex_wavetable table;
407 //
408 //    gsl_fft_complex_wavetable_alloc (points, &table);
409 //    gsl_fft_complex_init (points, &table);
410 //
411 //    Kwave::TopWidget *win = new Kwave::TopWidget(...);
412 //
413 //    Q_ASSERT(win);
414 //    if (win) {
415 //
416 //	Kwave::Signal *newsig = new Kwave::Signal(length, rate);
417 //	Q_ASSERT(newsig);
418 //
419 //	//assure 10 Hz for correction signal, this should not be audible
420 //	int slopesize = rate / 10;
421 //
422 //	double *slope = new double [slopesize];
423 //
424 //	if (slope && newsig) {
425 //	    for (int i = 0; i < slopesize; i++)
426 //		slope[i] = 0.5 + 0.5 * cos( ((double) i) * M_PI / slopesize);
427 //
428 //	    win->show();
429 //
430 //	    int *output = newsig->getSample();      //sample data
431 //	    complex *tmp = new complex [points];   //this window holds the data for ifft and after that part of the signal
432 //
433 //	    if (output && tmp && data) {
434 //		for (int i = 0; i < x; i++) {
435 //		    if (data[i]) memcpy (tmp, data[i], sizeof(complex)*points);
436 //		    gsl_fft_complex_inverse (tmp, points, &table);
437 //
438 //		    for (int j = 0; j < points; j++)
439 //			output[i*points + j] = (int)(tmp[j].real * ((1 << 23)-1));
440 //		}
441 //		int dif ;
442 //		int max;
443 //		for (int i = 1; i < x; i++) //remove gaps between windows
444 //		{
445 //		    max = slopesize;
446 //		    if (max > length - i*points) max = length - i * points;
447 //		    dif = output[i * points] - output[i * points - 1];
448 //		    if (dif < 2)
449 //			for (int j = 0; j < max; j++) output[i*points + j] += (int) (slope[j] * dif );
450 //		}
451 //
452 //		win->setSignal (new SignalManager (newsig));
453 //
454 //		if (tmp) delete[] tmp;
455 //	    } else {
456 //		if (newsig) delete newsig;
457 //		if (win) delete win;
458 //		KMsgBox::message (this, i18n("Error"), i18n("Out of memory !"), 2);
459 //	    }
460 //	}
461 //	if (slope) delete[] slope;
462 //    }
463 }
464 
465 //***************************************************************************
translatePixels2TF(const QPoint p,double * ms,double * f)466 void Kwave::SonagramWindow::translatePixels2TF(const QPoint p,
467                                                double *ms, double *f)
468 {
469     if (ms) {
470 	// get the time coordinate [0...(N_samples-1)* (1/f_sample) ]
471 	if (!qFuzzyIsNull(m_rate)) {
472 	    *ms = static_cast<double>(p.x()) *
473 	          static_cast<double>(m_points) * 1000.0 / m_rate;
474 	} else {
475 	    *ms = 0;
476 	}
477     }
478 
479     if (f) {
480 	// get the frequency coordinate
481 	double py = (m_points >= 2) ? (m_points / 2) - 1 : 0;
482 	double y = py - p.y();
483 	if (y < 0) y = 0;
484 	*f = y / py * (m_rate / 2.0);
485     }
486 }
487 
488 //***************************************************************************
updateScaleWidgets()489 void Kwave::SonagramWindow::updateScaleWidgets()
490 {
491     double ms;
492     double f;
493 
494     translatePixels2TF(QPoint(m_image.width() - 1, 0), &ms, &f);
495 
496     m_xscale->setMinMax(0, Kwave::toInt(rint(ms)));
497     m_yscale->setMinMax(0, Kwave::toInt(rint(f)));
498 }
499 
500 //***************************************************************************
~SonagramWindow()501 Kwave::SonagramWindow::~SonagramWindow()
502 {
503 }
504 
505 //***************************************************************************
setColorMode(int mode)506 void Kwave::SonagramWindow::setColorMode(int mode)
507 {
508     Q_ASSERT(mode >= 0);
509     Q_ASSERT(mode <= 1);
510 
511     if (mode != m_color_mode) {
512 	m_color_mode = mode;
513 	setImage(m_image);
514     }
515 }
516 
517 //***************************************************************************
setName(const QString & name)518 void Kwave::SonagramWindow::setName(const QString &name)
519 {
520     setWindowTitle((name.length()) ?
521 	i18n("Sonagram of %1", name) :
522 	i18n("Sonagram")
523     );
524 }
525 
526 //****************************************************************************
cursorPosChanged(const QPoint pos)527 void Kwave::SonagramWindow::cursorPosChanged(const QPoint pos)
528 {
529     QStatusBar *status = statusBar();
530     Q_ASSERT(status);
531     Q_ASSERT(m_points);
532     Q_ASSERT(!qFuzzyIsNull(m_rate));
533     if (!status) return;
534     if (m_image.isNull()) return;
535     if (!m_points) return;
536     if (qFuzzyIsNull(m_rate)) return;
537 
538     double ms;
539     double f;
540     double a;
541     translatePixels2TF(pos, &ms, &f);
542 
543     // item 1: time in milliseconds
544     if (m_status_time)
545 	m_status_time->setText(i18n("Time: %1", Kwave::ms2string(ms)));
546 
547     // item 2: frequency in Hz
548     if (m_status_freq)
549 	m_status_freq->setText(i18n("Frequency: %1 Hz", Kwave::toInt(f)));
550 
551     // item 3: amplitude in %
552     if (m_image.valid(pos.x(), pos.y())) {
553 	a = m_image.pixelIndex(pos.x(), pos.y()) * (100.0 / 254.0);
554     } else {
555 	a = 0.0;
556     }
557     if (m_status_ampl)
558 	m_status_ampl->setText(i18n("Amplitude: %1%", Kwave::toInt(a)));
559 }
560 
561 //****************************************************************************
setPoints(unsigned int points)562 void Kwave::SonagramWindow::setPoints(unsigned int points)
563 {
564     m_points = points;
565     updateScaleWidgets();
566 }
567 
568 //****************************************************************************
setRate(double rate)569 void Kwave::SonagramWindow::setRate(double rate)
570 {
571     m_rate = rate;
572     updateScaleWidgets();
573 }
574 
575 //***************************************************************************
576 //***************************************************************************
577