Skip to content

Commit 9add673

Browse files
committed
0.27.1
1 parent 49aa44b commit 9add673

File tree

10 files changed

+464
-328
lines changed

10 files changed

+464
-328
lines changed

buildspec.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@
4747
"uuids": {
4848
"windowsApp": "ad885c58-5ca9-44de-8f4f-1c12676626a9"
4949
},
50-
"version": "0.25.5",
50+
"version": "0.27.1",
5151
"website": "https://www.atkaudio.com"
5252
}

lib/atkaudio/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ target_compile_definitions(
4444
JUCE_USE_CURL=1
4545
JUCE_WEB_BROWSER=0
4646
JUCE_MODAL_LOOPS_PERMITTED=1 # we use QT event loop, so this is needed
47+
$<$<CONFIG:RelWithDebInfo>:JUCE_DEBUG=1> # Enable DBG() in RelWithDebInfo builds
4748
)
4849

4950
set_target_properties(${PROJECT_NAME} PROPERTIES

lib/atkaudio/src/atkaudio/AudioModule.h

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,7 @@ class atkAudioModule
2222
{
2323
public:
2424
atkAudioModule() = default;
25-
26-
virtual ~atkAudioModule()
27-
{
28-
// Clean up parent component on message thread
29-
if (parentComponent)
30-
{
31-
auto* parent = parentComponent.release();
32-
juce::MessageManager::callAsync([parent] { delete parent; });
33-
}
34-
}
25+
virtual ~atkAudioModule() = default;
3526

3627
// Disable copy
3728
atkAudioModule(const atkAudioModule&) = delete;
@@ -68,16 +59,28 @@ class atkAudioModule
6859
if (!window)
6960
return;
7061

71-
// Lazy initialization - add to desktop on first show
72-
if (visible && !window->isOnDesktop())
62+
// If window is already on desktop, just toggle visibility
63+
if (window->isOnDesktop())
7364
{
74-
ensureParentComponent();
65+
window->setVisible(visible);
66+
if (visible)
67+
window->toFront(true);
68+
return;
69+
}
7570

76-
void* parentHandle = getParentNativeHandle();
77-
if (parentHandle)
78-
window->addToDesktop(0, parentHandle);
71+
// Lazy initialization - add to desktop on first show
72+
if (visible)
73+
{
74+
// DocumentWindow (TopLevelWindow) requires calling through TopLevelWindow interface
75+
if (auto* topLevel = dynamic_cast<juce::TopLevelWindow*>(window))
76+
{
77+
topLevel->addToDesktop(); // TopLevelWindow has no-args version
78+
}
7979
else
80-
window->addToDesktop(0);
80+
{
81+
// Regular components can use flags
82+
window->addToDesktop(juce::ComponentPeer::windowAppearsOnTaskbar);
83+
}
8184

8285
// Center the window on screen
8386
if (auto* docWindow = dynamic_cast<juce::DocumentWindow*>(window))
@@ -114,43 +117,6 @@ class atkAudioModule
114117
* The window is created by derived class (can be lazy or in constructor)
115118
*/
116119
virtual juce::Component* getWindowComponent() = 0;
117-
118-
/**
119-
* Get the native parent handle for this module instance
120-
* Can be used by derived classes for auxiliary windows (e.g., ChannelClient's matrix/settings windows)
121-
* Returns the peer handle of the parent component if available
122-
*/
123-
void* getParentNativeHandle()
124-
{
125-
ensureParentComponent();
126-
if (parentComponent && parentComponent->getPeer())
127-
return parentComponent->getPeer()->getNativeHandle();
128-
return nullptr;
129-
}
130-
131-
private:
132-
/**
133-
* Ensure parent component exists for this module instance
134-
*/
135-
void ensureParentComponent()
136-
{
137-
if (!parentComponent)
138-
{
139-
parentComponent = std::make_unique<juce::Component>();
140-
parentComponent->setVisible(false);
141-
142-
// Get native window handle from OBS/Qt
143-
void* nativeHandle = atk::getQtMainWindowHandle();
144-
if (nativeHandle)
145-
parentComponent->addToDesktop(0, nativeHandle);
146-
else
147-
parentComponent->addToDesktop(0);
148-
}
149-
}
150-
151-
// Per-instance parent component for window ownership
152-
// Each module has its own parent to avoid shared state and X11 errors
153-
std::unique_ptr<juce::Component> parentComponent;
154120
};
155121

156122
} // namespace atk

lib/atkaudio/src/atkaudio/DeviceIo/DeviceIo.cpp

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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+
82228
private:
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+
102265
void atk::DeviceIo::getState(std::string& s)
103266
{
104267
pImpl->getState(s);

lib/atkaudio/src/atkaudio/DeviceIo/DeviceIo.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,21 @@ class DeviceIo : public atkAudioModule
1212
DeviceIo();
1313
~DeviceIo();
1414

15-
void process(float** buffer, int numChannels, int numSamples, double sampleRate);
15+
void process(float** buffer, int numChannels, int numSamples, double sampleRate) override;
1616

1717
void setMixInput(bool mixInput);
1818

19+
/**
20+
* Set output delay in milliseconds (applied before sending to hardware).
21+
* Range: 0-10000 ms
22+
*/
23+
void setOutputDelay(float delayMs);
24+
25+
/**
26+
* Get current output delay in milliseconds
27+
*/
28+
float getOutputDelay() const;
29+
1930
void getState(std::string& s) override;
2031
void setState(std::string& s) override;
2132

lib/atkaudio/src/atkaudio/DeviceIo/DeviceIoApp.h

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,17 +113,19 @@ class AudioAppMainWindow final : public DocumentWindow
113113
{
114114
public:
115115
AudioAppMainWindow(DeviceIoApp& demo)
116-
: DocumentWindow("", Colours::lightgrey, DocumentWindow::minimiseButton | DocumentWindow::closeButton, false)
116+
: DocumentWindow(
117+
"Audio Device Settings",
118+
Colours::lightgrey,
119+
DocumentWindow::minimiseButton | DocumentWindow::closeButton,
120+
false
121+
)
117122
, audioApp(demo)
118123
{
119-
setContentOwned(&demo, true);
124+
setUsingNativeTitleBar(true);
125+
setContentOwned(&audioApp, false); // Don't take ownership - Impl owns it
120126
setResizable(true, false);
121127

122-
// Position title bar buttons on the right (Windows-style), like Plugin Host
123-
setTitleBarButtonsRequired(DocumentWindow::minimiseButton | DocumentWindow::closeButton, false);
124-
125-
centreWithSize(demo.getWidth(), demo.getHeight());
126-
setVisible(false);
128+
// Don't add to desktop yet - AudioModule will handle this on first setVisible(true)
127129
}
128130

129131
~AudioAppMainWindow() override

0 commit comments

Comments
 (0)