-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathMtcInput.h
More file actions
331 lines (274 loc) · 11.7 KB
/
MtcInput.h
File metadata and controls
331 lines (274 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// Super Timecode Converter
// Copyright (c) 2026 Fiverecords -- MIT License
// https://github.com/fiverecords/SuperTimecodeConverter
#pragma once
#include <JuceHeader.h>
#include "TimecodeCore.h"
#include <atomic>
class MtcInput : public juce::MidiInputCallback
{
public:
MtcInput() = default;
~MtcInput() override
{
stop();
}
//==============================================================================
juce::StringArray getDeviceNames() const
{
juce::StringArray names;
for (auto& d : availableDevices)
names.add(d.name);
return names;
}
int getDeviceCount() const { return availableDevices.size(); }
juce::String getCurrentDeviceName() const
{
if (currentDeviceIndex >= 0 && currentDeviceIndex < availableDevices.size())
return availableDevices[currentDeviceIndex].name;
return "None";
}
void refreshDeviceList()
{
availableDevices = juce::MidiInput::getAvailableDevices();
}
//==============================================================================
bool start(int deviceIndex)
{
stop();
if (deviceIndex < 0 || deviceIndex >= availableDevices.size())
return false;
auto device = juce::MidiInput::openDevice(availableDevices[deviceIndex].identifier, this);
if (device != nullptr)
{
midiInput = std::move(device);
midiInput->start();
currentDeviceIndex = deviceIndex;
isRunningFlag.store(true, std::memory_order_relaxed);
resetState();
return true;
}
return false;
}
void stop()
{
isRunningFlag.store(false, std::memory_order_release);
if (midiInput != nullptr)
{
// Do NOT call midiInput->stop() or destroy it here. On macOS,
// CoreMIDI's MIDI thread may be mid-callback inside JUCE's UMP
// dispatcher. Both stop() and ~MidiInput() can deadlock or crash.
//
// Move the device to a retirement list. It stays alive until
// drainRetiredDevices() is called from the message thread timer
// (16ms later), by which time any in-flight callback has returned.
retiredDevices.push_back(std::move(midiInput));
}
currentDeviceIndex = -1;
}
/// Call periodically from the message thread (e.g. timerCallback at 60Hz)
/// to safely destroy MidiInput devices that were retired by stop().
/// By the time this runs (~16ms after stop()), CoreMIDI callbacks have
/// finished and the destructors are safe to call.
void drainRetiredDevices()
{
if (!retiredDevices.empty())
retiredDevices.clear();
}
bool getIsRunning() const { return isRunningFlag.load(std::memory_order_relaxed); }
//==============================================================================
// True if QF messages are actively arriving
bool isReceiving() const
{
if (!synced.load(std::memory_order_acquire))
return false;
double now = juce::Time::getMillisecondCounterHiRes();
double elapsed = now - lastQfReceiveTime.load(std::memory_order_relaxed);
// MTC at 24fps sends QF every ~10.4ms, at 30fps ~8.3ms
return elapsed < kSourceTimeoutMs;
}
Timecode getCurrentTimecode() const
{
if (!synced.load(std::memory_order_acquire))
return Timecode();
if (!isReceiving())
{
const juce::SpinLock::ScopedLockType lock(tcLock);
return lastSyncTimecode; // Frozen
}
Timecode syncTc;
double syncMs;
FrameRate fps;
{
const juce::SpinLock::ScopedLockType lock(tcLock);
syncTc = lastSyncTimecode;
syncMs = syncTimeMs;
fps = detectedFps;
}
double now = juce::Time::getMillisecondCounterHiRes();
double elapsed = now - syncMs;
if (elapsed < 0.0)
return syncTc;
double fpsDouble = frameRateToDouble(fps);
int maxFrames = frameRateToInt(fps);
double msPerFrame = 1000.0 / fpsDouble;
// Linear interpolation from last sync point.
// NOTE: for 29.97 DF, this uses simple frame counting (maxFrames per second)
// rather than true DF-aware counting. The DF correction at the end patches
// any landing on skipped frame numbers 0-1. This is exact for the typical
// interpolation range (a few dozen frames between QF syncs), because DF skips
// only occur at minute boundaries which are always >1798 frames apart.
int extraFrames = (int)(elapsed / msPerFrame);
int64_t syncTotal = (int64_t)syncTc.hours * 3600 * maxFrames
+ (int64_t)syncTc.minutes * 60 * maxFrames
+ (int64_t)syncTc.seconds * maxFrames
+ (int64_t)syncTc.frames;
int64_t currentTotal = syncTotal + extraFrames;
// Wrap at 24h so interpolation across midnight stays valid
int64_t dayFrames = (int64_t)24 * 3600 * maxFrames;
currentTotal = ((currentTotal % dayFrames) + dayFrames) % dayFrames;
Timecode result;
result.frames = (int)(currentTotal % maxFrames);
result.seconds = (int)((currentTotal / maxFrames) % 60);
result.minutes = (int)((currentTotal / (maxFrames * 60)) % 60);
result.hours = (int)((currentTotal / (maxFrames * 3600)) % 24);
// Drop-frame correction: interpolation may land on frames 0/1 at the
// start of a non-10th minute -- these frame numbers don't exist in DF
if (fps == FrameRate::FPS_2997
&& result.frames < 2
&& result.seconds == 0
&& (result.minutes % 10) != 0)
{
result.frames = 2;
}
return result;
}
FrameRate getDetectedFrameRate() const
{
const juce::SpinLock::ScopedLockType lock(tcLock);
return detectedFps;
}
//==============================================================================
void handleIncomingMidiMessage(juce::MidiInput*, const juce::MidiMessage& message) override
{
// Guard: after stop(), the device may still deliver a queued message
// before CoreMIDI fully disconnects. Ignore it.
if (!isRunningFlag.load(std::memory_order_acquire)) return;
auto rawData = message.getRawData();
int rawSize = message.getRawDataSize();
if (rawSize >= 2 && rawData[0] == 0xF1)
{
lastQfReceiveTime.store(juce::Time::getMillisecondCounterHiRes(), std::memory_order_relaxed);
int dataByte = rawData[1];
int index = (dataByte >> 4) & 0x07; // 0-7 guaranteed by mask
int value = dataByte & 0x0F;
mtcData[index] = value;
if (index == 7)
reconstructAndSync();
}
else if (message.isSysEx())
{
auto* sysex = message.getSysExData();
int sysexSize = message.getSysExDataSize();
if (sysexSize >= 8 &&
sysex[0] == 0x7F && // Universal Real Time
// sysex[1] = device ID (0x00-0x7F, accept any)
sysex[2] == 0x01 && sysex[3] == 0x01) // MTC Full Frame
{
lastQfReceiveTime.store(juce::Time::getMillisecondCounterHiRes(), std::memory_order_relaxed);
int hr = sysex[4];
int mn = sysex[5];
int sc = sysex[6];
int fr = sysex[7];
int rateCode = (hr >> 5) & 0x03;
hr &= 0x1F;
{
const juce::SpinLock::ScopedLockType lock(tcLock);
updateDetectedFps(rateCode);
lastSyncTimecode.hours = hr;
lastSyncTimecode.minutes = mn;
lastSyncTimecode.seconds = sc;
lastSyncTimecode.frames = fr;
syncTimeMs = juce::Time::getMillisecondCounterHiRes();
}
synced.store(true, std::memory_order_release);
}
}
}
private:
void reconstructAndSync()
{
int frames = mtcData[0] | (mtcData[1] << 4);
int seconds = mtcData[2] | (mtcData[3] << 4);
int minutes = mtcData[4] | (mtcData[5] << 4);
int hours = mtcData[6] | ((mtcData[7] & 0x01) << 4);
int rateCode = (mtcData[7] >> 1) & 0x03;
{
const juce::SpinLock::ScopedLockType lock(tcLock);
updateDetectedFps(rateCode);
int maxFrames = frameRateToInt(detectedFps);
// MTC quarter-frame messages describe the timecode from 2 frames
// prior (8 QFs x 1/4 frame = 2 frames of latency). Adding 2 compensates
// so the displayed timecode matches the current position.
// NOTE: this compensation assumes forward playback. Reverse or locate
// operations may briefly show a +/-4 frame discrepancy until the next
// full 8-QF cycle completes.
int64_t totalFrames = (int64_t)hours * 3600 * maxFrames
+ (int64_t)minutes * 60 * maxFrames
+ (int64_t)seconds * maxFrames
+ (int64_t)frames
+ 2;
lastSyncTimecode.frames = (int)(totalFrames % maxFrames);
lastSyncTimecode.seconds = (int)((totalFrames / maxFrames) % 60);
lastSyncTimecode.minutes = (int)((totalFrames / (maxFrames * 60)) % 60);
lastSyncTimecode.hours = (int)((totalFrames / (maxFrames * 3600)) % 24);
syncTimeMs = juce::Time::getMillisecondCounterHiRes();
}
synced.store(true, std::memory_order_release);
}
void updateDetectedFps(int rateCode)
{
switch (rateCode)
{
case 0:
// MTC rate code 0 means "24fps". SMPTE MTC has no code for
// 23.976, so if the user has selected FPS_2398 we preserve
// it rather than silently overwriting with FPS_24.
if (detectedFps != FrameRate::FPS_2398)
detectedFps = FrameRate::FPS_24;
break;
case 1: detectedFps = FrameRate::FPS_25; break;
case 2: detectedFps = FrameRate::FPS_2997; break;
case 3: detectedFps = FrameRate::FPS_30; break;
default: break; // Unknown rate code: keep previous value
}
}
void resetState()
{
for (int i = 0; i < 8; i++)
mtcData[i] = 0;
synced.store(false, std::memory_order_relaxed);
{
const juce::SpinLock::ScopedLockType lock(tcLock);
syncTimeMs = 0.0;
lastSyncTimecode = Timecode();
}
lastQfReceiveTime.store(0.0, std::memory_order_relaxed);
}
std::unique_ptr<juce::MidiInput> midiInput;
std::vector<std::unique_ptr<juce::MidiInput>> retiredDevices; // deferred destruction (see stop())
juce::Array<juce::MidiDeviceInfo> availableDevices;
int currentDeviceIndex = -1;
std::atomic<bool> isRunningFlag { false };
// Quarter-frame accumulator -- MIDI-callback-thread-only
int mtcData[8] = {};
// Protected by tcLock (written from MIDI thread, read from UI thread)
mutable juce::SpinLock tcLock;
Timecode lastSyncTimecode;
double syncTimeMs = 0.0;
FrameRate detectedFps = FrameRate::FPS_25;
// Atomic cross-thread fields
std::atomic<double> lastQfReceiveTime { 0.0 };
std::atomic<bool> synced { false };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MtcInput)
};