Skip to content

Commit 3d5687f

Browse files
committed
0.31.12
1 parent 6573c26 commit 3d5687f

File tree

15 files changed

+474
-311
lines changed

15 files changed

+474
-311
lines changed

lib/atkaudio/cmake/cpack.cmake

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,16 +215,9 @@ elseif(APPLE)
215215
COMPONENT plugin
216216
)
217217

218-
# Install scanner inside the bundle (for proper code signing)
219-
if(TARGET atkaudio-pluginforobs_scanner)
220-
install(
221-
TARGETS
222-
atkaudio-pluginforobs_scanner
223-
RUNTIME
224-
DESTINATION "${TARGET_NAME}.plugin/Contents/MacOS"
225-
COMPONENT plugin
226-
)
227-
endif()
218+
# Note: Scanner is copied into bundle during build (POST_BUILD in scanner/CMakeLists.txt)
219+
# This ensures it's included in the bundle before CPack packages it.
220+
# Do NOT install scanner separately here - it would be redundant and already in the bundle.
228221

229222
# macOS: data files are installed inside bundle Resources/ via MACOSX_PACKAGE_LOCATION
230223
# No separate data directory needed (unlike Windows/Linux)

lib/atkaudio/src/atkaudio/AtomicSharedPtr.h

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,114 @@
22

33
#include <atomic>
44
#include <memory>
5+
#include <vector>
56

67
namespace atk
78
{
89

10+
// Atomic shared_ptr wrapper compatible with Apple libc++ (which lacks std::atomic<shared_ptr>).
11+
// Uses a spinlock for synchronization - safe for infrequent updates from UI thread
12+
// with frequent reads from audio thread.
13+
//
14+
// THREAD SAFETY:
15+
// - All operations are thread-safe
16+
// - Readers spin briefly if a write is in progress (nanoseconds)
17+
//
18+
// DESTRUCTION SAFETY:
19+
// - Old values are held by the writer until the next store()
20+
// - This ensures destruction happens on the writer thread, not reader
21+
//
22+
// USAGE CONTRACT:
23+
// - Writers should be infrequent (UI thread updates)
24+
// - Readers can be frequent (audio thread)
25+
//
26+
// NOTE: memory_order parameters are accepted for API compatibility but ignored.
927
template <typename T>
1028
class AtomicSharedPtr
1129
{
1230
public:
1331
AtomicSharedPtr()
14-
: ptr(nullptr)
32+
: spinlock(new std::atomic<bool>(false))
1533
{
1634
}
1735

1836
explicit AtomicSharedPtr(std::shared_ptr<T> p)
19-
: ptr(nullptr)
37+
: ptr(std::move(p))
38+
, spinlock(new std::atomic<bool>(false))
2039
{
21-
store(std::move(p), std::memory_order_relaxed);
2240
}
2341

2442
~AtomicSharedPtr()
2543
{
26-
auto p = ptr.exchange(nullptr, std::memory_order_relaxed);
27-
delete p;
44+
delete spinlock;
2845
}
2946

30-
// Non-copyable and non-movable (like std::atomic)
47+
// Non-copyable and non-movable
3148
AtomicSharedPtr(const AtomicSharedPtr&) = delete;
3249
AtomicSharedPtr& operator=(const AtomicSharedPtr&) = delete;
3350
AtomicSharedPtr(AtomicSharedPtr&&) = delete;
3451
AtomicSharedPtr& operator=(AtomicSharedPtr&&) = delete;
3552

36-
std::shared_ptr<T> load(std::memory_order order = std::memory_order_seq_cst) const
53+
std::shared_ptr<T> load([[maybe_unused]] std::memory_order order = std::memory_order_acquire) const
3754
{
38-
auto p = ptr.load(order);
39-
if (p)
40-
return *p;
41-
return std::shared_ptr<T>();
55+
lock();
56+
auto result = ptr;
57+
unlock();
58+
return result;
59+
}
60+
61+
void store(std::shared_ptr<T> desired, [[maybe_unused]] std::memory_order order = std::memory_order_release)
62+
{
63+
(void)exchange(std::move(desired));
4264
}
4365

44-
void store(std::shared_ptr<T> desired, std::memory_order order = std::memory_order_seq_cst)
66+
[[nodiscard]] std::shared_ptr<T>
67+
exchange(std::shared_ptr<T> desired, [[maybe_unused]] std::memory_order order = std::memory_order_acq_rel)
4568
{
46-
auto newPtr = new std::shared_ptr<T>(std::move(desired));
47-
auto oldPtr = ptr.exchange(newPtr, order);
48-
delete oldPtr;
69+
lock();
70+
auto old = std::move(ptr);
71+
ptr = std::move(desired);
72+
unlock();
73+
74+
// Keep old values alive to ensure destruction happens here (writer thread),
75+
// not when reader's copy goes out of scope.
76+
if (old)
77+
retained.push_back(std::move(old));
78+
79+
// Remove entries where we're the sole owner (refcount == 1)
80+
// Safe to delete - no readers have copies
81+
auto it = retained.begin();
82+
while (it != retained.end())
83+
if (it->use_count() == 1)
84+
it = retained.erase(it);
85+
else
86+
++it;
87+
88+
return retained.empty() ? nullptr : retained.back();
4989
}
5090

51-
std::shared_ptr<T> exchange(std::shared_ptr<T> desired, std::memory_order order = std::memory_order_seq_cst)
91+
private:
92+
void lock() const
5293
{
53-
auto newPtr = new std::shared_ptr<T>(std::move(desired));
54-
auto oldPtr = ptr.exchange(newPtr, order);
55-
std::shared_ptr<T> result;
56-
if (oldPtr)
94+
bool expected = false;
95+
while (!spinlock->compare_exchange_weak(expected, true, std::memory_order_acquire))
5796
{
58-
result = *oldPtr;
59-
delete oldPtr;
97+
expected = false;
98+
while (spinlock->load(std::memory_order_relaxed))
99+
;
60100
}
61-
return result;
62101
}
63102

64-
private:
65-
mutable std::atomic<std::shared_ptr<T>*> ptr;
103+
void unlock() const
104+
{
105+
spinlock->store(false, std::memory_order_release);
106+
}
107+
108+
std::shared_ptr<T> ptr;
109+
std::atomic<bool>* spinlock;
110+
111+
// Prevent destruction on reader thread by keeping old values alive
112+
std::vector<std::shared_ptr<T>> retained;
66113
};
67114

68115
} // namespace atk

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ struct atk::DeviceIo::Impl
3535
// Update smoother if sample rate or target changed
3636
if (fadeGain.getTargetValue() != targetGain)
3737
{
38-
fadeGain.reset(sampleRate, fadeDurationSeconds);
38+
fadeGain.reset(sampleRate, fadeDurationSeconds.load(std::memory_order_acquire));
3939

4040
// Clear buffers when transitioning from bypass to active
4141
if (!currentBypass)
@@ -138,9 +138,20 @@ struct atk::DeviceIo::Impl
138138
juce::XmlElement state("DEVICEIO_STATE");
139139
state.setAttribute("outputDelayMs", outputDelayMs.load(std::memory_order_acquire));
140140

141-
auto deviceState = deviceIoApp->getDeviceManager().createStateXml();
142-
if (deviceState)
143-
state.addChildElement(new juce::XmlElement(*deviceState));
141+
// Use getAudioDeviceSetup() instead of createStateXml() to ensure
142+
// channel configuration is always captured correctly
143+
auto& dm = deviceIoApp->getDeviceManager();
144+
auto setup = dm.getAudioDeviceSetup();
145+
146+
auto* deviceSetup = new juce::XmlElement("DEVICESETUP");
147+
deviceSetup->setAttribute("deviceType", dm.getCurrentAudioDeviceType());
148+
deviceSetup->setAttribute("audioOutputDeviceName", setup.outputDeviceName);
149+
deviceSetup->setAttribute("audioInputDeviceName", setup.inputDeviceName);
150+
deviceSetup->setAttribute("audioDeviceRate", setup.sampleRate);
151+
deviceSetup->setAttribute("audioDeviceBufferSize", setup.bufferSize);
152+
deviceSetup->setAttribute("audioDeviceInChans", setup.inputChannels.toString(2));
153+
deviceSetup->setAttribute("audioDeviceOutChans", setup.outputChannels.toString(2));
154+
state.addChildElement(deviceSetup);
144155

145156
s = state.toString().toStdString();
146157
}
@@ -243,7 +254,7 @@ struct atk::DeviceIo::Impl
243254
bool mixInput = false;
244255
std::atomic<bool> bypass{false};
245256
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> fadeGain{1.0f};
246-
static constexpr double fadeDurationSeconds = 0.5;
257+
std::atomic<double> fadeDurationSeconds{0.5};
247258

248259
public:
249260
void setBypass(bool v)
@@ -255,6 +266,11 @@ struct atk::DeviceIo::Impl
255266
{
256267
return bypass.load(std::memory_order_acquire);
257268
}
269+
270+
void setFadeTime(double seconds)
271+
{
272+
fadeDurationSeconds.store(seconds, std::memory_order_release);
273+
}
258274
};
259275

260276
void atk::DeviceIo::process(float** buffer, int numChannels, int numSamples, double sampleRate)
@@ -273,6 +289,12 @@ bool atk::DeviceIo::isBypassed() const
273289
return pImpl ? pImpl->isBypassed() : false;
274290
}
275291

292+
void atk::DeviceIo::setFadeTime(double seconds)
293+
{
294+
if (pImpl)
295+
pImpl->setFadeTime(seconds);
296+
}
297+
276298
void atk::DeviceIo::setMixInput(bool mixInput)
277299
{
278300
pImpl->setMixInput(mixInput);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ class DeviceIo : public atkAudioModule
1818
void setBypass(bool shouldBypass);
1919
bool isBypassed() const;
2020

21+
// Set the fade time for bypass transitions (in seconds)
22+
void setFadeTime(double seconds);
23+
2124
void setMixInput(bool mixInput);
2225
void setOutputDelay(float delayMs);
2326
float getOutputDelay() const;

lib/atkaudio/src/atkaudio/DeviceIo2/DeviceIo2.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ struct atk::DeviceIo2::Impl : public juce::AsyncUpdater
3333
bool delayPrepared = false;
3434
std::atomic<bool> bypass{false};
3535
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> fadeGain{1.0f};
36-
static constexpr double fadeDurationSeconds = 0.5;
36+
std::atomic<double> fadeDurationSeconds{0.5};
3737

3838
enum class UpdateType
3939
{
@@ -203,7 +203,7 @@ struct atk::DeviceIo2::Impl : public juce::AsyncUpdater
203203
// Update smoother if target changed
204204
if (fadeGain.getTargetValue() != targetGain)
205205
{
206-
fadeGain.reset(sampleRate, fadeDurationSeconds);
206+
fadeGain.reset(sampleRate, fadeDurationSeconds.load(std::memory_order_acquire));
207207

208208
// Clear buffers when transitioning from bypass to active
209209
if (!currentBypass)
@@ -586,6 +586,12 @@ bool atk::DeviceIo2::isBypassed() const
586586
return pImpl ? pImpl->bypass.load(std::memory_order_acquire) : false;
587587
}
588588

589+
void atk::DeviceIo2::setFadeTime(double seconds)
590+
{
591+
if (pImpl)
592+
pImpl->fadeDurationSeconds.store(seconds, std::memory_order_release);
593+
}
594+
589595
void atk::DeviceIo2::setOutputDelay(float delayMs)
590596
{
591597
if (pImpl)

lib/atkaudio/src/atkaudio/DeviceIo2/DeviceIo2.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class DeviceIo2 : public atkAudioModule
1919
void setBypass(bool shouldBypass);
2020
bool isBypassed() const;
2121

22+
// Set the fade time for bypass transitions (in seconds)
23+
void setFadeTime(double seconds);
24+
2225
void setOutputDelay(float delayMs);
2326
float getOutputDelay() const;
2427

lib/atkaudio/src/atkaudio/ModuleInfrastructure/AudioServer/AudioServer.cpp

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,29 +1539,43 @@ void AudioServer::updateClientSubscriptions(void* clientId, const AudioClientSta
15391539
std::vector<ChannelSubscription> newOutput =
15401540
(newOutputIt != newOutputSubs.end()) ? newOutputIt->second : std::vector<ChannelSubscription>{};
15411541

1542-
// ATOMIC: Remove old + Add new in single locked operation
1542+
// ATOMIC: Update subscriptions in single locked operation
1543+
// IMPORTANT: Preserve existing SyncBuffers to avoid audio discontinuity
15431544
{
15441545
std::unique_lock<std::mutex> handlerLock(handler->clientBuffersMutex);
15451546
bool snapshotDirty = false;
15461547

15471548
auto bufferIt = handler->clientBuffers.find(clientId);
15481549
if (bufferIt != handler->clientBuffers.end())
15491550
{
1550-
// Clear old subscriptions
1551-
bufferIt->second.inputBuffer.reset();
1552-
bufferIt->second.outputBuffer.reset();
1553-
bufferIt->second.inputMappings.clear();
1554-
bufferIt->second.outputMappings.clear();
1555-
1556-
snapshotDirty = true;
1557-
15581551
// If no new subscriptions for this device, remove client entry entirely
15591552
if (newInput.empty() && newOutput.empty())
15601553
{
1554+
bufferIt->second.inputBuffer.reset();
1555+
bufferIt->second.outputBuffer.reset();
1556+
bufferIt->second.inputMappings.clear();
1557+
bufferIt->second.outputMappings.clear();
15611558
handler->clientBuffers.erase(bufferIt);
15621559
handler->rebuildSnapshotLocked();
15631560
continue;
15641561
}
1562+
1563+
// Preserve existing SyncBuffers - only clear mappings
1564+
// This prevents audio discontinuity when adding/removing channels
1565+
bufferIt->second.inputMappings.clear();
1566+
bufferIt->second.outputMappings.clear();
1567+
1568+
// Only reset buffers if subscription type is being removed entirely
1569+
if (newInput.empty() && bufferIt->second.inputBuffer)
1570+
{
1571+
bufferIt->second.inputBuffer.reset();
1572+
snapshotDirty = true;
1573+
}
1574+
if (newOutput.empty() && bufferIt->second.outputBuffer)
1575+
{
1576+
bufferIt->second.outputBuffer.reset();
1577+
snapshotDirty = true;
1578+
}
15651579
}
15661580
else if (newInput.empty() && newOutput.empty())
15671581
{

0 commit comments

Comments
 (0)