Repository navigation
Expand file tree
/
Copy pathMainComponent.h
More file actions
1041 lines (952 loc) · 49.5 KB
/
Copy pathMainComponent.h
File metadata and controls
1041 lines (952 loc) · 49.5 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 "TimecodeDisplay.h"
#include "TimecodeEngine.h"
#include "AppSettings.h"
#include "CustomLookAndFeel.h"
#include "LevelMeter.h"
#include "TrackMapEditor.h"
#include "MixerMapEditor.h"
#include "GeneratorPresetEditor.h"
#include "GeneratorWaveformView.h"
#include "GeneratorWaveformWindow.h"
#include "OscInputServer.h"
#include "NetworkUtils.h"
#include "UpdateChecker.h"
#include "MediaDisplay.h"
#include "ProDJLinkView.h"
#include "StageLinQView.h"
#include "StageLinQDbClient.h"
#include "TCNetOutput.h"
#include <vector>
#include <memory>
#include <map>
#include <set>
//==============================================================================
// The panels' horizontal sliders with a value box ("100%", "0 ms") on their
// right: gains, BPM SMOOTHING, the offsets. A right-click or a double-click
// resets one to its double-click return value, on the track and the same on
// the value box. JUCE gives the box the slider's tooltip but, for this
// style, none of its mouse events, and deletes the box and creates a new one
// in Slider::lookAndFeelChanged (a look-and-feel or colour change,
// setTextBoxStyle), so lookAndFeelChanged hands each new box to
// valueBoxReset. On the box:
// - a right-click resets, as on the track; inside the box's open editor it
// is the editor's own (its cut / copy / paste menu);
// - a double-click resets, as on the track, when both its clicks landed on
// the box, the first on the closed box. That first click opened the
// box's editor (a single click does, to type a value): the editor is
// closed, what it held discarded, and the slider reset. A third click
// of the same run on the box resets again (a triple-click). A
// double-click whose first click landed elsewhere (JUCE also counts one
// from the track, a few px to the left, onto the box) or inside an
// editor opened by an earlier click is the box's own (the editor opens,
// or selects text);
// - a single click opens the editor, as before: type a value and Enter.
// JUCE also resets the track on an Alt+click (Option+click on macOS), the
// default modifier of setDoubleClickReturnValue; the box does not copy that
// (an Alt+click there opens the editor).
// Message thread, as all mouse handling.
class GainSlider : public juce::Slider
{
public:
GainSlider()
{
setDoubleClickReturnValue(true, 100.0);
setTooltip("Right-click or double-click to reset");
attachValueBoxReset();
}
~GainSlider() override
{
if (auto* box = valueBox())
box->removeMouseListener(&valueBoxReset);
}
void mouseDown(const juce::MouseEvent& e) override
{
if (isResetClick(e)) { resetFromRightClick(); return; }
juce::Slider::mouseDown(e);
}
void lookAndFeelChanged() override
{
juce::Slider::lookAndFeelChanged();
attachValueBoxReset();
}
private:
static bool isResetClick(const juce::MouseEvent& e)
{
return e.mods.isRightButtonDown() || e.mods.isPopupMenu();
}
void resetFromRightClick() { setValue(getDoubleClickReturnValue(), juce::sendNotificationAsync); }
// The value box: the slider's one Label child (JUCE's valueBox).
juce::Label* valueBox() const
{
for (auto* c : getChildren())
if (auto* l = dynamic_cast<juce::Label*>(c))
return l;
return nullptr;
}
void attachValueBoxReset()
{
if (auto* box = valueBox())
box->addMouseListener(&valueBoxReset, true); // and its editor's events
}
// Notes each mouse-down on the box or its editor. JUCE counts the clicks
// of a mouse wherever they land: a press is the n-th click of a run when
// each of the n - 1 presses before it lies within 8 px (25 for touch) of
// it, the previous one within 400 ms (its double-click time) and older
// ones within 800 ms, with the same buttons, in the same window. A click
// continues a run on the box when the last one noted here was counted
// one click less, lies within 400 ms and 8 px of it with the same
// buttons, and itself began the run on the closed box or continued it.
// A click elsewhere in between (the track, a few px left of the box) is
// counted too, so the counts do not follow and the run on the box ends
// there -- unless JUCE drops the run's first click from the next press's
// count (it lies 8 px or more from that press): in four quick clicks
// zig-zagging over the box's edge, box, box, track, box, the fourth then
// continues the run and resets.
struct ValueBoxReset : juce::MouseListener
{
explicit ValueBoxReset(GainSlider& s) : owner(s) {}
void mouseDown(const juce::MouseEvent& e) override
{
auto* box = owner.valueBox();
if (box == nullptr) return;
const bool editing = box->isBeingEdited();
const bool onBox = e.getNumberOfClicks() == 1 ? ! editing : continuesRun(e);
last = { e.mouseDownTime, e.source.getLastMouseDownPosition(),
e.mods.withOnlyMouseButtons(), e.getNumberOfClicks(), onBox };
if (isResetClick(e) && ! editing)
owner.resetFromRightClick();
}
void mouseDoubleClick(const juce::MouseEvent& e) override
{
auto* box = owner.valueBox();
if (box == nullptr || ! last.runOnBox || last.time != e.mouseDownTime) return;
box->hideEditor(true); // the editor the first click opened
owner.Slider::mouseDoubleClick(e); // the track's double-click reset
}
bool continuesRun(const juce::MouseEvent& e) const
{
const auto d = (e.source.getLastMouseDownPosition() - last.pos)
* juce::Desktop::getInstance().getGlobalScaleFactor(); // JUCE compares unscaled
const float tolerance = e.source.isTouch() ? 25.0f : 8.0f;
return last.runOnBox
&& last.clicks == e.getNumberOfClicks() - 1
&& e.mouseDownTime - last.time < juce::RelativeTime::milliseconds(juce::MouseEvent::getDoubleClickTimeout())
&& std::abs(d.x) < tolerance && std::abs(d.y) < tolerance
&& last.buttons == e.mods.withOnlyMouseButtons();
}
struct Down
{
juce::Time time;
juce::Point<float> pos;
juce::ModifierKeys buttons;
int clicks = 0;
bool runOnBox = false; // all of its run on this box, the first click on the closed box
};
GainSlider& owner;
Down last;
};
ValueBoxReset valueBoxReset { *this };
};
//==============================================================================
// Small circular LED that flashes on beat detection
class BeatLed : public juce::Component
{
public:
BeatLed() { setOpaque(false); }
void flash()
{
brightness = 1.0f;
repaint();
}
void decay(float amount = 0.12f)
{
if (brightness > 0.0f)
{
brightness = juce::jmax(0.0f, brightness - amount);
repaint();
}
}
void setLedColour(juce::Colour c) { onColour = c; repaint(); }
void paint(juce::Graphics& g) override
{
auto area = getLocalBounds().toFloat().reduced(1.0f);
float side = juce::jmin(area.getWidth(), area.getHeight());
auto circle = juce::Rectangle<float>(side, side)
.withCentre(area.getCentre());
auto c = onColour.withAlpha(0.15f + 0.85f * brightness);
g.setColour(c);
g.fillEllipse(circle);
// Bright glow ring when active
if (brightness > 0.1f)
{
g.setColour(onColour.withAlpha(0.3f * brightness));
g.drawEllipse(circle.expanded(1.0f), 1.5f);
}
}
private:
juce::Colour onColour { 0xFFFF9900 };
float brightness = 0.0f;
};
//==============================================================================
class PanelContent : public juce::Component
{
public:
PanelContent() { setOpaque(false); }
void setContentHeight(int h)
{
if (getHeight() != h) setSize(getWidth(), h);
}
/// Add a section separator at the given Y position with optional label.
void addSectionSeparator(int y, const juce::String& label = {})
{
sectionSeps.push_back({ y, label });
}
/// Clear all separators (call at start of resized before re-adding)
void clearSectionSeparators() { sectionSeps.clear(); }
void paint(juce::Graphics& g) override
{
for (auto& sep : sectionSeps)
{
if (sep.label.isNotEmpty())
{
// Labeled separator: draw label text only (acts as visual divider)
g.setColour(juce::Colour(0xFF37474F));
g.setFont(juce::Font(juce::FontOptions(getMonoFontName(), 9.0f, juce::Font::bold)));
g.drawText(sep.label, 12, sep.y + 2, getWidth() - 24, 12,
juce::Justification::centredLeft);
}
else
{
// Unlabeled separator: subtle horizontal line
g.setColour(juce::Colour(0xFF1E2028));
g.drawHorizontalLine(sep.y, 12.0f, (float)(getWidth() - 12));
}
}
}
private:
struct SectionSep { int y; juce::String label; };
std::vector<SectionSep> sectionSeps;
};
//==============================================================================
class MainComponent : public juce::Component,
public juce::Timer
{
public:
MainComponent();
~MainComponent() override;
void paint(juce::Graphics&) override;
void resized() override;
void timerCallback() override;
bool keyPressed(const juce::KeyPress& key) override;
void onAudioScanComplete(const juce::Array<AudioDeviceEntry>& inputs,
const juce::Array<AudioDeviceEntry>& outputs);
/// Main window bounds persistence (called by MainWindow in Main.cpp).
/// setMainWindowBounds only stores them: the destructor's flushSettings
/// writes them, in the one save at exit (AUDIT UI-12).
juce::String getSavedMainWindowBounds() const { return settings.mainWindowBounds; }
bool isShowModeLocked() const { return settings.showModeLocked; }
void setMainWindowBounds(const juce::String& bounds) { settings.mainWindowBounds = bounds; }
private:
//==============================================================================
// Background audio device scanner
//==============================================================================
class AudioScanThread : public juce::Thread
{
public:
AudioScanThread(MainComponent* owner);
void run() override;
// Created and initialised on the message thread before startThread(),
// and deleted there only once run() has returned (see
// startAudioDeviceScan and ~MainComponent).
std::unique_ptr<juce::AudioDeviceManager> tempManager;
private:
juce::Component::SafePointer<MainComponent> safeOwner;
};
std::unique_ptr<AudioScanThread> scanThread;
bool settingsLoaded = false;
bool audioDevicesApplied = false; // the first scan's results opened the audio devices
juce::Array<AudioDeviceEntry> scannedAudioInputs;
juce::Array<AudioDeviceEntry> scannedAudioOutputs;
juce::Array<int> filteredInputIndices;
juce::Array<int> filteredOutputIndices;
// --- Custom Look & Feel ---
CustomLookAndFeel customLookAndFeel;
// --- Tooltips ---
// JUCE shows a component's tooltip (setTooltip) only through a
// juce::TooltipWindow, and before 1.9.15-beta2 there was none: no
// tooltip ever appeared (#24). One for the whole application: created
// with no parent, it goes on the desktop by itself, 700 ms after the
// mouse stops, with the tooltip of whatever component of this process
// is under the mouse, in any top-level window (this one, the floating
// waveform window, the editors). It draws with JUCE's default look and
// feel (LookAndFeel_V4's dark scheme: white bold text on 0xFF181F22).
// Its own 123 ms timer, on the message thread.
juce::TooltipWindow tooltipWindow { nullptr, 700 };
// --- Colours ---
juce::Colour bgDark { 0xFF12141A };
juce::Colour bgPanel { 0xFF14161C };
juce::Colour bgDarker { 0xFF0D0E12 };
juce::Colour borderCol { 0xFF1E2028 };
juce::Colour textDim { 0xFF37474F };
juce::Colour textMid { 0xFF546E7A };
juce::Colour textLight { 0xFF78909C };
juce::Colour textBright { 0xFFCFD8DC };
juce::Colour accentRed { 0xFFC62828 };
juce::Colour accentOrange{ 0xFFE65100 };
juce::Colour accentGreen { 0xFF2E7D32 };
juce::Colour accentPurple{ 0xFF6A1B9A };
juce::Colour accentCyan { 0xFF00838F };
juce::Colour accentBlue { 0xFF1565C0 };
//==============================================================================
// ENGINE MANAGEMENT
//==============================================================================
std::vector<std::unique_ptr<TimecodeEngine>> engines;
int selectedEngine = 0;
ProDJLinkInput sharedProDJLinkInput; // shared across all engines
StageLinQInput sharedStageLinQInput; // shared across all engines
StageLinQDbClient sharedStageLinQDb; // database client for Denon metadata + artwork
MixerMap sharedSlqMixerMap { MixerMapMode::Denon }; // Denon mixer map
MixerMap sharedMixerMap; // shared DJM parameter mapping
DbServerClient sharedDbClient; // shared across all engines (Phase 2)
TCNetOutput sharedTcnetOutput; // shared TCNet timecode broadcast
juce::String tcnetArtworkKey[TCNetOutput::kMaxLayers]; // track key per layer for artwork change detection
bool tcnetArtworkPending[TCNetOutput::kMaxLayers] = {}; // Pro DJ Link artwork not cached yet: looked up each tick
juce::String tcnetWaveformKey[TCNetOutput::kMaxLayers]; // track key per layer for waveform/beatgrid change detection
uint32_t tcnetWaveformVersion[TCNetOutput::kMaxLayers] = {}; // DbServerClient entry version last fed (Pro DJ Link)
// Per-layer anchor state for synthetic beat counter. See the loop in
// timerCallback() that feeds TCNet for the rationale.
struct TcnetBeatState
{
uint32_t anchorPlayheadMs = 0;
double beatAccum = 0.0;
bool valid = false;
};
TcnetBeatState tcnetBeatState[TCNetOutput::kMaxLayers];
TimecodeEngine& currentEngine() { return *engines[(size_t)selectedEngine]; }
const TimecodeEngine& currentEngine() const { return *engines[(size_t)selectedEngine]; }
void addEngine();
void removeEngine(int index);
void selectEngine(int index);
void renameEngine(int index);
//==============================================================================
// TAB BAR
//==============================================================================
class TabButton : public juce::TextButton
{
public:
TabButton(const juce::String& name) : juce::TextButton(name) {}
std::function<void()> onRightClick;
void mouseDown(const juce::MouseEvent& e) override
{
if (e.mods.isRightButtonDown() || e.mods.isPopupMenu())
{ if (onRightClick) onRightClick(); return; }
juce::TextButton::mouseDown(e);
}
};
std::vector<std::unique_ptr<TabButton>> tabButtons;
juce::TextButton btnAddEngine { "+" };
static constexpr int kTabBarHeight = 28;
static constexpr int kMiniStripRowH = 30;
void rebuildTabButtons();
void updateTabAppearance();
void showTabContextMenu(int index);
int getMiniStripHeight() const;
juce::Rectangle<int> miniStripArea; // cached from resized()
void paintMiniStrip(juce::Graphics& g);
void mouseDown(const juce::MouseEvent& e) override;
//==============================================================================
// SYNC UI <-> ENGINE
//==============================================================================
// Prevents onChange callbacks from firing during sync
bool syncing = false;
void syncUIFromEngine(); // Load engine state into UI controls
//==============================================================================
// UI COMPONENTS (single set, bound to selected engine)
//==============================================================================
TimecodeDisplay timecodeDisplay;
// Bottom bar repaint tracking
juce::String lastBottomBarStatus;
bool lastBottomBarActive = false;
// FPS auto-detect change tracking (for button state updates)
FrameRate lastDisplayedFps = FrameRate::FPS_30;
FrameRate lastDisplayedOutFps = FrameRate::FPS_30;
double beatFlashAccumMs = 0.0; // accumulates ms for BPM-based beat flash
// --- Collapse state (per-view, not per-engine) ---
bool inputConfigExpanded = true;
bool mtcOutExpanded = true;
bool artnetOutExpanded = true;
bool ltcOutExpanded = true;
bool thruOutExpanded = true;
bool laNetTCOutExpanded = true;
// --- Input buttons ---
juce::TextButton btnMtcIn { "MTC" };
juce::TextButton btnArtnetIn { "ART-NET" };
juce::TextButton btnSysTime { "GENERATOR" };
juce::TextButton btnLANetTCIn { "LA-NET" };
// Generator controls (visible when input = Generator)
juce::ToggleButton btnGenClock { "CLOCK" }; // system clock mode toggle
juce::TextButton btnGenPlay { "PLAY" };
juce::TextButton btnGenPause { "PAUSE" };
juce::TextButton btnGenStop { "STOP" };
juce::TextEditor txtGenStartTC;
juce::TextEditor txtLtcUserBits; // LTC OUT: 8-digit hex user-bits entry
juce::Label lblLtcUserBits;
// Mirrors of the global audio sample rate / buffer size, shown in the
// output panel. Same underlying setting as the input-panel pair -- the
// audio configuration is device-wide, not per direction or per engine --
// so changing either updates both.
juce::ComboBox cmbSampleRateOut; juce::Label lblSampleRateOut;
juce::ComboBox cmbBufferSizeOut; juce::Label lblBufferSizeOut;
juce::ComboBox cmbLtcUserBitsMode; // LTC OUT: manual / from LTC in / system date / name
juce::Label lblLtcUserBitsMode;
juce::Label lblLtcInUserBits; // LTC IN: recovered user-bits display
juce::TextEditor txtGenStopTC;
juce::Label lblGenStartTC;
juce::Label lblGenStopTC;
juce::ComboBox cmbGenPreset;
juce::StringArray genPresetComboNames; // preset name of the item with id i + 1 (populateGenPresetCombo)
int genPresetAbMarkedId = 0; // the item showing kGenPresetAbMarker, or 0
static constexpr const char* kGenPresetAbMarker = CustomLookAndFeel::kComboAbMarker; // #24, updateGenPresetAbMarker
juce::Label lblGenPreset;
juce::TextButton btnGenPrev { "<" };
juce::TextButton btnGenNext { ">" };
juce::TextButton btnGenGo { "GO" };
juce::TextButton btnGenPresetEdit { "EDIT" };
// The audio file each engine was last given with a preset (#24): the
// preset's path, or none for a preset without one, as GO, a browse with
// the audio stopped, < > and OSC /stc/N/gen/preset apply it
// (applyGenPresetToEngine) -- the file it loads, is still loading, or
// found missing on disk; after a restart, whose settings keep the
// preset's name and load no file, the file of that preset (and an A/B
// only when that was the file, saveGenAbLoopSettings; an earlier
// build's A/B as loadAndApplyNonAudioSettings restores it). An engine
// has a preset loaded when it names it and was given its file
// (genEngineHasPresetLoaded). Keyed by engine; removeEngine drops the
// entry, and an engine without one was given none. Message thread.
std::map<const TimecodeEngine*, juce::File> genPresetFileGiven;
// Engines whose genPresetFileGiven came from the settings, which load no
// file (#24): until GO, or a browse with the audio stopped, applies a
// preset to the engine, its audio status reads NOT LOADED for that
// preset's file, which may well be on disk, instead of FILE NOT FOUND
// (updateStatusLabels). removeEngine drops the entry. Message thread.
std::set<const TimecodeEngine*> genPresetFileNotLoaded;
// Generator audio playback (per-engine output device for preset audio files)
juce::ToggleButton btnGenAudioOut { "AUDIO PLAYBACK" };
juce::ComboBox cmbGenAudioDevice; juce::Label lblGenAudioDevice;
juce::ComboBox cmbGenAudioChannel; juce::Label lblGenAudioChannel;
juce::ComboBox cmbGenAudioFileMode; juce::Label lblGenAudioFileMode;
juce::Slider sldGenAudioVolume; juce::Label lblGenAudioVolume;
juce::Label lblGenAudioStatus;
// Generator waveform: mini view embedded in the panel + button to open
// the floating window with the full waveform / transport / TC display.
GeneratorWaveformView miniWaveform;
juce::TextButton btnGenWaveformOpen { "WAVEFORM" };
// OSC Input (global, controls generator from external OSC sources)
juce::ToggleButton btnOscIn { "OSC IN" };
juce::ComboBox cmbOscInputInterface;
juce::Label lblOscInputInterface;
juce::TextEditor txtOscInPort;
juce::Label lblOscInPort;
juce::Label lblOscInStatus;
OscInputServer oscInputServer;
juce::TextButton btnLtcIn { "LTC" };
juce::TextButton btnProDJLinkIn { "PRO DJ LINK" };
juce::TextButton btnStageLinQIn { "STAGELINQ" };
juce::TextButton btnHippoIn { "HIPPONET" };
juce::TextButton btnWinampIn { "WINAMP" };
// Winamp source expandable panel: read-only status / current track info.
// Winamp has no configuration of its own (it auto-attaches to whatever
// Winamp / WACUP / winamp.com instance is running), so the expanded
// panel shows live state instead of controls.
juce::Label lblWinampInfoHeader;
juce::Label lblWinampInfo;
// --- Output toggles ---
juce::ToggleButton btnMtcOut { "MTC OUT" };
juce::ToggleButton btnArtnetOut { "ART-NET OUT" };
juce::ToggleButton btnLtcOut { "LTC OUT" };
juce::ToggleButton btnThruOut { "AUDIO THRU" };
juce::ToggleButton btnLANetTCOut { "LA-NET OUT" };
// --- FPS buttons ---
juce::TextButton btnFps2398 { "23.976" };
juce::TextButton btnFps24 { "24" };
juce::TextButton btnFps25 { "25" };
juce::TextButton btnFps2997 { "29.97" };
juce::TextButton btnFps30 { "30" };
// --- FPS conversion ---
juce::ToggleButton btnFpsConvert { "FPS CONVERT" };
juce::TextButton btnOutFps2398 { "23.976" };
juce::TextButton btnOutFps24 { "24" };
juce::TextButton btnOutFps25 { "25" };
juce::TextButton btnOutFps2997 { "29.97" };
juce::TextButton btnOutFps30 { "30" };
// --- Collapse toggle buttons ---
juce::TextButton btnCollapseInput { "SETTINGS" };
juce::TextButton btnCollapseMtcOut { "" };
juce::TextButton btnCollapseArtnetOut { "" };
juce::TextButton btnCollapseLtcOut { "" };
juce::TextButton btnCollapseThruOut { "" };
juce::TextButton btnCollapseLANetTCOut { "" };
// --- Left panel (input config) ---
juce::ComboBox cmbAudioInputTypeFilter; juce::Label lblAudioInputTypeFilter;
juce::ComboBox cmbSampleRate; juce::Label lblSampleRate;
juce::ComboBox cmbBufferSize; juce::Label lblBufferSize;
juce::ComboBox cmbMidiInputDevice; juce::Label lblMidiInputDevice;
juce::ComboBox cmbInputFreewheel; juce::Label lblInputFreewheel; // AUDIT D10, signal inputs
juce::ComboBox cmbArtnetInputInterface; juce::Label lblArtnetInputInterface;
juce::ComboBox cmbHippoInputInterface; juce::Label lblHippoInputInterface;
juce::ComboBox cmbHippoTcChannel; juce::Label lblHippoTcChannel;
juce::ComboBox cmbLANetTCInputInterface; juce::Label lblLANetTCInputInterface;
// Pro DJ Link controls
juce::ComboBox cmbProDJLinkInterface; juce::Label lblProDJLinkInterface;
juce::ComboBox cmbProDJLinkPlayer; juce::Label lblProDJLinkPlayer;
juce::ComboBox cmbOnAirQuiet; juce::Label lblOnAirQuiet; // DESIGN D34, StageLinQ ON AIR
juce::ComboBox cmbPdlBridgeIdentity; juce::Label lblPdlBridgeIdentity;
juce::ComboBox cmbPdl95bMode; juce::Label lblPdl95bMode;
juce::ComboBox cmbStageLinQInterface; juce::Label lblStageLinQInterface;
// BPM Multiplier buttons (per-player, ProDJLink only)
// Single click: session override (temporary, cleared on track change).
// Double click: save to TrackMap (persistent, auto-loaded on track change).
// 0=off, 1=x2, 2=x4, -1=/2, -2=/4.
juce::TextButton btnBpmOff { "1x" };
juce::TextButton btnBpmX2 { "x2" };
juce::TextButton btnBpmX4 { "x4" };
juce::TextButton btnBpmD2 { "/2" };
juce::TextButton btnBpmD4 { "/4" };
void updateBpmMultButtonStates();
void saveBpmMultToTrackMap(int clickedMult);
juce::int64 lastBpmClickMs = 0;
int lastBpmClickMult = -999;
juce::Label lblProDJLinkTrackInfo;
juce::Label lblProDJLinkMetadata;
juce::Label lblNextCue; // "NEXT: DROP in 0:12"
juce::Label lblMixerStatus; // DJM model + fader values
ArtworkDisplay artworkDisplay; // Phase 2c: album art from CDJ
WaveformDisplay waveformDisplay; // Phase 3: color waveform from CDJ
uint32_t displayedWaveformTrackId = 0; // Pro DJ Link: rekordbox ID of the displayed waveform
uint32_t displayedWaveformVersion = 0; // Pro DJ Link: its DbServerClient entry version last read
uint32_t displayedArtworkId = 0; // Pro DJ Link: displayed artwork ID
juce::String displayedPdlMedia; // Pro DJ Link: "ip/slot" those IDs belong to (AUDIT META-5)
uint32_t displayedSlqTrackVersion = 0; // StageLinQ: track version on display
uint32_t slqPanelClearedFor = 0; // StageLinQ: track version the panel was cleared for (not in the DB)
/// The player holding the media of Pro DJ Link player `player`'s track,
/// as an address, and the slot it was loaded from: the key of
/// DbServerClient's exact lookups (AUDIT META-5), resolved as
/// TimecodeEngine::requestDbMetadata resolves it. Message thread.
struct PdlMediaSource { juce::String ip; uint8_t slot = 0; };
PdlMediaSource getPdlMediaSource(int player) const
{
uint8_t src = sharedProDJLinkInput.getLoadedPlayer(player);
if (src == 0) src = (uint8_t) player;
return { sharedProDJLinkInput.getPlayerIP((int) src), sharedProDJLinkInput.getLoadedSlot(player) };
}
// Features: TrackMap, MIDI Clock, OSC BPM, Ableton Link
juce::ToggleButton btnTrackMap { "TRACK MAP" };
juce::TextButton btnTrackMapEdit { "Track Map" };
juce::ToggleButton btnMidiClock { "MIDI CLOCK" };
juce::ToggleButton btnOscFwdBpm { "OSC BPM FWD" };
juce::TextEditor edOscFwdBpmAddr;
juce::Label lblOscFwdBpmAddr;
juce::TextEditor edOscFwdBpmCmd;
juce::Label lblOscFwdBpmCmd;
juce::ToggleButton btnOscMixerFwd { "OSC MIXER FWD" };
juce::ToggleButton btnMidiMixerFwd { "MIDI MIXER FWD" };
juce::ToggleButton btnArtnetMixerFwd { "ARTNET MIXER FWD" };
juce::ComboBox cmbArtMixNet, cmbArtMixSub, cmbArtMixUni;
juce::Label lblArtMixAddr;
juce::ComboBox cmbMidiMixCCCh;
juce::Label lblMidiMixCCCh;
juce::ComboBox cmbMidiMixNoteCh;
juce::Label lblMidiMixNoteCh;
juce::ToggleButton btnLink { "ABLETON LINK" };
juce::Label lblLinkStatus;
// Audio BPM detection (for non-DJ sources)
juce::ToggleButton btnAudioBpm { "AUDIO BPM" };
juce::Label lblBpmValue; // "128.0 BPM" display
BeatLed ledBeat; // beat flash indicator
GainSlider sldBpmSmoothing; juce::Label lblBpmSmoothing;
GainSlider sldBpmInputGain; juce::Label lblBpmInputGain;
juce::ComboBox cmbAudioBpmDevice; juce::Label lblAudioBpmDevice;
juce::ComboBox cmbAudioBpmChannel; juce::Label lblAudioBpmChannel;
LevelMeter mtrAudioBpm;
juce::Component::SafePointer<juce::DocumentWindow> trackMapWindow;
juce::Component::SafePointer<juce::DocumentWindow> genPresetWindow;
juce::Component::SafePointer<juce::DocumentWindow> genWaveformWindow;
std::unique_ptr<CuePointEditorWindow> cuePointWindow;
std::string cuePointTrackKey; // key of the entry being edited (for dangling ref safety)
juce::String cuePointEditedArtist; // entry's artist field, captured at editor open
juce::String cuePointEditedTitle; // entry's title field, captured at editor open
juce::TextButton btnMixerMapEdit { "Mixer Map" };
juce::Component::SafePointer<juce::DocumentWindow> mixerMapWindow;
juce::TextButton btnProDJLinkView { "PDL View" };
juce::TextButton btnStageLinQView { "SLQ View" };
juce::TextButton btnBackup { "Backup" };
juce::TextButton btnRestore { "Restore" };
juce::TextButton btnShowLock { "SHOW LOCK" };
juce::ToggleButton btnTcnetOut { "TCNET OUT" };
juce::ComboBox cmbTcnetInterface; juce::Label lblTcnetInterface;
juce::ComboBox cmbTcnetLayer; juce::Label lblTcnetLayer;
GainSlider sldTcnetOffset; juce::Label lblTcnetOffset;
// Global TCNet offset (ms): venue-wide latency compensation summed
// with each engine's per-layer offset. See AppSettings::tcnetGlobalOffsetMs.
GainSlider sldTcnetGlobalOffset; juce::Label lblTcnetGlobalOffset;
// Hippotizer output
juce::ToggleButton btnHippoOut { "HIPPONET OUT" };
juce::TextEditor txtHippoDestIp; juce::Label lblHippoDestIp;
juce::Label lblHippoOutStatus;
// On-air gate (Pro DJ Link only): require the CDJ to be flagged on-air
// by the DJM before the engine produces active timecode. Lets the DJ
// preview tracks on a deck without triggering output.
juce::ToggleButton btnOnAirGate { "ON-AIR ONLY" };
int showLockFlashCountdown = 0; // ticks remaining for flash feedback
/// Returns true if Show Lock is active and the action should be blocked.
/// Flashes the lock button red briefly to give visual feedback.
bool isShowLocked()
{
if (!settings.showModeLocked) return false;
// Flash feedback: briefly brighten the lock button (reset in timerCallback)
btnShowLock.setColour(juce::TextButton::buttonColourId, juce::Colour(0xFFFF3333));
showLockFlashCountdown = 18; // ~300ms at 60Hz
return true;
}
/// Same as isShowLocked(), but also reverts a ToggleButton's auto-flipped state.
/// JUCE ToggleButtons flip their state BEFORE onClick fires, so we must undo it.
bool isShowLockedToggle(juce::ToggleButton& btn)
{
if (!settings.showModeLocked) return false;
btn.setToggleState(!btn.getToggleState(), juce::dontSendNotification);
btnShowLock.setColour(juce::TextButton::buttonColourId, juce::Colour(0xFFFF3333));
showLockFlashCountdown = 18;
return true;
}
/// Same as isShowLocked(), but also reverts the UI to match engine state.
/// JUCE ComboBoxes and Sliders update their visual value BEFORE onChange
/// fires, so we re-sync the entire UI from engine state to undo the change.
bool isShowLockedRevert()
{
if (!settings.showModeLocked) return false;
syncUIFromEngine(); // restores all combos/sliders/toggles to engine state
btnShowLock.setColour(juce::TextButton::buttonColourId, juce::Colour(0xFFFF3333));
showLockFlashCountdown = 18;
return true;
}
/// Same as isShowLocked(), for a control that has already taken its new
/// value when its callback runs: `restore` puts back that control alone,
/// without notification. For sliders, which call back once per drag
/// step -- isShowLockedRevert's whole syncUIFromEngine per step was the
/// cost (AUDIT UI-3) -- and for single combos.
template <typename Restore>
bool isShowLockedRestore(Restore&& restore)
{
if (!settings.showModeLocked) return false;
restore();
btnShowLock.setColour(juce::TextButton::buttonColourId, juce::Colour(0xFFFF3333));
showLockFlashCountdown = 18;
return true;
}
/// isShowLockedRestore for a slider: puts back `value`, the one the
/// engine (or the settings) holds.
bool isShowLockedSlider(juce::Slider& sld, double value)
{
return isShowLockedRestore([&] { sld.setValue(value, juce::dontSendNotification); });
}
std::unique_ptr<juce::FileChooser> configFileChooser;
juce::ScopedMessageBox importConfirmBox;
std::unique_ptr<ProDJLinkViewWindow> proDJLinkViewWindow;
std::unique_ptr<StageLinQViewWindow> stageLinQViewWindow;
// Track change triggers
juce::ToggleButton btnTriggerMidi { "MIDI Trigger" };
juce::ComboBox cmbTriggerMidiDevice;
juce::ToggleButton btnTriggerOsc { "OSC Trigger" };
juce::TextEditor edOscIp;
juce::TextEditor edOscPort;
juce::ToggleButton btnArtnetTrigger { "ARTNET Trigger" };
juce::ComboBox cmbArtTrigNet, cmbArtTrigSub, cmbArtTrigUni;
juce::Label lblArtTrigAddr;
juce::ComboBox cmbArtnetDmxInterface; juce::Label lblArtnetDmxInterface;
juce::ComboBox cmbAudioInputDevice; juce::Label lblAudioInputDevice;
juce::ComboBox cmbAudioInputChannel; juce::Label lblAudioInputChannel;
GainSlider sldLtcInputGain; juce::Label lblLtcInputGain;
LevelMeter mtrLtcInput;
juce::ComboBox cmbThruInputChannel; juce::Label lblThruInputChannel;
GainSlider sldThruInputGain; juce::Label lblThruInputGain;
LevelMeter mtrThruInput;
juce::Label lblInputStatus;
// --- Left panel (scrollable, like right) ---
juce::Viewport leftViewport;
PanelContent leftContent;
// --- Right panel (scrollable) ---
juce::Viewport rightViewport;
PanelContent rightContent;
juce::ComboBox cmbAudioOutputTypeFilter; juce::Label lblAudioOutputTypeFilter;
juce::ComboBox cmbMidiOutputDevice; juce::Label lblMidiOutputDevice;
juce::ComboBox cmbArtnetOutputInterface; juce::Label lblArtnetOutputInterface;
juce::ComboBox cmbLANetTCOutputInterface; juce::Label lblLANetTCOutputInterface;
juce::ComboBox cmbAudioOutputDevice; juce::Label lblAudioOutputDevice;
juce::ComboBox cmbAudioOutputChannel; juce::Label lblAudioOutputChannel;
GainSlider sldLtcOutputGain; juce::Label lblLtcOutputGain;
juce::ToggleButton btnLtcHoldOnPause { "HOLD ON PAUSE" };
juce::ToggleButton btnLtcUserBitsReverse { "REVERSE DIGIT ORDER" }; // MANUAL user bits only
LevelMeter mtrLtcOutput;
juce::ComboBox cmbThruOutputDevice; juce::Label lblThruOutputDevice;
juce::ComboBox cmbThruOutputChannel; juce::Label lblThruOutputChannel;
GainSlider sldThruOutputGain; juce::Label lblThruOutputGain;
LevelMeter mtrThruOutput;
juce::Label lblOutputMtcStatus;
GainSlider sldMtcOffset; juce::Label lblMtcOffset;
juce::Label lblOutputArtnetStatus;
GainSlider sldArtnetOffset; juce::Label lblArtnetOffset;
juce::Label lblOutputLtcStatus;
GainSlider sldLtcOffset; juce::Label lblLtcOffset;
juce::Label lblOutputThruStatus;
juce::Label lblOutputLANetTCStatus;
GainSlider sldLANetTCOffset; juce::Label lblLANetTCOffset;
juce::TextButton btnRefreshDevices { "Refresh Devices" };
juce::HyperlinkButton btnGitHub { "github.com/fiverecords/SuperTimecodeConverter",
juce::URL("https://github.com/fiverecords/SuperTimecodeConverter") };
// --- Update checker ---
UpdateChecker updateChecker;
juce::HyperlinkButton btnUpdateAvailable { "", juce::URL() };
juce::TextButton btnCheckUpdates { "Check for updates" };
int updateCheckDelay = 0; // ticks to wait before checking
bool updateNotificationShown = false; // true once UI is updated
int updateResetCountdown = 0; // ticks to reset button text
AppSettings settings;
static constexpr int kStereoItemId = 10000;
static constexpr int kPlaceholderItemId = 10001;
// --- Save debounce ---
bool settingsDirty = false;
int settingsSaveCountdown = 0;
static constexpr int kSaveDelayTicks = 30;
// --- Methods ---
void startAudioDeviceScan();
bool stopAudioScanThread(); // false: the scan did not stop and was left behind
void populateMidiAndNetworkCombos();
// The device names the MIDI selectors were filled with, by item ID - 1.
// An item's text is the name plus an in-use marker, which a device's own
// name can look like ("Port [2]"), so a start resolves the shown item
// through these (midiDeviceToStart, AUDIT UI-6). Message thread.
juce::StringArray midiInputNames, midiOutputNames;
// Network interfaces are persisted as positions in the list of the day
// (AUDIT C13, deferred). Within a session each interface setting
// follows its interface when the list changes (AUDIT UI-6), also to a
// new address of the same interface name: nicLabels is the list, as
// "name (ip)", that the settings currently refer to; nicFollow holds
// the in-session position of each setting whose interface has moved
// since the settings were loaded, and goneNics names the interface of
// each setting whose interface is no longer listed -- such a setting
// keeps its position, and its selector shows nothing across
// repopulates (selectNicsFromSettings), until the interface is listed
// again, when it is selected again, or the operator picks another.
// The saved position (`settings`) is not moved: positions loaded at
// startup may already name another interface than the one configured
// -- the show NIC not up yet -- and following that one would save it
// (AUDIT C13). It changes only when a selector shows something other
// than its setting (saveNicSetting), as before. In the session, though,
// a setting follows the interface it named in the first list: with STC
// started before the show NIC was up, that is the interface listed at
// its position then, and the selectors and the next starts stay on it
// once the show NIC comes up, until the operator picks the show NIC or
// STC restarts (1.9.14-beta2 kept the position and moved to the show
// NIC; positions alone cannot tell this from a NIC plugged in ahead
// during a show). A component starts on the interface its selector
// shows -- with an empty selector, the one at the setting's in-session
// position -- looked up by its label in the interface list as it is
// when it starts (nicIdToStart). Keyed by the engine (nullptr: the
// global settings) and a NicField. The running components are not
// touched: each resolved its interface when it started. Message
// thread.
enum NicField : uint8_t { kNicArtnetIn, kNicLANetIn, kNicHippoIn, kNicArtnetOut, kNicLANetOut,
kNicArtnetDmx, kNicTcnet, kNicProDJLink, kNicStageLinQ };
juce::StringArray nicLabels;
std::map<std::pair<const void*, int>, int> nicFollow;
std::map<std::pair<const void*, int>, juce::String> goneNics;
void remapNicSettings(const juce::StringArray& newLabels);
bool isNicGone(const void* owner, NicField field) const { return goneNics.count({ owner, (int) field }) > 0; }
int nicSettingNow(const void* owner, NicField field, int saved) const
{
const auto it = nicFollow.find({ owner, (int) field });
return it != nicFollow.end() ? it->second : saved;
}
// The setting now names what the operator chose: neither moved nor gone.
void forgetNicMove(const void* owner, NicField field)
{
goneNics.erase({ owner, (int) field });
nicFollow.erase({ owner, (int) field });
}
void forgetNicMoves(const void* owner);
void saveNicSetting(int& saved, const juce::ComboBox& cmb, int offset, const void* owner, NicField field);
int nicSettingComboId(NicField field) const;
void selectNicsFromSettings(bool onlyEmpty);
int nicIdToStart(const juce::ComboBox& cmb, NicField field, bool* listed = nullptr) const;
void populateAudioCombos();
void repopulateTcnetLayerCombo();
void populateTypeFilterCombos();
void populateFilteredInputDeviceCombo();
void populateFilteredOutputDeviceCombos();
juce::String getDeviceInUseMarker(const juce::String& devName, const juce::String& typeName, bool isInput);
juce::StringArray getUniqueTypeNames(const juce::Array<AudioDeviceEntry>& entries) const;
juce::String getInputTypeFilter() const;
juce::String getOutputTypeFilter() const;
void loadAndApplyNonAudioSettings();
void applyAudioSettings();
void populateSampleRateCombo();
void populateBufferSizeCombo();
void applyUserBitsFieldMode(int mode);
void populateOutputAudioCombos();
double getPreferredSampleRate() const;
int getPreferredBufferSize() const;
/// Applies the global SAMPLE RATE / BUFFER SIZE to every open shared
/// device (AudioDeviceHub::reconfigureAll). Message thread.
void restartAllAudioDevices();
int findFilteredIndex(const juce::Array<int>& filteredIndices,
const juce::Array<AudioDeviceEntry>& entries,
const juce::String& typeName, const juce::String& deviceName);
AudioDeviceEntry getSelectedAudioInput() const;
AudioDeviceEntry getSelectedAudioOutput() const;
AudioDeviceEntry getSelectedThruOutput() const;
AudioDeviceEntry getSelectedGenAudioOutput() const;
// Engine-level start/stop (gathers params from UI, calls engine methods)
void startCurrentMtcInput();
void startCurrentArtnetInput();
void startCurrentLtcInput();
void startCurrentProDJLinkInput();
void startCurrentStageLinQInput();
void startCurrentHippotizerInput();
void startCurrentLANetTCInput();
/// scopeEngine < 0 opens the global map; otherwise that engine's
/// TrackMap override layer. The editor's scope selector re-opens it.
void openTrackMapEditor(int scopeEngine = -1);
void persistTrackMapScope(int scope); // save the map of that scope (-1 global) and refresh the engines
int trackMapEditorScope = -1; // scope of the editor in trackMapWindow
int cuePointEditorScope = -1; // scope the open cue point editor's entry belongs to
void openCuePointEditor(TrackMapEntry* entry);
void openMixerMapEditor();
void openProDJLinkView();
void openStageLinQView();
void exportConfig();
void importConfig();
void applyTriggerSettings();
void propagateGlobalSettings();
void startCurrentThruOutput();
void startCurrentMtcOutput();
void startCurrentArtnetOutput();
void startCurrentLANetTCOutput();
void startCurrentLtcOutput();
void startCurrentGenAudio();
void updateCurrentOutputStates();
void populateAudioInputChannels();
void populateAudioOutputChannels();
void populateThruOutputChannels();
void populateGenAudioChannels();
void populateAudioBpmChannels();
void restartAudioBpm();
// Returns the index of another engine that has Link active, or -1 if none.
int findLinkOwnerOtherThan(int engineIdx) const;
int getChannelFromCombo(const juce::ComboBox& cmb) const;
// Same as getChannelFromCombo, but falls back to the supplied settings
// values when the combo is uninitialised (no selection / no items yet).
// The startCurrent*Output() helpers run during settings restore BEFORE
// populate*Channels() has filled the combo, so reading directly from
// the combo at that point yields id=0 and silently routes audio to
// stereo, ignoring the user's saved per-channel selection. This
// helper hands back the persisted value in that window; once the
// combo is populated it behaves identically to getChannelFromCombo.
int getChannelFromComboOrSettings(const juce::ComboBox& cmb,
int settingsChannel,
bool settingsStereo) const;
void updateInputButtonStates();
void updateFpsButtonStates();
void updateOutputFpsButtonStates();
void updateDeviceSelectorVisibility();
void updateOnAirQuietVisibility(); // OFF AIR AT: StageLinQ with ON AIR selected (DESIGN D34)
void updateStatusLabels();
void updateNextCueLabel(TimecodeEngine& eng);
void updateGenStopTcLabel(const TimecodeEngine& eng); // STOP TC (0=FREE) / (LOOP) / (A/B), #24
static juce::String genStopTcLabelTooltip(GeneratorWaveformWindow::LoopInCharge state);
static double parseTimecodeToMs(const juce::String& tc, FrameRate fps);
static juce::String msToTimecodeString(double ms, FrameRate fps);
void populateGenPresetCombo();
// The preset combo by preset name: its items' texts can carry the A/B
// marker (#24, updateGenPresetAbMarker). Message thread.
juce::String selectedGenPresetName() const;
void selectGenPresetInCombo(const juce::String& name);
void updateGenPresetAbMarker();
// Generator presets (GO, the preset combo, OSC /stc/N/gen/preset):
// the engine side, static so the three paths share it, then the panel.
// A preset's "Loop timecode" (#24) and "Loop audio only"
// reach the engine with the audio file, or live: the preset editor's
// Save (applyGenPresetLoopBoxesLive) and OSC /stc/N/gen/looptc.
// fileGiven is the engine's genPresetFileGiven. Message thread.
static bool applyGenPresetToEngine(TimecodeEngine& eng, const GeneratorPreset& preset, bool withAudioFile,
juce::File& fileGiven);
void applyGenPresetLoopBoxesLive(const GeneratorPreset& preset);
static void browseGenPresetOnEngine(TimecodeEngine& eng, const GeneratorPreset& preset, juce::File& fileGiven);
static void goGenPresetOnEngine(TimecodeEngine& eng, const GeneratorPreset& preset, juce::File& fileGiven);
static juce::File genPresetAudioFile(const GeneratorPreset& preset); // its path as a File, none without one
bool genEngineHasPresetLoaded(const TimecodeEngine& eng, const GeneratorPreset& preset) const;
void saveGenAbLoopSettings(const TimecodeEngine& eng, EngineSettings& es) const;
void goGenPreset(int engineIndex, const GeneratorPreset& preset);
void showGenPresetInPanel(TimecodeEngine& eng);
void activateGenPreset(const juce::String& name); // GO on the selected engine
void goPanelGenPreset(); // the panel's GO and the waveform window's
void loadGenPresetToFields(const juce::String& name); // the combo's browse
// Direction: -1 = previous, +1 = next. Cycles cmbGenPreset and chooses
// between browse semantics (idle: just update fields) and hot-swap
// semantics (playing: activate immediately, like a CDJ deck-flip). Used
// by both the panel PREV/NEXT buttons and the waveform window's
// onPrev / onNext callbacks so all four paths share one definition.
void cycleGenPreset(int direction);
void openGeneratorPresetEditor();
void pushFpsToGenPresetEditor(); // the selected engine's rate, to an open preset editor
void openGeneratorWaveformWindow();
// The A/B loop's keys ([ ] L): keyPressed's and the floating waveform
// window's one path. Message thread.
bool handleGeneratorLoopKey(const juce::KeyPress& key);
void setupOscInputServer();
void handleOscMessage(const OscInputServer::Message& msg); // message thread
void startOscInput();
void stopOscInput();
void saveSettings();
void flushSettings();