File: SonagramPlugin.cpp

package info (click to toggle)
kwave 25.04.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 23,272 kB
  • sloc: cpp: 56,173; xml: 817; perl: 688; sh: 57; makefile: 11
file content (627 lines) | stat: -rw-r--r-- 20,727 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
/***************************************************************************
              SonagramPlugin.cpp  -  plugin that shows a sonagram window
                             -------------------
    begin                : Fri Jul 28 2000
    copyright            : (C) 2000 by Thomas Eschenbacher
    email                : Thomas.Eschenbacher@gmx.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "config.h"

#include <errno.h>
#include <math.h>
#include <stdlib.h>

#include <limits>
#include <new>

#include <QApplication>
#include <QColor>
#include <QFutureSynchronizer>
#include <QImage>
#include <QMutexLocker>
#include <QPointer>
#include <QString>
#include <QtConcurrentRun>

#include "libkwave/GlobalLock.h"
#include "libkwave/MessageBox.h"
#include "libkwave/MultiTrackReader.h"
#include "libkwave/Plugin.h"
#include "libkwave/PluginManager.h"
#include "libkwave/Sample.h"
#include "libkwave/SampleReader.h"
#include "libkwave/SignalManager.h"
#include "libkwave/Track.h"
#include "libkwave/Utils.h"
#include "libkwave/WindowFunction.h"

#include "libgui/OverViewCache.h"
#include "libgui/SelectionTracker.h"

#include "SonagramDialog.h"
#include "SonagramPlugin.h"
#include "SonagramWindow.h"

KWAVE_PLUGIN(sonagram, SonagramPlugin)

/**
 * interval for limiting the number of repaints per second [ms]
 */
#define REPAINT_INTERVAL 250

//***************************************************************************
Kwave::SonagramPlugin::SonagramPlugin(QObject *parent,
                                      const QVariantList &args)
    :Kwave::Plugin(parent, args),
     m_sonagram_window(nullptr),
     m_selection(nullptr),
     m_slices(0), m_fft_points(0),
     m_window_type(Kwave::WINDOW_FUNC_NONE), m_color(true),
     m_track_changes(true), m_follow_selection(false), m_image(),
     m_overview_cache(nullptr), m_slice_pool(), m_valid(MAX_SLICES, false),
     m_pending_jobs(), m_lock_job_list(), m_future(),
     m_repaint_timer()
{
    i18n("Sonagram");

    // connect the output ouf the sonagram worker thread
    connect(this, SIGNAL(sliceAvailable(Kwave::SonagramPlugin::Slice*)),
            this, SLOT(insertSlice(Kwave::SonagramPlugin::Slice*)),
            Qt::QueuedConnection);

    // connect repaint timer
    connect(&m_repaint_timer, SIGNAL(timeout()),
            this, SLOT(validate()));
}

//***************************************************************************
Kwave::SonagramPlugin::~SonagramPlugin()
{
    m_repaint_timer.stop();

    delete m_sonagram_window;
    m_sonagram_window = nullptr;

    delete m_selection;
    m_selection = nullptr;
}

//***************************************************************************
QStringList *Kwave::SonagramPlugin::setup(QStringList &previous_params)
{
    QStringList *result = nullptr;

    // try to interprete the list of previous parameters, ignore errors
    if (previous_params.count()) interpreteParameters(previous_params);

    QPointer<Kwave::SonagramDialog> dlg =
        new(std::nothrow) Kwave::SonagramDialog(*this);
    Q_ASSERT(dlg);
    if (!dlg) return nullptr;

    dlg->setWindowFunction(m_window_type);
    dlg->setColorMode(m_color ? 1 : 0);
    dlg->setTrackChanges(m_track_changes);
    dlg->setFollowSelection(m_follow_selection);

    if ((dlg->exec() == QDialog::Accepted) && dlg) {
        result = new(std::nothrow) QStringList();
        Q_ASSERT(result);
        if (result) dlg->parameters(*result);
    }

    delete dlg;
    return result;
}

//***************************************************************************
int Kwave::SonagramPlugin::interpreteParameters(QStringList &params)
{
    bool ok;
    QString param;

    // evaluate the parameter list
    if (params.count() != 5) return -EINVAL;

    param = params[0];
    m_fft_points = param.toUInt(&ok);
    if (!ok) return -EINVAL;
    if (m_fft_points > MAX_FFT_POINTS) m_fft_points = MAX_FFT_POINTS;

    param = params[1];
    m_window_type = Kwave::WindowFunction::findFromName(param);

    param = params[2];
    m_color = (param.toUInt(&ok) != 0);
    if (!ok) return -EINVAL;

    param = params[3];
    m_track_changes = (param.toUInt(&ok) != 0);
    if (!ok) return -EINVAL;

    param = params[4];
    m_follow_selection = (param.toUInt(&ok) != 0);
    if (!ok) return -EINVAL;

    return 0;
}

//***************************************************************************
int Kwave::SonagramPlugin::start(QStringList &params)
{
    // clean up leftovers from last run
    delete m_sonagram_window;
    m_sonagram_window = nullptr;
    delete m_selection;
    m_selection = nullptr;
    delete m_overview_cache;
    m_overview_cache = nullptr;

    Kwave::SignalManager &sig_mgr = signalManager();

    // interpret parameter list and abort if it contains invalid data
    int result = interpreteParameters(params);
    if (result) return result;

    // create an empty sonagram window
    m_sonagram_window = new(std::nothrow)
        Kwave::SonagramWindow(parentWidget(), signalName());
    Q_ASSERT(m_sonagram_window);
    if (!m_sonagram_window) return -ENOMEM;

    // if the signal closes, close the sonagram window too
    QObject::connect(&manager(), SIGNAL(sigClosed()),
                     m_sonagram_window, SLOT(close()));

    // get the current selection
    QVector<unsigned int> selected_channels;
    sample_index_t offset = 0;
    sample_index_t length = 0;
    length = selection(&selected_channels, &offset, nullptr, true);

    // abort if nothing is selected
    if (!length || selected_channels.isEmpty())
        return -EINVAL;

    // calculate the number of slices (width of image)
    m_slices = Kwave::toUint(ceil(static_cast<double>(length) /
                                  static_cast<double>(m_fft_points)));
    if (m_slices > MAX_SLICES) m_slices = MAX_SLICES;

    /* limit selection to INT_MAX samples (limitation of the cache index) */
    if ((length / m_fft_points) >= SAMPLE_INDEX_MAX) {
        Kwave::MessageBox::error(parentWidget(),
                                 i18n("File or selection too large"));
        return -EFBIG;
    }

    // create a selection tracker
    m_selection = new(std::nothrow) Kwave::SelectionTracker(
        &sig_mgr, offset, length, &selected_channels);
    Q_ASSERT(m_selection);
    if (!m_selection) return -ENOMEM;

    connect(m_selection, SIGNAL(sigTrackInserted(QUuid)),
            this,        SLOT(slotTrackInserted(QUuid)));
    connect(m_selection, SIGNAL(sigTrackDeleted(QUuid)),
            this,        SLOT(slotTrackDeleted(QUuid)));
    connect(
        m_selection,
        SIGNAL(sigInvalidated(const QUuid*,sample_index_t,sample_index_t)),
        this,
        SLOT(slotInvalidated(const QUuid*,sample_index_t,sample_index_t))
    );

    // create a new empty image
    createNewImage(m_slices, m_fft_points / 2);

    // set the overview
    m_overview_cache = new(std::nothrow)
        Kwave::OverViewCache(sig_mgr, offset, length, &selected_channels);
    Q_ASSERT(m_overview_cache);
    if (!m_overview_cache) return -ENOMEM;

    refreshOverview(); // <- this needs the m_overview_cache

    if (m_track_changes) {
        // stay informed about changes in the signal
        connect(m_overview_cache, SIGNAL(changed()),
                this, SLOT(refreshOverview()));
    } else {
        // overview cache is no longer needed
        delete m_overview_cache;
        m_overview_cache = nullptr;
    }

    // connect all needed signals
    connect(m_sonagram_window, SIGNAL(destroyed()),
            this, SLOT(windowDestroyed()));

    // activate the window with an initial image
    // and all necessary information
    m_sonagram_window->setColorMode((m_color) ? 1 : 0);
    m_sonagram_window->setImage(m_image);
    m_sonagram_window->setPoints(m_fft_points);
    m_sonagram_window->setRate(signalRate());
    m_sonagram_window->show();

    if (m_track_changes) {
        QObject::connect(static_cast<QObject*>(&(manager())),
            SIGNAL(sigSignalNameChanged(QString)),
            m_sonagram_window, SLOT(setName(QString)));
    }

    // increment the usage counter and release the plugin when the
    // sonagram window closed
    use();

    return 0;
}

//***************************************************************************
void Kwave::SonagramPlugin::makeAllValid()
{
    unsigned int             fft_points;
    unsigned int             slices;
    Kwave::window_function_t window_type;
    sample_index_t           first_sample;
    sample_index_t           last_sample;
    QBitArray                valid;
    QVector<unsigned int>    track_list;

    {
        QMutexLocker _lock(&m_lock_job_list);

        if (!m_selection) return;
        if (!m_selection->length() || (m_fft_points < 4)) return;

        fft_points   = m_fft_points;
        slices       = m_slices;
        window_type  = m_window_type;
        first_sample = m_selection->first();
        last_sample  = m_selection->last();
        valid        = m_valid;
        m_valid.fill(true);

        const QList<QUuid> selected_tracks(m_selection->allTracks());
        foreach (unsigned int track, signalManager().allTracks())
            if (selected_tracks.contains(signalManager().uuidOfTrack(track)))
                track_list.append(track);
    }
    const unsigned int tracks = static_cast<unsigned int>(track_list.count());

    Kwave::WindowFunction func(window_type);
    const QVector<double> windowfunction = func.points(fft_points);
    Q_ASSERT(windowfunction.count() == Kwave::toInt(fft_points));
    if (windowfunction.count() != Kwave::toInt(fft_points)) return;

    Kwave::MultiTrackReader source(Kwave::SinglePassForward,
        signalManager(), track_list, first_sample, last_sample);

//     qDebug("SonagramPlugin[%p]::makeAllValid() [%llu .. %llu]",
//      static_cast<void *>(this), first_sample, last_sample);

    QFutureSynchronizer<void> synchronizer;
    for (unsigned int slice_nr = 0; slice_nr < slices; slice_nr++) {
//      qDebug("SonagramPlugin::run(): calculating slice %d of %d",
//             slice_nr, m_slices);

        if (valid[slice_nr]) continue;

        // determine start of the stripe
        sample_index_t pos = first_sample + (slice_nr * fft_points);

        // get a new slice from the pool and initialize it
        Kwave::SonagramPlugin::Slice *slice = m_slice_pool.allocate();
        Q_ASSERT(slice);

        slice->m_index = slice_nr;
        memset(slice->m_input,  0x00, sizeof(slice->m_input));
        memset(slice->m_output, 0x00, sizeof(slice->m_output));

        if ((pos <= last_sample) && (tracks)) {
            // initialize result with zeroes
            memset(slice->m_result, 0x00, sizeof(slice->m_result));

            // seek to the start of the slice
            source.seek(pos);

            // we have a new slice, now fill it's input buffer
            double *in = slice->m_input;
            for (unsigned int j = 0; j < fft_points; j++) {
                double value = 0.0;
                if (!(source.eof())) {
                    for (unsigned int t = 0; t < tracks; t++) {
                        sample_t s = 0;
                        Kwave::SampleReader *reader = source[t];
                        Q_ASSERT(reader);
                        if (reader) *reader >> s;
                        value += sample2double(s);
                    }
                    value /= tracks;
                }
                in[j] = value * windowfunction[j];
            }

            // a background job is running soon
            // (for counterpart, see insertSlice(...) below [main thread])
            m_pending_jobs.lockForRead();

            // run the FFT in a background thread
            synchronizer.addFuture(QtConcurrent::run(
                &Kwave::SonagramPlugin::calculateSlice, this, slice)
            );
        } else {
            // range has been deleted -> fill with "empty"
            memset(slice->m_result, 0xFF, sizeof(slice->m_result));
            m_pending_jobs.lockForRead();
            emit sliceAvailable(slice);
        }

        if (shouldStop()) break;
    }

//     qDebug("SonagramPlugin::makeAllValid(): waiting for background jobs...");

    // wait for all worker threads
    synchronizer.waitForFinished();

    // wait for queued signals
    m_pending_jobs.lockForWrite();
    m_pending_jobs.unlock();

//     qDebug("SonagramPlugin::makeAllValid(): done.");
}

//***************************************************************************
void Kwave::SonagramPlugin::run(QStringList params)
{
    qDebug("SonagramPlugin::run()");
    Q_UNUSED(params)
    {
        // invalidate all slices
        QMutexLocker _lock(&m_lock_job_list);
        m_valid.fill(false);
    }
    makeAllValid();
}

//***************************************************************************
void Kwave::SonagramPlugin::calculateSlice(Kwave::SonagramPlugin::Slice *slice)
{
    fftw_plan p;

    // prepare for a 1-dimensional real-to-complex DFT
    {
        Kwave::GlobalLock _lock; // libfftw is not threadsafe!
        p = fftw_plan_dft_r2c_1d(
            m_fft_points,
            &(slice->m_input[0]),
            &(slice->m_output[0]),
            FFTW_ESTIMATE
        );
    }
    Q_ASSERT(p);
    if (!p) return;

    // calculate the fft (according to the specs, this is the one and only
    // libfft function that is threadsafe!)
    fftw_execute(p);

    // norm all values to [0...254] and use them as pixel value
    const double scale = static_cast<double>(m_fft_points) / 254.0;
    for (unsigned int j = 0; j < m_fft_points / 2; j++) {
        // get singal energy and scale to [0 .. 254]
        double rea = slice->m_output[j][0];
        double ima = slice->m_output[j][1];
        double a = ((rea * rea) + (ima * ima)) / scale;

        slice->m_result[j] = static_cast<unsigned char>(qMin(a, double(254.0)));
    }

    // free the allocated FFT resources
    {
        Kwave::GlobalLock _lock; // libfftw is not threadsafe!
        fftw_destroy_plan(p);
    }

    // emit the slice data to be synchronously inserted into
    // the current image in the context of the main thread
    // (Qt does the queuing for us)
    emit sliceAvailable(slice);
}

//***************************************************************************
void Kwave::SonagramPlugin::insertSlice(Kwave::SonagramPlugin::Slice *slice)
{
    // check: this must be called from the GUI thread only!
    Q_ASSERT(this->thread() == QThread::currentThread());
    Q_ASSERT(this->thread() == qApp->thread());

    Q_ASSERT(slice);
    if (!slice) return;

    QByteArray result;
    result.setRawData(reinterpret_cast<char *>(&(slice->m_result[0])),
                      m_fft_points / 2);
    unsigned int nr = slice->m_index;

    // forward the slice to the window to display it
    if (m_sonagram_window) m_sonagram_window->insertSlice(nr, result);

    // return the slice into the pool
    m_slice_pool.release(slice);

    // job is done
    m_pending_jobs.unlock();
}

//***************************************************************************
void Kwave::SonagramPlugin::createNewImage(const unsigned int width,
                                           const unsigned int height)
{
    // delete the previous image
    m_image = QImage();
    if (m_sonagram_window) m_sonagram_window->setImage(m_image);

    // do not create a new image if one dimension is zero!
    Q_ASSERT(width);
    Q_ASSERT(height);
    if (!width || !height) return;

    // also do not create if the image size is out of range
    Q_ASSERT(width <= 32767);
    Q_ASSERT(height <= 32767);
    if ((width >= 32767) || (height >= 32767)) return;

    // create the new image object
    m_image = QImage(width, height, QImage::Format_Indexed8);
    Q_ASSERT(!m_image.isNull());
    if (m_image.isNull()) return;

    // initialize the image's palette with transparecy
    m_image.setColorCount(256);
    for (int i = 0; i < 256; i++) {
        m_image.setColor(i, 0x00000000);
    }

    // fill the image with "empty" (transparent)
    m_image.fill(0xFF);
}

//***************************************************************************
void Kwave::SonagramPlugin::refreshOverview()
{
    if (!m_overview_cache || !m_sonagram_window) return;

    QColor fg = m_sonagram_window->palette().light().color();
    QColor bg = m_sonagram_window->palette().mid().color();
    QImage overview = m_overview_cache->getOverView(
       m_sonagram_window->width(), SONAGRAM_OVERVIEW_HEIGHT, fg, bg);

    m_sonagram_window->setOverView(overview);
}

//***************************************************************************
void Kwave::SonagramPlugin::requestValidation()
{
    // only re-start the repaint timer, this hides some GUI update artifacts
    if (!m_repaint_timer.isActive()) {
        m_repaint_timer.stop();
        m_repaint_timer.setSingleShot(true);
        m_repaint_timer.start(REPAINT_INTERVAL);
    }
}

//***************************************************************************
void Kwave::SonagramPlugin::validate()
{
    // wait for previously running jobs to finish
    if (m_future.isRunning()) {
        requestValidation();
        return; // job is still running, come back later...
    }

    // queue a background thread for updates
    m_future = QtConcurrent::run(&Kwave::SonagramPlugin::makeAllValid, this);
}

//***************************************************************************
void Kwave::SonagramPlugin::slotTrackInserted(const QUuid &track_id)
{
    QMutexLocker _lock(&m_lock_job_list);

    Q_UNUSED(track_id)

    // check for "track changes" mode
    if (!m_track_changes) return;

    // invalidate complete signal
    m_valid.fill(false, m_slices);
    requestValidation();
}

//***************************************************************************
void Kwave::SonagramPlugin::slotTrackDeleted(const QUuid &track_id)
{
    QMutexLocker _lock(&m_lock_job_list);

    Q_UNUSED(track_id)

    // check for "track changes" mode
    if (!m_track_changes) return;

    // invalidate complete signal
    m_valid.fill(false, m_slices);
    requestValidation();
}

//***************************************************************************
void Kwave::SonagramPlugin::slotInvalidated(const QUuid *track_id,
                                            sample_index_t first,
                                            sample_index_t last)
{
    QMutexLocker lock(&m_lock_job_list);

    Q_UNUSED(track_id)
//     qDebug("SonagramPlugin[%p]::slotInvalidated(%s, %llu, %llu)",
//          static_cast<void *>(this),
//         (track_id) ? DBG(track_id->toString()) : "*", first, last);

    // check for "track changes" mode
    if (!m_track_changes) return;

    // adjust offsets, absolute -> relative
    sample_index_t offset = (m_selection) ? m_selection->offset() : 0;
    Q_ASSERT(first >= offset);
    Q_ASSERT(last  >= offset);
    Q_ASSERT(last  >= first);
    first -= offset;
    last  -= offset;

    unsigned int first_idx = Kwave::toUint(first / m_fft_points);
    unsigned int last_idx;
    if (last >= (SAMPLE_INDEX_MAX - (m_fft_points - 1)))
        last_idx = m_slices - 1;
    else
        last_idx = Kwave::toUint(qMin(Kwave::round_up(last,
            static_cast<sample_index_t>(m_fft_points)) / m_fft_points,
            static_cast<sample_index_t>(m_slices - 1))
        );

    m_valid.fill(false, first_idx, last_idx + 1);
    requestValidation();
}

//***************************************************************************
void Kwave::SonagramPlugin::windowDestroyed()
{
    cancel();

    m_sonagram_window = nullptr; // closes itself !

    delete m_selection;
    m_selection = nullptr;

    delete m_overview_cache;
    m_overview_cache = nullptr;

    release();
}

//***************************************************************************
#include "SonagramPlugin.moc"
//***************************************************************************
//***************************************************************************

#include "moc_SonagramPlugin.cpp"