|
4 | 4 | #pragma once |
5 | 5 |
|
6 | 6 | #include <cmath> |
| 7 | +#include <algorithm> |
7 | 8 |
|
8 | 9 | #ifndef M_PI |
9 | | -#define M_PI ((float) 3.14159265358979323846) |
| 10 | +#define M_PI ((double) 3.14159265358979323846) |
10 | 11 | #endif |
11 | 12 | #ifndef M_SQRT2 |
12 | | -#define M_SQRT2 ((float) 1.41421356237309504880) |
| 13 | +#define M_SQRT2 ((double) 1.41421356237309504880) |
13 | 14 | #endif |
14 | 15 |
|
15 | 16 | namespace element { |
16 | 17 |
|
| 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 | + */ |
17 | 24 | class DelayLockedLoop |
18 | 25 | { |
19 | 26 | public: |
20 | 27 | /** Create a new DLL */ |
21 | 28 | 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) |
32 | 29 | { |
33 | | - reset (0.0, 1024.0, 44100.0); |
34 | | - resetLPF(); |
| 30 | + reset (0.0, 1.0 / 24.0, 1.0); |
35 | 31 | } |
36 | 32 |
|
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) |
39 | 39 | { |
40 | | - samplerate = rate; |
41 | | - periodSize = period; |
| 40 | + expectedPeriod = period; |
| 41 | + frequency = freq; |
42 | 42 |
|
43 | | - // initialize the loop |
44 | | - e2 = periodSize / samplerate; |
| 43 | + // Initialize timing state |
| 44 | + e2 = period; |
45 | 45 | t0 = now; |
46 | 46 | 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); |
47 | 54 | } |
48 | 55 |
|
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 | + */ |
50 | 59 | inline void update (double time) |
51 | 60 | { |
52 | 61 | const double e = time - t1; |
53 | 62 |
|
| 63 | + // Update timing estimates |
54 | 64 | t0 = t1; |
55 | 65 | t1 += b * e + e2; |
56 | 66 | 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 | + } |
57 | 75 | } |
58 | 76 |
|
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 | + */ |
60 | 81 | inline void setParams (double newBandwidth, double newFrequency) |
61 | 82 | { |
62 | | - bandwidth = newBandwidth; |
63 | 83 | frequency = newFrequency; |
64 | | - resetLPF(); |
| 84 | + lockedBandwidth = newBandwidth; |
| 85 | + if (locked) |
| 86 | + updateCoefficients (newBandwidth); |
65 | 87 | } |
66 | 88 |
|
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 |
69 | 91 | { |
70 | 92 | return (t1 - t0); |
71 | 93 | } |
72 | 94 |
|
| 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 | + |
73 | 107 | 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 |
76 | 127 |
|
77 | | - double bandwidth, frequency; |
78 | | - double omega, b, c; |
| 128 | + int updateCount = 0; |
| 129 | + bool locked = false; |
79 | 130 |
|
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) |
83 | 133 | { |
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); |
85 | 137 | b = M_SQRT2 * omega; |
86 | 138 | c = omega * omega; |
87 | 139 | } |
|
0 commit comments