Skip to content

Use AudioEngine ADM for PlatformAudio on Apple platforms#1215

Open
hiroshihorie wants to merge 5 commits into
hiroshi/adm-proxy-worker-threadfrom
hiroshi/audioengine-platformaudio
Open

Use AudioEngine ADM for PlatformAudio on Apple platforms#1215
hiroshihorie wants to merge 5 commits into
hiroshi/adm-proxy-worker-threadfrom
hiroshi/audioengine-platformaudio

Conversation

@hiroshihorie

@hiroshihorie hiroshihorie commented Jul 2, 2026

Copy link
Copy Markdown
Member

Note

Stacked on #1212 (worker-thread-affine AdmProxy) — review only the top 3 commits. Retarget to main after #1212 merges.

What

Switches the platform ADM on iOS and macOS to the AVAudioEngine based AudioEngineDevice. Deliberately minimal — no new Rust API surface:

  • AdmProxy creates the platform ADM with the kAppleAudioEngine audio layer on Apple platforms (kPlatformDefaultAudio elsewhere, Android unchanged). Creation stays in the proxy constructor on the worker thread, which the AudioEngine device binds its sequence checker to — the contract Make AdmProxy worker-thread-affine #1212 established.
  • AdmProxy forwards the ADM platform voice processing interface (topology, path availability/toggle, processing state). This is required plumbing, not new API: WebRTC's own audio processing resolution (AudioProcessingController in the voice engine) calls these on dependencies.adm when track audio options are applied, and manages the coupled Apple AEC+NS path itself.
  • Docs updated from VPIO wording to the AudioEngine ADM.

PlatformAudio's public API is unchanged; configure_audio_processing keeps its existing builtin-effects behavior. Platform-vs-software processing resolution is owned by WebRTC per track, where it lives since webrtc-sdk's runtime audio processing modes work.

Why

The AudioEngine ADM replaces the AudioUnit/CoreAudio-HAL default on Apple platforms, bringing runtime switchable voice processing (no engine rebuild to toggle AEC), proper device change handling, and parity with the Swift SDK's audio stack.

Dependencies — why CI is red

This PR cannot build against the currently pinned webrtc prebuilt (webrtc-51ef663): kAppleAudioEngine and the platform voice processing interface only exist in newer webrtc. The chain to green:

  1. Expose C++ factory for the AudioEngine audio device module webrtc-sdk/webrtc#260 merges ✅ merged. Fix macOS input device selection without voice processing in AudioEngineDevice webrtc-sdk/webrtc#261 (macOS device selection fix, found by testing this PR) should land before the prebuilt is cut.
  2. A new prebuilt is built and published.
  3. A WEBRTC_TAG bump commit is added here (webrtc-sys/build/src/lib.rs).

Until then this is verified against a local webrtc build via LK_CUSTOM_WEBRTC rather than by CI.

Testing

Verified end to end on macOS arm64 against a local libwebrtc build (webrtc-sdk m144_release + #260 + #261):

  • AudioEngineDevice confirmed active (not a fallback): with hardware processing preferred, builtin AEC/AGC/NS availability and active types flip to hardware — the coupled voice processing path enabling through AdmProxy on the worker thread.
  • cargo run -p livekit --example platform_audio (from Make AdmProxy worker-thread-affine #1212) passes all phases against the AudioEngine ADM: lifecycle, device enumeration/selection/hot-swap while recording, recording on real hardware, processing reconfiguration, 16-thread concurrent churn, full runtime teardown/reacquire cycles.
  • Found one pre-existing AudioEngineDevice bug in the process (macOS device selection with voice processing disabled) — fixed in Fix macOS input device selection without voice processing in AudioEngineDevice webrtc-sdk/webrtc#261; with that fix in the local build, device switching works in both processing modes.
  • Remaining for the prebuilt-backed CI + live room: platform playout under active streams, and a listen test for the ffi v0.3.15 #261 aggregate path.

Select the new kAppleAudioEngine audio layer when creating the platform
ADM on iOS and macOS. The AVAudioEngine based ADM supports runtime
switchable voice processing and device change handling. Construction
happens in the proxy constructor on the worker thread, which the
AudioEngine device binds its sequence checker to.

The proxy also forwards the ADM platform voice processing interface
(topology, path availability and toggle, processing state). WebRTC's
audio processing resolution calls these on the ADM when track audio
options are applied, so the coupled Apple AEC+NS path is managed by
WebRTC itself with no new Rust API surface.

Docs updated from VPIO wording to the AudioEngine ADM.

Requires a libwebrtc build that includes CreateAudioEngineDeviceModule.
The AudioSourceInterface hunk stopped applying after the fork added
SetOptions right where the patch expected the class to end. The build
script tolerates patch failures (git apply || true), so building a
prebuilt from current m144 would have silently produced a libwebrtc
without external audio source support. Content is unchanged, only the
surrounding context is regenerated against m144.
@hiroshihorie hiroshihorie force-pushed the hiroshi/audioengine-platformaudio branch from 675467c to dddb8f2 Compare July 6, 2026 15:21
@hiroshihorie hiroshihorie marked this pull request as ready for review July 6, 2026 15:23
devin-ai-integration[bot]

This comment was marked as resolved.

The active_*_type getters reported Hardware whenever the builtin effect
was available. On the AudioEngine ADM availability means allowed by
policy rather than currently active, so macOS would report hardware
processing while running the default software configuration. The
getters now reflect the device level options last applied through
configure_audio_processing, including a None result when the component
is disabled, which also corrects the same misreport on Android.

Also fixes doc contradictions: macOS is no longer lumped into the
"hardware not available" desktop wording, and the platform notes state
that Apple voice processing on macOS is available but opt in.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +1054 to 1060
let options = *self.handle.processing_options.lock();
if !options.echo_cancellation {
AudioProcessingType::None
} else if options.prefer_hardware_processing && self.is_hardware_aec_available() {
AudioProcessingType::Hardware
} else {
AudioProcessingType::Software

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Audio processing status queries return stale results after using individual toggle methods

The processing status queries read from a cached options snapshot (*self.handle.processing_options.lock() at livekit/src/platform_audio/mod.rs:1054) that is only written by configure_audio_processing, so using the individual toggle methods leaves the snapshot unchanged and the queries report the wrong state.

Impact: After toggling echo cancellation off via the convenience method, the status query still reports it as active (Software or Hardware), misleading callers.

Stale cached state after convenience method calls

The PR adds a processing_options: Mutex<AudioProcessingOptions> field to PlatformAdmHandle (livekit/src/platform_audio/mod.rs:304) and makes the active_aec_type, active_agc_type, and active_ns_type getters read from it (livekit/src/platform_audio/mod.rs:1054-1085). The configure_audio_processing method correctly writes to this field at livekit/src/platform_audio/mod.rs:1150.

However, the three convenience methods — set_echo_cancellation (livekit/src/platform_audio/mod.rs:1172-1180), set_auto_gain_control (livekit/src/platform_audio/mod.rs:1188-1196), and set_noise_suppression (livekit/src/platform_audio/mod.rs:1204-1212) — modify the actual hardware/builtin state but never update processing_options. Their doc comments even claim they are "equivalent to calling configure_audio_processing with only the [field] changed," but they are not.

Example scenario:

  1. PlatformAudio::new() sets processing_options to defaults (all enabled)
  2. audio.set_echo_cancellation(false, false) disables hardware AEC but leaves processing_options.echo_cancellation == true
  3. audio.active_aec_type() reads processing_options, sees echo_cancellation == true, and returns Software instead of None

(Refers to lines 1054-1061)

Prompt for agents
The three convenience methods set_echo_cancellation (line 1172), set_auto_gain_control (line 1188), and set_noise_suppression (line 1204) each modify the hardware/builtin audio processing state but do not update self.handle.processing_options. Since the active_aec_type, active_agc_type, and active_ns_type getters now read from processing_options to determine their return value, these convenience methods must also update the corresponding field in processing_options after successfully applying the change. Each method should lock processing_options and update the relevant field (echo_cancellation, auto_gain_control, or noise_suppression respectively) as well as prefer_hardware_processing to match the prefer_hardware argument. Alternatively, the convenience methods could be refactored to delegate to configure_audio_processing with the appropriate options, which would naturally keep the state in sync.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

The macOS default of prefer_hardware_processing=false predates this
branch and existed because AudioDeviceMac had no hardware processing at
all. With the AudioEngine ADM the capability exists, and Apple voice
processing is the stronger default for the hardware macOS users
actually have, built-in speaker to microphone echo in particular.
Opting out remains a single option: prefer_hardware_processing: false.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Open in Devin Review

Comment on lines +124 to 127
#[cfg(any(target_os = "ios", target_os = "macos"))]
prefer_hardware_processing: true,
#[cfg(not(target_os = "ios"))]
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
prefer_hardware_processing: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 macOS default changes from software to hardware audio processing

This PR changes the default prefer_hardware_processing from false to true on macOS (livekit/src/platform_audio/processing.rs:124-125). Combined with the ADM switch from kPlatformDefaultAudio to kAppleAudioEngine (webrtc-sys/src/adm_proxy.cpp:64-65), this means existing macOS applications that relied on AudioProcessingOptions::default() will silently switch from WebRTC's software AEC/AGC/NS to Apple's voice processing pipeline. The changeset acknowledges this and says users can pass prefer_hardware_processing: false to keep the old behavior, but this is a breaking behavioral change for any macOS app that doesn't explicitly set this field. Worth confirming this is acceptable for existing consumers.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +61 to +65
#if defined(WEBRTC_IOS) || defined(WEBRTC_MAC)
// Use the AVAudioEngine based ADM on Apple platforms. It supports runtime
// switchable voice processing and device change handling.
platform_adm_ = webrtc::CreateAudioDeviceModule(
env_, webrtc::AudioDeviceModule::kAppleAudioEngine);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 ADM type switch from CoreAudio to AudioEngine on macOS

The C++ constructor now selects kAppleAudioEngine instead of kPlatformDefaultAudio for both iOS and macOS (webrtc-sys/src/adm_proxy.cpp:61-65). This is a significant platform-level change: the AudioEngine ADM uses AVAudioEngine internally rather than the traditional CoreAudio HAL path. While this enables runtime-switchable voice processing, it also changes the audio I/O stack entirely. If kAppleAudioEngine is not available in the WebRTC build (e.g., an older fork or a build without the AudioEngine ADM compiled in), CreateAudioDeviceModule would return nullptr and the platform ADM would be set to null at line 77, falling back to synthetic-only mode with no platform audio. The error path handles this gracefully, but it's worth verifying the WebRTC build includes this ADM type.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@xianshijing-lk xianshijing-lk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks.

runtime: Arc<LkRuntime>,
// Device level processing configuration last applied via
// configure_audio_processing, used by the active_*_type getters
processing_options: Mutex<AudioProcessingOptions>,

@ladvoc ladvoc Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: A parking_lot::RwLock is probably a better fit over Mutex since reading is more common than updating settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants