Skip to content

Commit 59f75ce

Browse files
committed
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
1 parent e235cc5 commit 59f75ce

File tree

4 files changed

+108
-43
lines changed

4 files changed

+108
-43
lines changed

src/flexasio/FlexASIO/flexasio.cpp

+68-30
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ namespace flexasio {
237237
value = static_cast<Enum>(std::underlying_type_t<Enum>(value) + 1);
238238
}
239239

240+
PaTime GetDefaultSuggestedLatency(long bufferSizeInFrames, ASIOSampleRate sampleRate) {
241+
return 3 * bufferSizeInFrames / sampleRate;
242+
}
243+
240244
}
241245

242246
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 {
533537
Log() << "Returning: " << info->name << ", " << (info->isActive ? "active" : "inactive") << ", group " << info->channelGroup << ", type " << ::dechamps_ASIOUtil::GetASIOSampleTypeString(info->type);
534538
}
535539

536-
FlexASIO::OpenStreamResult FlexASIO::OpenStream(bool inputEnabled, bool outputEnabled, double sampleRate, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData)
540+
template <typename Functor>
541+
decltype(auto) FlexASIO::WithStreamParameters(bool inputEnabled, bool outputEnabled, double sampleRate, PaTime defaultSuggestedLatency, Functor functor) const
537542
{
538-
Log() << "CFlexASIO::OpenStream(inputEnabled = " << inputEnabled << ", outputEnabled = " << outputEnabled << ", sampleRate = " << sampleRate << ", framesPerBuffer = " << framesPerBuffer << ", callback = " << callback << ", callbackUserData = " << callbackUserData << ")";
539-
OpenStreamResult result;
540-
result.exclusive = hostApi.info.type == paWDMKS;
543+
Log() << "FlexASIO::WithStreamParameters(inputEnabled = " << inputEnabled << ", outputEnabled = " << outputEnabled << ", sampleRate = " << sampleRate << ")";
544+
545+
auto exclusivity = hostApi.info.type == paWDMKS ? StreamExclusivity::EXCLUSIVE : StreamExclusivity::SHARED;
541546

542547
PaStreamParameters common_parameters = { 0 };
543548
common_parameters.sampleFormat = paNonInterleaved;
544549
common_parameters.hostApiSpecificStreamInfo = NULL;
545-
common_parameters.suggestedLatency = 3 * framesPerBuffer / sampleRate;
550+
common_parameters.suggestedLatency = defaultSuggestedLatency;
546551

547552
PaWasapiStreamInfo common_wasapi_stream_info = { 0 };
548553
if (hostApi.info.type == paWASAPI) {
@@ -570,7 +575,7 @@ namespace flexasio {
570575
Log() << "Using " << (config.input.wasapiExclusiveMode ? "exclusive" : "shared") << " mode for input WASAPI stream";
571576
if (config.input.wasapiExclusiveMode) {
572577
input_wasapi_stream_info.flags |= paWinWasapiExclusive;
573-
result.exclusive = true;
578+
exclusivity = StreamExclusivity::EXCLUSIVE;
574579
}
575580
Log() << (config.input.wasapiAutoConvert ? "Enabling" : "Disabling") << " auto-conversion for input WASAPI stream";
576581
if (config.input.wasapiAutoConvert) {
@@ -602,7 +607,7 @@ namespace flexasio {
602607
Log() << "Using " << (config.output.wasapiExclusiveMode ? "exclusive" : "shared") << " mode for output WASAPI stream";
603608
if (config.output.wasapiExclusiveMode) {
604609
output_wasapi_stream_info.flags |= paWinWasapiExclusive;
605-
result.exclusive = true;
610+
exclusivity = StreamExclusivity::EXCLUSIVE;
606611
}
607612
Log() << (config.output.wasapiAutoConvert ? "Enabling" : "Disabling") << " auto-conversion for output WASAPI stream";
608613
if (config.output.wasapiAutoConvert) {
@@ -616,20 +621,26 @@ namespace flexasio {
616621
}
617622
}
618623

619-
result.stream = flexasio::OpenStream(
620-
inputEnabled ? &input_parameters : NULL,
621-
outputEnabled ? &output_parameters : NULL,
622-
sampleRate, framesPerBuffer, paPrimeOutputBuffersUsingStreamCallback, callback, callbackUserData);
623-
if (result.stream != nullptr) {
624-
const auto streamInfo = Pa_GetStreamInfo(result.stream.get());
625-
if (streamInfo == nullptr) {
626-
Log() << "Unable to get stream info";
627-
}
628-
else {
629-
Log() << "Stream info: " << DescribeStreamInfo(*streamInfo);
630-
}
624+
return functor(StreamParameters{
625+
.inputParameters = inputEnabled ? &input_parameters : NULL,
626+
.outputParameters = outputEnabled ? &output_parameters : NULL,
627+
.sampleRate = sampleRate,
628+
}, exclusivity);
629+
}
630+
631+
Stream FlexASIO::OpenStream(const StreamParameters& streamParameters, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData) const
632+
{
633+
Log() << "FlexASIO::OpenStream(framesPerBuffer = " << framesPerBuffer << ", callback = " << callback << ", callbackUserData = " << callbackUserData << ")";
634+
auto stream = flexasio::OpenStream(
635+
streamParameters, framesPerBuffer, paPrimeOutputBuffersUsingStreamCallback, callback, callbackUserData);
636+
const auto streamInfo = Pa_GetStreamInfo(stream.get());
637+
if (streamInfo == nullptr) {
638+
Log() << "Unable to get stream info";
631639
}
632-
return result;
640+
else {
641+
Log() << "Stream info: " << DescribeStreamInfo(*streamInfo);
642+
}
643+
return stream;
633644
}
634645

635646
bool FlexASIO::CanSampleRate(ASIOSampleRate sampleRate)
@@ -641,20 +652,25 @@ namespace flexasio {
641652
return false;
642653
}
643654

644-
if (preparedState.has_value() && preparedState->IsExclusive()) {
655+
if (preparedState.has_value() && preparedState->GetStreamExclusivity() == StreamExclusivity::EXCLUSIVE) {
645656
// Some applications will call canSampleRate() while the stream is running. If the stream is exclusive our probes will fail.
646657
// In that case we always say "yes" - always saying "no" confuses applications. See https://github.com/dechamps/FlexASIO/issues/66
658+
// TODO: now that we are using Pa_IsFormatSupported() instead of Pa_OpenStream() to probe sample rates, is this still necessary?
647659
Log() << "Faking sample rate " << sampleRate << " as available because an exclusive stream is currently running";
648660
return true;
649661
}
650662

663+
const auto checkParameters = [&](const StreamParameters& streamParameters, StreamExclusivity) {
664+
CheckFormatSupported(streamParameters);
665+
};
666+
651667
// We do not know whether the host application intends to use only input channels, only output channels, or both.
652668
// This logic ensures the driver is usable for all three use cases.
653669
bool available = false;
654670
if (inputDevice.has_value())
655671
try {
656672
Log() << "Checking if input supports this sample rate";
657-
OpenStream(true, false, sampleRate, paFramesPerBufferUnspecified, NoOpStreamCallback, nullptr);
673+
WithStreamParameters(/*inputEnabled=*/true, /*outputEnabled=*/false, sampleRate, /*suggestedLatency*/0, checkParameters);
658674
Log() << "Input supports this sample rate";
659675
available = true;
660676
}
@@ -664,7 +680,7 @@ namespace flexasio {
664680
if (outputDevice.has_value())
665681
try {
666682
Log() << "Checking if output supports this sample rate";
667-
OpenStream(false, true, sampleRate, paFramesPerBufferUnspecified, NoOpStreamCallback, nullptr);
683+
WithStreamParameters(/*inputEnabled=*/false, /*outputEnabled=*/true, sampleRate, /*suggestedLatency*/0, checkParameters);
668684
Log() << "Output supports this sample rate";
669685
available = true;
670686
}
@@ -778,7 +794,14 @@ namespace flexasio {
778794
bufferInfos.push_back(asioBufferInfo);
779795
}
780796
return bufferInfos;
781-
}()), openStreamResult(flexASIO.OpenStream(buffers.inputChannelCount > 0, buffers.outputChannelCount > 0, sampleRate, unsigned long(bufferSizeInFrames), &PreparedState::StreamCallback, this)),
797+
}()), streamWithExclusivity(flexASIO.WithStreamParameters(
798+
buffers.inputChannelCount > 0, buffers.outputChannelCount > 0, sampleRate, GetDefaultSuggestedLatency(bufferSizeInFrames, sampleRate),
799+
[&](const StreamParameters& streamParameters, StreamExclusivity streamExclusivity) {
800+
return StreamWithExclusivity{
801+
.stream = flexASIO.OpenStream(streamParameters, static_cast<unsigned long>(bufferSizeInFrames), &PreparedState::StreamCallback, this),
802+
.exclusivity = streamExclusivity,
803+
};
804+
})),
782805
configWatcher(flexASIO.configLoader, [this] { OnConfigChange(); }) {
783806
if (callbacks->asioMessage) ProbeHostMessages(callbacks->asioMessage);
784807
}
@@ -808,7 +831,7 @@ namespace flexasio {
808831

809832
long FlexASIO::ComputeLatencyFromStream(PaStream* stream, bool output, size_t bufferSizeInFrames) const
810833
{
811-
const PaStreamInfo* stream_info = Pa_GetStreamInfo(stream);
834+
const PaStreamInfo* stream_info = Pa_GetStreamInfo(&stream);
812835
if (!stream_info) throw ASIOException(ASE_HWMalfunction, "unable to get stream info");
813836

814837
// See https://github.com/dechamps/FlexASIO/issues/10.
@@ -828,11 +851,26 @@ namespace flexasio {
828851
const auto bufferSize = ComputeBufferSizes().preferred;
829852
Log() << "Assuming " << bufferSize << " as the buffer size";
830853

854+
// Since CreateBuffers() has not been called yet, we do not know if the application intends
855+
// to use only input channels, only output channels, or both. We arbitrarily decide to compute
856+
// the input latency assuming an input-only stream, and the output latency assuming an
857+
// output-only stream, because that makes this code least likely to fail. The tradeoff is this
858+
// will likely return wrong latencies for full duplex streams (which tend to have higher
859+
// latency due to the need for buffer adaptation).
860+
861+
const auto getLatency = [&](bool output) {
862+
return WithStreamParameters(
863+
/*inputEnabled=*/!output, /*outputEnabled=*/output, sampleRate, GetDefaultSuggestedLatency(bufferSize, sampleRate),
864+
[&](const StreamParameters& streamParameters, StreamExclusivity) {
865+
return ComputeLatencyFromStream(OpenStream(streamParameters, bufferSize, NoOpStreamCallback, nullptr).get(), output, bufferSize);
866+
});
867+
};
868+
831869
if (!inputDevice.has_value())
832870
*inputLatency = 0;
833871
else
834872
try {
835-
*inputLatency = ComputeLatencyFromStream(OpenStream(true, false, sampleRate, bufferSize, NoOpStreamCallback, nullptr).stream.get(), /*output=*/false, bufferSize);
873+
*inputLatency = getLatency(/*output=*/false);
836874
Log() << "Using input latency from successful stream probe";
837875
}
838876
catch (const std::exception& exception) {
@@ -843,7 +881,7 @@ namespace flexasio {
843881
*outputLatency = 0;
844882
else
845883
try {
846-
*outputLatency = ComputeLatencyFromStream(OpenStream(false, true, sampleRate, bufferSize, NoOpStreamCallback, nullptr).stream.get(), /*output=*/true, bufferSize);
884+
*outputLatency = getLatency(/*output=*/true);
847885
Log() << "Using output latency from successful stream probe";
848886
}
849887
catch (const std::exception& exception) {
@@ -856,8 +894,8 @@ namespace flexasio {
856894

857895
void FlexASIO::PreparedState::GetLatencies(long* inputLatency, long* outputLatency)
858896
{
859-
*inputLatency = flexASIO.ComputeLatencyFromStream(openStreamResult.stream.get(), /*output=*/false, buffers.bufferSizeInFrames);
860-
*outputLatency = flexASIO.ComputeLatencyFromStream(openStreamResult.stream.get(), /*output=*/true, buffers.bufferSizeInFrames);
897+
*inputLatency = flexASIO.ComputeLatencyFromStream(streamWithExclusivity.stream.get(), /*output=*/false, buffers.bufferSizeInFrames);
898+
*outputLatency = flexASIO.ComputeLatencyFromStream(streamWithExclusivity.stream.get(), /*output=*/true, buffers.bufferSizeInFrames);
861899
}
862900

863901
void FlexASIO::Start() {
@@ -885,7 +923,7 @@ namespace flexasio {
885923
hostSupportsOutputReady(preparedState.flexASIO.hostSupportsOutputReady) {}
886924

887925
void FlexASIO::PreparedState::RunningState::RunningState::Start() {
888-
activeStream = StartStream(preparedState.openStreamResult.stream.get());
926+
activeStream = StartStream(preparedState.streamWithExclusivity.stream.get());
889927
}
890928

891929
void FlexASIO::Stop() {

src/flexasio/FlexASIO/flexasio.h

+10-7
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,7 @@ namespace flexasio {
5959
GUID waveSubFormat;
6060
};
6161

62-
struct OpenStreamResult {
63-
Stream stream;
64-
bool exclusive;
65-
};
62+
enum class StreamExclusivity { SHARED, EXCLUSIVE };
6663

6764
class PortAudioHandle {
6865
public:
@@ -87,7 +84,7 @@ namespace flexasio {
8784
PreparedState(const PreparedState&) = delete;
8885
PreparedState(PreparedState&&) = delete;
8986

90-
bool IsExclusive() const { return openStreamResult.exclusive; }
87+
StreamExclusivity GetStreamExclusivity() const { return streamWithExclusivity.exclusivity; }
9188

9289
bool IsChannelActive(bool isInput, long channel) const;
9390

@@ -179,7 +176,11 @@ namespace flexasio {
179176
Buffers buffers;
180177
const std::vector<ASIOBufferInfo> bufferInfos;
181178

182-
const OpenStreamResult openStreamResult;
179+
struct StreamWithExclusivity final {
180+
Stream stream;
181+
StreamExclusivity exclusivity;
182+
};
183+
const StreamWithExclusivity streamWithExclusivity;
183184

184185
std::optional<RunningState> runningState;
185186
ConfigLoader::Watcher configWatcher;
@@ -210,7 +211,9 @@ namespace flexasio {
210211
long ComputeLatency(long latencyInFrames, bool output, size_t bufferSizeInFrames) const;
211212
long ComputeLatencyFromStream(PaStream* stream, bool output, size_t bufferSizeInFrames) const;
212213

213-
OpenStreamResult OpenStream(bool inputEnabled, bool outputEnabled, double sampleRate, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData);
214+
template <typename Functor>
215+
decltype(auto) WithStreamParameters(bool inputEnabled, bool outputEnabled, double sampleRate, PaTime suggestedLatency, Functor functor) const;
216+
Stream OpenStream(const StreamParameters&, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData) const;
214217

215218
const HWND windowHandle = nullptr;
216219
const ConfigLoader configLoader;

src/flexasio/FlexASIO/portaudio.cpp

+21-5
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,39 @@
55

66
namespace flexasio {
77

8+
namespace {
9+
10+
void LogStreamParameters(const StreamParameters& streamParameters) {
11+
Log() << "...input parameters: " << (streamParameters.inputParameters == nullptr ? "none" : DescribeStreamParameters(*streamParameters.inputParameters));
12+
Log() << "...output parameters: " << (streamParameters.outputParameters == nullptr ? "none" : DescribeStreamParameters(*streamParameters.outputParameters));
13+
Log() << "...sample rate: " << streamParameters.sampleRate << " Hz";
14+
}
15+
16+
}
17+
18+
void CheckFormatSupported(const StreamParameters& streamParameters) {
19+
Log() << "Checking that PortAudio supports format with...";
20+
LogStreamParameters(streamParameters);
21+
const auto error = Pa_IsFormatSupported(streamParameters.inputParameters, streamParameters.outputParameters, streamParameters.sampleRate);
22+
if (error != paFormatIsSupported) throw std::runtime_error(std::string("PortAudio does not support format: ") + Pa_GetErrorText(error));
23+
Log() << "Format is supported";
24+
}
25+
826
void StreamDeleter::operator()(PaStream* stream) throw() {
927
Log() << "Closing PortAudio stream " << stream;
1028
const auto error = Pa_CloseStream(stream);
1129
if (error != paNoError)
1230
Log() << "Unable to close PortAudio stream: " << Pa_GetErrorText(error);
1331
}
1432

15-
Stream OpenStream(const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData) {
33+
Stream OpenStream(const StreamParameters& streamParameters, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData) {
1634
Log() << "Opening PortAudio stream with...";
17-
Log() << "...input parameters: " << (inputParameters == nullptr ? "none" : DescribeStreamParameters(*inputParameters));
18-
Log() << "...output parameters: " << (outputParameters == nullptr ? "none" : DescribeStreamParameters(*outputParameters));
19-
Log() << "...sample rate: " << sampleRate << " Hz";
35+
LogStreamParameters(streamParameters);
2036
Log() << "...frames per buffer: " << framesPerBuffer;
2137
Log() << "...stream flags: " << GetStreamFlagsString(streamFlags);
2238
Log() << "...stream callback: " << streamCallback << " (user data " << userData << ")";
2339
PaStream* stream = nullptr;
24-
const auto error = Pa_OpenStream(&stream, inputParameters, outputParameters, sampleRate, framesPerBuffer, streamFlags, streamCallback, userData);
40+
const auto error = Pa_OpenStream(&stream, streamParameters.inputParameters, streamParameters.outputParameters, streamParameters.sampleRate, framesPerBuffer, streamFlags, streamCallback, userData);
2541
if (error != paNoError) throw std::runtime_error(std::string("unable to open PortAudio stream: ") + Pa_GetErrorText(error));
2642
if (stream == nullptr)throw std::runtime_error("Pa_OpenStream() unexpectedly returned null");
2743
Log() << "PortAudio stream opened: " << stream;

src/flexasio/FlexASIO/portaudio.h

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66

77
namespace flexasio {
88

9+
struct StreamParameters final {
10+
PaStreamParameters* inputParameters;
11+
PaStreamParameters* outputParameters;
12+
double sampleRate;
13+
};
14+
15+
void CheckFormatSupported(const StreamParameters&);
16+
917
struct StreamDeleter {
1018
void operator()(PaStream*) throw();
1119
};
1220
using Stream = std::unique_ptr<PaStream, StreamDeleter>;
13-
Stream OpenStream(const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData);
21+
Stream OpenStream(const StreamParameters&, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData);
1422

1523
struct StreamStopper {
1624
void operator()(PaStream*) throw();

0 commit comments

Comments
 (0)