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

mixxxdj / mixxx / 11600254202

30 Oct 2024 07:25PM CUT coverage: 32.157% (+0.2%) from 31.932%
11600254202

Pull #13172

github

web-flow
Merge 3ea8edddb into edae0ca3b
Pull Request #13172: Macros Backend respin

385 of 434 new or added lines in 10 files covered. (88.71%)

9 existing lines in 2 files now uncovered.

33651 of 104645 relevant lines covered (32.16%)

48463.94 hits per line

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

87.69
/src/engine/controls/macrocontrol.cpp
1
#include "macrocontrol.h"
2

3
#include <QRegExp>
4

5
#include "moc_macrocontrol.cpp"
6
#include "track/track.h"
7

8
namespace {
9
constexpr uint kRecordingTimerInterval = 100;
10
constexpr size_t kRecordingQueueSize = kRecordingTimerInterval / 10;
11

12
const QString recordingSuffix = QStringLiteral(" [Recording]");
13
const QString interruptedSuffix = QStringLiteral(" [Interrupted]");
14
const QRegExp kAutoGeneratedLabelRegExp(QStringLiteral("[0-9. ]+(\\[\\w*\\])?"));
15
} // namespace
16

17
ConfigKey MacroControl::getConfigKey(const QString& name) {
117,792✔
18
    return ConfigKey(m_group, QStringLiteral("macro_%1_%2").arg(m_slot).arg(name));
117,792✔
19
}
20

21
MacroControl::MacroControl(const QString& group, UserSettingsPointer pConfig, int slot)
14,724✔
22
        : EngineControl(group, pConfig),
23
          m_slot(slot),
14,724✔
24
          m_queuedJumpTarget(-1),
14,724✔
25
          m_recordedActions(kRecordingQueueSize),
14,724✔
26
          m_iNextAction(0),
14,724✔
27
          m_COStatus([this]() { return getConfigKey("status"); }()),
29,448✔
28
          m_CORecord([this]() { return getConfigKey("record"); }()),
29,448✔
29
          m_COPlay([this]() { return getConfigKey("play"); }()),
29,448✔
30
          m_COEnable([this]() { return getConfigKey("enable"); }()),
29,448✔
31
          m_COLoop([this]() { return getConfigKey("loop"); }()),
29,448✔
32
          m_activate([this]() { return getConfigKey("activate"); }()),
29,448✔
33
          m_toggle([this]() { return getConfigKey("toggle"); }()),
29,448✔
34
          m_clear([this]() { return getConfigKey("clear"); }()) {
58,896✔
35
    m_COStatus.setReadOnly();
14,724✔
36
    setStatus(Status::NoTrack);
14,724✔
37

38
    m_updateRecordingTimer.moveToThread(qApp->thread());
14,724✔
39
    connect(&m_updateRecordingTimer,
14,724✔
40
            &QTimer::timeout,
41
            this,
42
            &MacroControl::updateRecording);
43

44
    m_CORecord.setButtonMode(mixxx::control::ButtonMode::Toggle);
14,724✔
45
    connect(&m_CORecord,
14,724✔
46
            &ControlObject::valueChanged,
47
            this,
48
            &MacroControl::slotRecord);
49
    m_COPlay.setButtonMode(mixxx::control::ButtonMode::Toggle);
14,724✔
50
    connect(&m_COPlay,
14,724✔
51
            &ControlObject::valueChanged,
52
            this,
53
            &MacroControl::slotPlay);
54
    m_COEnable.setButtonMode(mixxx::control::ButtonMode::Toggle);
14,724✔
55
    connect(&m_COEnable,
14,724✔
56
            &ControlObject::valueChanged,
57
            this,
58
            &MacroControl::slotEnable);
59
    m_COLoop.setButtonMode(mixxx::control::ButtonMode::Toggle);
14,724✔
60
    connect(&m_COLoop,
14,724✔
61
            &ControlObject::valueChanged,
62
            this,
63
            &MacroControl::slotLoop);
64

65
    m_activate.setButtonMode(mixxx::control::ButtonMode::Toggle);
14,724✔
66
    connect(&m_activate,
14,724✔
67
            &ControlObject::valueChanged,
68
            this,
69
            &MacroControl::slotActivate);
70
    m_toggle.setButtonMode(mixxx::control::ButtonMode::Toggle);
14,724✔
71
    connect(&m_toggle,
14,724✔
72
            &ControlObject::valueChanged,
73
            this,
74
            &MacroControl::slotToggle);
75
    m_clear.setButtonMode(mixxx::control::ButtonMode::Toggle);
14,724✔
76
    connect(&m_clear,
14,724✔
77
            &ControlObject::valueChanged,
78
            this,
79
            &MacroControl::slotClear);
80
}
14,724✔
81

82
// FIXME(xeruf) Jumps while paused (e.g. via GotoAndStop) are not properly recorded
83
// since this function is not called
84
void MacroControl::process(const double dRate,
43,129✔
85
        mixxx::audio::FramePos currentPosition,
86
        const int iBufferSize) {
87
    Q_UNUSED(dRate);
88
    if (m_queuedJumpTarget.value() >= 0) {
43,129✔
89
        // if a cue press doesn't change the position, notifySeek isn't called,
90
        // thus m_queuedJumpTarget isn't reset
91
        if (getStatus() == Status::Armed) {
208✔
92
            // start the recording on a cue press even when there is no jump
93
            notifySeek(currentPosition);
2✔
94
        }
95
        m_queuedJumpTarget = mixxx::audio::FramePos(-1);
208✔
96
    }
97
    if (getStatus() != Status::Playing) {
43,129✔
98
        return;
43,118✔
99
    }
100
    const MacroAction& nextAction = m_pMacro->getActions().at(m_iNextAction);
11✔
101
    // The process method is called roughly every iBufferSize frames, the
102
    // tolerance range is double that to be safe. It is ahead of the position
103
    // because the seek is executed in the next EngineBuffer process cycle.
104
    if (currentPosition > nextAction.getSourcePosition() - iBufferSize &&
21✔
105
            currentPosition < nextAction.getSourcePosition() + iBufferSize) {
10✔
106
        seekExact(nextAction.getTargetPosition());
8✔
107
        m_iNextAction++;
8✔
108
        if (m_iNextAction == m_pMacro->size()) {
8✔
109
            if (m_pMacro->isLooped()) {
5✔
110
                m_iNextAction = 0;
2✔
111
            } else {
112
                setStatus(Status::Recorded);
3✔
113
            }
114
        }
115
    }
116
}
117

118
// TODO(xeruf) Verify that all active Macros are fully unloaded and inactive
119
// before a new track is loading - https://github.com/mixxxdj/mixxx/pull/2989#issuecomment-753465755
120
void MacroControl::trackLoaded(TrackPointer pNewTrack) {
8,678✔
121
    if (isRecording() && stopRecording()) {
8,678✔
122
        m_pMacro->setLabel(m_pMacro->getLabel().append(interruptedSuffix));
1✔
123
    }
124

125
    if (!pNewTrack) {
8,678✔
126
        m_pMacro = nullptr;
337✔
127
        setStatus(Status::NoTrack);
337✔
128
        return;
337✔
129
    }
130

131
    m_pMacro = pNewTrack->getMacro(m_slot);
8,341✔
132
    if (m_pMacro->getActions().isEmpty()) {
8,341✔
133
        setStatus(Status::Empty);
8,339✔
134
    } else {
135
        m_pMacro->isEnabled() ? play() : stop();
2✔
136
    }
137
}
138

139
void MacroControl::notifySeek(mixxx::audio::FramePos position) {
10,216✔
140
    if (m_queuedJumpTarget.value() < 0) {
10,216✔
141
        return;
10,209✔
142
    }
143
    auto queuedTarget = m_queuedJumpTarget;
437✔
144
    m_queuedJumpTarget = mixxx::audio::FramePos(-1);
437✔
145
    if (getStatus() == Status::Armed) {
437✔
146
        setStatus(Status::Recording);
4✔
147
    }
148
    if (getStatus() != Status::Recording) {
437✔
149
        return;
430✔
150
    }
151
    // Account for quantization so the jump stays in-phase but snaps exactly to the original target
152
    VERIFY_OR_DEBUG_ASSERT(m_recordedActions.try_emplace(
7✔
153
            frameInfo().currentPosition + (queuedTarget - position),
154
            queuedTarget)){};
155
}
156

157
void MacroControl::slotJumpQueued(mixxx::audio::FramePos samplePos) {
643✔
158
    m_queuedJumpTarget = samplePos;
643✔
159
}
643✔
160

161
MacroControl::Status MacroControl::getStatus() const {
108,551✔
162
    return Status(m_COStatus.get());
108,551✔
163
}
164

165
// TODO(xeruf) Waveform dimming when running
166
void MacroControl::setStatus(Status status) {
23,434✔
167
    m_COStatus.forceSet(static_cast<int>(status));
23,434✔
168
    m_COPlay.set(status == Status::Playing ? 1 : 0);
23,434✔
169
    m_CORecord.set(isRecording() ? 1 : 0);
23,434✔
170
}
23,434✔
171

172
MacroPointer MacroControl::getMacro() const {
20✔
173
    return m_pMacro;
20✔
174
}
175

176
bool MacroControl::isRecording() const {
32,151✔
177
    return getStatus() == Status::Armed || getStatus() == Status::Recording;
32,151✔
178
}
179

180
void MacroControl::play() {
10✔
181
    DEBUG_ASSERT(m_pMacro && !m_pMacro->getActions().isEmpty() && !isRecording());
10✔
182
    m_iNextAction = m_pMacro->size() > 1 ? 1 : 0;
10✔
183
    setStatus(Status::Playing);
10✔
184
    m_pMacro->setState(Macro::StateFlag::Enabled);
10✔
185
}
10✔
186

187
void MacroControl::stop() {
3✔
188
    DEBUG_ASSERT(m_pMacro && !m_pMacro->getActions().isEmpty() && !isRecording());
3✔
189
    m_iNextAction = INT_MAX;
3✔
190
    setStatus(Status::Recorded);
3✔
191
    m_pMacro->setState(Macro::StateFlag::Enabled, false);
3✔
192
}
3✔
193

194
bool MacroControl::updateRecording() {
6✔
195
    // qCDebug(macroLoggingCategory) << QThread::currentThread() <<
196
    // QTime::currentTime() << "Update recording status:" << getStatus() <<
197
    // "recording:" << isRecording();
198
    VERIFY_OR_DEBUG_ASSERT(isRecording()) {
6✔
NEW
199
        return false;
×
200
    }
201
    bool actionsRecorded = false;
6✔
202
    while (MacroAction* action = m_recordedActions.front()) {
13✔
203
        m_pMacro->addAction(*action);
7✔
204
        m_recordedActions.pop();
7✔
205
        actionsRecorded = true;
7✔
206
    }
7✔
207
    if (actionsRecorded && getStatus() == Status::Armed) {
6✔
NEW
208
        setStatus(Status::Recording);
×
209
    }
210
    return actionsRecorded;
6✔
211
}
212

213
bool MacroControl::stopRecording() {
6✔
214
    VERIFY_OR_DEBUG_ASSERT(isRecording()) {
6✔
NEW
215
        return false;
×
216
    }
217
    m_updateRecordingTimer.stop();
6✔
218
    updateRecording();
6✔
219
    m_pMacro->setLabel(m_pMacro->getLabel().remove(recordingSuffix));
6✔
220
    if (getStatus() == Status::Armed) {
6✔
221
        setStatus(Status::Empty);
2✔
222
        return false;
2✔
223
    }
224

225
    // This will still be the position of the previous track when called from trackLoaded
226
    // since trackLoaded is invoked before the SampleOfTrack of the controls is updated.
227
    m_pMacro->setEnd(frameInfo().currentPosition);
4✔
228
    if (m_pMacro->getLabel().isEmpty()) {
4✔
229
        // Automatically set the start position in seconds as label
230
        // if there is no user-defined one
231
        auto sampleRate = frameInfo().sampleRate;
3✔
232
        auto secPos = (sampleRate != 0) ? m_pMacro->getStart() / sampleRate
5✔
233
                                        : mixxx::audio::FramePos(0);
3✔
234
        m_pMacro->setLabel(QString::number(secPos.value(), 'f', 2));
3✔
235
    }
236
    setStatus(Status::Recorded);
4✔
237
    if (m_pMacro->isEnabled()) {
4✔
238
        slotGotoPlay();
3✔
239
    }
240
    return true;
4✔
241
}
242

243
void MacroControl::slotRecord(double value) {
11✔
244
    if (value == 0) {
11✔
245
        if (isRecording()) {
5✔
246
            stopRecording();
5✔
247
        }
248
        return;
5✔
249
    }
250
    switch (getStatus()) {
6✔
251
    case Status::Empty:
6✔
252
        setStatus(Status::Armed);
6✔
253
        break;
6✔
NEW
254
    case Status::Recorded:
×
NEW
255
        setStatus(Status::Recording);
×
NEW
256
        break;
×
NEW
257
    default:
×
NEW
258
        return;
×
259
    }
260
    DEBUG_ASSERT(m_updateRecordingTimer.thread() == QThread::currentThread());
6✔
261
    m_updateRecordingTimer.start(kRecordingTimerInterval);
6✔
262
    m_pMacro->setLabel(m_pMacro->getLabel().append(recordingSuffix));
6✔
263
}
264

265
void MacroControl::slotPlay(double value) {
2✔
266
    if (value > 0 && getStatus() == Status::Recorded) {
2✔
NEW
267
        play();
×
268
    } else if (value <= 0 && getStatus() == Status::Playing) {
2✔
269
        stop();
2✔
270
    }
271
}
2✔
272

273
void MacroControl::slotEnable(double value) {
1✔
274
    m_pMacro->setState(Macro::StateFlag::Enabled, value != 0);
1✔
275
}
1✔
276

277
void MacroControl::slotLoop(double value) {
2✔
278
    m_pMacro->setState(Macro::StateFlag::Looped, value != 0);
2✔
279
}
2✔
280

281
void MacroControl::slotClear(double value) {
2✔
282
    if (value == 0) {
2✔
NEW
283
        return;
×
284
    }
285
    if (getStatus() == Status::Recorded) {
2✔
286
        qCDebug(kMacroLoggingCategory) << "Clearing macro" << m_slot;
4✔
287
        m_pMacro->clear();
2✔
288
        // macro_X_clear keeps the label, unless it was auto-generated.
289
        if (kAutoGeneratedLabelRegExp.exactMatch(m_pMacro->getLabel())) {
2✔
NEW
290
            m_pMacro->setLabel("");
×
291
        }
292
        setStatus(Status::Empty);
2✔
293
    }
294
}
295

296
void MacroControl::slotActivate(double value) {
9✔
297
    if (value == 0) {
9✔
NEW
298
        return;
×
299
    }
300
    switch (getStatus()) {
9✔
301
    case Status::Recorded:
3✔
302
        play();
3✔
303
        break;
3✔
304
    case Status::Playing:
2✔
305
        slotGotoPlay();
2✔
306
        break;
2✔
307
    default:
4✔
308
        slotRecord(!isRecording());
4✔
309
    }
310
}
311

NEW
312
void MacroControl::slotToggle(double value) {
×
NEW
313
    if (value == 0) {
×
NEW
314
        return;
×
315
    }
NEW
316
    switch (getStatus()) {
×
NEW
317
    case Status::Recorded:
×
NEW
318
        play();
×
NEW
319
        break;
×
NEW
320
    case Status::Playing:
×
NEW
321
        stop();
×
NEW
322
        break;
×
NEW
323
    default:
×
NEW
324
        slotRecord(!isRecording());
×
325
    }
326
}
327

328
void MacroControl::slotGotoPlay(double value) {
6✔
329
    if (value > 0 && getStatus() > Status::Recording) {
6✔
330
        seekExact(m_pMacro->getStart());
6✔
331
        play();
6✔
332
    }
333
}
6✔
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

© 2025 Coveralls, Inc