From 930924496f966807b69f4f504f864c26c476009e Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 12:31:21 +0300 Subject: [PATCH 1/9] Stereo Prep Made the voice-processing bypass flag mutable so the new setter can update it without tripping the compiler (sdk/objc/native/src/audio/voice_processing_audio_unit.h:88, sdk/objc/native/src/audio/voice_processing_audio_unit.h:147). Annotated SetupAudioBuffersForActiveAudioSession, CreateAudioUnit, and the bypass helper to run on the ADM thread and added runtime RTC_DCHECK_RUN_ON guards, clearing the thread-safety diagnostics (sdk/objc/native/src/audio/audio_device_ios.h:201, sdk/objc/native/src/audio/audio_device_ios.h:204, sdk/objc/native/src/audio/audio_device_ios.h:212, sdk/objc/native/src/audio/audio_device_ios.mm:848, sdk/objc/native/src/audio/audio_device_ios.mm:909). --- sdk/objc/native/api/audio_device_module.h | 3 +- sdk/objc/native/src/audio/audio_device_ios.h | 15 +++++-- sdk/objc/native/src/audio/audio_device_ios.mm | 39 +++++++++++++++++-- .../src/audio/voice_processing_audio_unit.h | 7 +++- .../src/audio/voice_processing_audio_unit.mm | 8 ++++ 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/sdk/objc/native/api/audio_device_module.h b/sdk/objc/native/api/audio_device_module.h index 0ee04d4bf6..bcd9627b24 100644 --- a/sdk/objc/native/api/audio_device_module.h +++ b/sdk/objc/native/api/audio_device_module.h @@ -19,7 +19,8 @@ namespace webrtc { // If `bypass_voice_processing` is true, WebRTC will attempt to disable hardware -// audio processing on iOS. +// audio processing on iOS. Stereo capture is automatically configured to bypass +// voice processing even if this parameter is left as false. // Warning: Setting `bypass_voice_processing` will have unpredictable // consequences for the audio path in the device. It is not advisable to use in // most scenarios. diff --git a/sdk/objc/native/src/audio/audio_device_ios.h b/sdk/objc/native/src/audio/audio_device_ios.h index 9f1ed71aaa..db696e511e 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.h +++ b/sdk/objc/native/src/audio/audio_device_ios.h @@ -146,6 +146,9 @@ class AudioDeviceIOS : public AudioDeviceGeneric, int32_t SetStereoPlayout(bool enable) override; int32_t StereoPlayout(bool& enabled) const override; int32_t StereoRecordingIsAvailable(bool& available) override; + // Enabling stereo recording automatically bypasses the hardware voice + // processing (AEC/AGC) stage since the Voice Processing I/O unit only + // supports mono processing. See UpdateVoiceProcessingBypassRequirement(). int32_t SetStereoRecording(bool enable) override; int32_t StereoRecording(bool& enabled) const override; @@ -195,14 +198,19 @@ class AudioDeviceIOS : public AudioDeviceGeneric, // This method asks for the current hardware parameters and takes actions // if they should differ from what we have asked for initially. It also // defines `playout_parameters_` and `record_parameters_`. - void SetupAudioBuffersForActiveAudioSession(); + void SetupAudioBuffersForActiveAudioSession() RTC_RUN_ON(thread_); // Creates the audio unit. - bool CreateAudioUnit(); + bool CreateAudioUnit() RTC_RUN_ON(thread_); // Updates the audio unit state based on current state. void UpdateAudioUnit(bool can_play_or_record); + // Ensures the hardware voice-processing stage is bypassed when multi-channel + // playout or recording is requested. If the audio unit is already running, + // the updated state will be applied on the next initialization. + void UpdateVoiceProcessingBypassRequirement() RTC_RUN_ON(thread_); + // Configures the audio session for WebRTC. bool ConfigureAudioSession(); @@ -223,7 +231,8 @@ class AudioDeviceIOS : public AudioDeviceGeneric, void PrepareForNewStart(); // Determines whether voice processing should be enabled or disabled. - const bool bypass_voice_processing_; + const bool default_bypass_voice_processing_; + bool bypass_voice_processing_ RTC_GUARDED_BY(thread_); // Handle a user speaking during muted event AudioDeviceModule::MutedSpeechEventHandler muted_speech_event_handler_; diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 48f75d5c54..4987b7a4bd 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -99,7 +99,8 @@ static void LogDeviceInfo() { bool bypass_voice_processing, AudioDeviceModule::MutedSpeechEventHandler muted_speech_event_handler, AudioDeviceIOSRenderErrorHandler render_error_handler) - : bypass_voice_processing_(bypass_voice_processing), + : default_bypass_voice_processing_(bypass_voice_processing), + bypass_voice_processing_(bypass_voice_processing), muted_speech_event_handler_(muted_speech_event_handler), render_error_handler_(render_error_handler), disregard_next_render_error_(false), @@ -172,6 +173,7 @@ static void LogDeviceInfo() { // we will always tell the I/O audio unit to do a channel format conversion // to guarantee mono on the "input side" of the audio unit. UpdateAudioDeviceBuffer(); + UpdateVoiceProcessingBypassRequirement(); initialized_ = true; return InitStatus::OK; } @@ -797,6 +799,33 @@ static void LogDeviceInfo() { return true; } +void AudioDeviceIOS::UpdateVoiceProcessingBypassRequirement() { + RTC_DCHECK_RUN_ON(thread_); + + const bool multi_channel_requested = + playout_parameters_.channels() > 1 || record_parameters_.channels() > 1; + const bool desired_bypass = + default_bypass_voice_processing_ || multi_channel_requested; + + if (desired_bypass == bypass_voice_processing_) { + return; + } + + RTC_LOG(LS_INFO) << "Voice processing " + << (desired_bypass ? "will be bypassed" : "remains enabled") + << (multi_channel_requested + ? " because multi-channel I/O is requested." + : " as requested by the application."); + + bypass_voice_processing_ = desired_bypass; + + if (audio_unit_) { + audio_unit_->SetBypassVoiceProcessing(desired_bypass); + RTC_LOG(LS_INFO) << "Voice-processing bypass update will take effect the " + "next time the audio unit is initialized."; + } +} + void AudioDeviceIOS::UpdateAudioDeviceBuffer() { LOGI() << "UpdateAudioDevicebuffer"; // AttachAudioBuffer() is called at construction by the main class but check @@ -804,8 +833,8 @@ static void LogDeviceInfo() { RTC_DCHECK(audio_device_buffer_) << "AttachAudioBuffer must be called first"; RTC_DCHECK_GT(playout_parameters_.sample_rate(), 0); RTC_DCHECK_GT(record_parameters_.sample_rate(), 0); - RTC_DCHECK_EQ(playout_parameters_.channels(), 1); - RTC_DCHECK_EQ(record_parameters_.channels(), 1); + RTC_DCHECK_GT(playout_parameters_.channels(), 0); + RTC_DCHECK_GT(record_parameters_.channels(), 0); // Inform the audio device buffer (ADB) about the new audio format. audio_device_buffer_->SetPlayoutSampleRate(playout_parameters_.sample_rate()); audio_device_buffer_->SetPlayoutChannels(playout_parameters_.channels()); @@ -816,6 +845,7 @@ static void LogDeviceInfo() { void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() { LOGI() << "SetupAudioBuffersForActiveAudioSession"; + RTC_DCHECK_RUN_ON(thread_); // Verify the current values once the audio session has been activated. RTC_OBJC_TYPE(RTCAudioSession)* session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; @@ -863,6 +893,8 @@ static void LogDeviceInfo() { RTC_DCHECK_EQ(playout_parameters_.GetBytesPerBuffer(), record_parameters_.GetBytesPerBuffer()); + UpdateVoiceProcessingBypassRequirement(); + // Update the ADB parameters since the sample rate might have changed. UpdateAudioDeviceBuffer(); @@ -874,6 +906,7 @@ static void LogDeviceInfo() { } bool AudioDeviceIOS::CreateAudioUnit() { + RTC_DCHECK_RUN_ON(thread_); RTC_DCHECK(!audio_unit_); RTC_DCHECK(!playout_is_initialized_ && !recording_is_initialized_); if (audio_unit_ || playout_is_initialized_ || recording_is_initialized_) { diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.h b/sdk/objc/native/src/audio/voice_processing_audio_unit.h index fe3c87096e..5c394880f0 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.h +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.h @@ -83,6 +83,11 @@ class VoiceProcessingAudioUnit { // Initializes the underlying audio unit with the given sample rate. bool Initialize(Float64 sample_rate, bool enable_input); + // Updates whether the hardware voice-processing stage should be bypassed. + // The new value is applied the next time Initialize() is invoked. + void SetBypassVoiceProcessing(bool bypass); + bool bypass_voice_processing() const { return bypass_voice_processing_; } + // Starts the underlying audio unit. OSStatus Start(); @@ -139,7 +144,7 @@ class VoiceProcessingAudioUnit { // Deletes the underlying audio unit. void DisposeAudioUnit(); - const bool bypass_voice_processing_; + bool bypass_voice_processing_; const bool detect_mute_speech_; VoiceProcessingAudioUnitObserver* observer_; AudioUnit vpio_unit_; diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm index 238693de99..32f51e0899 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm @@ -212,6 +212,14 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { return state_; } +void VoiceProcessingAudioUnit::SetBypassVoiceProcessing(bool bypass) { + if (bypass_voice_processing_ == bypass) { + return; + } + bypass_voice_processing_ = bypass; + RTCLog(@"Voice processing bypass state updated to %d", bypass); +} + bool VoiceProcessingAudioUnit::Initialize(Float64 sample_rate, bool enable_input) { RTC_DCHECK_GE(state_, kUninitialized); RTCLog(@"Initializing audio unit with sample rate: %f", sample_rate); From 0a94e916fda04c2bc4cdd84e1febb872d096e200 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 12:47:38 +0300 Subject: [PATCH 2/9] Step 2 Stereo toggles now track per-direction channel counts, push the updated preferences into the shared audio-session config, and drive the bypass helper based on what callers request. sdk/objc/native/src/audio/audio_device_ios.mm:155 caches desired output/input channels from RTCAudioSessionConfiguration, pushes them back through UpdateAudioSessionChannelPreferences() (sdk/objc/native/src/audio/audio_device_ios.mm:841), and bases bypass decisions on the max of requested and active counts (sdk/objc/native/src/audio/audio_device_ios.mm:813). sdk/objc/native/src/audio/audio_device_ios.mm:905 reads the actual AVAudioSession channel counts when the session comes up, logs any fallback, and resets AudioParameters so the buffer wiring follows hardware limits. sdk/objc/native/src/audio/audio_device_ios.mm:1313 and sdk/objc/native/src/audio/audio_device_ios.mm:1370 implement stereo availability/toggle paths that update desired counts, refresh the audio device buffer, and recreate the fine buffer when needed. sdk/objc/native/src/audio/audio_device_module_ios.mm:340 lets ADM callers flip stereo modes directly, deferring buffer updates and bypass decisions to the backend instead of short-circuiting. --- sdk/objc/native/src/audio/audio_device_ios.h | 7 + sdk/objc/native/src/audio/audio_device_ios.mm | 153 ++++++++++++++++-- .../src/audio/audio_device_module_ios.mm | 16 +- 3 files changed, 149 insertions(+), 27 deletions(-) diff --git a/sdk/objc/native/src/audio/audio_device_ios.h b/sdk/objc/native/src/audio/audio_device_ios.h index db696e511e..22d57e9f9f 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.h +++ b/sdk/objc/native/src/audio/audio_device_ios.h @@ -211,6 +211,11 @@ class AudioDeviceIOS : public AudioDeviceGeneric, // the updated state will be applied on the next initialization. void UpdateVoiceProcessingBypassRequirement() RTC_RUN_ON(thread_); + // Updates the preferred channel counts stored in + // RTCAudioSessionConfiguration so forthcoming session activations request + // the desired layout. + void UpdateAudioSessionChannelPreferences() RTC_RUN_ON(thread_); + // Configures the audio session for WebRTC. bool ConfigureAudioSession(); @@ -233,6 +238,8 @@ class AudioDeviceIOS : public AudioDeviceGeneric, // Determines whether voice processing should be enabled or disabled. const bool default_bypass_voice_processing_; bool bypass_voice_processing_ RTC_GUARDED_BY(thread_); + size_t desired_playout_channels_ RTC_GUARDED_BY(thread_); + size_t desired_record_channels_ RTC_GUARDED_BY(thread_); // Handle a user speaking during muted event AudioDeviceModule::MutedSpeechEventHandler muted_speech_event_handler_; diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 4987b7a4bd..3e48be107a 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -14,6 +14,7 @@ #include "audio_device_ios.h" #include +#include #include #include "api/array_view.h" @@ -101,6 +102,8 @@ static void LogDeviceInfo() { AudioDeviceIOSRenderErrorHandler render_error_handler) : default_bypass_voice_processing_(bypass_voice_processing), bypass_voice_processing_(bypass_voice_processing), + desired_playout_channels_(1), + desired_record_channels_(1), muted_speech_event_handler_(muted_speech_event_handler), render_error_handler_(render_error_handler), disregard_next_render_error_(false), @@ -166,13 +169,18 @@ static void LogDeviceInfo() { // store the parameters now and then verify at a later stage. RTC_OBJC_TYPE(RTCAudioSessionConfiguration)* config = [RTC_OBJC_TYPE(RTCAudioSessionConfiguration) webRTCConfiguration]; - playout_parameters_.reset(config.sampleRate, config.outputNumberOfChannels); - record_parameters_.reset(config.sampleRate, config.inputNumberOfChannels); + desired_playout_channels_ = static_cast( + std::max(1, config.outputNumberOfChannels)); + desired_record_channels_ = static_cast( + std::max(1, config.inputNumberOfChannels)); + playout_parameters_.reset(config.sampleRate, desired_playout_channels_); + record_parameters_.reset(config.sampleRate, desired_record_channels_); // Ensure that the audio device buffer (ADB) knows about the internal audio // parameters. Note that, even if we are unable to get a mono audio session, // we will always tell the I/O audio unit to do a channel format conversion // to guarantee mono on the "input side" of the audio unit. UpdateAudioDeviceBuffer(); + UpdateAudioSessionChannelPreferences(); UpdateVoiceProcessingBypassRequirement(); initialized_ = true; return InitStatus::OK; @@ -802,8 +810,12 @@ static void LogDeviceInfo() { void AudioDeviceIOS::UpdateVoiceProcessingBypassRequirement() { RTC_DCHECK_RUN_ON(thread_); - const bool multi_channel_requested = - playout_parameters_.channels() > 1 || record_parameters_.channels() > 1; + const size_t effective_playout_channels = + std::max(playout_parameters_.channels(), desired_playout_channels_); + const size_t effective_record_channels = + std::max(record_parameters_.channels(), desired_record_channels_); + const bool multi_channel_requested = effective_playout_channels > 1 || + effective_record_channels > 1; const bool desired_bypass = default_bypass_voice_processing_ || multi_channel_requested; @@ -826,6 +838,16 @@ static void LogDeviceInfo() { } } +void AudioDeviceIOS::UpdateAudioSessionChannelPreferences() { + RTC_DCHECK_RUN_ON(thread_); + + RTC_OBJC_TYPE(RTCAudioSessionConfiguration)* config = + [RTC_OBJC_TYPE(RTCAudioSessionConfiguration) webRTCConfiguration]; + config.inputNumberOfChannels = desired_record_channels_; + config.outputNumberOfChannels = desired_playout_channels_; + [RTC_OBJC_TYPE(RTCAudioSessionConfiguration) setWebRTCConfiguration:config]; +} + void AudioDeviceIOS::UpdateAudioDeviceBuffer() { LOGI() << "UpdateAudioDevicebuffer"; // AttachAudioBuffer() is called at construction by the main class but check @@ -880,11 +902,26 @@ static void LogDeviceInfo() { // number of audio frames. // Example: IO buffer size = 0.008 seconds <=> 128 audio frames at 16kHz. // Hence, 128 is the size we expect to see in upcoming render callbacks. - playout_parameters_.reset( - sample_rate, playout_parameters_.channels(), io_buffer_duration); + const size_t output_channels = static_cast( + std::max(1, session.outputNumberOfChannels)); + const size_t input_channels = static_cast( + std::max(1, session.inputNumberOfChannels)); + if (output_channels != desired_playout_channels_) { + RTC_LOG(LS_WARNING) + << "Requested " << desired_playout_channels_ + << " output channel(s) but session configured " << output_channels + << "."; + } + if (input_channels != desired_record_channels_) { + RTC_LOG(LS_WARNING) + << "Requested " << desired_record_channels_ + << " input channel(s) but session configured " << input_channels + << "."; + } + + playout_parameters_.reset(sample_rate, output_channels, io_buffer_duration); RTC_DCHECK(playout_parameters_.is_complete()); - record_parameters_.reset( - sample_rate, record_parameters_.channels(), io_buffer_duration); + record_parameters_.reset(sample_rate, input_channels, io_buffer_duration); RTC_DCHECK(record_parameters_.is_complete()); RTC_LOG(LS_INFO) << " frames per I/O buffer: " << playout_parameters_.frames_per_buffer(); @@ -1265,32 +1302,116 @@ static void LogDeviceInfo() { } int32_t AudioDeviceIOS::StereoRecordingIsAvailable(bool& available) { - available = false; + RTC_DCHECK_RUN_ON(thread_); + + RTC_OBJC_TYPE(RTCAudioSession)* session = + [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; + available = session.maximumInputNumberOfChannels > 1; return 0; } int32_t AudioDeviceIOS::SetStereoRecording(bool enable) { - RTC_LOG_F(LS_WARNING) << "Not implemented"; - return -1; + LOGI() << "SetStereoRecording"; + RTC_DCHECK_RUN_ON(thread_); + + const size_t requested_channels = enable ? 2 : 1; + if (requested_channels == desired_record_channels_) { + return 0; + } + + bool stereo_available = false; + StereoRecordingIsAvailable(stereo_available); + if (enable && !stereo_available) { + RTC_LOG(LS_WARNING) << "Stereo recording is not supported on the current route."; + return -1; + } + + desired_record_channels_ = requested_channels; + + if (initialized_) { + if (record_parameters_.is_complete()) { + record_parameters_.reset(record_parameters_.sample_rate(), + requested_channels, + record_parameters_.frames_per_buffer()); + } else if (record_parameters_.is_valid()) { + record_parameters_.reset(record_parameters_.sample_rate(), + requested_channels); + } + } + + UpdateAudioSessionChannelPreferences(); + + if (initialized_) { + UpdateAudioDeviceBuffer(); + if (fine_audio_buffer_) { + fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_)); + } + } + + UpdateVoiceProcessingBypassRequirement(); + return 0; } int32_t AudioDeviceIOS::StereoRecording(bool& enabled) const { - enabled = false; + RTC_DCHECK_RUN_ON(thread_); + enabled = record_parameters_.channels() > 1; return 0; } int32_t AudioDeviceIOS::StereoPlayoutIsAvailable(bool& available) { - available = false; + RTC_DCHECK_RUN_ON(thread_); + + RTC_OBJC_TYPE(RTCAudioSession)* session = + [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; + available = session.maximumOutputNumberOfChannels > 1; return 0; } int32_t AudioDeviceIOS::SetStereoPlayout(bool enable) { - RTC_LOG_F(LS_WARNING) << "Not implemented"; - return -1; + LOGI() << "SetStereoPlayout"; + RTC_DCHECK_RUN_ON(thread_); + + const size_t requested_channels = enable ? 2 : 1; + if (requested_channels == desired_playout_channels_) { + return 0; + } + + bool stereo_available = false; + StereoPlayoutIsAvailable(stereo_available); + if (enable && !stereo_available) { + RTC_LOG(LS_WARNING) << "Stereo playout is not supported on the current route."; + return -1; + } + + desired_playout_channels_ = requested_channels; + + if (initialized_) { + if (playout_parameters_.is_complete()) { + playout_parameters_.reset(playout_parameters_.sample_rate(), + requested_channels, + playout_parameters_.frames_per_buffer()); + } else if (playout_parameters_.is_valid()) { + playout_parameters_.reset(playout_parameters_.sample_rate(), + requested_channels); + } + } + + UpdateAudioSessionChannelPreferences(); + + if (initialized_) { + UpdateAudioDeviceBuffer(); + if (fine_audio_buffer_) { + fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_)); + } + } + + UpdateVoiceProcessingBypassRequirement(); + return 0; } int32_t AudioDeviceIOS::StereoPlayout(bool& enabled) const { - enabled = false; + RTC_DCHECK_RUN_ON(thread_); + enabled = playout_parameters_.channels() > 1; return 0; } diff --git a/sdk/objc/native/src/audio/audio_device_module_ios.mm b/sdk/objc/native/src/audio/audio_device_module_ios.mm index 6d40f503a3..f24db66903 100644 --- a/sdk/objc/native/src/audio/audio_device_module_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_module_ios.mm @@ -340,11 +340,11 @@ int32_t AudioDeviceModuleIOS::SetStereoRecording(bool enable) { RTC_DLOG(LS_INFO) << __FUNCTION__ << "(" << enable << ")"; CHECKinitialized_(); - if (enable) { - RTC_LOG(LS_WARNING) << "recording in stereo is not supported"; + if (audio_device_->SetStereoRecording(enable) == -1) { + ReportError(kStereoRecordingFailed); + return -1; } - ReportError(kStereoRecordingFailed); - return -1; + return 0; } int32_t AudioDeviceModuleIOS::StereoRecording(bool* enabled) const { @@ -382,16 +382,10 @@ ReportError(kStereoPlayoutFailed); return -1; } - if (audio_device_->SetStereoPlayout(enable)) { - RTC_LOG(LS_WARNING) << "stereo playout is not supported"; + if (audio_device_->SetStereoPlayout(enable) == -1) { ReportError(kStereoPlayoutFailed); return -1; } - int8_t nChannels(1); - if (enable) { - nChannels = 2; - } - audio_device_buffer_.get()->SetPlayoutChannels(nChannels); return 0; } From e65b1b61e3543d887f790bc0088d41b7266190e1 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 13:35:26 +0300 Subject: [PATCH 3/9] Stereo Channel Plumbing Stereo Channel Plumbing Tracked desired playout/record channel counts up front and wired them into the audio-session prefs plus bypass logic (sdk/objc/native/src/audio/audio_device_ios.mm:172, sdk/objc/native/src/audio/audio_device_ios.h:214). Added ApplyChannelConfigurationChange() to tear down/reconfigure/restart the VPIO when toggles flip, with session locking and restart handling (sdk/objc/native/src/audio/audio_device_ios.mm:856). VoiceProcessingAudioUnit now stores per-direction channel counts, reprograms stream formats, and exposes a setter the ADM can call before (re)initializing (sdk/objc/native/src/audio/voice_processing_audio_unit.h:94, sdk/objc/native/src/audio/voice_processing_audio_unit.mm:227, sdk/objc/native/src/audio/voice_processing_audio_unit.mm:576). Recording/playout callbacks size buffers by frames * channels, update silence checks, and stream interleaved PCM to FineAudioBuffer so stereo pipes stay aligned (sdk/objc/native/src/audio/audio_device_ios.mm:428, sdk/objc/native/src/audio/audio_device_ios.mm:492). Stereo setters now validate route capabilities, refresh FineAudioBuffer, call the VPIO channel setter, and roll back cleanly if reconfiguration fails (sdk/objc/native/src/audio/audio_device_ios.mm:1395, sdk/objc/native/src/audio/audio_device_ios.mm:1484). SetupAudioBuffers re-reads hardware channel counts after AVAudioSession activation and pushes them to the VPIO so we adapt if iOS falls back to mono (sdk/objc/native/src/audio/audio_device_ios.mm:980). --- sdk/objc/native/src/audio/audio_device_ios.h | 5 + sdk/objc/native/src/audio/audio_device_ios.mm | 166 ++++++++++++++++-- .../src/audio/voice_processing_audio_unit.h | 13 +- .../src/audio/voice_processing_audio_unit.mm | 56 ++++-- 4 files changed, 216 insertions(+), 24 deletions(-) diff --git a/sdk/objc/native/src/audio/audio_device_ios.h b/sdk/objc/native/src/audio/audio_device_ios.h index 22d57e9f9f..8928f91af7 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.h +++ b/sdk/objc/native/src/audio/audio_device_ios.h @@ -216,6 +216,11 @@ class AudioDeviceIOS : public AudioDeviceGeneric, // the desired layout. void UpdateAudioSessionChannelPreferences() RTC_RUN_ON(thread_); + // Reconfigures the audio session and restarts the audio unit to apply + // updated channel counts while streaming. Returns true on success or when no + // reconfiguration is required. + bool ApplyChannelConfigurationChange() RTC_RUN_ON(thread_); + // Configures the audio session for WebRTC. bool ConfigureAudioSession(); diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 3e48be107a..2172a3e783 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -440,7 +440,9 @@ static void LogDeviceInfo() { // in combination with potential reallocations. // On real iOS devices, the size will only be set once (at first callback). record_audio_buffer_.Clear(); - record_audio_buffer_.SetSize(num_frames); + const size_t record_channels = record_parameters_.channels(); + const size_t record_samples = num_frames * record_channels; + record_audio_buffer_.SetSize(record_samples); // Get audio timestamp for the audio. // The timestamp will not have NTP time epoch, but that will be addressed by @@ -456,9 +458,9 @@ static void LogDeviceInfo() { AudioBufferList audio_buffer_list; audio_buffer_list.mNumberBuffers = 1; AudioBuffer* audio_buffer = &audio_buffer_list.mBuffers[0]; - audio_buffer->mNumberChannels = record_parameters_.channels(); + audio_buffer->mNumberChannels = record_channels; audio_buffer->mDataByteSize = - record_audio_buffer_.size() * VoiceProcessingAudioUnit::kBytesPerSample; + record_samples * VoiceProcessingAudioUnit::kBytesPerSample; audio_buffer->mData = reinterpret_cast(record_audio_buffer_.data()); // Obtain the recorded audio samples by initiating a rendering cycle. @@ -496,14 +498,16 @@ static void LogDeviceInfo() { // Verify 16-bit, noninterleaved mono PCM signal format. RTC_DCHECK_EQ(1, io_data->mNumberBuffers); AudioBuffer* audio_buffer = &io_data->mBuffers[0]; - RTC_DCHECK_EQ(1, audio_buffer->mNumberChannels); + const size_t playout_channels = playout_parameters_.channels(); + RTC_DCHECK_EQ(audio_buffer->mNumberChannels, + static_cast(playout_channels)); // Produce silence and give audio unit a hint about it if playout is not // activated. if (!playing_.load(std::memory_order_acquire)) { const size_t size_in_bytes = audio_buffer->mDataByteSize; RTC_CHECK_EQ(size_in_bytes / VoiceProcessingAudioUnit::kBytesPerSample, - num_frames); + num_frames * playout_channels); *flags |= kAudioUnitRenderAction_OutputIsSilence; memset(static_cast(audio_buffer->mData), 0, size_in_bytes); return noErr; @@ -562,11 +566,12 @@ static void LogDeviceInfo() { // `io_data` destination. fine_audio_buffer_->GetPlayoutData( webrtc::ArrayView(static_cast(audio_buffer->mData), - num_frames), + num_frames * playout_channels), playout_delay_ms); last_hw_output_latency_update_sample_count_ += num_frames; - total_playout_samples_count_.fetch_add(num_frames, std::memory_order_relaxed); + total_playout_samples_count_.fetch_add(num_frames * playout_channels, + std::memory_order_relaxed); total_playout_samples_duration_ms_.fetch_add( num_frames * 1000 / playout_parameters_.sample_rate(), std::memory_order_relaxed); @@ -848,6 +853,77 @@ static void LogDeviceInfo() { [RTC_OBJC_TYPE(RTCAudioSessionConfiguration) setWebRTCConfiguration:config]; } +bool AudioDeviceIOS::ApplyChannelConfigurationChange() { + RTC_DCHECK_RUN_ON(thread_); + + if (!audio_unit_ || !has_configured_session_) { + // Nothing active that requires reconfiguration right now. + return true; + } + + const bool was_started = + audio_unit_->GetState() == VoiceProcessingAudioUnit::kStarted; + const bool was_initialized = + audio_unit_->GetState() >= VoiceProcessingAudioUnit::kInitialized; + + if (was_started) { + RTCLog(@"Stopping audio unit to apply updated channel configuration."); + if (!audio_unit_->Stop()) { + RTCLogError(@"Failed to stop audio unit before channel update."); + } + PrepareForNewStart(); + } + + if (was_initialized) { + audio_unit_->Uninitialize(); + } + + UnconfigureAudioSession(); + + RTC_OBJC_TYPE(RTCAudioSession)* session = + [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; + NSError* error = nil; + [session lockForConfiguration]; + if (![session beginWebRTCSession:&error]) { + RTCLogError(@"Failed to begin WebRTC session during channel update: %@", + error.localizedDescription); + [session unlockForConfiguration]; + return false; + } + + bool configured = ConfigureAudioSessionLocked(); + if (!configured) { + [session endWebRTCSession:nil]; + [session unlockForConfiguration]; + RTCLogError(@"Failed to configure audio session for new channel layout."); + return false; + } + [session unlockForConfiguration]; + + SetupAudioBuffersForActiveAudioSession(); + + if (!audio_unit_->Initialize(playout_parameters_.sample_rate(), + recording_is_initialized_)) { + RTCLogError(@"Failed to initialize audio unit with updated channel layout."); + return false; + } + + if ((playing_.load(std::memory_order_acquire) || + recording_.load(std::memory_order_acquire)) && + audio_unit_->GetState() == VoiceProcessingAudioUnit::kInitialized) { + RTCLog(@"Restarting audio unit after channel update."); + OSStatus result = audio_unit_->Start(); + if (result != noErr) { + [session notifyAudioUnitStartFailedWithError:result]; + RTCLogError(@"Failed to restart audio unit after channel update, reason %d", + result); + return false; + } + } + + return true; +} + void AudioDeviceIOS::UpdateAudioDeviceBuffer() { LOGI() << "UpdateAudioDevicebuffer"; // AttachAudioBuffer() is called at construction by the main class but check @@ -923,12 +999,15 @@ static void LogDeviceInfo() { RTC_DCHECK(playout_parameters_.is_complete()); record_parameters_.reset(sample_rate, input_channels, io_buffer_duration); RTC_DCHECK(record_parameters_.is_complete()); + if (audio_unit_) { + audio_unit_->SetStreamChannelConfiguration(output_channels, input_channels); + } RTC_LOG(LS_INFO) << " frames per I/O buffer: " << playout_parameters_.frames_per_buffer(); - RTC_LOG(LS_INFO) << " bytes per I/O buffer: " + RTC_LOG(LS_INFO) << " playout bytes per I/O buffer: " << playout_parameters_.GetBytesPerBuffer(); - RTC_DCHECK_EQ(playout_parameters_.GetBytesPerBuffer(), - record_parameters_.GetBytesPerBuffer()); + RTC_LOG(LS_INFO) << " recording bytes per I/O buffer: " + << record_parameters_.GetBytesPerBuffer(); UpdateVoiceProcessingBypassRequirement(); @@ -957,6 +1036,9 @@ static void LogDeviceInfo() { return false; } + audio_unit_->SetStreamChannelConfiguration(desired_playout_channels_, + desired_record_channels_); + return true; } @@ -1314,6 +1396,10 @@ static void LogDeviceInfo() { LOGI() << "SetStereoRecording"; RTC_DCHECK_RUN_ON(thread_); + const size_t previous_desired_record = desired_record_channels_; + const AudioParameters previous_playout_parameters = playout_parameters_; + const AudioParameters previous_record_parameters = record_parameters_; + const size_t requested_channels = enable ? 2 : 1; if (requested_channels == desired_record_channels_) { return 0; @@ -1328,6 +1414,12 @@ static void LogDeviceInfo() { desired_record_channels_ = requested_channels; + if (audio_unit_) { + audio_unit_->SetStreamChannelConfiguration( + static_cast(desired_playout_channels_), + static_cast(desired_record_channels_)); + } + if (initialized_) { if (record_parameters_.is_complete()) { record_parameters_.reset(record_parameters_.sample_rate(), @@ -1349,6 +1441,28 @@ static void LogDeviceInfo() { } UpdateVoiceProcessingBypassRequirement(); + if (!ApplyChannelConfigurationChange()) { + desired_record_channels_ = previous_desired_record; + playout_parameters_ = previous_playout_parameters; + record_parameters_ = previous_record_parameters; + UpdateAudioSessionChannelPreferences(); + if (audio_unit_) { + audio_unit_->SetStreamChannelConfiguration( + static_cast(desired_playout_channels_), + static_cast(desired_record_channels_)); + } + if (initialized_) { + UpdateAudioDeviceBuffer(); + if (fine_audio_buffer_) { + fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_)); + } + } + UpdateVoiceProcessingBypassRequirement(); + if (!ApplyChannelConfigurationChange()) { + RTCLogError(@"Failed to restore previous channel configuration after recording update failure."); + } + return -1; + } return 0; } @@ -1371,6 +1485,10 @@ static void LogDeviceInfo() { LOGI() << "SetStereoPlayout"; RTC_DCHECK_RUN_ON(thread_); + const size_t previous_desired_playout = desired_playout_channels_; + const AudioParameters previous_playout_parameters = playout_parameters_; + const AudioParameters previous_record_parameters = record_parameters_; + const size_t requested_channels = enable ? 2 : 1; if (requested_channels == desired_playout_channels_) { return 0; @@ -1385,6 +1503,12 @@ static void LogDeviceInfo() { desired_playout_channels_ = requested_channels; + if (audio_unit_) { + audio_unit_->SetStreamChannelConfiguration( + static_cast(desired_playout_channels_), + static_cast(desired_record_channels_)); + } + if (initialized_) { if (playout_parameters_.is_complete()) { playout_parameters_.reset(playout_parameters_.sample_rate(), @@ -1406,6 +1530,28 @@ static void LogDeviceInfo() { } UpdateVoiceProcessingBypassRequirement(); + if (!ApplyChannelConfigurationChange()) { + desired_playout_channels_ = previous_desired_playout; + playout_parameters_ = previous_playout_parameters; + record_parameters_ = previous_record_parameters; + UpdateAudioSessionChannelPreferences(); + if (audio_unit_) { + audio_unit_->SetStreamChannelConfiguration( + static_cast(desired_playout_channels_), + static_cast(desired_record_channels_)); + } + if (initialized_) { + UpdateAudioDeviceBuffer(); + if (fine_audio_buffer_) { + fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_)); + } + } + UpdateVoiceProcessingBypassRequirement(); + if (!ApplyChannelConfigurationChange()) { + RTCLogError(@"Failed to restore previous channel configuration after playout update failure."); + } + return -1; + } return 0; } diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.h b/sdk/objc/native/src/audio/voice_processing_audio_unit.h index 5c394880f0..2a9d26b41d 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.h +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.h @@ -12,6 +12,7 @@ #define SDK_OBJC_NATIVE_SRC_AUDIO_VOICE_PROCESSING_AUDIO_UNIT_H_ #include +#include namespace webrtc { namespace ios_adm { @@ -88,6 +89,13 @@ class VoiceProcessingAudioUnit { void SetBypassVoiceProcessing(bool bypass); bool bypass_voice_processing() const { return bypass_voice_processing_; } + // Updates the desired channel counts for each direction. The new + // configuration will take effect on the next successful Initialize(). + void SetStreamChannelConfiguration(uint32_t playout_channels, + uint32_t record_channels); + uint32_t playout_channels() const { return playout_channels_; } + uint32_t record_channels() const { return record_channels_; } + // Starts the underlying audio unit. OSStatus Start(); @@ -139,13 +147,16 @@ class VoiceProcessingAudioUnit { // Returns the predetermined format with a specific sample rate. See // implementation file for details on format. - AudioStreamBasicDescription GetFormat(Float64 sample_rate) const; + AudioStreamBasicDescription GetFormat(Float64 sample_rate, + uint32_t channels) const; // Deletes the underlying audio unit. void DisposeAudioUnit(); bool bypass_voice_processing_; const bool detect_mute_speech_; + uint32_t playout_channels_; + uint32_t record_channels_; VoiceProcessingAudioUnitObserver* observer_; AudioUnit vpio_unit_; VoiceProcessingAudioUnit::State state_; diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm index 32f51e0899..a52943d587 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm @@ -10,6 +10,8 @@ #import "voice_processing_audio_unit.h" +#include + #include "rtc_base/checks.h" #include "system_wrappers/include/metrics.h" @@ -79,6 +81,8 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { VoiceProcessingAudioUnitObserver* observer) : bypass_voice_processing_(bypass_voice_processing), detect_mute_speech_(detect_mute_speech), + playout_channels_(1), + record_channels_(1), observer_(observer), vpio_unit_(nullptr), state_(kInitRequired) { @@ -220,15 +224,40 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { RTCLog(@"Voice processing bypass state updated to %d", bypass); } +void VoiceProcessingAudioUnit::SetStreamChannelConfiguration( + uint32_t playout_channels, + uint32_t record_channels) { + uint32_t sanitized_playout = std::max(1, playout_channels); + uint32_t sanitized_record = std::max(1, record_channels); + if (sanitized_playout == playout_channels_ && + sanitized_record == record_channels_) { + return; + } + playout_channels_ = sanitized_playout; + record_channels_ = sanitized_record; + RTCLog(@"Updated VPIO channel configuration. playout=%u record=%u", + static_cast(playout_channels_), + static_cast(record_channels_)); +} + bool VoiceProcessingAudioUnit::Initialize(Float64 sample_rate, bool enable_input) { RTC_DCHECK_GE(state_, kUninitialized); RTCLog(@"Initializing audio unit with sample rate: %f", sample_rate); OSStatus result = noErr; - AudioStreamBasicDescription format = GetFormat(sample_rate); - UInt32 size = sizeof(format); + const uint32_t playout_channels = + std::max(1, playout_channels_); + const uint32_t record_channels = std::max(1, record_channels_); + + AudioStreamBasicDescription playout_format = + GetFormat(sample_rate, playout_channels); + AudioStreamBasicDescription record_format = + GetFormat(sample_rate, record_channels); + UInt32 playout_size = sizeof(playout_format); + UInt32 record_size = sizeof(record_format); #if !defined(NDEBUG) - LogStreamDescription(format); + LogStreamDescription(playout_format); + LogStreamDescription(record_format); #endif UInt32 _enable_input = enable_input ? 1 : 0; @@ -249,8 +278,8 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, - &format, - size); + &record_format, + record_size); if (result != noErr) { RTCLogError(@"Failed to set format on output scope of input bus. " "Error=%ld.", @@ -263,8 +292,8 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, - &format, - size); + &playout_format, + playout_size); if (result != noErr) { RTCLogError(@"Failed to set format on input scope of output bus. " "Error=%ld.", @@ -545,23 +574,24 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { } AudioStreamBasicDescription VoiceProcessingAudioUnit::GetFormat( - Float64 sample_rate) const { + Float64 sample_rate, + uint32_t channels) const { // Set the application formats for input and output: // - use same format in both directions // - avoid resampling in the I/O unit by using the hardware sample rate // - linear PCM => noncompressed audio data format with one frame per packet - // - no need to specify interleaving since only mono is supported + // - audio data is interleaved across channels AudioStreamBasicDescription format; - RTC_DCHECK_EQ(1, RTC_CONSTANT_TYPE(RTCAudioSessionPreferredNumberOfChannels)); format.mSampleRate = sample_rate; format.mFormatID = kAudioFormatLinearPCM; format.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked; - format.mBytesPerPacket = kBytesPerSample; + format.mBytesPerPacket = kBytesPerSample * channels; format.mFramesPerPacket = 1; // uncompressed. - format.mBytesPerFrame = kBytesPerSample; - format.mChannelsPerFrame = RTC_CONSTANT_TYPE(RTCAudioSessionPreferredNumberOfChannels); + format.mBytesPerFrame = kBytesPerSample * channels; + format.mChannelsPerFrame = channels; format.mBitsPerChannel = 8 * kBytesPerSample; + format.mReserved = 0; return format; } From b5953ff1f9c351ea7960627bb6ba421ca45ec534 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 13:38:56 +0300 Subject: [PATCH 4/9] Update ReadMe --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9a93dca1ed..90525926b2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ This repository is a fork of the WebRTC project. The original README can be foun ### Fork Specifics +#### Features + +- Stereo playout/recording on iOS [#58](https://github.com/GetStream/webrtc/pull/58) + #### `.gitignore` Due to the fork specifics, the repo's `.gitignore` has been updated to match the fork's requirements. From 38f9d4f3cf039dbb7e709bd5ef4508cb23effc87 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 14:02:04 +0300 Subject: [PATCH 5/9] Address feedback --- sdk/objc/native/src/audio/audio_device_ios.h | 9 ++ sdk/objc/native/src/audio/audio_device_ios.mm | 95 +++++++++++-------- 2 files changed, 64 insertions(+), 40 deletions(-) diff --git a/sdk/objc/native/src/audio/audio_device_ios.h b/sdk/objc/native/src/audio/audio_device_ios.h index 8928f91af7..128468b2b4 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.h +++ b/sdk/objc/native/src/audio/audio_device_ios.h @@ -221,6 +221,15 @@ class AudioDeviceIOS : public AudioDeviceGeneric, // reconfiguration is required. bool ApplyChannelConfigurationChange() RTC_RUN_ON(thread_); + // Applies a channel configuration change and rolls back to the previous + // state if the update fails. Returns true when the change is successful. + bool ApplyChannelConfigurationChangeOrRollback( + const char* failure_log_message, + size_t previous_desired_playout, + size_t previous_desired_record, + const AudioParameters& previous_playout_parameters, + const AudioParameters& previous_record_parameters) RTC_RUN_ON(thread_); + // Configures the audio session for WebRTC. bool ConfigureAudioSession(); diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 2172a3e783..91a7dbe771 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -924,6 +924,47 @@ static void LogDeviceInfo() { return true; } +bool AudioDeviceIOS::ApplyChannelConfigurationChangeOrRollback( + const char* failure_log_message, + size_t previous_desired_playout, + size_t previous_desired_record, + const AudioParameters& previous_playout_parameters, + const AudioParameters& previous_record_parameters) { + RTC_DCHECK_RUN_ON(thread_); + + if (ApplyChannelConfigurationChange()) { + return true; + } + + desired_playout_channels_ = previous_desired_playout; + desired_record_channels_ = previous_desired_record; + playout_parameters_ = previous_playout_parameters; + record_parameters_ = previous_record_parameters; + + UpdateAudioSessionChannelPreferences(); + + if (audio_unit_) { + audio_unit_->SetStreamChannelConfiguration( + static_cast(desired_playout_channels_), + static_cast(desired_record_channels_)); + } + + if (initialized_) { + UpdateAudioDeviceBuffer(); + if (fine_audio_buffer_) { + fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_)); + } + } + + UpdateVoiceProcessingBypassRequirement(); + + if (!ApplyChannelConfigurationChange()) { + RTCLogError(@"%s", failure_log_message); + } + + return false; +} + void AudioDeviceIOS::UpdateAudioDeviceBuffer() { LOGI() << "UpdateAudioDevicebuffer"; // AttachAudioBuffer() is called at construction by the main class but check @@ -1396,6 +1437,7 @@ static void LogDeviceInfo() { LOGI() << "SetStereoRecording"; RTC_DCHECK_RUN_ON(thread_); + const size_t previous_desired_playout = desired_playout_channels_; const size_t previous_desired_record = desired_record_channels_; const AudioParameters previous_playout_parameters = playout_parameters_; const AudioParameters previous_record_parameters = record_parameters_; @@ -1441,26 +1483,12 @@ static void LogDeviceInfo() { } UpdateVoiceProcessingBypassRequirement(); - if (!ApplyChannelConfigurationChange()) { - desired_record_channels_ = previous_desired_record; - playout_parameters_ = previous_playout_parameters; - record_parameters_ = previous_record_parameters; - UpdateAudioSessionChannelPreferences(); - if (audio_unit_) { - audio_unit_->SetStreamChannelConfiguration( - static_cast(desired_playout_channels_), - static_cast(desired_record_channels_)); - } - if (initialized_) { - UpdateAudioDeviceBuffer(); - if (fine_audio_buffer_) { - fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_)); - } - } - UpdateVoiceProcessingBypassRequirement(); - if (!ApplyChannelConfigurationChange()) { - RTCLogError(@"Failed to restore previous channel configuration after recording update failure."); - } + if (!ApplyChannelConfigurationChangeOrRollback( + "Failed to restore previous channel configuration after recording update failure.", + previous_desired_playout, + previous_desired_record, + previous_playout_parameters, + previous_record_parameters)) { return -1; } return 0; @@ -1486,6 +1514,7 @@ static void LogDeviceInfo() { RTC_DCHECK_RUN_ON(thread_); const size_t previous_desired_playout = desired_playout_channels_; + const size_t previous_desired_record = desired_record_channels_; const AudioParameters previous_playout_parameters = playout_parameters_; const AudioParameters previous_record_parameters = record_parameters_; @@ -1530,26 +1559,12 @@ static void LogDeviceInfo() { } UpdateVoiceProcessingBypassRequirement(); - if (!ApplyChannelConfigurationChange()) { - desired_playout_channels_ = previous_desired_playout; - playout_parameters_ = previous_playout_parameters; - record_parameters_ = previous_record_parameters; - UpdateAudioSessionChannelPreferences(); - if (audio_unit_) { - audio_unit_->SetStreamChannelConfiguration( - static_cast(desired_playout_channels_), - static_cast(desired_record_channels_)); - } - if (initialized_) { - UpdateAudioDeviceBuffer(); - if (fine_audio_buffer_) { - fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_)); - } - } - UpdateVoiceProcessingBypassRequirement(); - if (!ApplyChannelConfigurationChange()) { - RTCLogError(@"Failed to restore previous channel configuration after playout update failure."); - } + if (!ApplyChannelConfigurationChangeOrRollback( + "Failed to restore previous channel configuration after playout update failure.", + previous_desired_playout, + previous_desired_record, + previous_playout_parameters, + previous_record_parameters)) { return -1; } return 0; From 64e8f0d505ac7be9ce1f33cd25fa7d74ce4dde2c Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 14:21:50 +0300 Subject: [PATCH 6/9] Address feedback vol.2 --- sdk/objc/native/src/audio/audio_device_ios.mm | 2 +- .../native/src/audio/voice_processing_audio_unit.h | 2 ++ .../native/src/audio/voice_processing_audio_unit.mm | 13 ++++++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 91a7dbe771..3523ab5e00 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -570,7 +570,7 @@ static void LogDeviceInfo() { playout_delay_ms); last_hw_output_latency_update_sample_count_ += num_frames; - total_playout_samples_count_.fetch_add(num_frames * playout_channels, + total_playout_samples_count_.fetch_add(num_frames, std::memory_order_relaxed); total_playout_samples_duration_ms_.fetch_add( num_frames * 1000 / playout_parameters_.sample_rate(), diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.h b/sdk/objc/native/src/audio/voice_processing_audio_unit.h index 2a9d26b41d..1d655e01ec 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.h +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.h @@ -150,6 +150,8 @@ class VoiceProcessingAudioUnit { AudioStreamBasicDescription GetFormat(Float64 sample_rate, uint32_t channels) const; + uint32_t SanitizeChannelCount(uint32_t channels) const; + // Deletes the underlying audio unit. void DisposeAudioUnit(); diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm index a52943d587..887a519d9a 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm @@ -227,8 +227,8 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { void VoiceProcessingAudioUnit::SetStreamChannelConfiguration( uint32_t playout_channels, uint32_t record_channels) { - uint32_t sanitized_playout = std::max(1, playout_channels); - uint32_t sanitized_record = std::max(1, record_channels); + const uint32_t sanitized_playout = SanitizeChannelCount(playout_channels); + const uint32_t sanitized_record = SanitizeChannelCount(record_channels); if (sanitized_playout == playout_channels_ && sanitized_record == record_channels_) { return; @@ -245,9 +245,8 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { RTCLog(@"Initializing audio unit with sample rate: %f", sample_rate); OSStatus result = noErr; - const uint32_t playout_channels = - std::max(1, playout_channels_); - const uint32_t record_channels = std::max(1, record_channels_); + const uint32_t playout_channels = SanitizeChannelCount(playout_channels_); + const uint32_t record_channels = SanitizeChannelCount(record_channels_); AudioStreamBasicDescription playout_format = GetFormat(sample_rate, playout_channels); @@ -595,6 +594,10 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { return format; } +uint32_t VoiceProcessingAudioUnit::SanitizeChannelCount(uint32_t channels) const { + return std::max(1, channels); +} + void VoiceProcessingAudioUnit::DisposeAudioUnit() { if (vpio_unit_) { switch (state_) { From e7ac2203ae6f86a7c29f860d825e2fff83495ddf Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 14:41:08 +0300 Subject: [PATCH 7/9] Address feedback vol.3 --- sdk/objc/native/src/audio/audio_device_ios.mm | 6 +++++ .../src/audio/voice_processing_audio_unit.h | 2 ++ .../src/audio/voice_processing_audio_unit.mm | 24 +++++++++++-------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 3523ab5e00..68cdf56763 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -1049,6 +1049,12 @@ static void LogDeviceInfo() { << playout_parameters_.GetBytesPerBuffer(); RTC_LOG(LS_INFO) << " recording bytes per I/O buffer: " << record_parameters_.GetBytesPerBuffer(); + if (playout_parameters_.GetBytesPerBuffer() != + record_parameters_.GetBytesPerBuffer()) { + RTC_LOG(LS_WARNING) << "Playout and recording bytes per I/O buffer differ: " + << playout_parameters_.GetBytesPerBuffer() << " vs " + << record_parameters_.GetBytesPerBuffer(); + } UpdateVoiceProcessingBypassRequirement(); diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.h b/sdk/objc/native/src/audio/voice_processing_audio_unit.h index 1d655e01ec..cd793882e5 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.h +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.h @@ -155,6 +155,8 @@ class VoiceProcessingAudioUnit { // Deletes the underlying audio unit. void DisposeAudioUnit(); + // Mutable: SetBypassVoiceProcessing() records the desired state so it can + // be applied the next time Initialize() is invoked. bool bypass_voice_processing_; const bool detect_mute_speech_; uint32_t playout_channels_; diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm index 887a519d9a..26971b4c27 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm @@ -227,14 +227,16 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { void VoiceProcessingAudioUnit::SetStreamChannelConfiguration( uint32_t playout_channels, uint32_t record_channels) { - const uint32_t sanitized_playout = SanitizeChannelCount(playout_channels); - const uint32_t sanitized_record = SanitizeChannelCount(record_channels); - if (sanitized_playout == playout_channels_ && - sanitized_record == record_channels_) { + const uint32_t effective_playout_channels = + SanitizeChannelCount(playout_channels); + const uint32_t effective_record_channels = + SanitizeChannelCount(record_channels); + if (effective_playout_channels == playout_channels_ && + effective_record_channels == record_channels_) { return; } - playout_channels_ = sanitized_playout; - record_channels_ = sanitized_record; + playout_channels_ = effective_playout_channels; + record_channels_ = effective_record_channels; RTCLog(@"Updated VPIO channel configuration. playout=%u record=%u", static_cast(playout_channels_), static_cast(record_channels_)); @@ -245,13 +247,15 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { RTCLog(@"Initializing audio unit with sample rate: %f", sample_rate); OSStatus result = noErr; - const uint32_t playout_channels = SanitizeChannelCount(playout_channels_); - const uint32_t record_channels = SanitizeChannelCount(record_channels_); + const uint32_t effective_playout_channels = + SanitizeChannelCount(playout_channels_); + const uint32_t effective_record_channels = + SanitizeChannelCount(record_channels_); AudioStreamBasicDescription playout_format = - GetFormat(sample_rate, playout_channels); + GetFormat(sample_rate, effective_playout_channels); AudioStreamBasicDescription record_format = - GetFormat(sample_rate, record_channels); + GetFormat(sample_rate, effective_record_channels); UInt32 playout_size = sizeof(playout_format); UInt32 record_size = sizeof(record_format); #if !defined(NDEBUG) From be7d01ee5b84f9ccff9438da18da3772ae0a6809 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 14:50:23 +0300 Subject: [PATCH 8/9] Address feedback vol.4 --- sdk/objc/native/src/audio/audio_device_ios.mm | 6 ++++++ sdk/objc/native/src/audio/voice_processing_audio_unit.mm | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 68cdf56763..176d66a178 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -1055,6 +1055,12 @@ static void LogDeviceInfo() { << playout_parameters_.GetBytesPerBuffer() << " vs " << record_parameters_.GetBytesPerBuffer(); } + if (playout_parameters_.GetBytesPerBuffer() != + record_parameters_.GetBytesPerBuffer()) { + RTC_LOG(LS_WARNING) << "Playout and recording bytes per I/O buffer differ: " + << playout_parameters_.GetBytesPerBuffer() << " vs " + << record_parameters_.GetBytesPerBuffer(); + } UpdateVoiceProcessingBypassRequirement(); diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm index 26971b4c27..a772e0f39d 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm @@ -598,8 +598,13 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { return format; } +namespace { +constexpr uint32_t kMaxSupportedChannels = 2; +} // namespace + uint32_t VoiceProcessingAudioUnit::SanitizeChannelCount(uint32_t channels) const { - return std::max(1, channels); + return std::min(kMaxSupportedChannels, + std::max(1, channels)); } void VoiceProcessingAudioUnit::DisposeAudioUnit() { From efcb6315474a53378e94b9f219ed8528df572a40 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 2 Oct 2025 14:58:48 +0300 Subject: [PATCH 9/9] Address feedback vol.5 --- sdk/objc/native/src/audio/audio_device_ios.mm | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index 176d66a178..68cdf56763 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -1055,12 +1055,6 @@ static void LogDeviceInfo() { << playout_parameters_.GetBytesPerBuffer() << " vs " << record_parameters_.GetBytesPerBuffer(); } - if (playout_parameters_.GetBytesPerBuffer() != - record_parameters_.GetBytesPerBuffer()) { - RTC_LOG(LS_WARNING) << "Playout and recording bytes per I/O buffer differ: " - << playout_parameters_.GetBytesPerBuffer() << " vs " - << record_parameters_.GetBytesPerBuffer(); - } UpdateVoiceProcessingBypassRequirement();