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

mcallegari / qlcplus / 34759607189

13 Sep 2026 01:20PM UTC coverage: 35.322% (-0.002%) from 35.324%
34759607189

push

github

mcallegari
qmlui: improve beat-based Shows (fix #2147)

6 of 28 new or added lines in 1 file covered. (21.43%)

1 existing line in 1 file now uncovered.

18628 of 52738 relevant lines covered (35.32%)

40947.1 hits per line

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

37.57
/engine/src/showrunner.cpp
1
/*
2
  Q Light Controller
3
  showrunner.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 <QMutex>
21
#include <QDebug>
22

23
#include "showrunner.h"
24
#include "function.h"
25
#include "track.h"
26
#include "show.h"
27
#include "doc.h"
28
#include "inputoutputmap.h"
29

30
#define TIMER_INTERVAL 50
31

32
static bool compareShowFunctions(const ShowFunction *sf1, const ShowFunction *sf2)
×
33
{
34
    if (sf1->startTime() < sf2->startTime())
×
35
        return true;
×
36
    return false;
×
37
}
38

39
ShowRunner::ShowRunner(const Doc* doc, quint32 showID, quint32 startTime)
3✔
40
    : QObject(NULL)
41
    , m_doc(doc)
3✔
42
    , m_currentTimeFunctionIndex(0)
3✔
43
    , m_elapsedTime(startTime)
3✔
44
    , m_currentBeatFunctionIndex(0)
3✔
45
    , m_elapsedBeats(0)
3✔
46
    , beatSynced(false)
3✔
47
    , m_syncElapsedTime(0)
3✔
48
    , m_syncBeatsTime(0)
3✔
49
    , m_totalRunTime(0)
3✔
50
    , m_totalRunBeats(0)
3✔
51
{
52
    Q_ASSERT(m_doc != NULL);
3✔
53
    Q_ASSERT(showID != Show::invalidId());
3✔
54

55
    m_show = qobject_cast<Show*>(m_doc->function(showID));
3✔
56
    if (m_show == NULL)
3✔
57
        return;
×
58

59
    /* startTime (e.g. coming from the cursor position) is always real
60
       milliseconds. If playback doesn't start from 0, m_elapsedBeats needs
61
       the equivalent estimate in "beats as ms" too, otherwise every Play
62
       would always start counting beats from 0 regardless of where
63
       playback actually begins - making beat-based Functions that should
64
       already be running (or long finished) at startTime behave as if
65
       playback had just begun. This is only a starting estimate: once the
66
       first real beat lands (see beatSynced in write()), m_elapsedBeats
67
       keeps advancing in lockstep with the actual beat clock from there */
68
    if (startTime > 0)
3✔
69
    {
NEW
70
        int bpm = m_doc->inputOutputMap()->bpmNumber();
×
NEW
71
        if (bpm > 0)
×
NEW
72
            m_elapsedBeats = Function::timeToBeats(startTime, 60000 / bpm);
×
73
    }
74

75
    foreach (Track *track, m_show->tracks())
9✔
76
    {
77
        // some sanity checks
78
        if (track == NULL ||
6✔
79
            track->id() == Track::invalidId())
3✔
80
                continue;
×
81

82
        if (track->isMute())
3✔
83
            continue;
×
84

85
        // get all the functions of the track and append them to the runner queue
86
        foreach (ShowFunction *sfunc, track->showFunctions())
9✔
87
        {
88
            if (sfunc->startTime() + sfunc->duration(m_doc) <= startTime)
3✔
89
                continue;
×
90

91
            Function *f = m_doc->function(sfunc->functionID());
3✔
92
            if (f == NULL)
3✔
93
                continue;
×
94

95
            if (f->tempoType() == Function::Time)
3✔
96
            {
97
                m_timeFunctions.append(sfunc);
3✔
98
                if (sfunc->startTime() + sfunc->duration(m_doc) > m_totalRunTime)
3✔
99
                    m_totalRunTime = sfunc->startTime() + sfunc->duration(m_doc);
3✔
100
            }
101
            else
102
            {
UNCOV
103
                m_beatFunctions.append(sfunc);
×
NEW
104
                if (sfunc->startTime() + sfunc->duration(m_doc) > m_totalRunBeats)
×
NEW
105
                    m_totalRunBeats = sfunc->startTime() + sfunc->duration(m_doc);
×
106
            }
107
        }
3✔
108

109
        // Initialize the intensity map
110
        m_intensityMap[track->id()] = 1.0;
3✔
111
    }
3✔
112

113
    std::sort(m_timeFunctions.begin(), m_timeFunctions.end(), compareShowFunctions);
3✔
114
    std::sort(m_beatFunctions.begin(), m_beatFunctions.end(), compareShowFunctions);
3✔
115

116
#if 1
117
    qDebug() << "Ordered list of ShowFunctions (time):";
3✔
118
    foreach (ShowFunction *sfunc, m_timeFunctions)
6✔
119
        qDebug() << "[Show] Function ID:" << sfunc->functionID() << "start time:" << sfunc->startTime() << "duration:" << sfunc->duration(m_doc);
6✔
120

121
    qDebug() << "Ordered list of ShowFunctions (beats):";
3✔
122
    foreach (ShowFunction *sfunc, m_beatFunctions)
3✔
123
        qDebug() << "[Show] Function ID:" << sfunc->functionID() << "start time:" << sfunc->startTime() << "duration:" << sfunc->duration(m_doc);
3✔
124
#endif
125
    m_runningQueue.clear();
3✔
126

127
    qDebug() << "ShowRunner created";
3✔
128
}
×
129

130
ShowRunner::~ShowRunner()
3✔
131
{
132
}
3✔
133

134
void ShowRunner::start()
×
135
{
136
    qDebug() << "ShowRunner started";
×
137
}
×
138

139
void ShowRunner::setPause(bool enable)
×
140
{
141
    for (int i = 0; i < m_runningQueue.count(); i++)
×
142
    {
143
        Function *f = m_runningQueue.at(i).first;
×
144
        f->setPause(enable);
×
145
    }
146
}
×
147

148
void ShowRunner::stop()
1✔
149
{
150
    m_elapsedTime = 0;
1✔
151
    m_elapsedBeats = 0;
1✔
152
    m_currentTimeFunctionIndex = 0;
1✔
153
    m_currentBeatFunctionIndex = 0;
1✔
154

155
    for (int i = 0; i < m_runningQueue.count(); i++)
2✔
156
    {
157
        Function *f = m_runningQueue.at(i).first;
1✔
158
        f->stop(functionParent());
1✔
159
    }
160

161
    m_runningQueue.clear();
1✔
162
    qDebug() << "ShowRunner stopped";
1✔
163
}
1✔
164

165
FunctionParent ShowRunner::functionParent() const
1✔
166
{
167
    return FunctionParent(FunctionParent::Function, m_show->id());
1✔
168
}
169

170
void ShowRunner::write(MasterTimer *timer)
×
171
{
172
    //qDebug() << Q_FUNC_INFO << "elapsed:" << m_elapsedTime << ", total:" << m_totalRunTime;
173

174
    // Phase 1. Check all the Functions that need to be started
175
    // m_timeFunctions is ordered by startup time, so when we found an entry
176
    // with start time greater than m_elapsed, this phase is over
177
    bool startFunctionsDone = false;
×
178

179
    // A Show can freely mix time-based and beat-based Functions on its
180
    // tracks (e.g. a beat-synced Chaser next to a Time-based Audio track),
181
    // regardless of the Show's own timeline display type. So beat tracking
182
    // must not depend on, nor gate, anything based on the Show's own
183
    // division: it only needs to run when this Show actually has beat-based
184
    // Functions to drive, and it must never block m_timeFunctions/m_elapsedTime
185
    // from progressing while waiting for the first beat to land.
NEW
186
    if (m_beatFunctions.isEmpty() == false && timer->isBeat())
×
187
    {
NEW
188
        if (beatSynced == false)
×
189
        {
NEW
190
            beatSynced = true;
×
NEW
191
            m_syncElapsedTime = m_elapsedTime;
×
NEW
192
            int syncBpm = timer->bpmNumber();
×
NEW
193
            m_syncBeatsTime = syncBpm > 0 ? Function::beatsToTime(m_elapsedBeats, 60000 / syncBpm) : 0;
×
NEW
194
            qDebug() << "Beat synced";
×
195
        }
196
        else
197
        {
NEW
198
            m_elapsedBeats += 1000;
×
199
        }
200
    }
201

202
    // check if there are time-based functions to start
203
    while (startFunctionsDone == false)
×
204
    {
205
        if (m_currentTimeFunctionIndex == m_timeFunctions.count())
×
206
            break;
×
207

208
        ShowFunction *sf = m_timeFunctions.at(m_currentTimeFunctionIndex);
×
209
        quint32 funcStartTime = sf->startTime();
×
210
        quint32 functionTimeOffset = 0;
×
211
        Function *f = m_doc->function(sf->functionID());
×
212
        if (f == nullptr)
×
213
        {
214
            m_currentTimeFunctionIndex++;
×
215
            continue;
×
216
        }
217

218
        // this should happen only when a Show is not started from 0
219
        if (m_elapsedTime > funcStartTime)
×
220
        {
221
            functionTimeOffset = m_elapsedTime - funcStartTime;
×
222
            funcStartTime = m_elapsedTime;
×
223
        }
224
        if (m_elapsedTime >= funcStartTime)
×
225
        {
226
            foreach (Track *track, m_show->tracks())
×
227
            {
228
                if (track->showFunctions().contains(sf))
×
229
                {
230
                    int intOverrideId = f->requestAttributeOverride(Function::Intensity, m_intensityMap[track->id()]);
×
231
                    //f->adjustAttribute(m_intensityMap[track->id()], Function::Intensity);
232
                    sf->setIntensityOverrideId(intOverrideId);
×
233
                    break;
×
234
                }
235
            }
×
236

237
            f->start(m_doc->masterTimer(), functionParent(), functionTimeOffset);
×
238
            m_runningQueue.append(QPair<Function *, quint32>(f, sf->startTime() + sf->duration(m_doc)));
×
239
            m_currentTimeFunctionIndex++;
×
240
        }
241
        else
242
            startFunctionsDone = true;
×
243
    }
244

245
    startFunctionsDone = false;
×
246

247
    // check if there are beat-based functions to start
248
    // (wait for the first real beat to land before considering any of
249
    // them, so m_elapsedBeats == 0 is not mistaken for "beat zero happened")
NEW
250
    while (startFunctionsDone == false && beatSynced)
×
251
    {
252
        if (m_currentBeatFunctionIndex == m_beatFunctions.count())
×
253
            break;
×
254

255
        ShowFunction *sf = m_beatFunctions.at(m_currentBeatFunctionIndex);
×
256
        quint32 funcStartTime = sf->startTime();
×
257
        quint32 functionTimeOffset = 0;
×
258
        Function *f = m_doc->function(sf->functionID());
×
259
        if (f == nullptr)
×
260
        {
261
            m_currentBeatFunctionIndex++;
×
262
            continue;
×
263
        }
264

265
        // this should happen only when a Show is not started from 0
266
        if (m_elapsedBeats > funcStartTime)
×
267
        {
268
            functionTimeOffset = m_elapsedBeats - funcStartTime;
×
269
            funcStartTime = m_elapsedBeats;
×
270
        }
271
        if (m_elapsedBeats >= funcStartTime)
×
272
        {
273
            foreach (Track *track, m_show->tracks())
×
274
            {
275
                if (track->showFunctions().contains(sf))
×
276
                {
277
                    int intOverrideId = f->requestAttributeOverride(Function::Intensity, m_intensityMap[track->id()]);
×
278
                    //f->adjustAttribute(m_intensityMap[track->id()], Function::Intensity);
279
                    sf->setIntensityOverrideId(intOverrideId);
×
280
                    break;
×
281
                }
282
            }
×
283

284
            f->start(m_doc->masterTimer(), functionParent(), functionTimeOffset);
×
285
            m_runningQueue.append(QPair<Function *, quint32>(f, sf->startTime() + sf->duration(m_doc)));
×
286
            m_currentBeatFunctionIndex++;
×
287
        }
288
        else
289
            startFunctionsDone = true;
×
290
    }
291

292
    // Phase 2. Check if we need to stop some running Functions
293
    // It is done in reverse order for two reasons:
294
    // 1- m_runningQueue is not ordered by stop time
295
    // 2- to avoid messing up with indices when an entry is removed
296
    for (int i = m_runningQueue.count() - 1; i >= 0; i--)
×
297
    {
298
        Function *func = m_runningQueue.at(i).first;
×
299
        quint32 stopTime = m_runningQueue.at(i).second;
×
300
        quint32 currTime = func->tempoType() == Function::Time ? m_elapsedTime : m_elapsedBeats;
×
301

302
        // if we passed the function stop time
303
        if (currTime >= stopTime)
×
304
        {
305
            // stop the function
306
            func->stop(functionParent());
×
307
            // remove it from the running queue
308
            m_runningQueue.removeAt(i);
×
309
        }
310
    }
311

312
    // Phase 3. Check if this is the end of the Show. A Show can mix
313
    // time-based and beat-based tracks, so it is only really over once
314
    // both the time-based and the beat-based timelines have completed.
315
    // While there are beat-based Functions but no beat has been detected
316
    // yet, the beat timeline hasn't even started, so it can't be "done".
NEW
317
    bool timeDone = m_elapsedTime >= m_totalRunTime;
×
NEW
318
    bool beatsDone = m_beatFunctions.isEmpty() ||
×
NEW
319
                      (beatSynced && m_elapsedBeats >= m_totalRunBeats);
×
320

NEW
321
    if (timeDone && beatsDone)
×
322
    {
323
        if (m_show != NULL)
×
324
            m_show->stop(functionParent());
×
325
        emit showFinished();
×
326
        return;
×
327
    }
328

329
    m_elapsedTime += MasterTimer::tick();
×
330

331
    // Report plain elapsed milliseconds: it advances smoothly on every
332
    // tick, and the UI scales it to a beat/bar position against the
333
    // current BPM (see ShowManager and TimeUtils.timeToBeatPosition()) so
334
    // it reacts immediately to live BPM changes.
335
    //
336
    // However, when the Show's own timeline is BPM based, m_elapsedTime is
337
    // real wall-clock time since the Show started, which includes however
338
    // long it took to wait for the first beat to sync (beat 0 is defined
339
    // by that sync moment, not by when the Show started) - reporting it
340
    // directly would make the cursor jump to an arbitrary, not
341
    // beat-aligned position the instant sync happens. Instead, report the
342
    // beat-zeroed resume position (m_syncBeatsTime) plus how much real
343
    // time has passed since the sync moment: this still advances smoothly
344
    // every tick (unlike reporting m_elapsedBeats directly, which only
345
    // moves in whole-beat jumps), while staying exactly beat-aligned at
346
    // the sync instant itself.
NEW
347
    if (m_show->timeDivisionType() == Show::Time)
×
348
    {
NEW
349
        emit timeChanged(m_elapsedTime);
×
350
    }
NEW
351
    else if (beatSynced)
×
352
    {
NEW
353
        emit timeChanged(m_syncBeatsTime + (m_elapsedTime - m_syncElapsedTime));
×
354
    }
355
}
356

357
/************************************************************************
358
 * Intensity
359
 ************************************************************************/
360

361
void ShowRunner::adjustIntensity(qreal fraction, const Track *track)
1✔
362
{
363
    if (track == NULL)
1✔
364
        return;
×
365

366
    qDebug() << Q_FUNC_INFO << "Track ID: " << track->id() << ", val:" << fraction;
1✔
367
    m_intensityMap[track->id()] = fraction;
1✔
368

369
    foreach (ShowFunction *sf, track->showFunctions())
3✔
370
    {
371
        Function *f = m_doc->function(sf->functionID());
1✔
372
        if (f == NULL)
1✔
373
            continue;
×
374

375
        for (int i = 0; i < m_runningQueue.count(); i++)
1✔
376
        {
377
            Function *rf = m_runningQueue.at(i).first;
×
378
            if (f == rf)
×
379
                f->adjustAttribute(fraction, sf->intensityOverrideId());
×
380
        }
381
    }
1✔
382
}
383

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc