33#include " DeviceIoApp.h"
44
55#include < juce_audio_utils/juce_audio_utils.h>
6+ #include < juce_dsp/juce_dsp.h>
67
78#define MAX_CHANNELS 256
89
@@ -35,11 +36,87 @@ struct atk::DeviceIo::Impl : public juce::Timer
3536 // processBlock
3637 void process (float ** buffer, int numChannels, int numSamples, double sampleRate)
3738 {
38- auto & fromObsBuffer = deviceIoApp->getFromObsBuffer ();
39- fromObsBuffer.write (buffer, numChannels, numSamples, sampleRate);
39+ // Ensure temp buffer is large enough
40+ if (tempBuffer.getNumChannels () < numChannels || tempBuffer.getNumSamples () < numSamples)
41+ tempBuffer.setSize (numChannels, numSamples, false , false , true );
4042
43+ // Read hardware input into temp buffer
4144 auto & toObsBuffer = deviceIoApp->getToObsBuffer ();
42- toObsBuffer.read (buffer, numChannels, numSamples, sampleRate, this ->mixInput );
45+ bool hasHardwareInput =
46+ toObsBuffer.read (tempBuffer.getArrayOfWritePointers (), numChannels, numSamples, sampleRate, false );
47+
48+ // Prepare output buffer for hardware (with delay applied)
49+ juce::AudioBuffer<float > hardwareOutputBuffer;
50+
51+ // Send to hardware output based on mode
52+ auto & fromObsBuffer = deviceIoApp->getFromObsBuffer ();
53+ if (hasHardwareInput)
54+ {
55+ if (this ->mixInput )
56+ {
57+ // When both HW input and output are selected with mix ON:
58+ // Send OBS input + hardware input to hardware output
59+ hardwareOutputBuffer.setSize (numChannels, numSamples, false , false , true );
60+ for (int ch = 0 ; ch < numChannels; ++ch)
61+ {
62+ auto * mixDest = hardwareOutputBuffer.getWritePointer (ch);
63+ const auto * obsInput = buffer[ch];
64+ const auto * hwInput = tempBuffer.getReadPointer (ch);
65+ for (int i = 0 ; i < numSamples; ++i)
66+ mixDest[i] = obsInput[i] + hwInput[i];
67+ }
68+ // Apply output delay before sending to hardware
69+ applyOutputDelay (hardwareOutputBuffer, numChannels, numSamples, sampleRate);
70+ fromObsBuffer
71+ .write (hardwareOutputBuffer.getArrayOfWritePointers (), numChannels, numSamples, sampleRate);
72+ }
73+ else
74+ {
75+ // When both HW input and output are selected with mix OFF:
76+ // Send only hardware input to hardware output
77+ hardwareOutputBuffer.setSize (numChannels, numSamples, false , false , true );
78+ for (int ch = 0 ; ch < numChannels; ++ch)
79+ hardwareOutputBuffer.copyFrom (ch, 0 , tempBuffer, ch, 0 , numSamples);
80+ // Apply output delay before sending to hardware
81+ applyOutputDelay (hardwareOutputBuffer, numChannels, numSamples, sampleRate);
82+ fromObsBuffer
83+ .write (hardwareOutputBuffer.getArrayOfWritePointers (), numChannels, numSamples, sampleRate);
84+ }
85+ }
86+ else
87+ {
88+ // No hardware input: send OBS input to hardware output
89+ hardwareOutputBuffer.setSize (numChannels, numSamples, false , false , true );
90+ for (int ch = 0 ; ch < numChannels; ++ch)
91+ std::memcpy (hardwareOutputBuffer.getWritePointer (ch), buffer[ch], numSamples * sizeof (float ));
92+ // Apply output delay before sending to hardware
93+ applyOutputDelay (hardwareOutputBuffer, numChannels, numSamples, sampleRate);
94+ fromObsBuffer.write (hardwareOutputBuffer.getArrayOfWritePointers (), numChannels, numSamples, sampleRate);
95+ }
96+
97+ // Send to OBS output based on mode
98+ if (hasHardwareInput)
99+ {
100+ // Handle mixing or replacing based on mixInput flag
101+ if (this ->mixInput )
102+ {
103+ // Mix: OBS input + HW input -> OBS output
104+ for (int ch = 0 ; ch < numChannels; ++ch)
105+ {
106+ auto * dest = buffer[ch];
107+ const auto * hwInput = tempBuffer.getReadPointer (ch);
108+ for (int i = 0 ; i < numSamples; ++i)
109+ dest[i] += hwInput[i];
110+ }
111+ }
112+ else
113+ {
114+ // Replace: HW input -> OBS output
115+ for (int ch = 0 ; ch < numChannels; ++ch)
116+ std::memcpy (buffer[ch], tempBuffer.getReadPointer (ch), numSamples * sizeof (float ));
117+ }
118+ }
119+ // else: No hardware input - pass through OBS audio unchanged
43120 }
44121
45122 juce::Component* getWindowComponent ()
@@ -57,6 +134,9 @@ struct atk::DeviceIo::Impl : public juce::Timer
57134 return ;
58135 }
59136
137+ // Add output delay to state
138+ state->setAttribute (" outputDelayMs" , outputDelayMs.load (std::memory_order_acquire));
139+
60140 auto stateString = state->toString ().toStdString ();
61141
62142 s = stateString;
@@ -71,6 +151,13 @@ struct atk::DeviceIo::Impl : public juce::Timer
71151 if (!element)
72152 return ;
73153
154+ // Restore output delay
155+ if (element->hasAttribute (" outputDelayMs" ))
156+ {
157+ float delayMs = static_cast <float >(element->getDoubleAttribute (" outputDelayMs" ));
158+ outputDelayMs.store (delayMs, std::memory_order_release);
159+ }
160+
74161 deviceManager->initialise (0 , 0 , element.get (), false );
75162 }
76163
@@ -79,12 +166,78 @@ struct atk::DeviceIo::Impl : public juce::Timer
79166 this ->mixInput = val;
80167 }
81168
169+ void setOutputDelay (float delayMs)
170+ {
171+ outputDelayMs.store (delayMs, std::memory_order_release);
172+ }
173+
174+ float getOutputDelay () const
175+ {
176+ return outputDelayMs.load (std::memory_order_acquire);
177+ }
178+
179+ void applyOutputDelay (juce::AudioBuffer<float >& buffer, int numChannels, int numSamples, double sampleRate)
180+ {
181+ // Prepare delay lines if not ready or parameters changed
182+ if (!delayPrepared || outputDelayLines.size () != numChannels)
183+ prepareOutputDelay (numChannels, numSamples, sampleRate);
184+
185+ // Get current delay setting
186+ float delayMs = outputDelayMs.load (std::memory_order_acquire);
187+ float delaySamples = (delayMs / 1000 .0f ) * static_cast <float >(sampleRate);
188+
189+ // Apply delay to each channel
190+ for (int ch = 0 ; ch < numChannels; ++ch)
191+ {
192+ if (ch < outputDelayLines.size ())
193+ {
194+ // Set target delay value
195+ outputDelaySmooth[ch].setTargetValue (delaySamples);
196+
197+ auto * channelData = buffer.getWritePointer (ch);
198+ for (int i = 0 ; i < numSamples; ++i)
199+ {
200+ outputDelayLines[ch].pushSample (0 , channelData[i]);
201+ channelData[i] = outputDelayLines[ch].popSample (0 , outputDelaySmooth[ch].getNextValue ());
202+ }
203+ }
204+ }
205+ }
206+
207+ void prepareOutputDelay (int numChannels, int numSamples, double sampleRate)
208+ {
209+ outputDelayLines.clear ();
210+ outputDelayLines.resize (numChannels);
211+
212+ for (auto & delayLine : outputDelayLines)
213+ {
214+ delayLine.prepare (juce::dsp::ProcessSpec{sampleRate, static_cast <uint32_t >(numSamples), 1 });
215+ delayLine.reset ();
216+ delayLine.setMaximumDelayInSamples (10 * static_cast <int >(sampleRate)); // 10 seconds max
217+ delayLine.setDelay (0 .0f );
218+ }
219+
220+ outputDelaySmooth.clear ();
221+ outputDelaySmooth.resize (numChannels);
222+ for (auto & smooth : outputDelaySmooth)
223+ smooth.reset (sampleRate, 0 .4f ); // 400ms smoothing time
224+
225+ delayPrepared = true ;
226+ }
227+
82228private:
83229 juce::AudioDeviceManager* deviceManager = nullptr ;
84230 DeviceIoApp* deviceIoApp = nullptr ;
85231 AudioAppMainWindow* mainWindow = nullptr ;
86232
87233 std::vector<juce::Interpolators::Lagrange> interpolators;
234+ juce::AudioBuffer<float > tempBuffer;
235+
236+ // Output delay (applied before sending to hardware)
237+ std::vector<juce::dsp::DelayLine<float , juce::dsp::DelayLineInterpolationTypes::Linear>> outputDelayLines;
238+ std::vector<juce::LinearSmoothedValue<float >> outputDelaySmooth;
239+ std::atomic<float > outputDelayMs{0 .0f };
240+ bool delayPrepared = false ;
88241
89242 bool mixInput = false ;
90243};
@@ -99,6 +252,16 @@ void atk::DeviceIo::setMixInput(bool mixInput)
99252 pImpl->setMixInput (mixInput);
100253}
101254
255+ void atk::DeviceIo::setOutputDelay (float delayMs)
256+ {
257+ pImpl->setOutputDelay (delayMs);
258+ }
259+
260+ float atk::DeviceIo::getOutputDelay () const
261+ {
262+ return pImpl->getOutputDelay ();
263+ }
264+
102265void atk::DeviceIo::getState (std::string& s)
103266{
104267 pImpl->getState (s);
0 commit comments