Skip to content

Commit 64a0dd7

Browse files
authored
Merge pull request #1032 from kushview/copilot/fix-midi-sync-bpm-update
[WIP] Fix MIDI clock sync to update BPM Faster
2 parents 244c004 + 85655e6 commit 64a0dd7

File tree

12 files changed

+685
-60
lines changed

12 files changed

+685
-60
lines changed

include/element/element.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ extern "C" {
1919
#ifdef _WIN32
2020
// windows exports
2121
#if defined(EL_SHARED_BUILD)
22-
#define EL_API __declspec(dllexport)
22+
#define EL_API __declspec (dllexport)
2323
#pragma warning(disable : 4251)
2424
#elif defined(EL_SHARED)
25-
#define EL_API __declspec(dllimport)
25+
#define EL_API __declspec (dllimport)
2626
#pragma warning(disable : 4251)
2727
#endif
28-
#define EL_PLUGIN_EXPORT EL_EXTERN __declspec(dllexport)
28+
#define EL_PLUGIN_EXPORT EL_EXTERN __declspec (dllexport)
2929
#else
3030
#if defined(EL_SHARED) || defined(EL_SHARED_BUILD)
3131
#define EL_API __attribute__ ((visibility ("default")))

include/element/porttype.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class PortType {
101101
inline bool operator!= (const ID& id) const { return (type != id); }
102102
inline bool operator== (const PortType& t) const { return (type == t.type); }
103103
inline bool operator!= (const PortType& t) const { return (type != t.type); }
104-
inline bool operator<(const PortType& t) const { return (type < t.type); }
104+
inline bool operator< (const PortType& t) const { return (type < t.type); }
105105

106106
inline operator int() const { return (int) this->type; }
107107

include/element/version.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class Version {
9595
inline bool operator== (const Version& o) const noexcept { return _hex == o._hex; }
9696
inline bool operator!= (const Version& o) const noexcept { return _hex != o._hex; }
9797
inline bool operator> (const Version& o) const noexcept { return _hex > o._hex; }
98-
inline bool operator<(const Version& o) const noexcept { return _hex < o._hex; }
98+
inline bool operator< (const Version& o) const noexcept { return _hex < o._hex; }
9999
inline bool operator>= (const Version& o) const noexcept { return _hex >= o._hex; }
100100
inline bool operator<= (const Version& o) const noexcept { return _hex <= o._hex; }
101101

src/delaylockedloop.hpp

Lines changed: 86 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,84 +4,136 @@
44
#pragma once
55

66
#include <cmath>
7+
#include <algorithm>
78

89
#ifndef M_PI
9-
#define M_PI ((float) 3.14159265358979323846)
10+
#define M_PI ((double) 3.14159265358979323846)
1011
#endif
1112
#ifndef M_SQRT2
12-
#define M_SQRT2 ((float) 1.41421356237309504880)
13+
#define M_SQRT2 ((double) 1.41421356237309504880)
1314
#endif
1415

1516
namespace element {
1617

18+
/** A Delay-Locked Loop for MIDI clock synchronization.
19+
*
20+
* This implementation uses an adaptive bandwidth approach for faster initial
21+
* convergence while maintaining stability once locked. The algorithm is based
22+
* on a second-order loop filter design.
23+
*/
1724
class DelayLockedLoop
1825
{
1926
public:
2027
/** Create a new DLL */
2128
inline DelayLockedLoop()
22-
: samplerate (44100.0),
23-
periodSize (1024.0),
24-
e2 (0),
25-
t0 (0),
26-
t1 (0),
27-
bandwidth (1.0f),
28-
frequency (0),
29-
omega (0),
30-
b (0),
31-
c (0)
3229
{
33-
reset (0.0, 1024.0, 44100.0);
34-
resetLPF();
30+
reset (0.0, 1.0 / 24.0, 1.0);
3531
}
3632

37-
/** Reset the DLL with a time, period and rate */
38-
inline void reset (double now, double period, double rate)
33+
/** Reset the DLL with an initial timestamp, expected period and update frequency.
34+
* @param now Initial timestamp
35+
* @param period Expected period between updates (in seconds)
36+
* @param freq Update frequency for bandwidth calculation
37+
*/
38+
inline void reset (double now, double period, double freq)
3939
{
40-
samplerate = rate;
41-
periodSize = period;
40+
expectedPeriod = period;
41+
frequency = freq;
4242

43-
// initialize the loop
44-
e2 = periodSize / samplerate;
43+
// Initialize timing state
44+
e2 = period;
4545
t0 = now;
4646
t1 = t0 + e2;
47+
48+
// Reset adaptive state
49+
updateCount = 0;
50+
locked = false;
51+
52+
// Start with higher bandwidth for faster initial convergence
53+
updateCoefficients (initialBandwidth);
4754
}
4855

49-
/** Update the DLL with the next timestamp */
56+
/** Update the DLL with the next timestamp.
57+
* @param time The timestamp of the incoming event
58+
*/
5059
inline void update (double time)
5160
{
5261
const double e = time - t1;
5362

63+
// Update timing estimates
5464
t0 = t1;
5565
t1 += b * e + e2;
5666
e2 += c * e;
67+
68+
// Adaptive bandwidth: transition from initial to locked bandwidth
69+
++updateCount;
70+
if (! locked && updateCount >= lockThreshold)
71+
{
72+
locked = true;
73+
updateCoefficients (lockedBandwidth);
74+
}
5775
}
5876

59-
/** Set the dll's parameters. Bandwidth / Frequency */
77+
/** Set the dll's bandwidth parameters.
78+
* @param newBandwidth Loop bandwidth
79+
* @param newFrequency Update frequency for normalization
80+
*/
6081
inline void setParams (double newBandwidth, double newFrequency)
6182
{
62-
bandwidth = newBandwidth;
6383
frequency = newFrequency;
64-
resetLPF();
84+
lockedBandwidth = newBandwidth;
85+
if (locked)
86+
updateCoefficients (newBandwidth);
6587
}
6688

67-
/** Return the difference in filtered time (t1 - t0) */
68-
inline double timeDiff()
89+
/** Return the filtered period estimate (t1 - t0) in seconds */
90+
inline double timeDiff() const
6991
{
7092
return (t1 - t0);
7193
}
7294

95+
/** Check if the DLL has achieved lock */
96+
inline bool isLocked() const
97+
{
98+
return locked;
99+
}
100+
101+
/** Get the current estimated period */
102+
inline double getPeriod() const
103+
{
104+
return e2;
105+
}
106+
73107
private:
74-
double samplerate, periodSize;
75-
double e2, t0, t1;
108+
// Timing state
109+
double e2 = 0; ///< Estimated period
110+
double t0 = 0; ///< Previous filtered timestamp
111+
double t1 = 0; ///< Current filtered timestamp
112+
113+
// Parameters
114+
double expectedPeriod = 1.0 / 24.0;
115+
double frequency = 1.0;
116+
117+
// Loop filter coefficients
118+
double b = 0;
119+
double c = 0;
120+
121+
// Adaptive bandwidth settings
122+
// Higher initial bandwidth = faster convergence but more jitter
123+
// Lower locked bandwidth = more stable but slower to track changes
124+
static constexpr double initialBandwidth = 0.5; // Fast convergence
125+
double lockedBandwidth = 0.1; // Stable tracking
126+
static constexpr int lockThreshold = 24; // Lock after ~1 beat
76127

77-
double bandwidth, frequency;
78-
double omega, b, c;
128+
int updateCount = 0;
129+
bool locked = false;
79130

80-
/** @internal Reset the LPF
81-
Called when bandwidth and frequency changes */
82-
inline void resetLPF()
131+
/** Update loop filter coefficients based on bandwidth */
132+
inline void updateCoefficients (double bandwidth)
83133
{
84-
omega = 2.0 * M_PI * bandwidth / frequency;
134+
// Second-order loop filter design
135+
// omega is the normalized angular frequency
136+
const double omega = 2.0 * M_PI * bandwidth / std::max (frequency, 1.0);
85137
b = M_SQRT2 * omega;
86138
c = omega * omega;
87139
}

src/engine/midiclock.cpp

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,61 @@
33

44
#include "engine/midiclock.hpp"
55

6+
#include <cmath>
7+
68
namespace element {
79

810
void MidiClock::process (const MidiMessage& msg)
911
{
1012
jassert (sampleRate > 0.0 && blockSize > 0);
1113
jassert (msg.isMidiClock() || msg.isSongPositionPointer());
1214

15+
const double timestamp = msg.getTimeStamp();
16+
1317
if (midiClockTicks <= 0)
1418
{
15-
dll.reset (msg.getTimeStamp(), (double) blockSize / sampleRate, 1.0);
16-
dll.setParams ((double) blockSize / sampleRate, 1.0);
19+
// Initialize the DLL with the first MIDI clock message.
20+
// Expected period at 120 BPM: 60/(120*24) = 0.02083 seconds per clock
21+
// We use a generic initial period estimate and let the DLL converge.
22+
const double initialPeriodEstimate = 60.0 / (120.0 * 24.0);
23+
dll.reset (timestamp, initialPeriodEstimate, 24.0);
1724
}
1825
else
1926
{
20-
dll.update (msg.getTimeStamp());
27+
dll.update (timestamp);
2128
}
2229

30+
// Notify listeners when signal is acquired
2331
if (midiClockTicks == syncPeriodTicks)
32+
{
2433
for (auto* listener : listeners)
2534
listener->midiClockSignalAcquired();
35+
}
2636

27-
if (midiClockTicks >= syncPeriodTicks && msg.getTimeStamp() - timeOfLastUpdate >= 1.0)
37+
// Update BPM more frequently for responsive sync
38+
if (midiClockTicks >= syncPeriodTicks)
2839
{
29-
lastKnownTimeDiff = dll.timeDiff();
30-
const double bpm = 60.0 / (lastKnownTimeDiff * 24.0);
31-
timeOfLastUpdate = msg.getTimeStamp();
40+
const double timeSinceUpdate = timestamp - timeOfLastUpdate;
41+
if (timeSinceUpdate >= bpmUpdateInterval)
42+
{
43+
lastKnownTimeDiff = dll.timeDiff();
44+
// MIDI clock has 24 pulses per quarter note (PPQN)
45+
const double bpm = 60.0 / (lastKnownTimeDiff * 24.0);
46+
47+
// Only update if BPM is in valid range and has changed significantly
48+
if (bpm >= 20.0 && bpm <= 999.0)
49+
{
50+
const double bpmDiff = std::abs (bpm - lastReportedBpm);
51+
if (bpmDiff >= bpmChangeThreshold || lastReportedBpm == 0.0)
52+
{
53+
lastReportedBpm = bpm;
54+
for (auto* listener : listeners)
55+
listener->midiClockTempoChanged (static_cast<float> (bpm));
56+
}
57+
}
3258

33-
if (bpm >= 20.0 && bpm <= 999.0)
34-
for (auto* listener : listeners)
35-
listener->midiClockTempoChanged (bpm);
59+
timeOfLastUpdate = timestamp;
60+
}
3661
}
3762

3863
++midiClockTicks;
@@ -44,6 +69,7 @@ void MidiClock::reset (const double sr, const int bs)
4469
blockSize = bs;
4570
timeOfLastUpdate = 0.0;
4671
lastKnownTimeDiff = 0.0;
72+
lastReportedBpm = 0.0;
4773
midiClockTicks = 0;
4874
}
4975

src/engine/midiclock.hpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,20 @@ class MidiClock
3737
DelayLockedLoop dll;
3838
double timeOfLastUpdate = 0.0;
3939
double lastKnownTimeDiff = 0.0;
40+
double lastReportedBpm = 0.0;
4041
int midiClockTicks = 0;
41-
int syncPeriodTicks = 48;
42-
[[maybe_unused]] double bpmUpdateSeconds = 1.0;
42+
43+
// Number of ticks required before signal is considered acquired
44+
// 24 ticks = 1 beat at MIDI standard 24 PPQN
45+
static constexpr int syncPeriodTicks = 24;
46+
47+
// Minimum interval between BPM updates (in seconds)
48+
// Shorter interval = more responsive but potentially more jittery
49+
static constexpr double bpmUpdateInterval = 0.25;
50+
51+
// Minimum BPM change required to trigger an update (hysteresis)
52+
// Prevents jittery updates when BPM is stable
53+
static constexpr double bpmChangeThreshold = 0.5;
4354

4455
Array<Listener*> listeners;
4556
};

src/filesystemwatcher.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,7 @@ class FileSystemWatcher::Impl
137137
CFArrayRef paths { nullptr };
138138
dispatch_queue_t queue { nullptr };
139139
FSEventStreamRef stream { nullptr };
140-
struct FSEventStreamContext context
141-
{
142-
};
140+
struct FSEventStreamContext context {};
143141
};
144142
#endif
145143

src/scripting/bindings.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ namespace element {
6565
namespace Lua {
6666

6767
//==============================================================================
68-
#if defined(EL_APPIMAGE)
68+
#if defined(ELEMENT_APPIMAGE)
6969
static File getAppImageLuaPath()
7070
{
7171
return File::getSpecialLocation (File::currentExecutableFile)
@@ -112,7 +112,7 @@ static String getApplicationLuaDir()
112112
static File getSystemLuaDir()
113113
{
114114
File dir;
115-
#if defined(EL_APPIMAGE)
115+
#if defined(ELEMENT_APPIMAGE)
116116
dir = getAppImageLuaPath().getFullPathName();
117117

118118
#elif defined(EL_LUADIR)

src/scripting/scriptmanager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ File ScriptManager::getSystemScriptsDir()
127127
{
128128
File dir;
129129

130-
#if defined(EL_APPIMAGE)
130+
#if defined(ELEMENT_APPIMAGE)
131131
dir = File::getSpecialLocation (File::currentExecutableFile)
132132
.getParentDirectory() // bin
133133
.getParentDirectory() // usr

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ add_test(NAME "GzipTests" COMMAND test_element --run_test=GzipTests)
3030
add_test(NAME "IONodeTests" COMMAND test_element --run_test=IONodeTests)
3131
add_test(NAME "LinearFadeTest" COMMAND test_element --run_test=LinearFadeTest)
3232
add_test(NAME "MidiChannelMapTest" COMMAND test_element --run_test=MidiChannelMapTest)
33+
add_test(NAME "MidiClockTest" COMMAND test_element --run_test=MidiClockTest)
3334
add_test(NAME "MidiProgramMapTests" COMMAND test_element --run_test=MidiProgramMapTests)
3435
add_test(NAME "NodeFactoryTests" COMMAND test_element --run_test=NodeFactoryTests)
3536
add_test(NAME "NodeObjectTests" COMMAND test_element --run_test=NodeObjectTests)

0 commit comments

Comments
 (0)