Skip to content

Commit d053e20

Browse files
ndonald2Dewb
andauthored
Add MIDI clock synchronization for external hardware or other software (elieserdejesus#1354)
* Add clock start/pulse to midi driver * Add MIDI clock methods to main controller * Proof of concept MIDI clock sync * Hacky latency offset * Stable MIDI sync * Fix settings persistence for sync outputs * Bump version to beta * Commit changes to translations files * Properly start/stop the sync and cleanup track graph * Only send start message one time * Add sync control in interval GUI * Avoid too many pulses and add continue message * Stop and continue each interval reset * Bump beta version num * Fix unusual auto generated file references * Changes for Windows and plugin builds * Revert version bump for PR * Code style: clean up control flow spacing * Refactor message sending (DRY output loop) * Remove stop/continue messages on new interval Co-authored-by: Michael Dewberry <michael.dewberry@gmail>
1 parent 47046ab commit d053e20

37 files changed

+678
-125
lines changed

PROJECTS/Jamtaba-common.pri

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ HEADERS += audio/vorbis/VorbisEncoder.h
6868
HEADERS += audio/RoomStreamerNode.h
6969
HEADERS += audio/NinjamTrackNode.h
7070
HEADERS += audio/MetronomeTrackNode.h
71+
HEADERS += audio/MidiSyncTrackNode.h
7172
HEADERS += audio/SamplesBufferResampler.h
7273
HEADERS += audio/SamplesBufferRecorder.h
7374
HEADERS += audio/Mp3Decoder.h
@@ -190,6 +191,7 @@ SOURCES += audio/core/Plugins.cpp
190191
SOURCES += audio/Mp3Decoder.cpp
191192
SOURCES += audio/NinjamTrackNode.cpp
192193
SOURCES += audio/MetronomeTrackNode.cpp
194+
SOURCES += audio/MidiSyncTrackNode.cpp
193195
SOURCES += audio/core/SamplesBuffer.cpp
194196
SOURCES += audio/core/PluginDescriptor.cpp
195197
SOURCES += audio/SamplesBufferResampler.cpp

src/Common/MainController.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,10 +641,11 @@ void MainController::storeWindowSettings(bool maximized, const QPointF &location
641641
}
642642

643643
void MainController::storeIOSettings(int firstIn, int lastIn, int firstOut, int lastOut, QString audioInputDevice, QString audioOutputDevice,
644-
const QList<bool> &midiInputsStatus)
644+
const QList<bool> &midiInputsStatus, const QList<bool> &syncOutputsStatus)
645645
{
646646
settings.setAudioSettings(firstIn, lastIn, firstOut, lastOut, audioInputDevice, audioOutputDevice);
647647
settings.setMidiSettings(midiInputsStatus);
648+
settings.setSyncSettings(syncOutputsStatus);
648649
}
649650

650651
void MainController::storeIOSettings(int firstIn, int lastIn, int firstOut, int lastOut, QString audioInputDevice, QString audioOutputDevice)
@@ -653,7 +654,8 @@ void MainController::storeIOSettings(int firstIn, int lastIn, int firstOut, int
653654
firstIn, lastIn,
654655
firstOut, lastOut,
655656
audioInputDevice, audioOutputDevice,
656-
settings.getMidiInputDevicesStatus()
657+
settings.getMidiInputDevicesStatus(),
658+
settings.getSyncOutputDevicesStatus()
657659
);
658660

659661
}

src/Common/MainController.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ class MainController : public QObject
210210
void storeWindowSettings(bool maximized, const QPointF &location, const QSize &size);
211211
void storeIOSettings(int firstIn, int lastIn, int firstOut, int lastOut,
212212
QString audioInputDevice, QString audioOutputDevice,
213-
const QList<bool> &midiInputStatus);
213+
const QList<bool> &midiInputStatus, const QList<bool> &syncOutputsStatus);
214214

215215
void storeIOSettings(int firstIn, int lastIn, int firstOut, int lastOut,
216216
QString audioInputDevice, QString audioOutputDevice);
@@ -289,6 +289,12 @@ class MainController : public QObject
289289

290290
void setAllLoopersStatus(bool activated);
291291

292+
// sync methods
293+
virtual void startMidiClock() const = 0;
294+
virtual void stopMidiClock() const = 0;
295+
virtual void continueMidiClock() const = 0;
296+
virtual void sendMidiClockPulse() const = 0;
297+
292298
// collapse settings
293299
void setLocalChannelsCollapsed(bool collapsed);
294300
void setBottomSectionCollapsed(bool collapsed);

src/Common/NinjamController.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "file/FileReader.h"
1414
#include "audio/NinjamTrackNode.h"
1515
#include "audio/MetronomeTrackNode.h"
16+
#include "audio/MidiSyncTrackNode.h"
1617
#include "audio/Resampler.h"
1718
#include "audio/SamplesBufferRecorder.h"
1819
#include "audio/vorbis/VorbisEncoder.h"
@@ -235,6 +236,7 @@ NinjamController::NinjamController(controller::MainController *mainController) :
235236
samplesInInterval(0),
236237
mainController(mainController),
237238
metronomeTrackNode(createMetronomeTrackNode(mainController->getSampleRate())),
239+
midiSyncTrackNode(new audio::MidiSyncTrackNode(mainController)),
238240
lastBeat(0),
239241
currentBpi(0),
240242
currentBpm(0),
@@ -264,6 +266,7 @@ void NinjamController::setBpm(int newBpm)
264266
currentBpm = newBpm;
265267
samplesInInterval = computeTotalSamplesInInterval();
266268
metronomeTrackNode->setSamplesPerBeat(getSamplesPerBeat());
269+
midiSyncTrackNode->setPulseTiming(currentBpi * 24, getSamplesPerBeat()/24.0);
267270

268271
emit currentBpmChanged(currentBpm);
269272
}
@@ -274,11 +277,21 @@ void NinjamController::setBpmBpi(int initialBpm, int initialBpi)
274277
currentBpm = initialBpm;
275278
samplesInInterval = computeTotalSamplesInInterval();
276279
metronomeTrackNode->setSamplesPerBeat(getSamplesPerBeat());
280+
midiSyncTrackNode->setPulseTiming(currentBpi * 24, getSamplesPerBeat()/24.0);
277281

278282
emit currentBpmChanged(currentBpm);
279283
emit currentBpiChanged(currentBpi);
280284
}
281285

286+
void NinjamController::setSyncEnabled(bool enabled)
287+
{
288+
if (enabled) {
289+
midiSyncTrackNode->start();
290+
} else {
291+
midiSyncTrackNode->stop();
292+
}
293+
}
294+
282295
void NinjamController::removeEncoder(int groupChannelIndex)
283296
{
284297
QMutexLocker locker(&mutex);
@@ -326,6 +339,7 @@ void NinjamController::process(const audio::SamplesBuffer &in, audio::SamplesBuf
326339
handleNewInterval();
327340

328341
metronomeTrackNode->setIntervalPosition(this->intervalPosition);
342+
midiSyncTrackNode->setIntervalPosition(this->intervalPosition);
329343
int currentBeat = intervalPosition / getSamplesPerBeat();
330344
if (currentBeat != lastBeat)
331345
{
@@ -433,6 +447,10 @@ void NinjamController::stop(bool emitDisconnectedSignal)
433447
{
434448
this->running = false;
435449

450+
// stop midi sync track
451+
this->midiSyncTrackNode->stop();
452+
mainController->removeTrack(MIDI_SYNC_TRACK_ID);
453+
436454
// store metronome settings
437455
auto metronomeTrack = mainController->getTrackNode(METRONOME_TRACK_ID);
438456
if (metronomeTrack)
@@ -553,6 +571,8 @@ void NinjamController::start(const ServerInfo &server)
553571
mainController->setTrackPan(METRONOME_TRACK_ID,
554572
mainController->getSettings().getMetronomePan());
555573

574+
mainController->addTrack(MIDI_SYNC_TRACK_ID, this->midiSyncTrackNode);
575+
556576
this->intervalPosition = lastBeat = 0;
557577

558578
auto ninjamService = mainController->getNinjamService();

src/Common/NinjamController.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class UserChannel;
1818

1919
namespace audio {
2020
class MetronomeTrackNode;
21+
class MidiSyncTrackNode;
2122
class SamplesBuffer;
2223
}
2324

@@ -30,6 +31,7 @@ using ninjam::client::User;
3031
using ninjam::client::UserChannel;
3132
using audio::SamplesBuffer;
3233
using audio::MetronomeTrackNode;
34+
using audio::MidiSyncTrackNode;
3335

3436
class NinjamController : public QObject
3537
{
@@ -56,9 +58,12 @@ class NinjamController : public QObject
5658
void setBpm(int newBpm);
5759
void setBpmBpi(int initialBpm, int initalBpi);
5860

61+
void setSyncEnabled(bool enabled);
62+
5963
void sendChatMessage(const QString &msg);
6064

6165
static const long METRONOME_TRACK_ID = 123456789; // just a number :)
66+
static const long MIDI_SYNC_TRACK_ID = 1123581113; // also just a number ;)
6267

6368
void recreateEncoders();
6469

@@ -123,6 +128,7 @@ class NinjamController : public QObject
123128
MainController *mainController;
124129

125130
MetronomeTrackNode *metronomeTrackNode;
131+
MidiSyncTrackNode *midiSyncTrackNode;
126132

127133
private:
128134
static QString getUniqueKeyForChannel(const UserChannel &channel, const QString &userFullName);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#include "MidiSyncTrackNode.h"
2+
#include "MainController.h"
3+
#include "MetronomeUtils.h"
4+
#include "audio/core/AudioDriver.h"
5+
#include <algorithm>
6+
7+
using audio::MidiSyncTrackNode;
8+
using audio::SamplesBuffer;
9+
10+
using namespace controller;
11+
12+
MidiSyncTrackNode::MidiSyncTrackNode(MainController *controller) :
13+
pulsesPerInterval(0),
14+
samplesPerPulse(0),
15+
intervalPosition(0),
16+
currentPulse(0),
17+
lastPlayedPulse(-1),
18+
running(false),
19+
hasSentStart(false),
20+
mainController(controller)
21+
{
22+
resetInterval();
23+
}
24+
25+
MidiSyncTrackNode::~MidiSyncTrackNode()
26+
{
27+
}
28+
29+
void MidiSyncTrackNode::setPulseTiming(long pulsesPerInterval, double samplesPerPulse)
30+
{
31+
if (pulsesPerInterval <= 0 || samplesPerPulse <= 0)
32+
qCritical() << "invalid sync timing params";
33+
34+
this->pulsesPerInterval = pulsesPerInterval;
35+
this->samplesPerPulse = samplesPerPulse;
36+
resetInterval();
37+
}
38+
39+
void MidiSyncTrackNode::resetInterval()
40+
{
41+
intervalPosition = 0;
42+
lastPlayedPulse = -1;
43+
}
44+
45+
void MidiSyncTrackNode::setIntervalPosition(long intervalPosition)
46+
{
47+
if (samplesPerPulse <= 0)
48+
return;
49+
50+
this->intervalPosition = intervalPosition;
51+
this->currentPulse = ((double)intervalPosition / samplesPerPulse);
52+
}
53+
54+
void MidiSyncTrackNode::start()
55+
{
56+
running = true;
57+
}
58+
59+
void MidiSyncTrackNode::stop()
60+
{
61+
running = false;
62+
hasSentStart = false;
63+
mainController->stopMidiClock();
64+
}
65+
66+
void MidiSyncTrackNode::processReplacing(const SamplesBuffer &in, SamplesBuffer &out,
67+
int SampleRate, std::vector<midi::MidiMessage> &midiBuffer)
68+
{
69+
if (pulsesPerInterval <= 0 || samplesPerPulse <= 0)
70+
return;
71+
72+
if (currentPulse == 0 && currentPulse != lastPlayedPulse) {
73+
if (running && !hasSentStart) {
74+
mainController->startMidiClock();
75+
hasSentStart = true;
76+
}
77+
// qDebug() << "Pulses played in interval: " << lastPlayedPulse;
78+
lastPlayedPulse = -1;
79+
}
80+
81+
while (currentPulse < pulsesPerInterval && currentPulse - lastPlayedPulse >= 1) {
82+
mainController->sendMidiClockPulse();
83+
lastPlayedPulse++;
84+
}
85+
AudioNode::processReplacing(in, out, SampleRate, midiBuffer);
86+
}

src/Common/audio/MidiSyncTrackNode.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#ifndef MIDISYNCTRACKNODE_H
2+
#define MIDISYNCTRACKNODE_H
3+
4+
#include "core/AudioNode.h"
5+
6+
namespace controller{
7+
class MainController;
8+
}
9+
10+
namespace audio {
11+
12+
using controller::MainController;
13+
14+
class MidiSyncTrackNode : public audio::AudioNode
15+
{
16+
17+
public:
18+
MidiSyncTrackNode(MainController *controller);
19+
20+
~MidiSyncTrackNode();
21+
void processReplacing(const SamplesBuffer &in, SamplesBuffer &out, int sampleRate, std::vector<midi::MidiMessage> &midiBuffer) override;
22+
void setPulseTiming(long pulsesPerInterval, double samplesPerPulse);
23+
void setIntervalPosition(long intervalPosition);
24+
void resetInterval();
25+
26+
void start();
27+
void stop();
28+
29+
private:
30+
long pulsesPerInterval;
31+
double samplesPerPulse;
32+
long intervalPosition;
33+
int currentPulse;
34+
int lastPlayedPulse;
35+
bool running;
36+
bool hasSentStart;
37+
38+
MainController *mainController;
39+
};
40+
41+
} // namespace
42+
43+
#endif // MIDISYNCTRACKNODE_H

src/Common/gui/NinjamPanel.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ void NinjamPanel::setupSignals()
141141
connect(ui->comboBpi, SIGNAL(activated(QString)), this, SLOT(handleBpiComboActication(QString)));
142142
connect(ui->comboBpm, SIGNAL(activated(QString)), this, SLOT(handleBpmComboActication(QString)));
143143

144+
connect(ui->checkboxSync, SIGNAL(clicked(bool)), this, SIGNAL(midiSyncChanged(bool)));
144145
}
145146

146147
void NinjamPanel::handleBpiComboActication(const QString &newBpi)

src/Common/gui/NinjamPanel.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class NinjamPanel : public QFrame
7171
void accentsBeatsChanged(const QList<int> &);
7272
void hostSyncStateChanged(bool syncWithHost);
7373
void intervalShapeChanged(int newShape);
74+
void midiSyncChanged(bool syncOn);
7475

7576
protected:
7677
void changeEvent(QEvent *) override;

0 commit comments

Comments
 (0)