diff --git a/src/flexasio/FlexASIO/flexasio.cpp b/src/flexasio/FlexASIO/flexasio.cpp index 543255a1..f6c54ac9 100644 --- a/src/flexasio/FlexASIO/flexasio.cpp +++ b/src/flexasio/FlexASIO/flexasio.cpp @@ -772,7 +772,7 @@ namespace flexasio { return result; }()), hostSupportsOutputReady(preparedState.flexASIO.hostSupportsOutputReady), - activeStream(StartStream(preparedState.stream.get())) {} + activeStream(preparedState.stream.get()) {} void FlexASIO::Stop() { if (!preparedState.has_value()) throw ASIOException(ASE_InvalidMode, "stop() called before createBuffers()"); @@ -844,6 +844,14 @@ namespace flexasio { memset(output_samples[output_channel_index], 0, frameCount * outputSampleSizeInBytes); } + // Some backends (e.g. WASAPI) issue the first stream callback from within Pa_StartStream(). + // This is problematic because some host applications (e.g. foo_out_asio) wait for Start() to finish + // before returning from bufferSwitch(), resulting in a deadlock. See: https://github.com/dechamps/FlexASIO/issues/60 + // To work around this problem, unblock Start() if it is still running by the time we reach this point. + // One possible downside of this approach is that we might not report Start() errors properly if they occur + // after the first stream callback fires. + if (state == InitialState()) activeStream.EndWaitForStartOutcome(); + // See dechamps_ASIOUtil/BUFFERS.md for the gory details of how ASIO buffer management works. if (state != State::PRIMING) { diff --git a/src/flexasio/FlexASIO/flexasio.h b/src/flexasio/FlexASIO/flexasio.h index e29564f1..cd8473b6 100644 --- a/src/flexasio/FlexASIO/flexasio.h +++ b/src/flexasio/FlexASIO/flexasio.h @@ -151,7 +151,8 @@ namespace flexasio { PreparedState& preparedState; const bool host_supports_timeinfo; const bool hostSupportsOutputReady; - State state = hostSupportsOutputReady ? State::PRIMING : State::PRIMED; + State InitialState() const { return hostSupportsOutputReady ? State::PRIMING : State::PRIMED; } + State state = InitialState(); // The index of the "unlocked" buffer (or "half-buffer", i.e. 0 or 1) that contains data not currently being processed by the ASIO host. long driverBufferIndex = state == State::PRIMING ? 1 : 0; std::atomic<SamplePosition> samplePosition; @@ -162,7 +163,7 @@ namespace flexasio { Win32HighResolutionTimer win32HighResolutionTimer; Registration registration{ preparedState.runningState, *this }; - const ActiveStream activeStream; + ActiveStream activeStream; }; static int StreamCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) throw(); diff --git a/src/flexasio/FlexASIO/portaudio.cpp b/src/flexasio/FlexASIO/portaudio.cpp index 70580528..f7ea3873 100644 --- a/src/flexasio/FlexASIO/portaudio.cpp +++ b/src/flexasio/FlexASIO/portaudio.cpp @@ -3,8 +3,25 @@ #include "log.h" #include "../FlexASIOUtil/portaudio.h" +#include <Objbase.h> +#include <comdef.h> + namespace flexasio { + namespace { + + class COMInitializer { + public: + COMInitializer() { + ::_com_util::CheckError(CoInitializeEx(NULL, COINIT_MULTITHREADED)); + } + ~COMInitializer() { + CoUninitialize(); + } + }; + + } + void StreamDeleter::operator()(PaStream* stream) throw() { Log() << "Closing PortAudio stream " << stream; const auto error = Pa_CloseStream(stream); @@ -28,19 +45,65 @@ namespace flexasio { return Stream(stream); } - void StreamStopper::operator()(PaStream* stream) throw() { + ActiveStream::ActiveStream(PaStream* stream) : stream(stream), startThread([this] { + std::exception_ptr exception; + try { + Log() << "Starting PortAudio stream " << this->stream; + { + COMInitializer comInitializer; // Required because WASAPI Pa_StartStream() calls into the COM library + const auto error = Pa_StartStream(this->stream); + if (error != paNoError) throw std::runtime_error(std::string("unable to start PortAudio stream: ") + Pa_GetErrorText(error)); + } + Log() << "PortAudio stream started"; + } + catch (...) { + exception = std::current_exception(); + } + Log() << "Setting start outcome"; + try { + if (exception) + promisedOutcome.set_exception(exception); + else + promisedOutcome.set_value(); + } + catch (const std::future_error& future_error) { + if (future_error.code() != std::future_errc::promise_already_satisfied) throw; + Log() << "Start outcome already set"; + return; + } + Log() << "Start outcome set"; + }) { + Log() << "Waiting for start outcome"; + promisedOutcome.get_future().get(); + Log() << "Start outcome is OK"; + } + + ActiveStream::StartThread::~StartThread() { + if (thread.joinable()) { + Log() << "Waiting for start thread to finish"; + thread.join(); + Log() << "Start thread finished"; + } + } + + void ActiveStream::EndWaitForStartOutcome() { + Log() << "Ending wait for outcome"; + try { + promisedOutcome.set_value(); + } + catch (const std::future_error& future_error) { + if (future_error.code() != std::future_errc::promise_already_satisfied) throw; + Log() << "Start outcome already set"; + return; + } + Log() << "Outcome wait ended"; + } + + ActiveStream::~ActiveStream() { Log() << "Stopping PortAudio stream " << stream; const auto error = Pa_StopStream(stream); if (error != paNoError) Log() << "Unable to stop PortAudio stream: " << Pa_GetErrorText(error); } - ActiveStream StartStream(PaStream* const stream) { - Log() << "Starting PortAudio stream " << stream; - const auto error = Pa_StartStream(stream); - if (error != paNoError) throw std::runtime_error(std::string("unable to start PortAudio stream: ") + Pa_GetErrorText(error)); - Log() << "PortAudio stream started"; - return ActiveStream(stream); - } - } diff --git a/src/flexasio/FlexASIO/portaudio.h b/src/flexasio/FlexASIO/portaudio.h index 84d4d66e..e529ce37 100644 --- a/src/flexasio/FlexASIO/portaudio.h +++ b/src/flexasio/FlexASIO/portaudio.h @@ -3,6 +3,8 @@ #include <portaudio.h> #include <memory> +#include <future> +#include <thread> namespace flexasio { @@ -12,10 +14,27 @@ namespace flexasio { using Stream = std::unique_ptr<PaStream, StreamDeleter>; Stream OpenStream(const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData); - struct StreamStopper { - void operator()(PaStream*) throw(); + class ActiveStream { + public: + ActiveStream(PaStream*); + ~ActiveStream(); + + void EndWaitForStartOutcome(); + + private: + class StartThread { + public: + template <typename... Args> StartThread(Args&&... args) : thread(std::forward<Args>(args)...) {} + ~StartThread(); + + private: + std::thread thread; + }; + + PaStream* const stream; + + std::promise<void> promisedOutcome; + StartThread startThread; }; - using ActiveStream = std::unique_ptr<PaStream, StreamStopper>; - ActiveStream StartStream(PaStream*); }