From ef631fe8cfa07269de667f2d586e59e138a30c1e Mon Sep 17 00:00:00 2001 From: Anthony Chandler Date: Tue, 18 Aug 2026 08:49:31 +0200 Subject: [PATCH] Add relative jog OSC command and Hold on Pause toggle for LTC Out - New OSC command /stc/N/gen/jog (float/int) performs a relative seek via the existing setGeneratorPosition()/getGeneratorCurrentMs(), preserving Playing/Paused state (unlike stop, which resets to Start TC). Intended for jog-back/jog-forward buttons (e.g. Bitfocus Companion) during live programming sessions. - New 'Hold on Pause' toggle for LTC Out (default off, matches existing behavior): when enabled, LTC output continues encoding the frozen timecode instead of going silent while the generator is paused, so downstream LTC receivers see continuous signal instead of a dropout. --- AppSettings.h | 4 ++++ LtcOutput.h | 6 +++++- MainComponent.cpp | 24 ++++++++++++++++++++++++ MainComponent.h | 1 + 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/AppSettings.h b/AppSettings.h index e64a03b..f2d5304 100644 --- a/AppSettings.h +++ b/AppSettings.h @@ -1183,6 +1183,7 @@ struct EngineSettings int ltcInputGain = 100; int thruInputGain = 100; int ltcOutputGain = 100; + bool ltcHoldOnPause = false; int thruOutputGain = 100; // Audio BPM detection (for non-DJ sources) @@ -1284,6 +1285,7 @@ struct EngineSettings obj->setProperty("ltcInputGain", ltcInputGain); obj->setProperty("thruInputGain", thruInputGain); obj->setProperty("ltcOutputGain", ltcOutputGain); + obj->setProperty("ltcHoldOnPause", ltcHoldOnPause); obj->setProperty("thruOutputGain", thruOutputGain); obj->setProperty("audioBpmEnabled", audioBpmEnabled); @@ -1412,6 +1414,7 @@ struct EngineSettings ltcInputGain = clampGain(getInt("ltcInputGain", 100)); thruInputGain = clampGain(getInt("thruInputGain", 100)); ltcOutputGain = clampGain(getInt("ltcOutputGain", 100)); + ltcHoldOnPause = getBool("ltcHoldOnPause", false); thruOutputGain = clampGain(getInt("thruOutputGain", 100)); audioBpmEnabled = getBool("audioBpmEnabled", false); @@ -1695,6 +1698,7 @@ struct AppSettings es.ltcInputGain = clampGain(getInt("ltcInputGain", 100)); es.thruInputGain = clampGain(getInt("thruInputGain", 100)); es.ltcOutputGain = clampGain(getInt("ltcOutputGain", 100)); + es.ltcHoldOnPause = getBool("ltcHoldOnPause", false); es.thruOutputGain = clampGain(getInt("thruOutputGain", 100)); es.audioBpmEnabled = getBool("audioBpmEnabled", false); diff --git a/LtcOutput.h b/LtcOutput.h index a8a4c69..ff765a4 100644 --- a/LtcOutput.h +++ b/LtcOutput.h @@ -122,6 +122,9 @@ class LtcOutput : private juce::AudioIODeviceCallback } bool isPaused() const { return paused.load(std::memory_order_relaxed); } + void setHoldOnPause(bool h) { holdOnPause.store(h, std::memory_order_relaxed); } + bool getHoldOnPause() const { return holdOnPause.load(std::memory_order_relaxed); } + void setOutputGain(float gain) { outputGain.store(juce::jlimit(0.0f, 2.0f, gain), std::memory_order_relaxed); } float getOutputGain() const { return outputGain.load(std::memory_order_relaxed); } @@ -144,6 +147,7 @@ class LtcOutput : private juce::AudioIODeviceCallback std::atomic packedPendingTc { 0 }; std::atomic pendingFps { FrameRate::FPS_25 }; std::atomic paused { false }; + std::atomic holdOnPause { false }; std::atomic outputGain { 1.0f }; std::atomic peakLevel { 0.0f }; std::atomic pitchMultiplier { 1.0 }; @@ -307,7 +311,7 @@ class LtcOutput : private juce::AudioIODeviceCallback if (outputChannelData[ch]) std::memset(outputChannelData[ch], 0, sizeof(float) * (size_t)numSamples); - if (paused.load(std::memory_order_relaxed)) + if (paused.load(std::memory_order_relaxed) && !holdOnPause.load(std::memory_order_relaxed)) return; int selCh = selectedChannel.load(std::memory_order_relaxed); diff --git a/MainComponent.cpp b/MainComponent.cpp index 3ca24ac..32521db 100644 --- a/MainComponent.cpp +++ b/MainComponent.cpp @@ -1690,6 +1690,16 @@ MainComponent::MainComponent() rightContent.addAndMakeVisible(mtrLtcOutput); mtrLtcOutput.setMeterColour(accentPurple); sldLtcOutputGain.onValueChange = [this] { if (!syncing) { currentEngine().getLtcOutput().setOutputGain((float)sldLtcOutputGain.getValue() / 100.0f); saveSettings(); } }; + rightContent.addAndMakeVisible(btnLtcHoldOnPause); + styleOutputToggle(btnLtcHoldOnPause, accentPurple); + btnLtcHoldOnPause.onClick = [this] + { + if (syncing) return; + if (isShowLocked()) { btnLtcHoldOnPause.setToggleState(!btnLtcHoldOnPause.getToggleState(), juce::dontSendNotification); return; } + currentEngine().getLtcOutput().setHoldOnPause(btnLtcHoldOnPause.getToggleState()); + saveSettings(); + }; + rightContent.addAndMakeVisible(lblOutputLtcStatus); styleLabel(lblOutputLtcStatus); lblOutputLtcStatus.setColour(juce::Label::textColourId, accentPurple); rightContent.addAndMakeVisible(sldLtcOffset); styleOffsetSlider(sldLtcOffset); rightContent.addAndMakeVisible(lblLtcOffset); lblLtcOffset.setText("LTC OFFSET:", juce::dontSendNotification); styleLabel(lblLtcOffset); @@ -2247,6 +2257,7 @@ void MainComponent::syncUIFromEngine() sldLtcInputGain.setValue(eng.getLtcInput().getInputGain() * 100.0f, juce::dontSendNotification); sldThruInputGain.setValue(eng.getLtcInput().getPassthruGain() * 100.0f, juce::dontSendNotification); sldLtcOutputGain.setValue(eng.getLtcOutput().getOutputGain() * 100.0f, juce::dontSendNotification); + btnLtcHoldOnPause.setToggleState(eng.getLtcOutput().getHoldOnPause(), juce::dontSendNotification); if (eng.getAudioThru()) sldThruOutputGain.setValue(eng.getAudioThru()->getOutputGain() * 100.0f, juce::dontSendNotification); @@ -4181,6 +4192,7 @@ void MainComponent::loadAndApplyNonAudioSettings() eng.getLtcInput().setInputGain((float)es.ltcInputGain / 100.0f); eng.getLtcInput().setPassthruGain((float)es.thruInputGain / 100.0f); eng.getLtcOutput().setOutputGain((float)es.ltcOutputGain / 100.0f); + eng.getLtcOutput().setHoldOnPause(es.ltcHoldOnPause); if (eng.getAudioThru()) eng.getAudioThru()->setOutputGain((float)es.thruOutputGain / 100.0f); @@ -4526,6 +4538,7 @@ void MainComponent::flushSettings() es.ltcInputGain = (int)(eng.getLtcInput().getInputGain() * 100.0f); es.thruInputGain = (int)(eng.getLtcInput().getPassthruGain() * 100.0f); es.ltcOutputGain = (int)(eng.getLtcOutput().getOutputGain() * 100.0f); + es.ltcHoldOnPause = eng.getLtcOutput().getHoldOnPause(); if (eng.getAudioThru()) es.thruOutputGain = (int)(eng.getAudioThru()->getOutputGain() * 100.0f); @@ -5278,6 +5291,7 @@ void MainComponent::updateDeviceSelectorVisibility() cmbAudioOutputDevice.setVisible(showLtcConfig); lblAudioOutputDevice.setVisible(showLtcConfig); cmbAudioOutputChannel.setVisible(showLtcConfig); lblAudioOutputChannel.setVisible(showLtcConfig); sldLtcOutputGain.setVisible(showLtcConfig); lblLtcOutputGain.setVisible(showLtcConfig); + btnLtcHoldOnPause.setVisible(showLtcConfig); mtrLtcOutput.setVisible(showLtcConfig); sldLtcOffset.setVisible(showLtcConfig); lblLtcOffset.setVisible(showLtcConfig); lblOutputLtcStatus.setVisible(eng.isOutputLtcEnabled()); @@ -5675,6 +5689,15 @@ void MainComponent::setupOscInputServer() { eng.generatorPause(); } + else if (cmd == "/stc/gen/jog") + { + float deltaSec = msg.getFloat(0, 0.0f); + if (deltaSec != 0.0f) + { + double newMs = eng.getGeneratorCurrentMs() + (double)deltaSec * 1000.0; + eng.setGeneratorPosition(newMs); + } + } else if (cmd == "/stc/gen/stop") { eng.generatorStop(); @@ -6806,6 +6829,7 @@ void MainComponent::resized() layCombo(lblAudioOutputDevice, cmbAudioOutputDevice, rp); layCombo(lblAudioOutputChannel, cmbAudioOutputChannel, rp); laySlider(lblLtcOutputGain, sldLtcOutputGain, rp); + if (btnLtcHoldOnPause.isVisible()) btnLtcHoldOnPause.setBounds(rp.removeFromTop(btnH)); if (mtrLtcOutput.isVisible()) layMeter(mtrLtcOutput, rp); if (sldLtcOffset.isVisible()) laySlider(lblLtcOffset, sldLtcOffset, rp); } diff --git a/MainComponent.h b/MainComponent.h index 02ffc1b..f369ff1 100644 --- a/MainComponent.h +++ b/MainComponent.h @@ -540,6 +540,7 @@ class MainComponent : public juce::Component, juce::ComboBox cmbAudioOutputDevice; juce::Label lblAudioOutputDevice; juce::ComboBox cmbAudioOutputChannel; juce::Label lblAudioOutputChannel; GainSlider sldLtcOutputGain; juce::Label lblLtcOutputGain; + juce::ToggleButton btnLtcHoldOnPause { "HOLD ON PAUSE" }; LevelMeter mtrLtcOutput; juce::ComboBox cmbThruOutputDevice; juce::Label lblThruOutputDevice; juce::ComboBox cmbThruOutputChannel; juce::Label lblThruOutputChannel;