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