Repository navigation
Expand file tree
/
Copy pathTimecodeEngine.h
More file actions
6168 lines (5649 loc) · 302 KB
/
Copy pathTimecodeEngine.h
File metadata and controls
6168 lines (5649 loc) · 302 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
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Super Timecode Converter
// Copyright (c) 2026 Fiverecords -- MIT License
// https://github.com/fiverecords/SuperTimecodeConverter
#pragma once
#include <JuceHeader.h>
#include "TimecodeCore.h"
#include "MtcInput.h"
#include "MtcOutput.h"
#include "ArtnetInput.h"
#include "ArtnetOutput.h"
#include "LANetTimecodeInput.h"
#include "LANetTimecodeOutput.h"
#include "LtcInput.h"
#include "LtcOutput.h"
#include "ProDJLinkInput.h"
#include "StageLinQInput.h"
#include "OnAirFollow.h"
#include "HippotizerInput.h"
#include "HippotizerOutput.h"
#include "WinampInput.h"
#include <cmath>
#include "DbServerClient.h"
#include "TriggerOutput.h"
#include "LinkBridge.h"
#include "AudioThru.h"
#include "AudioBpmInput.h"
#include "GeneratorAudioPlayer.h"
#include "AppSettings.h"
#include "MixerMap.h"
#include <memory>
//==============================================================================
// TimecodeEngine -- one independent routing pipeline
//
// Each engine owns: 1 input source -> N output destinations.
// AudioThru is only available on the primary engine (index 0).
//==============================================================================
// All public methods of TimecodeEngine are designed to be called exclusively
// from the JUCE message thread, which also runs tick(); the engine's own
// state needs no lock. The protocol handlers run on their own threads
// (MIDI, network, audio) and publish what the engine reads through atomics,
// sequence locks (LTC, Art-Net and LA-Net in, the Pro DJ Link position) or
// short locks of their own, taken by the getters the engine calls (e.g.
// MtcInput's spin lock, WinampInput's position anchor, HippotizerInput's
// channel lock, a Pro DJ Link player's identity, DbServerClient's cache,
// StageLinQ's strings under a mutex).
inline constexpr int kPrimaryEngineIndex = 0;
inline constexpr int kMaxEngines = 8;
class TimecodeEngine
{
public:
enum class InputSource { MTC, ArtNet, SystemTime, LTC, ProDJLink, StageLinQ, Hippotizer, Winamp, LANetTC };
//--------------------------------------------------------------------------
explicit TimecodeEngine(int index, const juce::String& name = {})
: engineIndex(index),
engineName(name.isEmpty() ? ("ENGINE " + juce::String(index + 1)) : name)
{
std::fill(std::begin(lastSentMixer), std::end(lastSentMixer), -1); lastMixerPktCount = 0;
// Named in "IN USE BY <name>" when another engine's MTC is refused on
// the port this engine streams on (DESIGN D32).
mtcOutput.setOwnerName(engineName);
// Only the primary engine (index 0) gets AudioThru
if (index == kPrimaryEngineIndex)
audioThru = std::make_unique<AudioThru>();
// Re-seek the audio transport to the engine's current playhead
// after every settled load, unless the generator is Stopped.
// While Playing: attachReaderToTransport() sets
// transport.setPosition(0) the moment the source is wired, but the
// engine has been ticking forward for the duration of the load --
// the audio played "late" until the operator paused and played;
// the seek also starts the transport. While Paused it only
// positions the transport (seekSeconds does not start a paused
// player): a click on the timeline while a load is pending turns
// Stopped into Paused, and its own seek was dropped (AUDIT LTC-4),
// and a play from Paused does not seek, so the audio started at 0
// in the new file, or at the old position, while the timecode ran
// from the click. Stopped needs nothing: a play from Stopped
// seeks first (generatorPlay). Captured by juce::WeakReference so
// a load completing after this engine has been destroyed (engines
// can be removed at runtime) is a no-op rather than a
// use-after-free. Runs on the message thread (callAsync
// dispatch), same context that owns genState and genCurrentMs, so
// no atomics or locks needed.
//
// First the player's own loop is settled against the timecode loop
// (#24): the request could only guess it, the file's length being
// unknown until now (syncGeneratorPlayerLooping).
generatorAudioPlayer.onLoadCompleted =
[weak = juce::WeakReference<TimecodeEngine>(this)]()
{
auto* self = weak.get();
if (self == nullptr) return;
self->syncGeneratorPlayerLooping();
if (self->genState == GeneratorState::Stopped) return;
const double audioPosSec = juce::jmax(0.0,
(self->genCurrentMs - self->genStartMs) / 1000.0);
self->generatorAudioPlayer.seekSeconds(audioPosSec);
};
}
~TimecodeEngine()
{
// Stop MIDI clock timer first (runs on HighResolutionTimer thread)
setMidiClockEnabled(false);
triggerOutput.stopMidi();
triggerOutput.disconnectOsc();
// Shutdown order: outputs first, then inputs
stopMtcOutput();
stopArtnetOutput();
stopLANetTCOutput();
stopLtcOutput();
stopHippotizerOutput();
stopThruOutput();
stopMtcInput();
stopArtnetInput();
stopLANetTCInput();
stopLtcInput();
stopHippotizerInput();
stopWinampInput();
stopAudioBpm();
// ProDJLink is shared -- not stopped per-engine
// StageLinQ is shared -- not stopped per-engine
}
//==========================================================================
// Identity
//==========================================================================
int getIndex() const { return engineIndex; }
juce::String getName() const { return engineName; }
void setName(const juce::String& name) { engineName = name; mtcOutput.setOwnerName(name); }
bool isPrimary() const { return engineIndex == kPrimaryEngineIndex; }
// Called after engine deletion to fix indices so isPrimary() stays correct
// and AudioThru is created for the new primary engine if needed.
// NOTE: The caller (MainComponent::removeEngine) is responsible for
// restarting AudioThru on the new primary engine after reindexing,
// because that requires UI state (device combos) that the engine doesn't own.
void reindex(int newIndex)
{
// If we were the primary engine and are being moved away, destroy AudioThru
// to avoid a stale handler referencing a deleted LtcInput.
if (engineIndex == kPrimaryEngineIndex && newIndex != kPrimaryEngineIndex)
{
stopThruOutput();
audioThru.reset();
outputThruEnabled = false;
}
engineIndex = newIndex;
// Create AudioThru if we just became the primary engine
if (newIndex == kPrimaryEngineIndex && !audioThru)
audioThru = std::make_unique<AudioThru>();
}
//==========================================================================
// Input source
//==========================================================================
InputSource getActiveInput() const { return activeInput; }
FrameRate getCurrentFps() const { return currentFps; }
/// Message thread, like the tick() that writes it. (A spin lock taken
/// here and nowhere else guarded nothing and is gone, AUDIT ENG-16.)
Timecode getCurrentTimecode() const { return currentTimecode; }
bool isSourceActive() const { return sourceActive; }
bool getUserOverrodeLtcFps() const { return userOverrodeLtcFps; }
void setInputSource(InputSource source)
{
// Stop current input
switch (activeInput)
{
case InputSource::MTC: stopMtcInput(); break;
case InputSource::ArtNet: stopArtnetInput(); break;
case InputSource::LTC: stopLtcInput(); break;
case InputSource::ProDJLink: stopProDJLinkInput(); break;
case InputSource::StageLinQ: stopStageLinQInput(); break;
case InputSource::Hippotizer: stopHippotizerInput(); break;
case InputSource::Winamp: stopWinampInput(); break;
case InputSource::LANetTC: stopLANetTCInput(); break;
default: break;
}
userOverrodeLtcFps = false;
mtcParkedValid = false;
forwardParkedLocate = false;
activeInput = source;
sourceActive = false;
// Switching the input source is an explicit user transition; any
// pending natural-EOF marker from the Generator path is no longer
// relevant and must not survive the round-trip.
genEndedAtEof = false;
// Releasing the Generator audio output device when the engine
// switches to a non-Generator input is important for shows where
// multiple engines might want to share the same physical output.
// Without this, an engine that previously had AUDIO PLAYBACK on
// would keep its device open even after the user switched its
// input to MTC / LTC / etc., effectively locking that device out
// of any other engine's selector even though no playback is or
// ever could be happening on this engine again.
//
// The toggle button state and persisted settings are NOT touched:
// if the user switches the input back to the Generator later, the
// device opens again exactly as it was, transparent to them.
if (source != InputSource::SystemTime && generatorAudioPlayer.isDeviceOpen())
generatorAudioPlayer.closeDevice();
// A millisecond source cannot have rate conversion (see
// isMillisecondSource): drop it, and render at the user's chosen
// output rate, which is the only rate such a source has.
if (isMillisecondSource(source))
{
fpsConvertEnabled = false;
currentFps = outputFps;
setOutputFrameRate(outputFps);
}
// Reset the TrackMap cache on every source change: the track, its
// offset and its BPM multiplier belong to the source that found
// them. It used to survive a switch to another DJ source or to
// Winamp, so Winamp without a title went on applying the previous
// DJ track's offset (AUDIT ENG-13).
trackMapped = false;
cachedTrackId = 0;
cachedLoadedPlayer = cachedLoadedSlot = 0;
cachedMediaChanges = 0;
cachedOffH = cachedOffM = cachedOffS = cachedOffF = 0;
cachedBpmMultiplier = 0;
bpmPlayerOverride = kBpmNoOverride;
cachedTrackArtist.clear();
cachedTrackTitle.clear();
cachedTrackDurationSec = 0;
dbRequestIp.clear();
// Disable Link to avoid publishing stale tempo on the network
// (unless audio BPM is active -- it will keep feeding Link); the DJ
// sources and Winamp keep it.
if (source != InputSource::ProDJLink && source != InputSource::StageLinQ
&& source != InputSource::Winamp && !audioBpmEnabled)
linkBridge.setEnabled(false);
// The mixer forward's DMX frame and its send-once memory belong to
// the source that filled them (AUDIT ENG-10): after StageLinQ, Pro DJ
// Link went on sending the Denon faders every 100 ms in the channels
// it did not write. The next source starts from an empty frame and
// sends every value again.
resetMixerForward();
// Clear armed cues whenever the input source changes. The list
// was populated by either a TrackMap entry (ProDJLink / StageLinQ)
// or a Generator preset (SystemTime); leaving any of them, the old
// entries are no longer valid and would fire spuriously against
// unrelated playhead values. MainComponent will repopulate via
// setGeneratorCuePoints() when the new source is SystemTime with
// a preset selected.
armedCues.clear();
rawGeneratorCues.clear();
lastCueCheckMs = cueFlagsAtMs = cueStopRawMs = 0;
cueArmPending = cueWasPlaying = cueRelocated = cueFlagsAtIncl = false;
// Note: actual start is deferred to the caller (MainComponent),
// which gathers device params from UI before calling startXxxInput().
if (source == InputSource::SystemTime)
{
sourceActive = genClockMode; // clock mode: always active; transport mode: wait for Play
genLastTickTime = juce::Time::getMillisecondCounterHiRes(); // prevent time jump
clockFollower.reset(); // clock mode starts exactly on the wall clock
}
}
void setFrameRate(FrameRate fps)
{
const bool fpsChanged = (currentFps != fps);
currentFps = fps;
FrameRate outRate = getEffectiveOutputFps();
mtcOutput.setFrameRate(outRate);
artnetOutput.setFrameRate(outRate);
laNetTCOutput.setFrameRate(outRate);
ltcOutput.setFrameRate(outRate);
hippotizerOutput.setFrameRate(outRate);
// Re-derive Generator armed cues with the new fps so the frame
// component of each cue's TC maps to the right absolute-ms. Only
// matters when there is an active Generator preset; the TrackMap
// armed cues use ms directly and don't need rearming here.
if (fpsChanged && ! rawGeneratorCues.empty())
rearmGeneratorCues();
}
void setUserOverrodeLtcFps(bool v) { userOverrodeLtcFps = v; }
//==========================================================================
// FPS conversion
//==========================================================================
bool isFpsConvertEnabled() const { return fpsConvertEnabled; }
FrameRate getOutputFps() const { return outputFps; }
Timecode getOutputTimecode() const { return outputTimecode; }
/// LTC output holes (#19) are counted on the audio thread; here, once per
/// tick on the message thread, each new one is written to ltc_gaps.log
/// next to settings.json: when it happened, on which engine and device,
/// how long the callback was late and what the device period was. A
/// logic-analyser capture can then be lined up with STC's own view of
/// the event -- @mungewell asked for exactly this. Nothing is written
/// while no gaps occur. The file is bounded (AUDIT LTC-5, BENCH B32):
/// see appendLtcGapLogLine.
void logLtcOutputGaps()
{
const auto stamp = [] {
const auto now = juce::Time::getCurrentTime();
return now.formatted("%Y-%m-%d %H:%M:%S") + "." + juce::String(now.getMilliseconds()).paddedLeft('0', 3);
};
const int gaps = ltcOutput.getOutputGapCount();
if (gaps != lastLoggedGapCount)
{
lastLoggedGapCount = gaps;
juce::String line = stamp()
+ " " + engineName
+ " " + ltcOutput.getCurrentDeviceName()
+ " gap " + juce::String(ltcOutput.getLastGapMs(), 1) + " ms"
+ " period " + juce::String(ltcOutput.getActualBufferSize() * 1000.0
/ juce::jmax(1.0, ltcOutput.getActualSampleRate()), 1) + " ms"
+ (ltcOutput.getLastGapReseeded() ? " hole, re-seeded" : " late, absorbed")
+ " total " + juce::String(gaps) + "\n";
appendLtcGapLogLine(line);
}
// Value corrections and re-seeds (DESIGN D30), same file, so a capture of a
// skipped or repeated frame can be lined up with STC's own view.
const int events = ltcOutput.getTrackEventCount();
if (events != lastLoggedTrackCount)
{
lastLoggedTrackCount = events;
const char* what = "?";
switch (ltcOutput.getLastTrackKind())
{
case 1: what = "ahead or held: repeated a frame"; break;
case 2: what = "behind: skipped a frame"; break;
case 3: what = "seek: snapped"; break;
case 4: what = "re-seed after hole"; break;
case 5: what = "re-seed after snap"; break;
}
juce::String line = stamp()
+ " " + engineName
+ " track " + what
+ " d " + juce::String((int) ltcOutput.getLastTrackD())
+ " next " + ltcOutput.getLastTrackNext().toString()
+ " ref " + ltcOutput.getLastTrackRef().toString()
+ " total " + juce::String(events) + "\n";
appendLtcGapLogLine(line);
}
}
private:
/// One line into ltc_gaps.log (logLtcOutputGaps), the same text as
/// always. Every engine writes the same file, so its state is shared:
/// message thread only (the audio thread only counts). The path is
/// worked out once -- AppSettings::getSettingsFile() creates the folder
/// on every call -- and the size is followed from the bytes written.
/// Past kLtcGapLogMaxBytes the file moves to ltc_gaps.log.1, replacing
/// the previous one, and a new one starts: the two hold about twice that
/// at most. The "late, absorbed" lines BENCH B32 calls normal used to
/// make it grow without bound on every machine (AUDIT LTC-5). If the
/// move fails (the .1 held open elsewhere), writing goes on and the move
/// is tried again at the next line.
static void appendLtcGapLogLine(const juce::String& line)
{
struct GapLog { juce::File file; juce::int64 bytes = -1; };
static GapLog log;
if (log.bytes < 0)
{
log.file = AppSettings::getSettingsFile().getSiblingFile("ltc_gaps.log");
log.bytes = log.file.getSize(); // 0 when there is none yet
}
if (log.bytes >= kLtcGapLogMaxBytes)
{
log.file.moveFileTo(log.file.getSiblingFile("ltc_gaps.log.1"));
log.bytes = log.file.getSize(); // 0 unless the move failed
}
if (log.file.appendText(line))
log.bytes += (juce::int64) line.getNumBytesAsUTF8() + 1; // appendText writes the '\n' as "\r\n"
}
static constexpr juce::int64 kLtcGapLogMaxBytes = 5 * 1024 * 1024;
public:
/// Playback speed of the source as a ratio (1.0 = nominal), for
/// protocols that carry it (TCNet layer speed). DJ sources: the PLL's
/// pitch; everything else runs at 1.0. 0.0 while frozen or stopped.
double getSourceSpeedRatio() const
{
if (activeInput == InputSource::ProDJLink || activeInput == InputSource::StageLinQ)
return (activeInput == InputSource::ProDJLink && pdlTcFrozen) ? 0.0 : pll.pitch;
return sourceActive ? 1.0 : 0.0;
}
/// Track position in ms of the followed deck, read from the input as it
/// stands (UI cursor, cue-point editor, beat synthesis, TCNet layer
/// position). Despite the name, Pro DJ Link and StageLinQ give the
/// latest packet's position (on an NXS2 the input's beat-derived
/// estimate, not the engine's grid position), without the engine's
/// interpolation and without the Track Map offset; Winamp gives its
/// position as polled and interpolated by WinampInput. 0 for every
/// other source. So TCNet carries this raw position next to an
/// interpolated, offset timecode (AUDIT ENG-14). Message thread.
uint32_t getSmoothedPlayheadMs() const
{
if (activeInput == InputSource::StageLinQ && sharedStageLinQ != nullptr)
{
int ep = getEffectivePlayer();
return (ep >= 1) ? sharedStageLinQ->getPlayheadMs(ep) : 0;
}
if (activeInput == InputSource::Winamp)
{
int32_t p = winampInput.getPositionMs();
return p > 0 ? (uint32_t)p : 0;
}
if (activeInput != InputSource::ProDJLink) return 0; // non-DJ sources use tcToMs in TCNet
if (sharedProDJLink == nullptr) return 0;
int ep = getEffectivePlayer();
if (ep < 1) return 0;
return sharedProDJLink->getPlayheadMs(ep);
}
/// Play position as 0.0-1.0 ratio (for waveform cursor), from the same
/// raw input position as getSmoothedPlayheadMs (AUDIT ENG-14).
/// For Winamp this is position / duration as reported by the player.
/// While the duration is still being decoded (very first ~1 s of a
/// freshly loaded VBR file) the ratio is held at 0 to avoid jumping
/// the cursor to a meaningless value.
float getSmoothedPlayPositionRatio() const
{
if (activeInput == InputSource::StageLinQ && sharedStageLinQ != nullptr)
{
int ep = getEffectivePlayer();
return (ep >= 1) ? sharedStageLinQ->getPlayPositionRatio(ep) : 0.0f;
}
if (activeInput == InputSource::Winamp)
{
int32_t posMs = winampInput.getPositionMs();
int32_t durSec = winampInput.getDurationSec();
if (durSec <= 0 || posMs <= 0) return 0.0f;
float r = (float)posMs / (float)(durSec * 1000);
return juce::jlimit(0.0f, 1.0f, r);
}
if (activeInput != InputSource::ProDJLink) return 0.0f;
if (sharedProDJLink == nullptr) return 0.0f;
int ep = getEffectivePlayer();
if (ep < 1) return 0.0f;
return sharedProDJLink->getPlayPositionRatio(ep);
}
FrameRate getEffectiveOutputFps() const
{
return fpsConvertEnabled ? outputFps : currentFps;
}
void setFpsConvertEnabled(bool enabled)
{
if (enabled && isMillisecondSource(activeInput))
enabled = false; // meaningless for a ms source; never allow the invariant to break
fpsConvertEnabled = enabled;
if (!enabled)
{
outputFps = currentFps;
setOutputFrameRate(currentFps);
}
}
/// Two output channel selections clash when they touch the same channel;
/// -1 means the stereo pair (channels 0 and 1).
static bool channelsOverlap(int a, int b)
{
auto touches = [](int sel, int ch) { return sel == -1 ? (ch == 0 || ch == 1) : sel == ch; };
if (a == -1 && b == -1) return true;
if (a == -1) return touches(a, b);
if (b == -1) return touches(b, a);
return a == b;
}
/// Sources that deliver a millisecond position rather than frames: the
/// user's output rate IS their rate, so rate conversion has no meaning
/// for them and currentFps must always equal outputFps. The UI hides
/// FPS CONVERT for these; the engine enforces it here regardless of what
/// a settings file says (see setInputSource / setFpsConvertEnabled).
static bool isMillisecondSource(InputSource src)
{
return src == InputSource::ProDJLink || src == InputSource::StageLinQ
|| src == InputSource::Winamp;
}
void setOutputFrameRate(FrameRate fps)
{
outputFps = fps;
// A millisecond source has no inherent frame rate: the user's fps
// choice IS the current fps. (StageLinQ was missing from this list,
// so a stale outputFps could disagree with currentFps and the
// timecode was built at one rate and converted as if at the other.)
if (isMillisecondSource(activeInput))
currentFps = fps;
FrameRate outRate = getEffectiveOutputFps();
mtcOutput.setFrameRate(outRate);
artnetOutput.setFrameRate(outRate);
laNetTCOutput.setFrameRate(outRate);
ltcOutput.setFrameRate(outRate);
hippotizerOutput.setFrameRate(outRate);
}
//==========================================================================
// Output enables & offsets
//==========================================================================
bool isOutputMtcEnabled() const { return outputMtcEnabled; }
bool isOutputArtnetEnabled() const { return outputArtnetEnabled; }
bool isOutputLtcEnabled() const { return outputLtcEnabled; }
bool isOutputThruEnabled() const { return outputThruEnabled; }
bool isOutputLANetTCEnabled() const { return outputLANetTCEnabled; }
void setOutputMtcEnabled(bool e) { outputMtcEnabled = e; }
void setOutputArtnetEnabled(bool e) { outputArtnetEnabled = e; }
void setOutputLtcEnabled(bool e) { outputLtcEnabled = e; }
void setOutputThruEnabled(bool e) { outputThruEnabled = e; }
void setOutputTcnetEnabled(bool e) { outputTcnetEnabled = e; }
void setOutputLANetTCEnabled(bool e) { outputLANetTCEnabled = e; }
bool isOutputTcnetEnabled() const { return outputTcnetEnabled; }
void setOutputHippoEnabled(bool e) { outputHippoEnabled = e; }
bool isOutputHippoEnabled() const { return outputHippoEnabled; }
void setTcnetLayer(int l) { tcnetLayer = juce::jlimit(0, 3, l); }
int getTcnetLayer() const { return tcnetLayer; }
int getMtcOutputOffset() const { return mtcOutputOffset; }
int getArtnetOutputOffset() const { return artnetOutputOffset; }
int getLtcOutputOffset() const { return ltcOutputOffset; }
int getLANetTCOutputOffset() const { return laNetTCOutputOffset; }
// Offsets are clamped to the +/-30 frame range the UI and the settings
// loader use, so every entry point agrees.
void setMtcOutputOffset(int v) { mtcOutputOffset = juce::jlimit(-30, 30, v); }
void setArtnetOutputOffset(int v) { artnetOutputOffset = juce::jlimit(-30, 30, v); }
void setLtcOutputOffset(int v) { ltcOutputOffset = juce::jlimit(-30, 30, v); }
// LTC user-bits source modes (issue #13 follow-up).
static constexpr int kUserBitsManual = 0; // fixed operator-entered value
static constexpr int kUserBitsFromLtcIn = 1; // passthrough from this engine's LTC input
static constexpr int kUserBitsSystemDate = 2; // current local date and time zone, SMPTE ST 309
static constexpr int kUserBitsName = 3; // four ISO characters, SMPTE 12M-1 sec. 8.4.2
static constexpr int kUserBitsDebugBuffers = 4; // DEBUG: audio buffer counter in every group (#19)
/// Pack four characters into the 32-bit user-bits word per SMPTE
/// ST 12-1 sec. 8.4.2: four ISO codes, each occupying two binary
/// groups. The FIRST character goes in binary groups 7 and 8, with its
/// least significant nibble in group 7 and its most significant nibble
/// in group 8; the remaining three characters go in groups 5/6, 3/4 and
/// 1/2. Combined with the group ordering used by the encoder (group 1
/// is the most significant hex digit of the displayed word), character
/// one therefore lands in the LOW nibbles of the word.
/// Seven-bit ISO codes are converted to eight-bit by clearing bit 7,
/// as the standard requires. Short strings are padded with spaces.
static uint32_t packNameUserBits(const juce::String& text)
{
juce::String t = text.substring(0, 4);
while (t.length() < 4) t += " ";
uint32_t word = 0;
for (int i = 0; i < 4; ++i)
{
auto c = (uint32_t)(t[i]) & 0x7Fu; // force to 8-bit ISO, bit 7 = 0
// Character i occupies groups (7,8) for i=0, (5,6) for i=1,
// (3,4) for i=2, (1,2) for i=3. Group g holds hex digit
// (8-g) counted from the low end of the word.
const int lowGroup = 7 - i * 2; // 7, 5, 3, 1
const int shiftLow = (lowGroup - 1) * 4; // group g = nibble g-1 of the word
const int shiftHigh = lowGroup * 4;
word |= (c & 0x0Fu) << shiftLow; // LS nibble in lower group
word |= ((c >> 4) & 0x0Fu) << shiftHigh; // MS nibble in next group
}
return word;
}
/// LTC user bits, set from an operator-entered hex string (up to 8 hex
/// digits; blank or unparseable = 0). Stores the normalised text and,
/// in MANUAL mode, pushes the 32-bit value straight to the encoder.
/// In the dynamic modes the stored text is kept but the encoder is fed
/// by refreshDynamicUserBits() instead.
void setLtcUserBitsHex(const juce::String& hex)
{
ltcUserBitsHex = normaliseUserBitsHex(hex);
pushManualUserBits();
}
/// MANUAL mode only: send the typed digits in the opposite group order,
/// for readers that print binary group 1 as the most significant digit
/// (the GoPro convention, and STC's own until 2026-09). ST 12 sets no
/// order for free-form hex, so this is the operator matching the reader
/// in front of them. Meaningless in the other modes: DATE and NAME are
/// addressed by group number as the standards specify, and FROM LTC IN
/// passes the groups through unchanged.
void setLtcUserBitsReversed(bool reversed)
{
ltcUserBitsReversed = reversed;
pushManualUserBits();
}
bool isLtcUserBitsReversed() const { return ltcUserBitsReversed; }
static uint32_t reverseNibbles(uint32_t v)
{
uint32_t r = 0;
for (int i = 0; i < 8; ++i) r |= ((v >> (i * 4)) & 0xFu) << ((7 - i) * 4);
return r;
}
void pushManualUserBits()
{
if (ltcUserBitsMode != kUserBitsManual) return;
const uint32_t v = parseUserBitsHex(ltcUserBitsHex);
ltcOutput.setUserBits(ltcUserBitsReversed ? reverseNibbles(v) : v);
}
/// Four-character label used by the NAME user-bits mode. Blank falls
/// back to STC<engine number>.
void setLtcUserBitsName(const juce::String& n)
{
ltcUserBitsName = n.substring(0, 4);
lastSystemDateCheckMs = 0.0;
}
juce::String getLtcUserBitsName() const { return ltcUserBitsName; }
/// Output latency compensation currently applied to the LTC encoder, in
/// ms. Derived from the audio device, not a user setting.
double getLtcLatencyCompMs() const { return ltcOutput.getLatencyCompensationMs(); }
/// User-bits source mode. Switching back to MANUAL immediately restores
/// the operator's stored value so the field and the wire agree again.
void setLtcUserBitsMode(int mode)
{
ltcUserBitsMode = juce::jlimit(kUserBitsManual, kUserBitsDebugBuffers, mode);
pushManualUserBits();
ltcOutput.setBufferCounterUserBits(ltcUserBitsMode == kUserBitsDebugBuffers);
// Binary group flags say what the groups carry (12M-1 sec. 8.4.1):
// NAME declares an eight-bit character set (BGF0), DATE declares
// ST 309 date and time zone (BGF2, ST 309 Table 3, unspecified clock
// reference); FROM LTC IN takes the input's from the next tick on
// (refreshDynamicUserBits); the other modes are free-form data,
// BGF 0/0/0.
ltcOutput.setBinaryGroupFlags(ltcUserBitsMode == kUserBitsName ? 0x1
: ltcUserBitsMode == kUserBitsSystemDate ? 0x4
: 0x0);
lastSystemDateCheckMs = 0.0; // force a recompute on the next tick
}
int getLtcUserBitsMode() const { return ltcUserBitsMode; }
/// The value currently going out on the wire, whatever the mode.
uint32_t getEffectiveLtcUserBits() const { return ltcOutput.getUserBits(); }
/// SMPTE ST 309:2012 Table 2 -- time zone code (0-63) for a local offset
/// from UTC in minutes, DST already included in the offset. The code's
/// two hex digits read as decimal for whole hours: 00 = UTC, 01-12 =
/// UTC-01:00 .. UTC-12:00, 13-25 = UTC+13:00 down to UTC+01:00. Half
/// hours: 0A-0F = -00:30 .. -05:30, 1A-1F = -06:30 .. -11:30, 3F-3A =
/// +00:30 .. +05:30, 2F-2A = +06:30 .. +11:30; 32 = +12:45 (Chatham).
/// Anything the table has no code for is 39, "unknown".
static uint8_t st309TimeZoneCode(int offsetMinutes)
{
const bool negative = offsetMinutes < 0;
const int absMin = std::abs(offsetMinutes);
const int hours = absMin / 60;
const int minutes = absMin % 60;
auto decimalAsHex = [](int n) -> uint8_t { return (uint8_t)(((n / 10) << 4) | (n % 10)); };
if (minutes == 0)
{
if (hours == 0) return 0x00;
if (negative) { if (hours <= 12) return decimalAsHex(hours); }
else { if (hours <= 13) return decimalAsHex(26 - hours); }
}
else if (minutes == 30)
{
if (negative) { if (hours <= 5) return (uint8_t)(0x0A + hours);
if (hours <= 11) return (uint8_t)(0x1A + hours - 6); }
else { if (hours <= 5) return (uint8_t)(0x3F - hours);
if (hours <= 11) return (uint8_t)(0x2F - (hours - 6)); }
}
else if (minutes == 45 && !negative && hours == 12)
return 0x32;
return 0x39;
}
/// Inverse of st309TimeZoneCode. Returns false for codes the table
/// leaves undefined, deprecated, user-defined or unknown.
static bool st309OffsetMinutesForCode(uint8_t code, int& offsetMinutes)
{
for (int m = -12 * 60; m <= 13 * 60; m += 15)
if (st309TimeZoneCode(m) == code && code != 0x39)
{
offsetMinutes = m;
return true;
}
return false;
}
static juce::String st309ZoneText(uint8_t code)
{
int m = 0;
if (!st309OffsetMinutesForCode(code, m))
return code == 0x38 ? "user-defined zone"
: code == 0x39 ? "zone unknown"
: "zone code " + juce::String::toHexString(code).paddedLeft('0', 2).toUpperCase();
if (m == 0) return "UTC";
const int a = std::abs(m);
return "UTC" + juce::String(m < 0 ? "-" : "+")
+ juce::String(a / 60).paddedLeft('0', 2) + ":" + juce::String(a % 60).paddedLeft('0', 2);
}
/// Current local date and time zone packed per SMPTE ST 309:2012 (Table
/// 1 and Table 4): binary groups 1-6 carry the date as YYMMDD with units
/// before tens in ascending group order (3 September 2026 = groups 3 0 9
/// 0 6 2), group 7 the low four bits of the zone code, group 8 its high
/// two bits plus the DST flag (bit 2) and the MJD flag (bit 3, 0 = YYMMDD
/// with the time address in local time). Group 1 is the least
/// significant hex digit of the 32-bit word, as everywhere else in STC,
/// so the word reads "<zone><date>" in hex: 3 September 2026 in Central
/// European summer time is 0x64260903.
/// Emitted with BGF2 = 1 (ST 309 Table 3, unspecified clock reference).
static uint32_t systemDateUserBits()
{
const auto now = juce::Time::getCurrentTime();
const int yy = juce::jlimit(0, 99, now.getYear() % 100);
const int mm = now.getMonth() + 1; // JUCE months are 0-based
const int dd = now.getDayOfMonth();
const uint8_t tz = st309TimeZoneCode(now.getUTCOffsetSeconds() / 60);
const bool dst = now.isDaylightSavingTime();
const uint32_t bg1 = (uint32_t)(dd % 10), bg2 = (uint32_t)(dd / 10);
const uint32_t bg3 = (uint32_t)(mm % 10), bg4 = (uint32_t)(mm / 10);
const uint32_t bg5 = (uint32_t)(yy % 10), bg6 = (uint32_t)(yy / 10);
const uint32_t bg7 = (uint32_t)(tz & 0x0F);
const uint32_t bg8 = (uint32_t)((tz >> 4) & 0x03) | (dst ? 0x4u : 0u); // MJD flag 0
return bg1 | (bg2 << 4) | (bg3 << 8) | (bg4 << 12)
| (bg5 << 16) | (bg6 << 20) | (bg7 << 24) | (bg8 << 28);
}
/// Normalised user-bits hex ("" when zero/unset), for the UI and saving.
juce::String getLtcUserBitsHex() const { return ltcUserBitsHex; }
/// Parse up to 8 hex digits into a 32-bit user-bits word (0 on empty).
static uint32_t parseUserBitsHex(const juce::String& hex)
{
juce::String h = hex.retainCharacters("0123456789abcdefABCDEF");
if (h.isEmpty()) return 0;
if (h.length() > 8) h = h.getLastCharacters(8); // low 32 bits win
return (uint32_t) h.getHexValue64();
}
/// Canonical form: hex chars only, upper-case, no leading zeros ("" if 0).
static juce::String normaliseUserBitsHex(const juce::String& hex)
{
uint32_t v = parseUserBitsHex(hex);
if (v == 0) return {};
return juce::String::toHexString((juce::int64) v).toUpperCase();
}
/// Human reading of a user-bits word for the UI, always with the raw 8
/// hex digits. With the binary group flags from the decoder it can say
/// what the sender declared: ST 309 date and zone (BGF2 = 1, BGF0 = 0),
/// or four eight-bit characters (BGF0 = 1). With no flags it falls back
/// to the BCD heuristics: user bits are conventionally BCD, one decimal
/// digit per 4-bit group, so when every group holds 0-9 the hex string
/// IS the decimal reading, shown as a date when it parses as a plausible
/// YYYYMMDD. (A binary decimal integer would render 0x20260724 as
/// 539300644, meaningless for the date/reel conventions these bits
/// carry.)
static juce::String describeUserBits(uint32_t v, uint8_t bgf = 0)
{
juce::String hex = juce::String::toHexString((juce::int64) v)
.paddedLeft('0', 8).toUpperCase();
auto group = [v](int g) -> int { return (int)((v >> ((g - 1) * 4)) & 0xF); }; // g = 1..8, BG1 = low nibble
if ((bgf & 0x5) == 0x4) // ST 309 (Table 3): BGF2 = 1, BGF0 = 0
{
const uint8_t tz = (uint8_t)(group(7) | ((group(8) & 0x3) << 4));
const bool dst = (group(8) & 0x4) != 0;
const bool mjd = (group(8) & 0x8) != 0;
juce::String date;
if (!mjd)
{
const int dd = group(2) * 10 + group(1), mm = group(4) * 10 + group(3), yy = group(6) * 10 + group(5);
if (mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31)
date = "20" + juce::String(yy).paddedLeft('0', 2) + "-"
+ juce::String(mm).paddedLeft('0', 2) + "-" + juce::String(dd).paddedLeft('0', 2);
}
else
{
// Six BCD digits, ascending magnitude in groups 1-6 (Table 5).
long mjdDays = 0;
for (int g = 6; g >= 1; --g) mjdDays = mjdDays * 10 + group(g);
date = "MJD " + juce::String((juce::int64) mjdDays) + " = " + civilDateFromMjd(mjdDays) + " UTC,";
}
if (date.isEmpty()) return hex + " (ST 309, invalid date)";
return hex + " (" + date + " " + st309ZoneText(tz) + (dst ? " DST" : "") + ")";
}
if ((bgf & 0x1) == 0x1) // eight-bit character set (12M-1 sec. 8.4.2)
{
juce::String text;
for (int i = 0; i < 4; ++i)
{
const int lowGroup = 7 - i * 2; // same layout as packNameUserBits
const int c = group(lowGroup) | (group(lowGroup + 1) << 4);
text += (c >= 0x20 && c < 0x7F) ? juce::String::charToString((juce::juce_wchar) c) : juce::String(".");
}
return hex + " (\"" + text + "\")";
}
bool allDecimal = true;
for (int g = 0; g < 8; ++g)
if (((v >> (g * 4)) & 0xF) > 9) { allDecimal = false; break; }
if (!allDecimal || v == 0)
return hex;
const int yyyy = (int) hex.substring(0, 4).getIntValue();
const int mm = (int) hex.substring(4, 6).getIntValue();
const int dd = (int) hex.substring(6, 8).getIntValue();
if (yyyy >= 1900 && yyyy <= 2099 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31)
return hex + " (" + hex.substring(0, 4) + "-"
+ hex.substring(4, 6) + "-" + hex.substring(6, 8) + ")";
return hex + " (dec " + hex.trimCharactersAtStart("0") + ")";
}
/// Civil date (YYYY-MM-DD) for a modified Julian date (Fliegel and Van
/// Flandern, via JD = MJD + 2400001 for the noon integer day).
static juce::String civilDateFromMjd(long mjd)
{
long l = mjd + 2400001 + 68569;
const long n = 4 * l / 146097;
l = l - (146097 * n + 3) / 4;
const long i = 4000 * (l + 1) / 1461001;
l = l - 1461 * i / 4 + 31;
const long j = 80 * l / 2447;
const long d = l - 2447 * j / 80;
l = j / 11;
const long m = j + 2 - 12 * l;
const long y = 100 * (n - 49) + i + l;
return juce::String((juce::int64) y) + "-" + juce::String((juce::int64) m).paddedLeft('0', 2)
+ "-" + juce::String((juce::int64) d).paddedLeft('0', 2);
}
void setTcnetOutputOffsetMs(int v) { tcnetOutputOffsetMs = juce::jlimit(-1000, 1000, v); }
void setLANetTCOutputOffset(int v) { laNetTCOutputOffset = juce::jlimit(-30, 30, v); }
int getTcnetOutputOffsetMs() const { return tcnetOutputOffsetMs; }
// On-air gate (requires CDJ on-air flag from DJM for engine to be active)
bool isOnAirGateEnabled() const { return onAirGateEnabled; }
void setOnAirGateEnabled(bool e) { onAirGateEnabled = e; }
// ON AIR on StageLinQ, OFF AIR AT (DESIGN D34): the level at or below which a
// deck counts as quiet -- 0 SILENCE (default), 1 -80 dB, 2 -60 dB,
// 3 -40 dB (StageLinQ::onAirQuietLevel). Pro DJ Link has no level:
// the DJM decides on-air there.
int getOnAirQuietStep() const { return onAirQuietStep; }
void setOnAirQuietStep(int step) { onAirQuietStep = juce::jlimit(0, 3, step); }
/// Returns true if the on-air gate allows the engine to be active.
/// If the gate is disabled, always returns true. Otherwise consults the
/// CDJ's on-air flag -- this reflects the DJM mixer's full gating logic
/// (channel fader + cross-fader + EQ kill + mute), computed by the mixer
/// itself and transmitted to the CDJs. The DJ sees the same "ON AIR"
/// indicator light on the CDJ display.
bool isOnAirGateOpen() const
{
if (!onAirGateEnabled) return true;
if (activeInput != InputSource::ProDJLink) return true;
if (sharedProDJLink == nullptr) return true;
// If no DJM is active on the network, the on-air flag is meaningless
// (CDJs default to false without a DJM broadcast). Keep the gate open
// so the engine isn't silenced just because the DJ unplugged the mixer.
if (!sharedProDJLink->hasMixerFaderData()) return true;
int ep = getEffectivePlayer();
// If XF mode hasn't resolved yet (ep == 0), or the player has no
// channel on this mixer, don't block the engine. A DJM-V10 reports
// channels 5 and 6; they stopped at 4 here while ON AIR follow
// scans all six (AUDIT ENG-9).
if (ep < 1 || ep > sharedProDJLink->getMixerChannelCount()) return true;
return sharedProDJLink->isPlayerOnAir(ep);
}
//==========================================================================
// Protocol handlers -- direct access for device queries
//==========================================================================
MtcInput& getMtcInput() { return mtcInput; }
MtcOutput& getMtcOutput() { return mtcOutput; }
ArtnetInput& getArtnetInput() { return artnetInput; }
ArtnetOutput& getArtnetOutput() { return artnetOutput; }
LtcInput& getLtcInput() { return ltcInput; }
LtcOutput& getLtcOutput() { return ltcOutput; }
ProDJLinkInput& getProDJLinkInput() { jassert(sharedProDJLink != nullptr); return *sharedProDJLink; }
void setSharedProDJLinkInput(ProDJLinkInput* shared) { sharedProDJLink = shared; }
StageLinQInput& getStageLinQInput() { jassert(sharedStageLinQ != nullptr); return *sharedStageLinQ; }
LANetTimecodeInput& getLANetTCInput() { return laNetTCInput; }
LANetTimecodeOutput& getLANetTCOutput() { return laNetTCOutput; }
void setSharedStageLinQInput(StageLinQInput* shared) { sharedStageLinQ = shared; }
void setDbServerClient(DbServerClient* client) { dbClient = client; }
int getProDJLinkPlayer() const { return proDJLinkPlayer; }
/// Returns the physical player (1-6) actually being followed.
/// Fixed mode: same as proDJLinkPlayer.
/// XF / MASTER mode: the dynamically resolved player.
int getEffectivePlayer() const
{
return isAutoMode() ? resolvedXfPlayer : proDJLinkPlayer;
}
void setProDJLinkPlayer(int p)
{
proDJLinkPlayer = juce::jlimit(1, kPlayerOnAir, p);
resolvedXfPlayer = 0; // force re-resolve on next tick
resetProDJLinkCache();
bpmPlayerOverride = kBpmNoOverride;
lastSentClockBpm = -1.0f;
lastSentOscBpm = -1.0f;
}
AudioThru* getAudioThru() { return audioThru.get(); }
//==========================================================================
// Start / Stop input protocols
//==========================================================================
/// MTC input on device `deviceIndex` of the MIDI input list as it is
/// now. A negative index -- the operator's device is not listed, or
/// none was picked -- opens nothing: STC never opens a MIDI port the
/// operator did not pick. It used to open the first device, and the
/// next save then named that one in the settings (AUDIT UI-6).
/// Message thread.
bool startMtcInput(int deviceIndex)
{
stopMtcInput();
mtcInput.refreshDeviceList();
if (deviceIndex >= 0 && mtcInput.start(deviceIndex))
{
inputStatusText = "RX: " + mtcInput.getCurrentDeviceName();
return true;
}
inputStatusText = (deviceIndex < 0) ? "NO MIDI DEVICE AVAILABLE" : "FAILED TO OPEN DEVICE";
return false;
}
void stopMtcInput() { mtcInput.stop(); }
bool startArtnetInput(int interfaceIndex)
{
stopArtnetInput();
if (interfaceIndex < 0) interfaceIndex = 0;
artnetInput.refreshNetworkInterfaces();
if (artnetInput.start(interfaceIndex, 6454))
{
inputStatusText = "RX ON " + artnetInput.getBindInfo();
if (artnetInput.didFallBackToAllInterfaces())
inputStatusText += " [FALLBACK]";
return true;
}
inputStatusText = "FAILED TO BIND PORT 6454";
return false;
}
void stopArtnetInput() { artnetInput.stop(); }
bool startLANetTCInput(int interfaceIndex)
{
stopLANetTCInput();
if (interfaceIndex < 0) interfaceIndex = 0;
laNetTCInput.refreshNetworkInterfaces();
if (laNetTCInput.start(interfaceIndex, 8201))
{
inputStatusText = "RX ON " + laNetTCInput.getBindInfo();
if (laNetTCInput.didFallBackToAllInterfaces())
inputStatusText += " [FALLBACK]";
return true;
}
inputStatusText = "FAILED TO BIND PORT 8201";
return false;
}
void stopLANetTCInput() { laNetTCInput.stop(); }
bool startLtcInput(const juce::String& typeName, const juce::String& devName,
int ltcChannel, int thruChannel = -1,
double sampleRate = 0, int bufferSize = 0)
{
stopLtcInput();
if (devName.isEmpty()) { inputStatusText = "NO AUDIO DEVICE AVAILABLE"; return false; }
if (ltcInput.start(typeName, devName, ltcChannel, thruChannel, sampleRate, bufferSize))
{
inputStatusText = "RX: " + ltcInput.getCurrentDeviceName()
+ " Ch " + juce::String(ltcChannel + 1)
+ " " + describeAudioFormat(ltcInput.getActualSampleRate(), ltcInput.getActualBufferSize());
return true;
}
inputStatusText = "FAILED TO OPEN AUDIO DEVICE";
return false;