Skip to content

Sync post-M145 upstream fixes (full parity with C++ reference) - #36

Open
dignifiedquire wants to merge 22 commits into
mainfrom
sync/post-m145-fixes
Open

dignifiedquire wants to merge 22 commits into
mainfrom
sync/post-m145-fixes

Conversation

@dignifiedquire

@dignifiedquire dignifiedquire commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR syncs sonora with the post-M145 upstream WebRTC fixes, for full parity between the Rust port and its C++ reference. The Rust port gets the upstream changes it was missing. The cpp submodule moves to a fork branch with the same commits cherry-picked, so the Rust-vs-C++ comparisons stay in lockstep.

Important

Depends on dignifiedquire/webrtc-audio-processing#1. The submodule points at fork commit 3b33b6e. Merge the fork PR with a merge commit first; squash or rebase would orphan 3b33b6e.

User-visible change: IVC defaults

InputVolumeControllerConfig::default() held test-style values that matched neither M145 nor upstream. It is now upstream's production Config{} after d9b92fe1bc:

Field Before After
Target range (dBFS) [−30, −18] [−50, −12]
enable_clipping_predictor false true
update_input_volume_wait_frames 0 100
Speech probability / ratio thresholds 0.5 / 0.8 0.7 / 0.6

With input_volume_controller enabled, this changes recommended_stream_analog_level(). Whether the processed samples change depends on what happens to that recommendation:

  • Fixed analog level (the caller ignores the recommendation): samples are unchanged.
  • The caller applies the recommendation to its microphone or via set_stream_analog_level(), or capture_level_adjustment.analog_mic_gain_emulation is enabled (the emulated analog gain follows the recommendation): the output level changes. A probe found 1.1–1.2 million of 1.44 million samples changed.

There's a CHANGELOG entry under "Unreleased".

Upstream commits

  • Ported to Rust (9):
    • a6d83994aa: stale AECm documentation only; the port never had AECm.
    • 7f5a8b656c
    • d460e60e19: headroom fix. The test that asserted the buggy value now asserts 0.
    • d9b92fe1bc
    • e10cd19640: the non-neural-estimator parts, including the MovingAverage start-up normalization and the config_changed flow.
    • fe9c0a1de4, 4252c6051d, 8dd6b47c83
    • 86ef7fa42d
  • Already in Rust, now also in C++ (6):
    • 297352a2fd, 573e746914 and 7c388cbabb, via 9e4b402. That commit's message swaps the first two hashes.
    • 32feae39d0, 05d6bc269c and 401392b7e6: the port never had these field trials.

Commits

  • bf41756 port: Remove aec_mobile (AECm) config from audio_processing.h
  • 3ea7be9 port(aec3): Propagate delay estimator fields to AecState.
  • 9e455f7 port(aec3): AEC3: Fix render buffer headroom calculation on underruns
  • d09e7ff port(agc2): Make max target input level for input controller -12dB
  • dce1a5c port(aec3): Audio: Enable dynamic on-the-fly AEC3 configuration updates for the suppressor gain computation
  • b4fe2fc port(aec3): Remove the usage of the field-trial WebRTC-Aec3AecStateFullResetKillSwitch
  • cb89e6d port(aec3): Remove the usage of the field-trial WebRTC-Aec3CoarseFilterResetHangoverKillSwitch
  • 946592e port(aec3): Remove the usage of the field-trial WebRTC-Aec3DeactivateInitialStateResetKillSwitch
  • 492e58d port(aec3): aec: fix wrong sentinel usage in echo metrics
  • 8149e8c chore(cpp): update submodule with post-M145 upstream cherry-picks
  • 1629a49 test: compare 48 kHz stereo echo pipeline with decorrelated channels against C++
  • 1147f9c docs: note post-M145 upstream cherry-picks
  • f556978 test(aec3): check that comfort noise is identical on all channels
  • 4a021c1 test(aec3): port SuppressionGainTest.BasicGainComputation and cover config_changed
  • c1375c1 test(agc2): build IVC tests on the production config where upstream does
  • 45cad1a fix(sonora-sys): rebuild the shim when the C++ headers or library change
  • 5dc45e4 docs: keep the 0.1.0 changelog entry and name sonora-ffi as the backend
  • 024f9ce test: give the stereo echo comparison a tolerance that catches mono render
  • 8a829c4 chore(cpp): update submodule with the AVX2 reference and NEWS fixes
  • 828cc67 docs: note that the neural residual echo estimator is not ported
  • 7a129c1 docs: correct the IVC and AEC3 changelog claims and the ML-REE notes
  • 72366c3 docs: say how the IVC recommendation reaches the output

Tests added or strengthened

  • stereo_echo_pipeline_matches_cpp (sonora-bench): 48 kHz stereo with different signals on each channel, against C++, with tolerance 0.02. It fails if render or capture falls back to mono. It cannot detect a regression of the comfort-noise or filter-switching changes, which move the output by less than 1e-5; its doc comment says so.
  • Comfort noise: a test that fails if the channels draw independent random phases again.
  • SuppressionGainTest.BasicGainComputation: ported, plus a config_changed test. Deliberately breaking the code in three ways each made a test fail.
  • IVC tests: those that upstream builds from Config{} now use the production default.
  • sonora-sys build: build.rs now reruns when the C++ headers, the installed library, or WEBRTC_CPP_ROOT change. Before, the shim could silently link against stale headers.

Verification (local, macOS arm64)

  • cargo fmt --all --check, and both CI clippy -D warnings commands
  • cargo test --workspace: 772 passed, 0 failed, 0 ignored
  • RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps, and cargo +1.91.1 check --workspace --all-targets
  • sonora-bench against the rebuilt C++: 27 passed, 0 failed
  • C++ suite with the Rust backend (CI flags and filter): 2327 passed, 0 failed. Also 0 failed under emulated x86_64.
  • Real x86 hardware and GitHub CI: this PR's run covers both.

Known and deferred

  • Lost upstream format test: the submodule bump takes upstream 1d1ee4503c, which turns AudioProcessingTest.Formats (53 format-conversion cases) into an error-code-only test. That removes the only Rust-vs-C++ output check across rate and channel conversion; sonora-bench covers only matched formats. Follow-up: add sonora-bench comparisons for conversions (for example 48k stereo to 16k mono, 44.1 kHz, mono to stereo).
  • Input volume controller tests: the production defaults now run clipping prediction, the 100-frame wait and the 0.7/0.6 thresholds, but upstream's controller-level tests for those paths are not ported, and there is no Rust-vs-C++ comparison for the controller. This gap predates this PR. Follow-up.
  • ML-REE: the neural residual echo estimator is not ported; the sonora-aec3 README and the top-level README say why.
  • There is no Rust-vs-C++ comparison of the IVC default. The C++ IVC suites are compiled out under the Rust backend, and one would need new shim functions.
  • There is no targeted test for the joint coarse/refined filter choice: it is inline in EchoRemover::process, as it is upstream.
  • An older difference: 48k_*/ec_only differs from C++ by 0.156 against a tolerance of 0.2. The number is identical before this branch, and it hasn't been investigated.
  • CHANGELOG still has the stale "0.1.0 (unreleased)" heading and no 0.2.0 entry. This predates the branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx

dignifiedquire and others added 19 commits September 29, 2026 14:48
Upstream a6d83994aa removes EchoCanceller::mobile_mode from
AudioProcessing::Config and drops "(has no effect for the mobile mode)"
from the enforce_high_pass_filtering comment. The port never had AECm
or mobile_mode. This commit removes the one stale reference, the
"Has no effect in mobile mode." sentence on
EchoCanceller::enforce_high_pass_filtering in config.rs.

No other file in crates/, fuzz/, README.md, or CHANGELOG.md mentions
mobile mode or AECm. The generated C header
(crates/sonora-ffi/include/wap_audio_processing.h) has no doc text for
this field, and cbindgen does not parse the sonora crate
(parse_deps = false), so the header does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Upstream: https://webrtc.googlesource.com/src/+/7f5a8b656cf69708359a8179d226dda8d2206205

Remove the write-only DelayEstimate::blocks_since_last_change and
blocks_since_last_update fields and RenderDelayController's
delay_change_counter. RenderDelayController now stores each new
estimate as-is and takes last_delay_estimate_quality from the computed
buffer delay. AecState::FilterDelay stores every reported external
delay, not only changed ones. Bit-exact: the dropped state was never
read, and FilterDelay only checks whether an external delay exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Upstream: https://webrtc.googlesource.com/src/+/d460e60e1955be72f3479a944fabde401fe848a2

RenderBuffer::headroom now returns 0 when the write and read indices
meet on an underrun, instead of the full buffer size, and its
debug_assert becomes headroom < size. The existing unit test asserted
the buggy value; it now expects 0, and a new test checks every
write == read position. The only consumer is EchoAudibility's
stationarity lookahead, which runs only with
use_stationarity_properties, so default-config output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Upstream d9b92fe1bc sets the input volume controller (IVC) production
target_range_max_dbfs to -12 dBFS (it was -30 at M145) and removes the
WebRTC-Agc2MaxSpeechLevelExperimental switch and its
target_range_experimental_max_dbfs field. The port never had that
field or switch.

The Rust InputVolumeControllerConfig::default() did not match M145's
production InputVolumeController::Config{} either. It held test-like
values. The APM pipeline builds the IVC from this default
(audio_processing_impl.rs initialize_gain_controller2), in the same way
that C++ APM builds it from Config{}. The default now equals upstream's
production Config{} after d9b92fe1bc:

  field                            old    M145   new (= d9b92fe1bc)
  min_input_volume                 20     20     20
  clipped_level_min                70     70     70
  clipped_level_step               15     15     15
  clipped_ratio_threshold          0.1    0.1    0.1
  clipped_wait_frames              300    300    300
  enable_clipping_predictor        false  true   true
  target_range_max_dbfs            -18    -30    -12
  target_range_min_dbfs            -30    -50    -50
  update_input_volume_wait_frames  0      100    100
  speech_probability_threshold     0.5    0.7    0.7
  speech_ratio_threshold           0.8    0.6    0.6

User-visible change: audio samples are unchanged. When
GainController2::input_volume_controller is enabled,
recommended_stream_analog_level() now follows upstream. The target
range is [-50, -12] dBFS. Speech-level updates happen at most once
every 100 frames (1 s), and only when at least 60% of those frames have
a speech probability of 0.7 or higher. Before, they could happen on
every 10 ms frame. Clipping prediction is on, so the volume can also be
lowered on predicted clipping, not only on detected clipping.

The repository has no maintained changelog (CHANGELOG.md was not
updated for the 0.1.0 or 0.2.0 releases), so this commit message
records the change.

Tests:
- Add default_config_matches_upstream_production_config, which pins
  every field to the upstream header at d9b92fe1bc.
- Port upstream CheckIsAlive, which runs the production config over
  1/2/3/6 channels at 8/16/32/48 kHz. Both tests fail against the old
  default.
- clipping_parameters_verified and the two clipping-predictor
  enable/disable tests now start from get_test_config() instead of the
  production default. Upstream uses its CreateInputVolumeController()
  test factory for these tests. Their assertions are unchanged.
- The gain_controller2 tests keep
  InputVolumeControllerConfig::default(), because upstream uses
  InputVolumeControllerConfig{} at the same sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
…es for the suppressor gain computation

Upstream: https://webrtc.googlesource.com/src/+/e10cd19640a9bf6f53cd071d1dd58051e1c0b0fc

Port the parts that apply without the NeuralResidualEchoEstimator:
- MovingAverage divides by the number of inputs seen so far
  (1/(n+1), saturating at mem_len) instead of always by mem_len, and
  gains update_memory_length.
- The nearend detectors gain set_config; DominantNearendDetector now
  stores its config struct.
- SuppressionGain::get_gain takes the active Suppressor config and a
  config_changed flag, which triggers update_state_depending_on_config
  (smoother window, detector config, GainParameters::set_config).
  SuppressionGain no longer keeps the whole EchoCanceller3Config; the
  write-only state_change_duration_blocks and
  initial_state_change_counter, and use_unbounded_echo_spectrum, are
  gone.
- EchoRemover passes config.suppressor and config_changed = false.

Skipped (ML-REE only, not ported): NeuralResidualEchoEstimator::
AdjustConfig, EchoRemover's ML-REE suppressor config selection and the
coarse-filter-output gate on IsMlReeActive, ResidualEchoEstimator::
IsMlReeActive and its branch restructure (equivalent without ML-REE),
the neural estimator impl and audioproc_f changes.

Tests: MovingAverage average expectations follow upstream (/1, /2, /3),
plus upstream's UpdateMemoryLength and UpdateStateDependingOnConfig
tests; new tests check that a window change drops old history and that
set_config and the smoother resize take effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
…llResetKillSwitch

Upstream: https://webrtc.googlesource.com/src/+/fe9c0a1de4135aba3d28a9aaad5e920502f7d562

Drop AecState::full_reset_at_echo_path_change, which the port pinned
to true (the field trial's default). AecState now always does the full
reset on a delay change. No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
…erResetHangoverKillSwitch

Upstream: https://webrtc.googlesource.com/src/+/4252c6051dd87f7a02f8f27263683fbe00d6752e

Drop Subtractor::use_coarse_filter_reset_hangover, which the port
pinned to true (the field trial's default). The coarse filter reset
hangover now always disallows the diverged-leakage adaptation. No
behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
…InitialStateResetKillSwitch

Upstream: https://webrtc.googlesource.com/src/+/8dd6b47c8302d32657c988d09ecc7fde38ae9479

Drop AecState::deactivate_initial_state_reset_at_echo_path_change,
which the port pinned to false (the field trial's default). The initial
state is now always reset on a delay change. No behavior change.
subtractor_analyzer_reset_at_echo_path_change stays, because upstream
main still gates it by a field trial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Upstream: https://webrtc.googlesource.com/src/+/86ef7fa42dc3429bb4813aa98699c70ad570cac9

EchoRemoverMetrics::reset_metrics now starts the ERLE floor at 1000
and its ceiling at 0, like the ERL metric, so each tracks the real
extreme. Before, the floor started at 0 and the ceiling at 1000, so
neither could move. A new test checks that ERL and ERLE floors and
ceilings follow the updated values. Audio output is unchanged; the
port does not report these metrics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Move the cpp submodule from b5059ca to feb3c50 on the fork branch
sync/post-m145-fixes. The branch cherry-picks these upstream WebRTC
commits onto the M145 base (branch-heads/7632), oldest first:

- a6d83994aa Remove aec_mobile (AECm) config from audio_processing.h
- 297352a2fd Update comfort noise to be applied identically on all channels
- 573e746914 Correct the switching between the coarse and refined filters
- 7c388cbabb Changing default values of multi channel processing to be true
- 7f5a8b656c Propagate delay estimator fields to AecState
- d460e60e19 AEC3: Fix render buffer headroom calculation on underruns
- d9b92fe1bc Make max target input level for input controller -12dB
- e10cd19640 Enable dynamic on-the-fly AEC3 configuration updates for the
  suppressor gain computation
- fe9c0a1de4 Remove field-trial WebRTC-Aec3AecStateFullResetKillSwitch
- 32feae39d0 Remove field-trial WebRTC-Aec3TransparentModeKillSwitch
- 4252c6051d Remove field-trial WebRTC-Aec3CoarseFilterResetHangoverKillSwitch
- 8dd6b47c83 Remove field-trial WebRTC-Aec3DeactivateInitialStateResetKillSwitch
- 86ef7fa42d aec: fix wrong sentinel usage in echo metrics
- 05d6bc269c Remove field-trial WebRTC-Aec3HighPassFilterEchoReference
- 401392b7e6 Remove field-trial WebRTC-Aec3EchoSaturationDetectionKillSwitch

Prerequisites also cherry-picked: 8cd8620980 (Remove the possibility to
run AECm), needed by a6d83994aa, and 1d1ee4503c (Refactor
AudioProcessingTest), needed by the tests of 7c388cbabb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
…against C++

The existing full-pipeline comparison never processes a render signal
and feeds the same signal to both channels, so it never reaches the
multi-channel AEC3 paths. The new stereo_echo_pipeline_matches_cpp test
runs EC + NS + AGC2 at 48 kHz stereo for 500 frames with independent
noise on the two render channels and a different echo mix plus near-end
noise on each capture channel. This exercises the multi-channel upstream
changes 7c388cbabb (multi-channel defaults), 297352a2fd (shared comfort
noise) and 573e746914 (one coarse/refined decision for all channels).

It uses the existing PIPELINE_TOL and also asserts that both outputs
differ between channels, which fails if either side downmixes capture to
mono. Against the pre-sync C++ fork (b5059ca, multi-channel defaults
false) the test fails on that check.

sonora-sys gains process_reverse_stream_f32_2ch, mirroring
process_stream_f32_2ch, because the shim only had a mono reverse path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
The port and the C++ reference are now based on M145 (branch-heads/7632)
plus 15 later upstream WebRTC fixes, the newest from M156. The C++ fork's
NEWS file lists them. The History entry about the M145 upgrade is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Upstream 297352a2fd generates the random sin-table indices once per
block and applies them to every capture channel. The existing
correct_level test checks only per-channel power, which is the same
when each channel draws its own phases, so no test failed if the port
regressed to per-channel indices.

identical_noise_on_all_channels feeds the same noise estimate to three
channels over several blocks and asserts that the lower- and upper-band
noise is bit-identical across channels. Verified: it fails when the
indices are regenerated per channel (the pre-297352a2fd behaviour).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
…onfig_changed

No test called SuppressionGain::get_gain, so the config_changed branch
added for upstream e10cd19640 was unguarded: dropping it, or updating
the state on every call, still passed.

- basic_gain_computation ports upstream's BasicGainComputation with the
  e10cd19640 signature (suppressor config and config_changed=false):
  strong noise or nearend gives unity gain, and strong echo on one of
  two capture channels drives the shared gain to zero.
- get_gain_applies_config_only_when_changed passes a different
  suppressor config with config_changed=false (state unchanged) and
  then true (new tunings and a one-block nearend window take effect).

Verified: removing the config_changed block, or making it
unconditional, fails the second test; limiting lower_band_gain to
channel 0 fails the first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Upstream input_volume_controller_unittest.cc (at d9b92fe1bc) constructs
15 tests from InputVolumeController::Config{} with designated overrides,
for example {.min_input_volume = GetParam()}, so they run on the
production defaults. The Rust ports used ..get_test_config() instead
(target range [-30, -18] dBFS, clipping predictor off, clipped level
min 165, no update wait), so none of them exercised the production
controller that the pipeline now uses.

These tests now use ..InputVolumeControllerConfig::default(); tests
that upstream builds from GetInputVolumeControllerTestConfig() or
CreateInputVolumeController() keep get_test_config(). All 39 IVC tests
pass. Verified: changing the default clipped_wait_frames now also fails
waiting_period_between_clipping_checks, which previously ignored the
default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
The shim compiles webrtc::AudioProcessing::Config and other C++ types,
so their layout and default member initializers (for example the
multi_channel_* defaults changed by upstream 7c388cbabb, or the
EchoCanceller layout changed by a6d83994aa) are fixed when the shim is
built. build.rs only reran when shim.h, shim.cc or bridge.rs changed,
so after a submodule bump a warm target dir kept a shim built against
the old headers and compared Rust against a stale C++ config.

build.rs now also reruns when anything under {cpp_root}/webrtc, the
pkg-config include dirs or the pkg-config library dirs changes, and
when WEBRTC_CPP_ROOT changes. The shim includes headers from both the
source tree (webrtc/api/audio/audio_processing.h) and the install
prefix (api/array_view.h and others), so both are watched.

Verified with cargo build -v: touching cpp/webrtc/api/audio/
audio_processing.h left sonora-sys Fresh before this change; after it,
touching that header, the installed header or the installed dylib each
marks sonora-sys Dirty and rebuilds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
7bf030d rewrote the "0.1.0 (unreleased)" entry to claim M145 plus 15
later upstream fixes, but v0.1.0 and the 0.2.0 crates have shipped
without most of them. The 0.1.0 line is restored and a new Unreleased
section lists what this branch changes for users: the IVC production
defaults and the AEC3 ports. It notes that the multi-channel defaults,
shared comfort noise and joint filter choice already shipped in 0.2.0
(9e4b402 is an ancestor of sonora-v0.2.0).

README.md said the C++ suite runs with the Rust backend "linked via the
sonora-sys FFI bridge". The -Drust-backend meson option links
libsonora_ffi.a (cpp/meson.build:201-246); sonora-sys is the opposite
bridge, used by sonora-bench to call C++ from Rust.

The "up to M156" wording is kept: chromiumdash maps M155 to branch 8059
and M156 to 8078, and upstream branch-heads/8078 contains 86ef7fa42d,
05d6bc269c and 401392b7e6 while branch-heads/8059 does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
…ender

stereo_echo_pipeline_matches_cpp used PIPELINE_TOL (0.2). Its channel
check catches capture downmixed to mono, but with Rust's
multi_channel_render set to false (the default before upstream
7c388cbabb) the Rust/C++ max diff only grows from 0.0050/0.0056 (L/R) to
0.055/0.046, so the test still passed. The doc comment and 1a31f72
also claimed it exercises 297352a2fd and 573e746914, but reverting
either one in Rust changes the max diff by less than 1e-5.

The test now uses its own STEREO_ECHO_TOL = 0.02, and the doc comment
states what it guards and what it cannot detect. Measured on macOS
arm64 and on x86_64 Ubuntu 24.04 (GCC 13, -march=native, Docker
linux/amd64 emulation on Apple silicon, not a native x86 host): the
baseline diffs agree to within 1e-5 and the mono-render diffs to within
2e-6 across the two, so the margin is about 3.6x above the baseline
and 2.3x below the regression. Verified: the test now fails with
multi_channel_render = false (tolerance) and with
multi_channel_capture = false (channel check). The unit tests added in
d3904ab cover 297352a2fd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
Moves the fork from feb3c50 to 3b33b6e on sync/post-m145-fixes:
- 8fa9626 regenerates output_data_float_avx2.pb for the multi-channel
  defaults (x86_64, GCC 13, -march=native; not yet confirmed on
  native x86 hardware)
- 3b33b6e lists the multi-channel AEC3 output changes, the headroom
  change, the removed field trials and the test changes in NEWS

No library source changes, so cpp/install and the Rust/C++ comparison
are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
@codecov-commenter

codecov-commenter commented Sep 29, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.64694% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.69%. Comparing base (413f64e) to head (72366c3).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #36      +/-   ##
==========================================
+ Coverage   93.64%   93.69%   +0.04%     
==========================================
  Files         146      146              
  Lines       31130    31484     +354     
  Branches    31130    31484     +354     
==========================================
+ Hits        29153    29499     +346     
- Misses       1753     1762       +9     
+ Partials      224      223       -1     
Flag Coverage Δ
rust 93.69% <96.64%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Upstream AEC3 can use an optional, experimental neural residual echo
estimator (ML-REE). The port skips it: upstream ships it disabled, it
needs TensorFlow Lite (C++) and a trained model that is not public, and
the C++ reference sonora validates against does not build it. Record
that in the sonora-aec3 docs (rendered on docs.rs) and point to it from
the main README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
@jacksongoode

jacksongoode commented Sep 29, 2026 •

Copy link
Copy Markdown

Hey @dignifiedquire , cool to see your work on this project!

I was curious, do you imagine you will support the neural aec backend within sonora? Or is it a bit too complicated outside the scope here?

dignifiedquire and others added 2 commits October 1, 2026 13:22
The changelog said the new input volume controller defaults leave the
processed samples unchanged. That is false when
capture_level_adjustment.analog_mic_gain_emulation is enabled: the
recommended level is stored as the emulated analog gain
(audio_processing_impl.rs:769-770) and applied as pre-gain to the next
capture frame (:486-490), so the output level changes.

The AEC3 entry listed the suppressor config update port (e10cd19640) as
if it were a new capability. Nothing in the port can trigger it, so
describe it as internal with no behaviour change.

The sonora-aec3 README said the suppressor config switch is ported for
parity. Only the receiving side is: echo_remover passes the fixed
config.suppressor and false. Say so.

Upstream removed the "experimental API" note from the ML-REE creator
header in 25cc1a2275; it is still present at M145 (branch-heads/7632).
Say it was marked experimental at M145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
7a129c1 changed the input volume controller bullet to "Processed samples
are unchanged unless capture_level_adjustment.analog_mic_gain_emulation
is enabled". That is still false for the documented use of
recommended_stream_analog_level(): a caller that passes the
recommendation back with set_stream_analog_level() changes the applied
volume, which sets applied_input_volume_changed
(audio_processing_impl.rs:298-304). AGC2 then resets its speech level
estimator and saturation protector (gain_controller2.rs:241-248, via
audio_processing_impl.rs:706), and with the echo canceller enabled AEC3
receives an echo path gain change (audio_processing_impl.rs:508). The new
defaults change when the volume changes, so the output changes even for
identical input.

Measured with AGC2 (input volume controller and adaptive digital gain),
emulation off, on near48_stereo.pcm (channel 0, 1,344,000 samples) before
and after d09e7ff: with a fixed level of 128, 0 samples differ; with
the recommendation fed back each frame, 1,191,182, 1,144,735 and
1,203,175 samples differ at input scales 0.05, 0.3 and 1.0.

The bullet now says that the controller does not modify samples itself,
names both ways its recommendation reaches the output, and says that the
output changes for callers that apply it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H29DamSLugosGXJSz1e5Yx
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