Skip to content

Commit

Permalink
Use Pa_IsFormatSupported in CanSampleRate
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dechamps committed May 26, 2024
1 parent e235cc5 commit 59f75ce
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 43 deletions.
98 changes: 68 additions & 30 deletions src/flexasio/FlexASIO/flexasio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ namespace flexasio {
value = static_cast<Enum>(std::underlying_type_t<Enum>(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 };
Expand Down Expand Up @@ -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 <typename Functor>
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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<unsigned long>(bufferSizeInFrames), &PreparedState::StreamCallback, this),
.exclusivity = streamExclusivity,
};
})),
configWatcher(flexASIO.configLoader, [this] { OnConfigChange(); }) {
if (callbacks->asioMessage) ProbeHostMessages(callbacks->asioMessage);
}
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down
17 changes: 10 additions & 7 deletions src/flexasio/FlexASIO/flexasio.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ namespace flexasio {
GUID waveSubFormat;
};

struct OpenStreamResult {
Stream stream;
bool exclusive;
};
enum class StreamExclusivity { SHARED, EXCLUSIVE };

class PortAudioHandle {
public:
Expand All @@ -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;

Expand Down Expand Up @@ -179,7 +176,11 @@ namespace flexasio {
Buffers buffers;
const std::vector<ASIOBufferInfo> bufferInfos;

const OpenStreamResult openStreamResult;
struct StreamWithExclusivity final {
Stream stream;
StreamExclusivity exclusivity;
};
const StreamWithExclusivity streamWithExclusivity;

std::optional<RunningState> runningState;
ConfigLoader::Watcher configWatcher;
Expand Down Expand Up @@ -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 <typename Functor>
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;
Expand Down
26 changes: 21 additions & 5 deletions src/flexasio/FlexASIO/portaudio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,39 @@

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);
if (error != paNoError)
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;
Expand Down
10 changes: 9 additions & 1 deletion src/flexasio/FlexASIO/portaudio.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<PaStream, StreamDeleter>;
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();
Expand Down

0 comments on commit 59f75ce

Please sign in to comment.