• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mcallegari / qlcplus / 26064181639

18 May 2026 10:29PM UTC coverage: 35.004% (-0.03%) from 35.037%
26064181639

Pull #2025

github

web-flow
Merge 108f14ffe into 2aa9da5af
Pull Request #2025: Fix Show Manager edge cases around timing and previews

0 of 70 new or added lines in 6 files covered. (0.0%)

6 existing lines in 1 file now uncovered.

18293 of 52259 relevant lines covered (35.0%)

41120.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/ui/src/showmanager/audioitem.cpp
1
/*
2
  Q Light Controller Plus
3
  audioitem.cpp
4

5
  Copyright (C) Massimo Callegari
6

7
  Licensed under the Apache License, Version 2.0 (the "License");
8
  you may not use this file except in compliance with the License.
9
  You may obtain a copy of the License at
10

11
      http://www.apache.org/licenses/LICENSE-2.0.txt
12

13
  Unless required by applicable law or agreed to in writing, software
14
  distributed under the License is distributed on an "AS IS" BASIS,
15
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
  See the License for the specific language governing permissions and
17
  limitations under the License.
18
*/
19

20
#include <QApplication>
21
#include <QPainter>
22
#include <qmath.h>
23
#include <QDebug>
24
#include <QMenu>
25

26
#include "audioitem.h"
27
#include "trackitem.h"
28
#include "audiodecoder.h"
29
#include "audioplugincache.h"
30

31
AudioItem::AudioItem(Audio *aud, ShowFunction *func)
×
32
    : ShowItem(func)
33
    , m_audio(aud)
×
34
    , m_previewLeftAction(NULL)
×
35
    , m_previewRightAction(NULL)
×
36
    , m_previewStereoAction(NULL)
×
37
    , m_preview(NULL)
×
38
{
39
    Q_ASSERT(aud != NULL);
×
40

41
    if (func->color().isValid())
×
42
        setColor(func->color());
×
43
    else
44
        setColor(ShowFunction::defaultColor(Function::AudioType));
×
45

46
    if (func->duration() == 0)
×
47
        func->setDuration(aud->totalDuration());
×
48

49
    calculateWidth();
×
50
    connect(m_audio, SIGNAL(changed(quint32)),
×
51
            this, SLOT(slotAudioChanged(quint32)));
52

53
    /* Preview actions */
54
    m_previewLeftAction = new QAction(tr("Preview Left Channel"), this);
×
55
    m_previewLeftAction->setCheckable(true);
×
56
    connect(m_previewLeftAction, SIGNAL(triggered(bool)),
×
57
            this, SLOT(slotAudioPreviewLeft()));
58
    m_previewRightAction = new QAction(tr("Preview Right Channel"), this);
×
59
    m_previewRightAction->setCheckable(true);
×
60
    connect(m_previewRightAction, SIGNAL(triggered(bool)),
×
61
            this, SLOT(slotAudioPreviewRight()));
62
    m_previewStereoAction = new QAction(tr("Preview Stereo Channels"), this);
×
63
    m_previewStereoAction->setCheckable(true);
×
64
    connect(m_previewStereoAction, SIGNAL(triggered(bool)),
×
65
            this, SLOT(slotAudioPreviewStereo()));
66
}
×
67

NEW
68
AudioItem::~AudioItem()
×
69
{
NEW
70
    stopWaveformPreviewThreads();
×
71

NEW
72
    QMutexLocker locker(&m_previewMutex);
×
NEW
73
    delete m_preview;
×
NEW
74
    m_preview = NULL;
×
NEW
75
}
×
76

UNCOV
77
void AudioItem::calculateWidth()
×
78
{
79
    int newWidth = 0;
×
80
    qint64 audio_duration = m_audio->totalDuration();
×
81

82
    if (audio_duration != 0)
×
83
        newWidth = ((50/(float)getTimeScale()) * (float)audio_duration) / 1000;
×
84
    else
85
        newWidth = 100;
×
86

87
    if (newWidth < (50 / m_timeScale))
×
88
        newWidth = 50 / m_timeScale;
×
89
    setWidth(newWidth);
×
90
}
×
91

92
void AudioItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
×
93
{
94
    Q_UNUSED(option);
95
    Q_UNUSED(widget);
96

97
    float timeScale = 50/(float)m_timeScale;
×
98

99
    ShowItem::paint(painter, option, widget);
×
100

NEW
101
    QPixmap preview;
×
102
    {
NEW
103
        QMutexLocker locker(&m_previewMutex);
×
NEW
104
        if (m_preview != NULL)
×
NEW
105
            preview = *m_preview;
×
NEW
106
    }
×
107

NEW
108
    if (preview.isNull() == false)
×
109
    {
110
        // show preview here
NEW
111
        painter->drawPixmap(0, 0, preview.scaled(m_width, TRACK_HEIGHT - 4));
×
112
    }
113

114
    if (m_audio->fadeInSpeed() != 0)
×
115
    {
116
        int fadeXpos = (timeScale * (float)m_audio->fadeInSpeed()) / 1000;
×
117
        painter->setPen(QPen(Qt::gray, 1));
×
118
        painter->drawLine(1, TRACK_HEIGHT - 4, fadeXpos, 2);
×
119
    }
120

121
    if (m_audio->fadeOutSpeed() != 0)
×
122
    {
123
        int fadeXpos = (timeScale * (float)m_audio->fadeOutSpeed()) / 1000;
×
124
        painter->setPen(QPen(Qt::gray, 1));
×
125
        painter->drawLine(m_width - fadeXpos, 2, m_width - 1, TRACK_HEIGHT - 4);
×
126
    }
127

128
    ShowItem::postPaint(painter);
×
129
}
×
130

131
void AudioItem::setTimeScale(int val)
×
132
{
133
    ShowItem::setTimeScale(val);
×
134
    calculateWidth();
×
135
}
×
136

137
void AudioItem::setDuration(quint32 msec, bool stretch)
×
138
{
139
    Q_UNUSED(msec)
140
    Q_UNUSED(stretch)
141
    // nothing to do
142
}
×
143

144
QString AudioItem::functionName() const
×
145
{
146
    if (m_audio)
×
147
        return m_audio->name();
×
148
    return QString();
×
149
}
150

151
Audio *AudioItem::getAudio() const
×
152
{
153
    return m_audio;
×
154
}
155

156
void AudioItem::updateWaveformPreview()
×
157
{
NEW
158
    for (PreviewThread *thread : m_previewThreads)
×
NEW
159
        thread->requestInterruption();
×
160

161
    PreviewThread *waveformThread = new PreviewThread;
×
162
    waveformThread->setAudioItem(this);
×
NEW
163
    m_previewThreads.append(waveformThread);
×
NEW
164
    connect(waveformThread, &PreviewThread::finished, this,
×
NEW
165
            [this, waveformThread]()
×
166
            {
NEW
167
                m_previewThreads.removeOne(waveformThread);
×
NEW
168
                waveformThread->deleteLater();
×
NEW
169
            });
×
170
    waveformThread->start();
×
171
}
×
172

NEW
173
void AudioItem::stopWaveformPreviewThreads()
×
174
{
NEW
175
    for (PreviewThread *thread : m_previewThreads)
×
NEW
176
        thread->requestInterruption();
×
177

NEW
178
    for (PreviewThread *thread : m_previewThreads)
×
179
    {
NEW
180
        disconnect(thread, &PreviewThread::finished, this, nullptr);
×
NEW
181
        thread->wait();
×
NEW
182
        delete thread;
×
183
    }
184

NEW
185
    m_previewThreads.clear();
×
NEW
186
}
×
187

UNCOV
188
void AudioItem::slotAudioChanged(quint32)
×
189
{
190
    updateWaveformPreview();
×
191
    prepareGeometryChange();
×
192
    calculateWidth();
×
193
    if (m_function)
×
194
        m_function->setDuration(m_audio->totalDuration());
×
195
}
×
196

197
void AudioItem::slotAudioPreviewLeft()
×
198
{
199
    m_previewRightAction->setChecked(false);
×
200
    m_previewStereoAction->setChecked(false);
×
201
    updateWaveformPreview();
×
202
}
×
203

204
void AudioItem::slotAudioPreviewRight()
×
205
{
206
    m_previewLeftAction->setChecked(false);
×
207
    m_previewStereoAction->setChecked(false);
×
208
    updateWaveformPreview();
×
209
}
×
210

211
void AudioItem::slotAudioPreviewStereo()
×
212
{
213
    m_previewLeftAction->setChecked(false);
×
214
    m_previewRightAction->setChecked(false);
×
215
    updateWaveformPreview();
×
216
}
×
217

218
void AudioItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *)
×
219
{
220
    QMenu menu;
×
221
    QFont menuFont = qApp->font();
×
222
    menuFont.setPixelSize(14);
×
223
    menu.setFont(menuFont);
×
224

225
    if (m_audio->getAudioDecoder() != NULL)
×
226
    {
227
        AudioDecoder *ad = m_audio->getAudioDecoder();
×
228
        AudioParameters ap = ad->audioParameters();
×
229

230
        if (ap.channels() == 1)
×
231
            m_previewLeftAction->setText(tr("Preview Mono"));
×
232
        menu.addAction(m_previewLeftAction);
×
233
        if (ap.channels() == 2)
×
234
        {
235
            m_previewLeftAction->setText(tr("Preview Left Channel"));
×
236
            menu.addAction(m_previewRightAction);
×
237
            menu.addAction(m_previewStereoAction);
×
238
        }
239
        menu.addSeparator();
×
240
    }
241

242
    foreach (QAction *action, getDefaultActions())
×
243
        menu.addAction(action);
×
244

245
    menu.exec(QCursor::pos());
×
246
}
×
247

NEW
248
PreviewThread::PreviewThread(QObject *parent)
×
249
    : QThread(parent)
NEW
250
    , m_item(NULL)
×
251
{
NEW
252
}
×
253

UNCOV
254
void PreviewThread::setAudioItem(AudioItem *item)
×
255
{
256
    m_item = item;
×
257
}
×
258

259
qint32 PreviewThread::getSample(const unsigned char *data, quint32 idx, int sampleSize) const
×
260
{
261
    qint32 value = 0;
×
262
    if (sampleSize == 1)
×
263
    {
264
        value = (qint32)data[idx];
×
265
    }
266
    else if (sampleSize == 2)
×
267
    {
268
        qint16 *array = (qint16 *)data;
×
269
        value = array[idx / 2];
×
270
    }
271
    else if (sampleSize == 3 || sampleSize == 4)
×
272
    {
273
        qint32 *array = (qint32 *)data;
×
274
        value = array[idx / 4] >> 16;
×
275
    }
276
    //qDebug() << "sampleValue:" << value;
277
    return value;
×
278
}
279

280
void PreviewThread::run()
×
281
{
NEW
282
    if (m_item == NULL || isInterruptionRequested())
×
NEW
283
        return;
×
284

285
    bool left = m_item->m_previewLeftAction->isChecked() || m_item->m_previewStereoAction->isChecked();
×
286
    bool right = m_item->m_previewRightAction->isChecked() || m_item->m_previewStereoAction->isChecked();
×
287

288
    if ((left || right) && m_item->m_audio->getAudioDecoder() != NULL)
×
289
    {
290
        AudioDecoder *ad = m_item->m_audio->doc()->audioPluginCache()->getDecoderForFile(m_item->m_audio->getSourceFileName());
×
291
        AudioParameters ap = ad->audioParameters();
×
292
        ad->seek(0);
×
293
        // 1- find out how many samples have to be represented on a single pixel on a 1:1 time scale
294
        int sampleSize = ap.sampleSize();
×
295
        int channels = ap.channels();
×
296
        int oneSecondSamples = ap.sampleRate() * channels;
×
297
        int onePixelSamples = oneSecondSamples / 50;
×
298

299
        qint32 maxValue = 0;
×
300
        // 24 and 32 bit samples would produce a RMS too high, so let's
301
        // work on 16bit values
302
        if (sampleSize > 2)
×
303
            sampleSize = 2;
×
304

305
        if (left && right)
×
306
            maxValue = 0x7F << (8 * (sampleSize - 1));
×
307
        else
308
            maxValue = 0x3F << (8 * (sampleSize - 1));
×
309

310
        quint32 onePixelReadLen = onePixelSamples * sampleSize;
×
311

312
        // 2- decode the whole file and fill a QPixmap with a sample block RMS value for each pixel
313
        qint64 dataRead = 1;
×
314
        unsigned char audioData[onePixelReadLen * 4];
×
315
        quint32 audioDataOffset = 0;
×
316
        QPixmap *preview = new QPixmap((50 * m_item->m_audio->totalDuration()) / 1000, 76);
×
317
        preview->fill(Qt::transparent);
×
318
        QPainter p(preview);
×
319
        int xpos = 0;
×
320

321
        qDebug() << "Audio duration:" << m_item->m_audio->totalDuration() <<
×
322
                    ", channels:" << channels << ", pixmap width:" << preview->width() <<
×
323
                    ", maxValue:" << maxValue << ", samples:" << sampleSize;
×
324
        qDebug() << "Samples per second:" << oneSecondSamples << ", for one pixel:" << onePixelSamples <<
×
325
                    ", onePixelReadLen:" << onePixelReadLen;
×
326

327
        {
NEW
328
            QMutexLocker locker(&m_item->m_previewMutex);
×
NEW
329
            delete m_item->m_preview;
×
NEW
330
            m_item->m_preview = NULL;
×
NEW
331
        }
×
UNCOV
332
        m_item->update();
×
333

NEW
334
        while (dataRead && !isInterruptionRequested())
×
335
        {
336
            quint32 tmpExceedData = 0;
×
337
            if (audioDataOffset < onePixelReadLen)
×
338
            {
339
                dataRead = ad->read((char *)audioData + audioDataOffset, onePixelReadLen * 2);
×
340
                if (dataRead > 0)
×
341
                {
342
                    if ((quint32)dataRead + audioDataOffset >= onePixelReadLen)
×
343
                    {
344
                        tmpExceedData = (dataRead + audioDataOffset) - onePixelReadLen;
×
345
                        dataRead = onePixelReadLen;
×
346
                    }
347
                    else
348
                    {
349
                        qDebug() << "Not enough data. Requested:" << onePixelReadLen << "got:" << dataRead;
×
350
                        audioDataOffset = dataRead;
×
351
                        continue;
×
352
                    }
353
                }
354
            }
355
            else
356
            {
357
                dataRead = onePixelReadLen;
×
358
                tmpExceedData = audioDataOffset - onePixelReadLen;
×
359
            }
360

361
            if (dataRead == onePixelReadLen)
×
362
            {
363
                quint32 i = 0;
×
364
                // calculate the RMS value (peak) for this data block
365
                qint64 rmsLeft = 0;
×
366
                qint64 rmsRight = 0;
×
367
                bool done = false;
×
368
                while (!done)
×
369
                {
370
                    if (left)
×
371
                    {
372
                        qint32 sampleVal = getSample(audioData, i, sampleSize);
×
373
                        rmsLeft += (sampleVal * sampleVal);
×
374
                    }
375
                    i += sampleSize;
×
376

377
                    if (channels == 2)
×
378
                    {
379
                        if (right)
×
380
                        {
381
                            qint32 sampleVal = getSample(audioData, i, sampleSize);
×
382
                            rmsRight += (sampleVal * sampleVal);
×
383
                        }
384
                        i += sampleSize;
×
385
                    }
386

387
                    if (i >= dataRead)
×
388
                    {
389
                        //qDebug() << "i:" << i << "xpos:" << xpos;
390
                        done = true;
×
391
                    }
392
                }
393

394
                if (left)
×
395
                    rmsLeft = sqrt(rmsLeft / onePixelSamples);
×
396
                if (right)
×
397
                    rmsRight = sqrt(rmsRight / onePixelSamples);
×
398
                //qDebug() << "sample" << i << "RMS right:" << rmsRight << ", RMS left:" << rmsLeft;
399

400
                // 3- Draw the actual waveform
401
                unsigned short lineHeightLeft = 0, lineHeightRight = 0;
×
402

403
                if (left)
×
404
                    lineHeightLeft = (76 * rmsLeft) / maxValue;
×
405
                if (right)
×
406
                    lineHeightRight = (76 * rmsRight) / maxValue;
×
407

408
                if (left && right)
×
409
                {
410
                    if (lineHeightLeft > 1)
×
411
                        p.drawLine(xpos, 19 - (lineHeightLeft / 2), xpos, 19 + (lineHeightLeft / 2));
×
412
                    else
413
                        p.drawLine(xpos, 19, xpos + 1, 19);
×
414

415
                    if (lineHeightRight > 1)
×
416
                        p.drawLine(xpos, 51 - (lineHeightRight / 2), xpos, 51 + (lineHeightRight / 2));
×
417
                    else
418
                        p.drawLine(xpos, 51, xpos + 1, 51);
×
419
                }
420
                else
421
                {
422
                    unsigned short lineHeight = left ? lineHeightLeft : lineHeightRight;
×
423

424
                    if (lineHeight > 1)
×
425
                        p.drawLine(xpos, 38 - (lineHeight / 2), xpos, 38 + (lineHeight / 2));
×
426
                    else
427
                        p.drawLine(xpos, 38, xpos + 1, 38);
×
428
                    //qDebug() << "Data read: " << dataRead << ", rms: " << rmsRight << ", line height: " << lineHeight << ", xpos = " << xpos;
429
                }
430
                xpos++;
×
431

432
                if (tmpExceedData > 0)
×
433
                {
434
                    //qDebug() << "Exceed data found: " << tmpExceedData;
435
                    memmove(audioData, audioData + onePixelReadLen, tmpExceedData);
×
436
                    audioDataOffset = tmpExceedData;
×
437
                }
438
                else
439
                    audioDataOffset = 0;
×
440
            }
441
        }
442
        //qDebug() << "Iterations done: " << xpos;
443
        delete ad;
×
NEW
444
        if (isInterruptionRequested())
×
445
        {
NEW
446
            delete preview;
×
NEW
447
            return;
×
448
        }
449

450
        {
NEW
451
            QMutexLocker locker(&m_item->m_previewMutex);
×
NEW
452
            m_item->m_preview = preview;
×
NEW
453
        }
×
UNCOV
454
    }
×
455
    else // no preview selected. Delete pixmap
456
    {
NEW
457
        QMutexLocker locker(&m_item->m_previewMutex);
×
458
        delete m_item->m_preview;
×
459
        m_item->m_preview = NULL;
×
UNCOV
460
    }
×
461

NEW
462
    if (!isInterruptionRequested())
×
NEW
463
        m_item->update();
×
464
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc