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