1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

WASAPI: Fix bug where an external device sample rate change could permanently stop audio processing

This commit is contained in:
attila 2025-11-24 16:05:35 +01:00 committed by Attila Szarvas
parent e26c69e7bf
commit fd1c29e56b

View file

@ -412,12 +412,25 @@ static bool supportsSampleRateConversion (WASAPIDeviceMode deviceMode) noexcept
} }
//============================================================================== //==============================================================================
struct WASAPIDeviceBaseDelegate
{
virtual ~WASAPIDeviceBaseDelegate() = default;
virtual void deviceOpened (IMMDevice*) = 0;
virtual void deviceSampleRateChanged (IMMDevice*) = 0;
virtual void deviceSessionBecameInactive (IMMDevice*) = 0;
virtual void deviceSessionExpired (IMMDevice*) = 0;
virtual void deviceSessionBecameActive (IMMDevice*) = 0;
};
class WASAPIDeviceBase class WASAPIDeviceBase
{ {
public: public:
WASAPIDeviceBase (const ComSmartPtr<IMMDevice>& d, WASAPIDeviceMode mode) WASAPIDeviceBase (const ComSmartPtr<IMMDevice>& d,
WASAPIDeviceMode mode,
WASAPIDeviceBaseDelegate& delegateIn)
: device (d), : device (d),
deviceMode (mode) deviceMode (mode),
delegate (delegateIn)
{ {
clientEvent = CreateEvent (nullptr, false, false, nullptr); clientEvent = CreateEvent (nullptr, false, false, nullptr);
@ -468,8 +481,7 @@ public:
if (client == nullptr || ! tryInitialisingWithBufferSize (bufferSizeSamples)) if (client == nullptr || ! tryInitialisingWithBufferSize (bufferSizeSamples))
return false; return false;
sampleRateHasChanged = false; delegate.deviceOpened (device);
shouldShutdown = false;
channelMaps.clear(); channelMaps.clear();
@ -503,22 +515,22 @@ public:
void deviceSampleRateChanged() void deviceSampleRateChanged()
{ {
sampleRateHasChanged = true; delegate.deviceSampleRateChanged (device);
} }
void deviceSessionBecameInactive() void deviceSessionBecameInactive()
{ {
isActive = false; delegate.deviceSessionBecameInactive (device);
} }
void deviceSessionExpired() void deviceSessionExpired()
{ {
shouldShutdown = true; delegate.deviceSessionExpired (device);
} }
void deviceSessionBecameActive() void deviceSessionBecameActive()
{ {
isActive = true; delegate.deviceSessionBecameActive (device);
} }
std::optional<BigInteger> getDefaultLayout() const std::optional<BigInteger> getDefaultLayout() const
@ -537,6 +549,8 @@ public:
WASAPIDeviceMode deviceMode; WASAPIDeviceMode deviceMode;
WASAPIDeviceBaseDelegate& delegate;
double sampleRate = 0, defaultSampleRate = 0; double sampleRate = 0, defaultSampleRate = 0;
int numChannels = 0, actualNumChannels = 0, maxNumChannels = 0, defaultNumChannels = 0; int numChannels = 0, actualNumChannels = 0, maxNumChannels = 0, defaultNumChannels = 0;
int minBufferSize = 0, defaultBufferSize = 0, latencySamples = 0; int minBufferSize = 0, defaultBufferSize = 0, latencySamples = 0;
@ -548,7 +562,6 @@ public:
Array<int> channelMaps; Array<int> channelMaps;
UINT32 actualBufferSize = 0; UINT32 actualBufferSize = 0;
int bytesPerSample = 0, bytesPerFrame = 0; int bytesPerSample = 0, bytesPerFrame = 0;
std::atomic<bool> sampleRateHasChanged { false }, shouldShutdown { false }, isActive { true };
virtual void updateFormat (bool isFloat) = 0; virtual void updateFormat (bool isFloat) = 0;
@ -919,10 +932,7 @@ private:
class WASAPIInputDevice final : public WASAPIDeviceBase class WASAPIInputDevice final : public WASAPIDeviceBase
{ {
public: public:
WASAPIInputDevice (const ComSmartPtr<IMMDevice>& d, WASAPIDeviceMode mode) using WASAPIDeviceBase::WASAPIDeviceBase;
: WASAPIDeviceBase (d, mode)
{
}
~WASAPIInputDevice() override ~WASAPIInputDevice() override
{ {
@ -971,7 +981,7 @@ public:
return false; return false;
purgeInputBuffers(); purgeInputBuffers();
isActive = true; delegate.deviceSessionBecameActive (device);
return true; return true;
} }
@ -1067,10 +1077,7 @@ private:
class WASAPIOutputDevice final : public WASAPIDeviceBase class WASAPIOutputDevice final : public WASAPIDeviceBase
{ {
public: public:
WASAPIOutputDevice (const ComSmartPtr<IMMDevice>& d, WASAPIDeviceMode mode) using WASAPIDeviceBase::WASAPIDeviceBase;
: WASAPIDeviceBase (d, mode)
{
}
~WASAPIOutputDevice() override ~WASAPIOutputDevice() override
{ {
@ -1116,7 +1123,7 @@ public:
if (! check (client->Start())) if (! check (client->Start()))
return false; return false;
isActive = true; delegate.deviceSessionBecameActive (device);
return true; return true;
} }
@ -1193,6 +1200,7 @@ private:
//============================================================================== //==============================================================================
class WASAPIAudioIODevice final : public AudioIODevice, class WASAPIAudioIODevice final : public AudioIODevice,
public Thread, public Thread,
private WASAPIDeviceBaseDelegate,
private AsyncUpdater private AsyncUpdater
{ {
public: public:
@ -1402,8 +1410,8 @@ public:
if (inputDevice != nullptr) ResetEvent (inputDevice->clientEvent); if (inputDevice != nullptr) ResetEvent (inputDevice->clientEvent);
if (outputDevice != nullptr) ResetEvent (outputDevice->clientEvent); if (outputDevice != nullptr) ResetEvent (outputDevice->clientEvent);
// No lock here; background thread is not running flags.fetch_and (~(flagShutdown | flagInputSampleRateDidChange | flagOutputSampleRateDidChange),
flags &= ~(flagShutdown | flagSampleRateChanged); std::memory_order_acq_rel);
startThread (Priority::high); startThread (Priority::high);
Thread::sleep (5); Thread::sleep (5);
@ -1432,7 +1440,7 @@ public:
} }
} }
flags |= flagOpen; flags.fetch_or (flagOpen, std::memory_order_acq_rel);
return lastError; return lastError;
} }
@ -1451,17 +1459,17 @@ public:
if (outputDevice != nullptr) outputDevice->close(); if (outputDevice != nullptr) outputDevice->close();
// Background thread has stopped at this point // Background thread has stopped at this point
flags &= ~flagOpen; flags.fetch_and (~flagOpen, std::memory_order_acq_rel);
} }
bool isOpen() override bool isOpen() override
{ {
return ((flags & flagOpen) != 0) && isThreadRunning(); return ((flags.load (std::memory_order_acquire) & flagOpen) != 0) && isThreadRunning();
} }
bool isPlaying() override bool isPlaying() override
{ {
return ((flags & (flagOpen | flagStarted)) == (flagOpen | flagStarted)) && isThreadRunning(); return ((flags.load (std::memory_order_acquire) & (flagOpen | flagStarted)) == (flagOpen | flagStarted)) && isThreadRunning();
} }
void start (AudioIODeviceCallback* call) override void start (AudioIODeviceCallback* call) override
@ -1469,13 +1477,13 @@ public:
{ {
const ScopedLock sl (startStopLock); const ScopedLock sl (startStopLock);
if ((flags & (flagOpen | flagStarted)) != flagOpen || call == nullptr) if ((flags.load (std::memory_order_acquire) & (flagOpen | flagStarted)) != flagOpen || call == nullptr)
return; return;
if (! isThreadRunning()) if (! isThreadRunning())
{ {
// something's gone wrong and the thread's stopped // something's gone wrong and the thread's stopped
flags &= ~flagOpen; flags.fetch_and (~flagOpen, std::memory_order_acq_rel);
return; return;
} }
} }
@ -1486,7 +1494,7 @@ public:
const ScopedLock sl (startStopLock); const ScopedLock sl (startStopLock);
callback = call; callback = call;
flags |= flagStarted; flags.fetch_or (flagStarted, std::memory_order_acq_rel);
} }
} }
@ -1496,7 +1504,7 @@ public:
{ {
const ScopedLock sl (startStopLock); const ScopedLock sl (startStopLock);
const auto wasStarted = (flags.fetch_and (~flagStarted) & flagStarted) != 0; const auto wasStarted = (flags.fetch_and (~flagStarted, std::memory_order_acq_rel) & flagStarted) != 0;
return wasStarted ? callback : nullptr; return wasStarted ? callback : nullptr;
}); });
@ -1534,17 +1542,16 @@ public:
while (! threadShouldExit()) while (! threadShouldExit())
{ {
if ((outputDevice != nullptr && outputDevice->shouldShutdown) const auto loadedFlags = flags.load (std::memory_order_acquire);
|| (inputDevice != nullptr && inputDevice->shouldShutdown))
{
flags |= flagShutdown;
triggerAsyncUpdate();
if ((loadedFlags & (flagShutdown | flagOutputSampleRateDidChange | flagInputSampleRateDidChange)) != 0)
{
triggerAsyncUpdate();
break; break;
} }
auto inputDeviceActive = (inputDevice != nullptr && inputDevice->isActive); const auto inputDeviceActive = (loadedFlags & flagInputIsActive) != 0;
auto outputDeviceActive = (outputDevice != nullptr && outputDevice->isActive); const auto outputDeviceActive = (loadedFlags & flagOutputIsActive) != 0;
if (! inputDeviceActive && ! outputDeviceActive) if (! inputDeviceActive && ! outputDeviceActive)
continue; continue;
@ -1568,20 +1575,12 @@ public:
} }
inputDevice->copyBuffersFromReservoir (ins.getArrayOfWritePointers(), numInputBuffers, bufferSize); inputDevice->copyBuffersFromReservoir (ins.getArrayOfWritePointers(), numInputBuffers, bufferSize);
if (inputDevice->sampleRateHasChanged)
{
flags |= flagSampleRateChanged;
triggerAsyncUpdate();
break;
}
} }
{ {
const ScopedTryLock sl (startStopLock); const ScopedTryLock sl (startStopLock);
if (sl.isLocked() && (flags & flagStarted) != 0) if (sl.isLocked() && (loadedFlags & flagStarted) != 0)
{ {
callback->audioDeviceIOCallbackWithContext (ins.getArrayOfReadPointers(), callback->audioDeviceIOCallbackWithContext (ins.getArrayOfReadPointers(),
numInputBuffers, numInputBuffers,
@ -1601,14 +1600,6 @@ public:
// Note that this function is handed the input device so it can check for the event and make sure // Note that this function is handed the input device so it can check for the event and make sure
// the input reservoir is filled up correctly even when bufferSize > device actualBufferSize // the input reservoir is filled up correctly even when bufferSize > device actualBufferSize
outputDevice->copyBuffers (outs.getArrayOfReadPointers(), numOutputBuffers, bufferSize, inputDevice.get(), *this); outputDevice->copyBuffers (outs.getArrayOfReadPointers(), numOutputBuffers, bufferSize, inputDevice.get(), *this);
if (outputDevice->sampleRateHasChanged)
{
flags |= flagSampleRateChanged;
triggerAsyncUpdate();
break;
}
} }
} }
} }
@ -1618,6 +1609,45 @@ public:
String lastError; String lastError;
private: private:
void deviceOpened (IMMDevice* d) override
{
const auto newFlags = inputDevice == nullptr || inputDevice->device != d
? ~flagOutputSampleRateDidChange
: ~flagInputSampleRateDidChange;
flags.fetch_and (newFlags, std::memory_order_acq_rel);
}
void deviceSampleRateChanged (IMMDevice* d) override
{
const auto newFlags = inputDevice == nullptr || inputDevice->device != d
? flagOutputSampleRateDidChange
: flagInputSampleRateDidChange;
flags.fetch_or (newFlags, std::memory_order_acq_rel);
}
void deviceSessionBecameInactive (IMMDevice* d) override
{
const auto newFlags = inputDevice == nullptr || inputDevice->device != d
? flagOutputIsActive
: flagInputIsActive;
flags.fetch_and (~newFlags, std::memory_order_acq_rel);
}
void deviceSessionExpired (IMMDevice*) override
{
flags.fetch_or (flagShutdown, std::memory_order_acq_rel);
}
void deviceSessionBecameActive (IMMDevice* d) override
{
const auto newFlags = inputDevice == nullptr || inputDevice->device != d
? flagOutputIsActive
: flagInputIsActive;
flags.fetch_or (newFlags, std::memory_order_acq_rel);
}
std::atomic<int> flags { 0 };
// Device stats... // Device stats...
std::unique_ptr<WASAPIInputDevice> inputDevice; std::unique_ptr<WASAPIInputDevice> inputDevice;
std::unique_ptr<WASAPIOutputDevice> outputDevice; std::unique_ptr<WASAPIOutputDevice> outputDevice;
@ -1629,15 +1659,17 @@ private:
Array<double> sampleRates; Array<double> sampleRates;
Array<int> bufferSizes; Array<int> bufferSizes;
// Active state...
std::atomic<int> flags { 0 };
enum Flags enum Flags
{ {
flagOpen = 1 << 0, flagOpen = 1 << 0,
flagStarted = 1 << 1, flagStarted = 1 << 1,
flagShutdown = 1 << 2, flagShutdown = 1 << 2,
flagSampleRateChanged = 1 << 3,
flagInputSampleRateDidChange = 1 << 3,
flagInputIsActive = 1 << 4,
flagOutputSampleRateDidChange = 1 << 5,
flagOutputIsActive = 1 << 6,
}; };
int currentBufferSizeSamples = 0; int currentBufferSizeSamples = 0;
@ -1680,9 +1712,9 @@ private:
auto flow = getDataFlow (device); auto flow = getDataFlow (device);
if (deviceId == inputDeviceId && flow == eCapture) if (deviceId == inputDeviceId && flow == eCapture)
inputDevice.reset (new WASAPIInputDevice (device, deviceMode)); inputDevice.reset (new WASAPIInputDevice (device, deviceMode, *this));
else if (deviceId == outputDeviceId && flow == eRender) else if (deviceId == outputDeviceId && flow == eRender)
outputDevice.reset (new WASAPIOutputDevice (device, deviceMode)); outputDevice.reset (new WASAPIOutputDevice (device, deviceMode, *this));
} }
return (outputDeviceId.isEmpty() || (outputDevice != nullptr && outputDevice->isOk())) return (outputDeviceId.isEmpty() || (outputDevice != nullptr && outputDevice->isOk()))
@ -1700,7 +1732,8 @@ private:
inputDevice = nullptr; inputDevice = nullptr;
}; };
const auto prevFlags = flags.fetch_and (~(flagShutdown | flagSampleRateChanged)); const auto prevFlags = flags.fetch_and (~(flagShutdown | flagInputSampleRateDidChange | flagOutputSampleRateDidChange),
std::memory_order_acq_rel);
if ((prevFlags & flagShutdown) != 0) if ((prevFlags & flagShutdown) != 0)
{ {
@ -1708,10 +1741,10 @@ private:
return; return;
} }
if ((prevFlags & flagSampleRateChanged) == 0) if ((prevFlags & (flagInputSampleRateDidChange | flagOutputSampleRateDidChange)) == 0)
return; return;
auto sampleRateChangedByInput = (inputDevice != nullptr && inputDevice->sampleRateHasChanged); const auto sampleRateChangedByInput = (prevFlags & flagInputSampleRateDidChange) != 0;
closeDevices(); closeDevices();
initialise(); initialise();