-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathLtcInput.h
More file actions
496 lines (428 loc) · 20.2 KB
/
LtcInput.h
File metadata and controls
496 lines (428 loc) · 20.2 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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
// Super Timecode Converter
// Copyright (c) 2026 Fiverecords -- MIT License
// https://github.com/fiverecords/SuperTimecodeConverter
#pragma once
#include <JuceHeader.h>
#include "TimecodeCore.h"
#include <atomic>
#include <cmath>
#include <cstring>
class LtcInput : private juce::AudioIODeviceCallback
{
public:
LtcInput() = default;
~LtcInput() { stop(); }
//==============================================================================
// Start with explicit device type and name
// typeName: audio device type (e.g. "Windows Audio", "ASIO")
// deviceName: raw device name
// ltcChannel: channel index carrying LTC signal
// thruChannel: channel index to capture for passthrough (-1 = disabled)
// sampleRate: preferred sample rate (0 = device default)
// bufferSize: preferred buffer size (0 = device default)
//==============================================================================
bool start(const juce::String& typeName, const juce::String& devName,
int ltcChannel, int thruChannel = -1,
double sampleRate = 0, int bufferSize = 0)
{
stop();
selectedChannel.store(ltcChannel, std::memory_order_relaxed);
passthruChannel.store(thruChannel, std::memory_order_relaxed);
currentDeviceName = devName;
currentTypeName = typeName;
deviceManager.closeAudioDevice();
// Register all device types without opening anything
deviceManager.initialise(128, 0, nullptr, false);
// Switch to requested type WITHOUT auto-opening (false)
if (typeName.isNotEmpty())
deviceManager.setCurrentAudioDeviceType(typeName, false);
// Scan so the device name is recognised
if (auto* type = deviceManager.getCurrentDeviceTypeObject())
type->scanForDevices();
// Open the specific device -- single open call
auto setup = deviceManager.getAudioDeviceSetup();
setup.inputDeviceName = devName;
setup.outputDeviceName = "";
setup.useDefaultInputChannels = true;
setup.useDefaultOutputChannels = false;
if (sampleRate > 0) setup.sampleRate = sampleRate;
if (bufferSize > 0) setup.bufferSize = bufferSize;
auto err = deviceManager.setAudioDeviceSetup(setup, true);
if (err.isNotEmpty())
return false;
auto* device = deviceManager.getCurrentAudioDevice();
if (!device)
return false;
numChannelsAvailable = device->getActiveInputChannels().countNumberOfSetBits();
int selCh = selectedChannel.load(std::memory_order_relaxed);
int thruCh = passthruChannel.load(std::memory_order_relaxed);
if (selCh >= numChannelsAvailable)
selCh = 0;
if (thruCh >= numChannelsAvailable)
thruCh = -1;
// Prevent passthrough from using the same channel as LTC decode
if (thruCh >= 0 && thruCh == selCh)
thruCh = -1;
selectedChannel.store(selCh, std::memory_order_relaxed);
passthruChannel.store(thruCh, std::memory_order_relaxed);
currentSampleRate = device->getCurrentSampleRate();
currentBufferSize = device->getCurrentBufferSizeSamples();
// resetDecoder() and resetPassthruBuffer() are called by
// audioDeviceAboutToStart() when addAudioCallback triggers the device;
// only peak levels need explicit reset here since they're not part of
// the device-start callback.
ltcPeakLevel.store(0.0f, std::memory_order_relaxed);
thruPeakLevel.store(0.0f, std::memory_order_relaxed);
deviceManager.addAudioCallback(this);
isRunningFlag.store(true, std::memory_order_relaxed);
return true;
}
void stop()
{
if (isRunningFlag.load(std::memory_order_relaxed))
{
deviceManager.removeAudioCallback(this);
deviceManager.closeAudioDevice();
isRunningFlag.store(false, std::memory_order_relaxed);
}
}
//==============================================================================
bool getIsRunning() const { return isRunningFlag.load(std::memory_order_relaxed); }
juce::String getCurrentDeviceName() const { return currentDeviceName; }
juce::String getCurrentTypeName() const { return currentTypeName; }
int getSelectedChannel() const { return selectedChannel.load(std::memory_order_relaxed); }
int getPassthruChannel() const { return passthruChannel.load(std::memory_order_relaxed); }
int getChannelCount() const { return numChannelsAvailable; }
double getActualSampleRate() const { return currentSampleRate; }
int getActualBufferSize() const { return currentBufferSize; }
Timecode getCurrentTimecode() const
{
return unpackTimecode(packedTimecode.load(std::memory_order_relaxed));
}
FrameRate getDetectedFrameRate() const
{
return detectedFps.load(std::memory_order_relaxed);
}
bool isReceiving() const
{
auto now = juce::Time::getMillisecondCounterHiRes();
return (now - lastFrameTime.load(std::memory_order_relaxed)) < kSourceTimeoutMs;
}
//==============================================================================
// Independent gain controls
//==============================================================================
void setInputGain(float gain) { inputGain.store(juce::jlimit(0.0f, 2.0f, gain), std::memory_order_relaxed); }
float getInputGain() const { return inputGain.load(std::memory_order_relaxed); }
void setPassthruGain(float gain) { passthruGain.store(juce::jlimit(0.0f, 2.0f, gain), std::memory_order_relaxed); }
float getPassthruGain() const { return passthruGain.load(std::memory_order_relaxed); }
//==============================================================================
// Peak levels for metering (0.0 - 1.0+)
//==============================================================================
float getLtcPeakLevel() const { return ltcPeakLevel.load(std::memory_order_relaxed); }
float getThruPeakLevel() const { return thruPeakLevel.load(std::memory_order_relaxed); }
void resetPeakLevels() { ltcPeakLevel.store(0.0f, std::memory_order_relaxed); thruPeakLevel.store(0.0f, std::memory_order_relaxed); }
//==============================================================================
// Passthrough ring buffer
//==============================================================================
int readPassthruSamples(float* dest, int numSamples)
{
if (!passthruBuffer)
{
std::memset(dest, 0, sizeof(float) * (size_t)numSamples);
return 0;
}
uint32_t wp = passthruWritePos.load(std::memory_order_acquire);
uint32_t rp = passthruReadPos.load(std::memory_order_relaxed);
uint32_t available = wp - rp; // works correctly with unsigned wrap-around
int toRead = (int)juce::jmin((uint32_t)numSamples, available);
// Track underruns: if we can't supply all requested samples, it's an underrun
if (toRead < numSamples)
passthruUnderruns.fetch_add(1, std::memory_order_relaxed);
for (int i = 0; i < toRead; i++)
dest[i] = passthruBuffer[(rp + (uint32_t)i) & RING_MASK];
// Zero-fill any samples we couldn't supply (silence instead of old data)
for (int i = toRead; i < numSamples; i++)
dest[i] = 0.0f;
passthruReadPos.store(rp + (uint32_t)toRead, std::memory_order_release);
return toRead;
}
bool hasPassthruChannel() const { return passthruChannel.load(std::memory_order_relaxed) >= 0; }
uint32_t getPassthruUnderruns() const { return passthruUnderruns.load(std::memory_order_relaxed); }
uint32_t getPassthruOverruns() const { return passthruOverruns.load(std::memory_order_relaxed); }
void resetPassthruCounters() { passthruUnderruns.store(0, std::memory_order_relaxed); passthruOverruns.store(0, std::memory_order_relaxed); }
// Snap the read position to the current write position so the next reader
// starts from fresh data instead of consuming stale buffered samples.
// Call this just before starting AudioThru while LtcInput is already running.
void syncPassthruReadPosition()
{
passthruReadPos.store(passthruWritePos.load(std::memory_order_acquire),
std::memory_order_release);
}
private:
juce::AudioDeviceManager deviceManager;
juce::String currentDeviceName;
juce::String currentTypeName;
std::atomic<bool> isRunningFlag { false };
std::atomic<int> selectedChannel { 0 };
std::atomic<int> passthruChannel { -1 };
int numChannelsAvailable = 0;
double currentSampleRate = 48000.0;
int currentBufferSize = 512;
std::atomic<float> inputGain { 1.0f };
std::atomic<float> passthruGain { 1.0f };
std::atomic<float> ltcPeakLevel { 0.0f };
std::atomic<float> thruPeakLevel { 0.0f };
//==============================================================================
// Passthrough ring buffer (SPSC: single producer = audio callback,
// single consumer = AudioThru callback). Uses unsigned wrap-around
// arithmetic so writePos/readPos never need resetting during operation.
// Heap-allocated to keep class size reasonable (~128KB buffer).
//==============================================================================
static constexpr int RING_SIZE = 32768;
static constexpr uint32_t RING_MASK = RING_SIZE - 1;
std::unique_ptr<float[]> passthruBuffer;
std::atomic<uint32_t> passthruWritePos { 0 };
std::atomic<uint32_t> passthruReadPos { 0 };
std::atomic<uint32_t> passthruUnderruns { 0 };
std::atomic<uint32_t> passthruOverruns { 0 };
// Safe to call from audioDeviceAboutToStart(): JUCE guarantees no audio
// callbacks are active during device start, so no concurrent reader/writer.
void resetPassthruBuffer()
{
passthruWritePos.store(0, std::memory_order_relaxed);
passthruReadPos.store(0, std::memory_order_relaxed);
if (!passthruBuffer)
passthruBuffer = std::make_unique<float[]>(RING_SIZE);
std::memset(passthruBuffer.get(), 0, sizeof(float) * RING_SIZE);
}
std::atomic<uint64_t> packedTimecode { 0 };
std::atomic<FrameRate> detectedFps { FrameRate::FPS_25 };
std::atomic<double> lastFrameTime { 0.0 };
// LTC decoder state -- audio-callback-thread-only (no synchronisation needed)
bool signalHigh = false;
static constexpr float kHysteresisThreshold = 0.05f;
int64_t samplesSinceEdge = 0; // int64_t: prevents overflow at 192kHz without signal (~3h with int)
double bitPeriodEstimate = 0.0;
bool halfBitPending = false;
bool firstEdgeAfterReset = true;
uint64_t shiftRegLow = 0;
uint16_t shiftRegHigh = 0;
static constexpr uint16_t LTC_SYNC_WORD = 0xBFFC;
double samplesSinceLastSync = 0.0;
int consecutiveGoodFrames = 0;
void resetDecoder()
{
signalHigh = false;
samplesSinceEdge = 0;
halfBitPending = false;
firstEdgeAfterReset = true;
shiftRegLow = 0;
shiftRegHigh = 0;
samplesSinceLastSync = 0.0;
consecutiveGoodFrames = 0;
// Initial bit period estimate: use ~27fps midpoint (2160 transitions/sec)
// to minimize convergence time across all frame rates (24-30fps)
bitPeriodEstimate = currentSampleRate / 2160.0;
}
void pushBit(int bit)
{
shiftRegLow = (shiftRegLow >> 1) | (static_cast<uint64_t>(shiftRegHigh & 1) << 63);
shiftRegHigh = static_cast<uint16_t>((shiftRegHigh >> 1) | ((bit & 1) << 15));
if (shiftRegHigh == LTC_SYNC_WORD)
onSyncWordDetected();
}
void onSyncWordDetected()
{
uint64_t d = shiftRegLow;
int frameUnits = static_cast<int>( d & 0x0F);
int frameTens = static_cast<int>((d >> 8) & 0x03);
int secUnits = static_cast<int>((d >> 16) & 0x0F);
int secTens = static_cast<int>((d >> 24) & 0x07);
int minUnits = static_cast<int>((d >> 32) & 0x0F);
int minTens = static_cast<int>((d >> 40) & 0x07);
int hourUnits = static_cast<int>((d >> 48) & 0x0F);
int hourTens = static_cast<int>((d >> 56) & 0x03);
bool dropFrame = ((d >> 10) & 0x01) != 0;
int frames = frameTens * 10 + frameUnits;
int seconds = secTens * 10 + secUnits;
int minutes = minTens * 10 + minUnits;
int hours = hourTens * 10 + hourUnits;
if (hours > 23 || minutes > 59 || seconds > 59 || frames > 29)
{
consecutiveGoodFrames = 0;
samplesSinceLastSync = 0.0;
return;
}
// Only compute fps from inter-frame period if the gap is reasonable
// (< 2 seconds). Longer gaps mean signal was lost/corrupt and the
// measured period would be meaningless for rate detection.
if (samplesSinceLastSync > 0.0 && samplesSinceLastSync < currentSampleRate * 2.0)
{
double framePeriodSec = samplesSinceLastSync / currentSampleRate;
double measuredFps = 1.0 / framePeriodSec;
FrameRate detected = FrameRate::FPS_25;
// NOTE: LTC cannot distinguish 23.976fps from 24fps -- both use 80 bits
// per frame with no drop-frame flag. The ~0.1% rate difference is too
// small to measure reliably from frame-to-frame period. If 23.976 support
// is needed, the user must manually select the frame rate.
if (measuredFps < 24.5) detected = FrameRate::FPS_24;
else if (measuredFps < 27.0) detected = FrameRate::FPS_25;
else if (dropFrame) detected = FrameRate::FPS_2997;
else detected = FrameRate::FPS_30;
consecutiveGoodFrames++;
if (consecutiveGoodFrames >= 3)
detectedFps.store(detected, std::memory_order_relaxed);
}
else
{
consecutiveGoodFrames = 1;
}
samplesSinceLastSync = 0.0;
packedTimecode.store(packTimecode(hours, minutes, seconds, frames),
std::memory_order_relaxed);
lastFrameTime.store(juce::Time::getMillisecondCounterHiRes(), std::memory_order_relaxed);
}
void onEdgeDetected(int64_t intervalSamples)
{
if (firstEdgeAfterReset)
{
firstEdgeAfterReset = false;
return;
}
double interval = static_cast<double>(intervalSamples);
double halfBit = bitPeriodEstimate * 0.5;
double threshold = bitPeriodEstimate * 0.75;
if (interval < halfBit * 0.4 || interval > bitPeriodEstimate * 1.8)
{
halfBitPending = false;
return;
}
if (interval < threshold)
{
if (halfBitPending)
{
pushBit(1);
halfBitPending = false;
double measured = interval * 2.0;
bitPeriodEstimate = bitPeriodEstimate * 0.95 + measured * 0.05;
}
else
{
halfBitPending = true;
}
}
else
{
if (halfBitPending)
halfBitPending = false;
pushBit(0);
bitPeriodEstimate = bitPeriodEstimate * 0.95 + interval * 0.05;
}
}
//==============================================================================
void audioDeviceIOCallbackWithContext(const float* const* inputChannelData,
int numInputCh, float* const*, int,
int numSamples,
const juce::AudioIODeviceCallbackContext&) override
{
// --- Passthrough: capture channel into ring buffer ---
int pCh = passthruChannel.load(std::memory_order_relaxed);
if (pCh >= 0 && pCh < numInputCh
&& inputChannelData[pCh] && passthruBuffer)
{
const float* thruData = inputChannelData[pCh];
const float pGain = passthruGain.load(std::memory_order_relaxed);
uint32_t wp = passthruWritePos.load(std::memory_order_relaxed);
uint32_t rp = passthruReadPos.load(std::memory_order_acquire);
uint32_t used = wp - rp; // unsigned wrap-around gives correct count
uint32_t freeSlots = RING_SIZE - used; // includes the 1 reserved sentinel slot
float thruPeak = 0.0f;
// Reserve 1 sentinel slot to distinguish full from empty.
// Need freeSlots >= 2 because 1 is the sentinel -- only (freeSlots-1)
// are actually writable. Effective ring capacity is RING_SIZE-1
// (32767 samples ~ 682ms @48kHz), which is plenty for bridging
// the latency between input and AudioThru output callbacks.
int toWrite = (freeSlots >= 2)
? (int)juce::jmin((uint32_t)numSamples, freeSlots - 1)
: 0;
// Track overruns: if we can't write all samples, input is outpacing output
if (toWrite < numSamples)
passthruOverruns.fetch_add(1, std::memory_order_relaxed);
// Measure peak over ALL incoming samples (including those that won't
// fit in the ring buffer) so the meter reflects the true input level
// even during overruns. Write only the samples that fit.
for (int i = 0; i < numSamples; i++)
{
float s = thruData[i] * pGain;
float a = std::abs(s);
if (a > thruPeak) thruPeak = a;
if (i < toWrite)
passthruBuffer[(wp + (uint32_t)i) & RING_MASK] = s;
}
passthruWritePos.store(wp + (uint32_t)toWrite, std::memory_order_release);
thruPeakLevel.store(thruPeak, std::memory_order_relaxed);
}
// --- LTC decode on the selected channel ---
int sCh = selectedChannel.load(std::memory_order_relaxed);
if (numInputCh <= 0 || sCh >= numInputCh)
return;
const float* data = inputChannelData[sCh];
if (!data)
return;
const float gain = inputGain.load(std::memory_order_relaxed);
// Fixed threshold: the gain slider amplifies the signal before edge
// detection, so raising gain genuinely helps decode weak LTC signals.
// A fixed threshold keeps the hysteresis behaviour predictable while
// letting the user compensate for low-level inputs.
const float effectiveThreshold = kHysteresisThreshold;
float ltcPeak = 0.0f;
for (int i = 0; i < numSamples; ++i)
{
float sample = data[i] * gain;
float a = std::abs(sample);
if (a > ltcPeak) ltcPeak = a;
samplesSinceEdge++;
samplesSinceLastSync += 1.0;
bool edgeDetected = false;
if (signalHigh)
{
if (sample < -effectiveThreshold)
{
signalHigh = false;
edgeDetected = true;
}
}
else
{
if (sample > effectiveThreshold)
{
signalHigh = true;
edgeDetected = true;
}
}
if (edgeDetected)
{
onEdgeDetected(samplesSinceEdge);
samplesSinceEdge = 0;
}
}
ltcPeakLevel.store(ltcPeak, std::memory_order_relaxed);
}
void audioDeviceAboutToStart(juce::AudioIODevice* device) override
{
if (device)
{
currentSampleRate = device->getCurrentSampleRate();
currentBufferSize = device->getCurrentBufferSizeSamples();
numChannelsAvailable = device->getActiveInputChannels().countNumberOfSetBits();
}
resetDecoder();
resetPassthruBuffer();
}
void audioDeviceStopped() override
{
ltcPeakLevel.store(0.0f, std::memory_order_relaxed);
thruPeakLevel.store(0.0f, std::memory_order_relaxed);
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LtcInput)
};