From 59f75ce183431f8b50f7f209790090c5a608b08e Mon Sep 17 00:00:00 2001 From: Etienne Dechamps Date: Sun, 26 May 2024 15:45:22 +0100 Subject: [PATCH] Use Pa_IsFormatSupported in CanSampleRate This should make CanSampleRate more efficient and less disruptive (e.g. if exclusive streams are used) since the answer can be provided without opening an actual stream. Fixes #188 --- src/flexasio/FlexASIO/flexasio.cpp | 98 ++++++++++++++++++++--------- src/flexasio/FlexASIO/flexasio.h | 17 ++--- src/flexasio/FlexASIO/portaudio.cpp | 26 ++++++-- src/flexasio/FlexASIO/portaudio.h | 10 ++- 4 files changed, 108 insertions(+), 43 deletions(-) diff --git a/src/flexasio/FlexASIO/flexasio.cpp b/src/flexasio/FlexASIO/flexasio.cpp index e587df7b..4896b56a 100644 --- a/src/flexasio/FlexASIO/flexasio.cpp +++ b/src/flexasio/FlexASIO/flexasio.cpp @@ -237,6 +237,10 @@ namespace flexasio { value = static_cast(std::underlying_type_t(value) + 1); } + PaTime GetDefaultSuggestedLatency(long bufferSizeInFrames, ASIOSampleRate sampleRate) { + return 3 * bufferSizeInFrames / sampleRate; + } + } constexpr FlexASIO::SampleType FlexASIO::float32 = { ::dechamps_cpputil::endianness == ::dechamps_cpputil::Endianness::LITTLE ? ASIOSTFloat32LSB : ASIOSTFloat32MSB, paFloat32, 4, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT }; @@ -533,16 +537,17 @@ namespace flexasio { Log() << "Returning: " << info->name << ", " << (info->isActive ? "active" : "inactive") << ", group " << info->channelGroup << ", type " << ::dechamps_ASIOUtil::GetASIOSampleTypeString(info->type); } - FlexASIO::OpenStreamResult FlexASIO::OpenStream(bool inputEnabled, bool outputEnabled, double sampleRate, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData) + template + decltype(auto) FlexASIO::WithStreamParameters(bool inputEnabled, bool outputEnabled, double sampleRate, PaTime defaultSuggestedLatency, Functor functor) const { - Log() << "CFlexASIO::OpenStream(inputEnabled = " << inputEnabled << ", outputEnabled = " << outputEnabled << ", sampleRate = " << sampleRate << ", framesPerBuffer = " << framesPerBuffer << ", callback = " << callback << ", callbackUserData = " << callbackUserData << ")"; - OpenStreamResult result; - result.exclusive = hostApi.info.type == paWDMKS; + Log() << "FlexASIO::WithStreamParameters(inputEnabled = " << inputEnabled << ", outputEnabled = " << outputEnabled << ", sampleRate = " << sampleRate << ")"; + + auto exclusivity = hostApi.info.type == paWDMKS ? StreamExclusivity::EXCLUSIVE : StreamExclusivity::SHARED; PaStreamParameters common_parameters = { 0 }; common_parameters.sampleFormat = paNonInterleaved; common_parameters.hostApiSpecificStreamInfo = NULL; - common_parameters.suggestedLatency = 3 * framesPerBuffer / sampleRate; + common_parameters.suggestedLatency = defaultSuggestedLatency; PaWasapiStreamInfo common_wasapi_stream_info = { 0 }; if (hostApi.info.type == paWASAPI) { @@ -570,7 +575,7 @@ namespace flexasio { Log() << "Using " << (config.input.wasapiExclusiveMode ? "exclusive" : "shared") << " mode for input WASAPI stream"; if (config.input.wasapiExclusiveMode) { input_wasapi_stream_info.flags |= paWinWasapiExclusive; - result.exclusive = true; + exclusivity = StreamExclusivity::EXCLUSIVE; } Log() << (config.input.wasapiAutoConvert ? "Enabling" : "Disabling") << " auto-conversion for input WASAPI stream"; if (config.input.wasapiAutoConvert) { @@ -602,7 +607,7 @@ namespace flexasio { Log() << "Using " << (config.output.wasapiExclusiveMode ? "exclusive" : "shared") << " mode for output WASAPI stream"; if (config.output.wasapiExclusiveMode) { output_wasapi_stream_info.flags |= paWinWasapiExclusive; - result.exclusive = true; + exclusivity = StreamExclusivity::EXCLUSIVE; } Log() << (config.output.wasapiAutoConvert ? "Enabling" : "Disabling") << " auto-conversion for output WASAPI stream"; if (config.output.wasapiAutoConvert) { @@ -616,20 +621,26 @@ namespace flexasio { } } - result.stream = flexasio::OpenStream( - inputEnabled ? &input_parameters : NULL, - outputEnabled ? &output_parameters : NULL, - sampleRate, framesPerBuffer, paPrimeOutputBuffersUsingStreamCallback, callback, callbackUserData); - if (result.stream != nullptr) { - const auto streamInfo = Pa_GetStreamInfo(result.stream.get()); - if (streamInfo == nullptr) { - Log() << "Unable to get stream info"; - } - else { - Log() << "Stream info: " << DescribeStreamInfo(*streamInfo); - } + return functor(StreamParameters{ + .inputParameters = inputEnabled ? &input_parameters : NULL, + .outputParameters = outputEnabled ? &output_parameters : NULL, + .sampleRate = sampleRate, + }, exclusivity); + } + + Stream FlexASIO::OpenStream(const StreamParameters& streamParameters, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData) const + { + Log() << "FlexASIO::OpenStream(framesPerBuffer = " << framesPerBuffer << ", callback = " << callback << ", callbackUserData = " << callbackUserData << ")"; + auto stream = flexasio::OpenStream( + streamParameters, framesPerBuffer, paPrimeOutputBuffersUsingStreamCallback, callback, callbackUserData); + const auto streamInfo = Pa_GetStreamInfo(stream.get()); + if (streamInfo == nullptr) { + Log() << "Unable to get stream info"; } - return result; + else { + Log() << "Stream info: " << DescribeStreamInfo(*streamInfo); + } + return stream; } bool FlexASIO::CanSampleRate(ASIOSampleRate sampleRate) @@ -641,20 +652,25 @@ namespace flexasio { return false; } - if (preparedState.has_value() && preparedState->IsExclusive()) { + if (preparedState.has_value() && preparedState->GetStreamExclusivity() == StreamExclusivity::EXCLUSIVE) { // Some applications will call canSampleRate() while the stream is running. If the stream is exclusive our probes will fail. // In that case we always say "yes" - always saying "no" confuses applications. See https://github.com/dechamps/FlexASIO/issues/66 + // TODO: now that we are using Pa_IsFormatSupported() instead of Pa_OpenStream() to probe sample rates, is this still necessary? Log() << "Faking sample rate " << sampleRate << " as available because an exclusive stream is currently running"; return true; } + const auto checkParameters = [&](const StreamParameters& streamParameters, StreamExclusivity) { + CheckFormatSupported(streamParameters); + }; + // We do not know whether the host application intends to use only input channels, only output channels, or both. // This logic ensures the driver is usable for all three use cases. bool available = false; if (inputDevice.has_value()) try { Log() << "Checking if input supports this sample rate"; - OpenStream(true, false, sampleRate, paFramesPerBufferUnspecified, NoOpStreamCallback, nullptr); + WithStreamParameters(/*inputEnabled=*/true, /*outputEnabled=*/false, sampleRate, /*suggestedLatency*/0, checkParameters); Log() << "Input supports this sample rate"; available = true; } @@ -664,7 +680,7 @@ namespace flexasio { if (outputDevice.has_value()) try { Log() << "Checking if output supports this sample rate"; - OpenStream(false, true, sampleRate, paFramesPerBufferUnspecified, NoOpStreamCallback, nullptr); + WithStreamParameters(/*inputEnabled=*/false, /*outputEnabled=*/true, sampleRate, /*suggestedLatency*/0, checkParameters); Log() << "Output supports this sample rate"; available = true; } @@ -778,7 +794,14 @@ namespace flexasio { bufferInfos.push_back(asioBufferInfo); } return bufferInfos; - }()), openStreamResult(flexASIO.OpenStream(buffers.inputChannelCount > 0, buffers.outputChannelCount > 0, sampleRate, unsigned long(bufferSizeInFrames), &PreparedState::StreamCallback, this)), + }()), streamWithExclusivity(flexASIO.WithStreamParameters( + buffers.inputChannelCount > 0, buffers.outputChannelCount > 0, sampleRate, GetDefaultSuggestedLatency(bufferSizeInFrames, sampleRate), + [&](const StreamParameters& streamParameters, StreamExclusivity streamExclusivity) { + return StreamWithExclusivity{ + .stream = flexASIO.OpenStream(streamParameters, static_cast(bufferSizeInFrames), &PreparedState::StreamCallback, this), + .exclusivity = streamExclusivity, + }; + })), configWatcher(flexASIO.configLoader, [this] { OnConfigChange(); }) { if (callbacks->asioMessage) ProbeHostMessages(callbacks->asioMessage); } @@ -808,7 +831,7 @@ namespace flexasio { long FlexASIO::ComputeLatencyFromStream(PaStream* stream, bool output, size_t bufferSizeInFrames) const { - const PaStreamInfo* stream_info = Pa_GetStreamInfo(stream); + const PaStreamInfo* stream_info = Pa_GetStreamInfo(&stream); if (!stream_info) throw ASIOException(ASE_HWMalfunction, "unable to get stream info"); // See https://github.com/dechamps/FlexASIO/issues/10. @@ -828,11 +851,26 @@ namespace flexasio { const auto bufferSize = ComputeBufferSizes().preferred; Log() << "Assuming " << bufferSize << " as the buffer size"; + // Since CreateBuffers() has not been called yet, we do not know if the application intends + // to use only input channels, only output channels, or both. We arbitrarily decide to compute + // the input latency assuming an input-only stream, and the output latency assuming an + // output-only stream, because that makes this code least likely to fail. The tradeoff is this + // will likely return wrong latencies for full duplex streams (which tend to have higher + // latency due to the need for buffer adaptation). + + const auto getLatency = [&](bool output) { + return WithStreamParameters( + /*inputEnabled=*/!output, /*outputEnabled=*/output, sampleRate, GetDefaultSuggestedLatency(bufferSize, sampleRate), + [&](const StreamParameters& streamParameters, StreamExclusivity) { + return ComputeLatencyFromStream(OpenStream(streamParameters, bufferSize, NoOpStreamCallback, nullptr).get(), output, bufferSize); + }); + }; + if (!inputDevice.has_value()) *inputLatency = 0; else try { - *inputLatency = ComputeLatencyFromStream(OpenStream(true, false, sampleRate, bufferSize, NoOpStreamCallback, nullptr).stream.get(), /*output=*/false, bufferSize); + *inputLatency = getLatency(/*output=*/false); Log() << "Using input latency from successful stream probe"; } catch (const std::exception& exception) { @@ -843,7 +881,7 @@ namespace flexasio { *outputLatency = 0; else try { - *outputLatency = ComputeLatencyFromStream(OpenStream(false, true, sampleRate, bufferSize, NoOpStreamCallback, nullptr).stream.get(), /*output=*/true, bufferSize); + *outputLatency = getLatency(/*output=*/true); Log() << "Using output latency from successful stream probe"; } catch (const std::exception& exception) { @@ -856,8 +894,8 @@ namespace flexasio { void FlexASIO::PreparedState::GetLatencies(long* inputLatency, long* outputLatency) { - *inputLatency = flexASIO.ComputeLatencyFromStream(openStreamResult.stream.get(), /*output=*/false, buffers.bufferSizeInFrames); - *outputLatency = flexASIO.ComputeLatencyFromStream(openStreamResult.stream.get(), /*output=*/true, buffers.bufferSizeInFrames); + *inputLatency = flexASIO.ComputeLatencyFromStream(streamWithExclusivity.stream.get(), /*output=*/false, buffers.bufferSizeInFrames); + *outputLatency = flexASIO.ComputeLatencyFromStream(streamWithExclusivity.stream.get(), /*output=*/true, buffers.bufferSizeInFrames); } void FlexASIO::Start() { @@ -885,7 +923,7 @@ namespace flexasio { hostSupportsOutputReady(preparedState.flexASIO.hostSupportsOutputReady) {} void FlexASIO::PreparedState::RunningState::RunningState::Start() { - activeStream = StartStream(preparedState.openStreamResult.stream.get()); + activeStream = StartStream(preparedState.streamWithExclusivity.stream.get()); } void FlexASIO::Stop() { diff --git a/src/flexasio/FlexASIO/flexasio.h b/src/flexasio/FlexASIO/flexasio.h index 54b6cae8..6e742484 100644 --- a/src/flexasio/FlexASIO/flexasio.h +++ b/src/flexasio/FlexASIO/flexasio.h @@ -59,10 +59,7 @@ namespace flexasio { GUID waveSubFormat; }; - struct OpenStreamResult { - Stream stream; - bool exclusive; - }; + enum class StreamExclusivity { SHARED, EXCLUSIVE }; class PortAudioHandle { public: @@ -87,7 +84,7 @@ namespace flexasio { PreparedState(const PreparedState&) = delete; PreparedState(PreparedState&&) = delete; - bool IsExclusive() const { return openStreamResult.exclusive; } + StreamExclusivity GetStreamExclusivity() const { return streamWithExclusivity.exclusivity; } bool IsChannelActive(bool isInput, long channel) const; @@ -179,7 +176,11 @@ namespace flexasio { Buffers buffers; const std::vector bufferInfos; - const OpenStreamResult openStreamResult; + struct StreamWithExclusivity final { + Stream stream; + StreamExclusivity exclusivity; + }; + const StreamWithExclusivity streamWithExclusivity; std::optional runningState; ConfigLoader::Watcher configWatcher; @@ -210,7 +211,9 @@ namespace flexasio { long ComputeLatency(long latencyInFrames, bool output, size_t bufferSizeInFrames) const; long ComputeLatencyFromStream(PaStream* stream, bool output, size_t bufferSizeInFrames) const; - OpenStreamResult OpenStream(bool inputEnabled, bool outputEnabled, double sampleRate, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData); + template + decltype(auto) WithStreamParameters(bool inputEnabled, bool outputEnabled, double sampleRate, PaTime suggestedLatency, Functor functor) const; + Stream OpenStream(const StreamParameters&, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData) const; const HWND windowHandle = nullptr; const ConfigLoader configLoader; diff --git a/src/flexasio/FlexASIO/portaudio.cpp b/src/flexasio/FlexASIO/portaudio.cpp index 70580528..9d450bd2 100644 --- a/src/flexasio/FlexASIO/portaudio.cpp +++ b/src/flexasio/FlexASIO/portaudio.cpp @@ -5,6 +5,24 @@ namespace flexasio { + namespace { + + void LogStreamParameters(const StreamParameters& streamParameters) { + Log() << "...input parameters: " << (streamParameters.inputParameters == nullptr ? "none" : DescribeStreamParameters(*streamParameters.inputParameters)); + Log() << "...output parameters: " << (streamParameters.outputParameters == nullptr ? "none" : DescribeStreamParameters(*streamParameters.outputParameters)); + Log() << "...sample rate: " << streamParameters.sampleRate << " Hz"; + } + + } + + void CheckFormatSupported(const StreamParameters& streamParameters) { + Log() << "Checking that PortAudio supports format with..."; + LogStreamParameters(streamParameters); + const auto error = Pa_IsFormatSupported(streamParameters.inputParameters, streamParameters.outputParameters, streamParameters.sampleRate); + if (error != paFormatIsSupported) throw std::runtime_error(std::string("PortAudio does not support format: ") + Pa_GetErrorText(error)); + Log() << "Format is supported"; + } + void StreamDeleter::operator()(PaStream* stream) throw() { Log() << "Closing PortAudio stream " << stream; const auto error = Pa_CloseStream(stream); @@ -12,16 +30,14 @@ namespace flexasio { Log() << "Unable to close PortAudio stream: " << Pa_GetErrorText(error); } - Stream OpenStream(const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData) { + Stream OpenStream(const StreamParameters& streamParameters, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData) { Log() << "Opening PortAudio stream with..."; - Log() << "...input parameters: " << (inputParameters == nullptr ? "none" : DescribeStreamParameters(*inputParameters)); - Log() << "...output parameters: " << (outputParameters == nullptr ? "none" : DescribeStreamParameters(*outputParameters)); - Log() << "...sample rate: " << sampleRate << " Hz"; + LogStreamParameters(streamParameters); Log() << "...frames per buffer: " << framesPerBuffer; Log() << "...stream flags: " << GetStreamFlagsString(streamFlags); Log() << "...stream callback: " << streamCallback << " (user data " << userData << ")"; PaStream* stream = nullptr; - const auto error = Pa_OpenStream(&stream, inputParameters, outputParameters, sampleRate, framesPerBuffer, streamFlags, streamCallback, userData); + const auto error = Pa_OpenStream(&stream, streamParameters.inputParameters, streamParameters.outputParameters, streamParameters.sampleRate, framesPerBuffer, streamFlags, streamCallback, userData); if (error != paNoError) throw std::runtime_error(std::string("unable to open PortAudio stream: ") + Pa_GetErrorText(error)); if (stream == nullptr)throw std::runtime_error("Pa_OpenStream() unexpectedly returned null"); Log() << "PortAudio stream opened: " << stream; diff --git a/src/flexasio/FlexASIO/portaudio.h b/src/flexasio/FlexASIO/portaudio.h index 84d4d66e..8d147ede 100644 --- a/src/flexasio/FlexASIO/portaudio.h +++ b/src/flexasio/FlexASIO/portaudio.h @@ -6,11 +6,19 @@ namespace flexasio { + struct StreamParameters final { + PaStreamParameters* inputParameters; + PaStreamParameters* outputParameters; + double sampleRate; + }; + + void CheckFormatSupported(const StreamParameters&); + struct StreamDeleter { void operator()(PaStream*) throw(); }; using Stream = std::unique_ptr; - Stream OpenStream(const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData); + Stream OpenStream(const StreamParameters&, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData); struct StreamStopper { void operator()(PaStream*) throw();