Skip to content

AEC in Unity Audio - #381

Merged
MaxHeimbrock merged 11 commits into
mainfrom
max/meet-unity-aec
Sep 22, 2026
Merged

MaxHeimbrock merged 11 commits into
mainfrom
max/meet-unity-aec

Conversation

@MaxHeimbrock

@MaxHeimbrock MaxHeimbrock commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Background

Unity Audio had no echo cancellation, only PlatformAudio had AEC.

There is a new PlayoutReference which captures the final game audio from the AudioListener in the scene. That is the far-end input for the AEC

With audio processing enabled, the RtcAudioSource (which represents our microphone) creates an AudioProcessor, which itself holds the AudioProcessingModule (APM) wrapping the native APM from WebRTC through FFI. We already had all the APM stuff exposed through FFI anyways, so this is not in this PR.

The AudioProcessor receives the mic input from RtcAudioSource and the scene audio from the PlayoutReference plus an occasional new delay hint (every 2s) and does its processing using its AudioProcessingModule`, before sending the audio input to the native side.

With audio processing disabled, the input audio still goes straight to the native side.

image

API surface

Type Member Change
AudioProcessingOptions bool PreferHardware Unchanged. Documented as PlatformAudioSource only.
MicrophoneSource MicrophoneSource(string, GameObject, AudioProcessingOptions) Added constructor.
RtcAudioSource protected RtcAudioSource(RtcAudioSourceType, AudioProcessingOptions) Added constructor.
RtcAudioSource protected RtcAudioSource(RtcAudioSourceType, uint, uint, AudioProcessingOptions?) Added constructor.
RtcAudioSource bool AudioProcessingEnabled { get; } Added.
RtcAudioSource protected void ResetAudioProcessing() Added.
PlayoutReference MonoBehaviour New component. No public members; the SDK adds it to the active AudioListener when echo cancellation is on.

Nothing removed or changed in signature. AudioProcessingOptions keeps its three stage flags; The FFI wrapper AudioProcessingModule is internal; the public entry point is MicrophoneSource / RtcAudioSource with AudioProcessingOptions.

Verification

Tested manually on MacOS, iOS and Android

@MaxHeimbrock MaxHeimbrock changed the title The first version is already working in Meet on MacOS AEC in Unity Audio Sep 4, 2026
MaxHeimbrock and others added 4 commits September 7, 2026 11:20
libwebrtc instantiates the high-pass filter whenever AEC or NS is enabled
(echo_canceller.enforce_high_pass_filtering defaults to true) and the
platform ADM path always runs it, so the flag only ever mattered with AEC
and NS both off. Hardcode it on for the Unity-audio path so both paths
match, and keep the FFI wrapper off the public surface: sources configure
it through AudioProcessingOptions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The struct and RtcAudioSource.AudioProcessingStats only mirrored counters
the processor kept for diagnostics. Nothing in the processing path read
them, and the only consumers were Meet's periodic log and two test
assertions. The one signal that would otherwise go silent, the module
rejecting a chunk, now warns once from the main thread the same way the
unsupported-rate bypass does.

Tests: the chunk count moves to the AudioProcessor level through a
counting sink; the source-level test keeps the push path covered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remote AudioSources play at Unity's default volume again. The 0.7 came
over from the review package as their tuning; on-device testing at full
gain was acceptable, and the sample should start with fewer knobs. The
custom editor loses its Unity Audio section and shows the AEC3 note
directly under the processing toggles when PlatformAudio is off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread Samples~/Meet/Assets/Scenes/MeetApp.unity
Comment thread Runtime/Scripts/Audio/Processing/AudioProcessor.cs
Comment thread Runtime/Scripts/Audio/RtcAudioSource.cs Outdated
@MaxHeimbrock
MaxHeimbrock marked this pull request as ready for review September 17, 2026 13:07
- Meet scene: point the token config back at the tracked asset instead
  of a gitignored local config
- Meet sample: restore the Publish Data button and drop a duplicate log;
  stop calling PlatformAudio the default in the class comment
- RtcAudioSource: keep feeding the processing stage while muted so AEC3
  sees the near end next to its reference; the processed output is
  dropped instead
- AudioProcessor: only run the maintenance loop for echo cancellation
  and only when a coroutine host exists, since the loop never finishes
  and would otherwise be drained synchronously; use unscaled time;
  check the sample rate per block instead of latching a bypass;
  Dispose(bool) keeps the finalizer path off PlayoutReference state
- PlayoutReference: fix the doc header, add DisallowMultipleComponent

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@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.

The review is still ongoing, I am posting what I have now.

Comment thread Runtime/Scripts/Audio/Processing/AudioProcessingDelayHint.cs
Comment thread Runtime/Scripts/Audio/Processing/AudioProcessingDelayHint.cs
Comment thread Runtime/Scripts/Audio/Processing/AudioProcessingModule.cs
Comment thread Runtime/Scripts/Audio/Processing/AudioProcessingModule.cs
Comment thread Runtime/Scripts/Audio/Processing/AudioProcessingModule.cs
Comment thread Runtime/Scripts/Audio/Processing/AudioProcessingModule.cs
Comment thread Runtime/Scripts/Audio/Processing/AudioProcessingModule.cs
Comment thread Runtime/Scripts/Audio/RtcAudioSource.cs
Comment thread Runtime/Scripts/Audio/Processing/PlayoutReference.cs Outdated
/// <summary>Main thread. The component stays on the listener; it is inert without consumers.</summary>
internal static void Release()
{
if (_consumers > 0) _consumers--;

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.

what will happen if _consumers == 0 ?

@MaxHeimbrock MaxHeimbrock Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had another look and realized this was a bit confusing. I now coupled the Acquire with subscribing to the event delegate AudioRead. I also renamed the _listener to _sceneAudioListener because it was easily confused for the _consumer with that name.

Now the delivery is also clearly coupled to having a _sceneAudioListener as well as consumer, which we don't count ourselves but just look if we have event subscriber. Beforehand it was already doing that in AudioRead?.Invoke because of the ?, but the _deliver flag was kind of split that way.

I hope this has a better readability.

Comment thread Runtime/Scripts/Audio/Processing/PlayoutReference.cs Outdated
Comment thread Runtime/Scripts/Audio/RtcAudioSource.cs Outdated

@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

@MaxHeimbrock
MaxHeimbrock merged commit 986fba4 into main Sep 22, 2026
11 checks passed
@MaxHeimbrock
MaxHeimbrock deleted the max/meet-unity-aec branch September 22, 2026 07:36
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.

2 participants