diff --git a/src/audio_utils.hpp b/src/audio_utils.hpp index cbe9dae..07e66f7 100644 --- a/src/audio_utils.hpp +++ b/src/audio_utils.hpp @@ -43,7 +43,10 @@ struct StreamParams { std::string device_name; int sample_rate; int num_channels; - double latency_seconds; + // Hint passed to PortAudio when opening the stream. PortAudio may choose the closest + // viable latency instead, rounding up to the next practical value. + // Use get_stream_latency() after opening the stream to retrieve the actual latency PortAudio selected. + double suggested_latency_seconds; bool is_input; PaStreamCallback* callback; void* user_data; // Points to AudioStreamContext* or PlaybackBuffer* @@ -171,9 +174,9 @@ inline StreamParams setupStreamFromConfig(const ConfigParams& params, const double default_latency = (direction == StreamDirection::Input) ? deviceInfo->defaultLowInputLatency : deviceInfo->defaultLowOutputLatency; - stream_params.latency_seconds = params.latency_ms.has_value() ? params.latency_ms.value() / 1000.0 : default_latency; + stream_params.suggested_latency_seconds = params.latency_ms.has_value() ? params.latency_ms.value() / 1000.0 : default_latency; - VIAM_SDK_LOG(debug) << "[setupStreamFromConfig] Using latency " << stream_params.latency_seconds; + VIAM_SDK_LOG(debug) << "[setupStreamFromConfig] Using latency " << stream_params.suggested_latency_seconds; // Validate num_channels against device's max channels const int max_channels = (direction == StreamDirection::Input) ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; @@ -202,7 +205,7 @@ inline void openStream(PaStream*& stream, const StreamParams& params, const audi stream_params.device = params.device_index; stream_params.channelCount = params.num_channels; stream_params.sampleFormat = paInt16; - stream_params.suggestedLatency = params.latency_seconds; + stream_params.suggestedLatency = params.suggested_latency_seconds; stream_params.hostApiSpecificStreamInfo = nullptr; // Determine which parameter is input and which is output @@ -218,7 +221,7 @@ inline void openStream(PaStream*& stream, const StreamParams& params, const audi << " - Sample rate: " << params.sample_rate << " Hz\n" << " - Channels: " << params.num_channels << "\n" << " - Format: 16-bit PCM\n" - << " - Latency: " << params.latency_seconds << " seconds"; + << " - Latency: " << params.suggested_latency_seconds << " seconds"; VIAM_SDK_LOG(error) << buffer.str(); throw std::runtime_error(buffer.str()); } @@ -280,6 +283,18 @@ inline void shutdown_stream(PaStream* stream, const audio::portaudio::PortAudioI } } +// Returns the actual latency reported by PortAudio after stream open (inputLatency for +// input streams, outputLatency for output streams). Falls back to suggested_latency_seconds +// if stream info is unavailable. +inline double get_stream_latency(PaStream* stream, const StreamParams& params, const audio::portaudio::PortAudioInterface* pa = nullptr) { + audio::portaudio::RealPortAudio real_pa; + const audio::portaudio::PortAudioInterface& audio_interface = pa ? *pa : real_pa; + const PaStreamInfo* stream_info = audio_interface.getStreamInfo(stream); + if (!stream_info) + return params.suggested_latency_seconds; + return params.is_input ? stream_info->inputLatency : stream_info->outputLatency; +} + inline void restart_stream(PaStream*& stream, const StreamParams& params, const audio::portaudio::PortAudioInterface* pa = nullptr) { // In production pa is nullptr and real_pa is used. For testing, pa is the mock pa audio::portaudio::RealPortAudio real_pa; diff --git a/src/microphone.cpp b/src/microphone.cpp index 556b1ae..923b839 100644 --- a/src/microphone.cpp +++ b/src/microphone.cpp @@ -157,11 +157,11 @@ Microphone::Microphone(viam::sdk::Dependencies deps, viam::sdk::ResourceConfig c requested_sample_rate_ = setup.config_params.sample_rate.value_or(setup.stream_params.sample_rate); // User's requested rate, defaults to device rate num_channels_ = setup.stream_params.num_channels; - latency_ = setup.stream_params.latency_seconds; audio_context_ = setup.audio_context; historical_throttle_ms_ = setup.config_params.historical_throttle_ms.value_or(DEFAULT_HISTORICAL_THROTTLE_MS); audio::utils::restart_stream(stream_, setup.stream_params, pa_); + latency_ = audio::utils::get_stream_latency(stream_, setup.stream_params, pa_); } } @@ -275,7 +275,7 @@ void Microphone::reconfigure(const viam::sdk::Dependencies& deps, const viam::sd requested_sample_rate_ = setup.config_params.sample_rate.value_or( setup.stream_params.sample_rate); // User's requested rate, defaults to device rate num_channels_ = setup.stream_params.num_channels; - latency_ = setup.stream_params.latency_seconds; + latency_ = audio::utils::get_stream_latency(stream_, setup.stream_params, pa_); audio_context_ = setup.audio_context; historical_throttle_ms_ = setup.config_params.historical_throttle_ms.value_or(DEFAULT_HISTORICAL_THROTTLE_MS); } diff --git a/src/speaker.cpp b/src/speaker.cpp index c408ea5..8d8e54e 100644 --- a/src/speaker.cpp +++ b/src/speaker.cpp @@ -30,9 +30,9 @@ Speaker::Speaker(viam::sdk::Dependencies deps, viam::sdk::ResourceConfig cfg, au device_name_ = setup.stream_params.device_name; sample_rate_ = setup.stream_params.sample_rate; num_channels_ = setup.stream_params.num_channels; - latency_ = setup.stream_params.latency_seconds; audio_context_ = setup.audio_context; audio::utils::restart_stream(stream_, setup.stream_params, pa_); + latency_ = audio::utils::get_stream_latency(stream_, setup.stream_params, pa_); volume_ = setup.config_params.volume; if (volume_) { audio::volume::set_volume(device_name_, *volume_); @@ -390,7 +390,7 @@ void Speaker::reconfigure(const vsdk::Dependencies& deps, const vsdk::ResourceCo device_name_ = setup.stream_params.device_name; sample_rate_ = setup.stream_params.sample_rate; num_channels_ = setup.stream_params.num_channels; - latency_ = setup.stream_params.latency_seconds; + latency_ = audio::utils::get_stream_latency(stream_, setup.stream_params, pa_); audio_context_ = setup.audio_context; volume_ = setup.config_params.volume; if (volume_) { diff --git a/test/audio_utils_test.cpp b/test/audio_utils_test.cpp index 2fc3022..9cd006d 100644 --- a/test/audio_utils_test.cpp +++ b/test/audio_utils_test.cpp @@ -162,7 +162,7 @@ TEST_F(AudioUtilsTest, SetupStreamFromConfigUsesProvidedValues) { EXPECT_EQ(stream_params.sample_rate, 44100); EXPECT_EQ(stream_params.num_channels, 2); - EXPECT_DOUBLE_EQ(stream_params.latency_seconds, 0.1); + EXPECT_DOUBLE_EQ(stream_params.suggested_latency_seconds, 0.1); } TEST_F(AudioUtilsTest, SetupStreamFromConfigOutputDirection) { @@ -364,6 +364,48 @@ TEST_F(AudioUtilsTest, SetupAudioDeviceUsesConfigParams) { EXPECT_EQ(setup.config_params.device_name, "My Device"); } +TEST_F(AudioUtilsTest, GetStreamLatencyFallsBackWhenStreamInfoNull) { + using ::testing::Return; + ON_CALL(*mock_pa_, getStreamInfo(::testing::_)).WillByDefault(Return(nullptr)); + + audio::utils::StreamParams params; + params.suggested_latency_seconds = 0.05; + params.is_input = false; + + double latency = audio::utils::get_stream_latency(nullptr, params, mock_pa_.get()); + EXPECT_DOUBLE_EQ(latency, 0.05); +} + +TEST_F(AudioUtilsTest, GetStreamLatencyReturnsOutputLatency) { + using ::testing::Return; + PaStreamInfo stream_info; + stream_info.inputLatency = 0.02; + stream_info.outputLatency = 0.04; + ON_CALL(*mock_pa_, getStreamInfo(::testing::_)).WillByDefault(Return(&stream_info)); + + audio::utils::StreamParams params; + params.suggested_latency_seconds = 0.01; + params.is_input = false; + + double latency = audio::utils::get_stream_latency(nullptr, params, mock_pa_.get()); + EXPECT_DOUBLE_EQ(latency, 0.04); +} + +TEST_F(AudioUtilsTest, GetStreamLatencyReturnsInputLatency) { + using ::testing::Return; + PaStreamInfo stream_info; + stream_info.inputLatency = 0.02; + stream_info.outputLatency = 0.04; + ON_CALL(*mock_pa_, getStreamInfo(::testing::_)).WillByDefault(Return(&stream_info)); + + audio::utils::StreamParams params; + params.suggested_latency_seconds = 0.01; + params.is_input = true; + + double latency = audio::utils::get_stream_latency(nullptr, params, mock_pa_.get()); + EXPECT_DOUBLE_EQ(latency, 0.02); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); ::testing::AddGlobalTestEnvironment(new test_utils::AudioTestEnvironment);