Skip to content

feat(audio): add optional playback echo cancellation - #1228

Open
HKLHaoBin wants to merge 7 commits into
Open-Less:betafrom
HKLHaoBin:feat/issue-1223-echo-cancellation
Open

HKLHaoBin wants to merge 7 commits into
Open-Less:betafrom
HKLHaoBin:feat/issue-1223-echo-cancellation

Conversation

@HKLHaoBin

@HKLHaoBin HKLHaoBin commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add optional desktop playback echo cancellation for dictation while keeping system playback audible.
  • Capture a best-effort render/monitor reference on Windows/macOS and PipeWire/PulseAudio Linux paths; keep microphone-only fallback when reference capture is unavailable.
  • Add bounded reference buffering, adaptive NLMS cancellation, underrun/reset handling, local-only preference persistence, desktop settings, localization, and platform documentation. Android retains the microphone-only path.
  • Treat CPAL microphone Xrun notifications as recoverable rather than immediately raising Fatal and cancelling usable capture. Other stream errors remain fatal, and the existing no-callback watchdog thresholds/liveness rules are unchanged.
  • Merge current beta (43df23be) into the feature branch without rewriting its history, preserving upstream voice activation, Linux microphone format fallback and current CI gates. Both new voice-activation processing configurations now retain the user's AEC preference.

Fixes #1223

Why the diagnostic probes are intentionally retained

The original recording failure is intermittent and depends on hardware, drivers and playback conditions. A single tester's short run cannot reliably reproduce it. We are retaining the existing [DEBUG-ISSUE1223] P1–P5 structured diagnostics while expanding real-device testing and review, rather than removing the evidence needed to distinguish microphone failures from playback-reference glitches:

  • P1: AEC requested / playback reference available or microphone-only fallback.
  • P2: Microphone stream error classification, including xrun_recoverable.
  • P3: Playback-reference stream error classification (not the same failure path as P2).
  • P4: Processing-queue or no-callback watchdog faults.
  • P5: First data callback and the next callback following an error, with independent microphone/reference counters.

These probes record controlled states/error-kind enums, run-local correlation labels, counters, frame counts and timestamps—not audio samples, transcription text, credentials or provider configuration. Callback diagnostics are emitted only on the first callback or the callback following an error, not for every packet. Existing ordinary application logs are separate and should still be privacy-reviewed before sharing; this statement does not mean the entire application log is safe to publish.

Evidence and limits: The reporter's earlier instrumented log captured five microphone P2 Xruns followed by non-empty microphone callbacks and then cancellation under the old error handling. The targeted fix changes only the Xrun classification. A subsequent local hardware smoke test completed six AEC-enabled recordings through outcome=Inserted; it observed reference-side P3 Xruns followed by reference callbacks, but no microphone P2 Xrun. This is positive smoke-test evidence, not confirmation that microphone Xrun recovery, all device/driver combinations, long-term stability, or echo-cancellation quality have been validated. Lack of a new issue comment is not treated as proof of resolution.

Removal criteria: Retain the probes until broader real-device feedback includes the original failure conditions and the chain microphone P2 xrun_recoverable → same-capture microphone P5 data → normal dictation completion, with confirmation that the original symptom is resolved. Diagnostic cleanup will be a separate follow-up; user logs will not be automatically deleted or cleared.

Validation

  • Latest integration/fix commit: 07e3bc6a5284124622f7c6131caf4aa41a3d9254 (merge commit: 2b274ea1).
  • Review Bugbot found two missing AEC fields in the upstream voice-activation processing configurations; both were fixed and re-review returned No bugs found.
  • The first post-merge macOS CI exposed a further CPAL API incompatibility in upstream's newly added native audio cue (sample_rate.0 and a borrowed output StreamConfig). The complete failed job log was inspected; 07e3bc6a adapts those two calls to CPAL 0.18.2 and includes the actual native cue module in the standalone recorder harness. Bugbot review of this patch: No bugs found. No diagnostic probes were removed.
  • Previous pre-merge Windows recorder harness: 49 passed / 0 failed. Historical evidence only; the merged recorder/voice-activation/native-cue checks require the current-commit run.
  • Current-commit upstream PR CI passed (attempt 2): https://github.com/Open-Less/openless/actions/runs/39610791809 . The macOS job completed 618 passed / 0 failed / 5 ignored, including the native cue test and Xrun regression tests. Attempt 1 had a shared-cache credentials-test assertion failure (617 passed / 1 failed / 5 ignored); one same-SHA targeted rerun passed without changing tests, assertions or production credentials behavior. The earlier failure remains recorded; this does not establish the cause of that intermittent test failure.
  • Full CI with explicit Intel macOS runner passed: https://github.com/HKLHaoBin/openless/actions/runs/39610786967 . Windows, macOS, Linux, Android, Harmony and UI jobs passed. The separate Rust 1.99 MSRV job is intentionally skipped by upstream's stable-equals-MSRV condition, not bypassed by this PR.
  • Android APK passed: https://github.com/HKLHaoBin/openless/actions/runs/39610801656 . Release publishing is intentionally skipped for this branch build.
  • Windows desktop installer is ready: https://github.com/HKLHaoBin/openless/actions/runs/39610793440/artifacts/12351574447 . This is the installer artifact, not the updater bundle.
  • Native capsule passed on Windows and macOS (attempt 2): https://github.com/Open-Less/openless/actions/runs/39610791543 . The current PR check rollup has no failed or pending jobs; only the intentional MSRV skip described above remains.
  • Intel macOS desktop packaging passed on the same commit (attempt 2): https://github.com/HKLHaoBin/openless/actions/runs/39610793440/artifacts/12355752436 . The first attempt's DMG hdiutil resource-busy error did not recur on the bounded rerun. No packaging code or safety checks were changed.
  • Remaining external capacity blockers — not green: the Apple Silicon desktop job and default push-CI macOS job again received no hosted ARM runner on attempt 2 (empty runner name and steps; annotation: The job was not acquired by Runner of type hosted even after multiple attempts, with ARM capacity notices). Full available logs and annotations were retained. The bounded rerun is finished; no further automatic rerun is queued, no runner architecture or gate was silently replaced, and an Apple Silicon installer for this commit is not yet available.
  • All code/PR checks listed above refer to 07e3bc6a5284124622f7c6131caf4aa41a3d9254. Passing PR CI and available installers do not imply that the all-platform artifact workflow is green or that the rare real-hardware microphone P2 recovery has been confirmed.
  • The failed first merge CI remains visible as evidence: https://github.com/Open-Less/openless/actions/runs/39608848541 . Runs for 2b274ea1 do not verify the newer fix.
  • All compilation, tests and artifact builds run in GitHub Actions; no local compilation, tests, dependency builds or packaging were performed.

Testing notes / not covered

The feature is best effort. Device, driver, Bluetooth, protected-content, Linux ALSA, and output-route limitations may cause microphone-only fallback. The render route is bound at session start; changing it requires starting a new dictation session. Please test with real playback and microphone hardware, especially the original reproduction scenario in #1223. Successful insertion events do not by themselves establish transcription accuracy or acoustic echo suppression quality.

When sharing evidence, retain the original log locally and submit only privacy-reviewed diagnostic excerpts. Do not post audio, transcription content, keys or other unrelated personal data.

🤖 Generated with Claude Code

HKLHaoBin and others added 2 commits October 18, 2026 06:52
Add bounded render-reference processing for desktop dictation, preserve playback without muting, expose a local-only best-effort setting, and document platform limitations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Update the standalone recorder module harness to match the production cpal API and expose the shared render-reference buffer used by the recorder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add per-capture structured probes for reference availability, native CPAL errors, first and post-error callbacks, queue backpressure and watchdog faults. Keep microphone error behavior unchanged until the diagnostic build is reproduced on affected hardware. Cover diagnostic counters and error-kind redaction with recorder tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@H-Chris233 H-Chris233 added area:recording Recording area enhancement New feature or request experimental Experimental feature or change large-change Large or risky change needs-review Needs review needs-tests Needs tests P2 Medium priority platform:linux Linux-specific platform:macos macOS-specific platform:windows Windows-specific labels Oct 20, 2026
HKLHaoBin and others added 3 commits October 21, 2026 01:34
Separate CPAL Xrun notifications from fatal microphone errors after instrumented Windows logs showed data callbacks continuing immediately after each xrun. Keep capture liveness unchanged so the existing watchdog still detects stalled streams, preserve device and invalidated-stream failures, and retain diagnostic probes for hardware verification. Add regression coverage for xrun handling, repeated notifications, real faults and stopped capture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keep push and pull request defaults unchanged while allowing manual CI dispatches to select macos-15-intel when hosted ARM runners cannot acquire jobs. Apply the same explicit choice to macOS stable and MSRV checks, preserve all gates, and cover the routing contract in the frontend test suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Integrate voice activation and Linux native-format fallback with AEC. Preserve microphone Xrun recovery, diagnostics and regression coverage; combine both sets of local preferences and current dependency versions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HKLHaoBin
HKLHaoBin marked this pull request as ready for review October 21, 2026 13:39
Use the current sample-rate alias and pass StreamConfig by value. Include the native cue module in the recorder harness so its CPAL calls are compiled by the standalone remote checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@appergb appergb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

自动只读审查结论:建议暂缓合并(FAIL)

  • PR:#1228 feat(audio): add optional playback echo cancellation
  • 审查基线:base beta@43df23be3259… / head 07e3bc6a5284…(merge-base = base;当前 PR head 未变)
  • 范围:全部 36 个变更文件(+1599/−179)+ 必要调用方;只读审查,未修改/提交代码、未合并、未包含密钥或完整 patch
  • 结论:FAIL —— 7 个 P2、12 个 P3,无 P0/P1。契约、默认关闭、serde 兼容、云同步排除、参考不可用回退与既有测试基线是健康的;问题集中在 AEC 主路径的正确性、降级可观测性和平台能力判断。

已执行并通过的验证(本机只读执行):

  • cargo test --locked -p openless-core --lib audio → exit 0,76 passed(含 5 个新增 AEC 测试)
  • cargo test --locked -p openless-core --lib → exit 0,1355 passed / 3 ignored
  • python3 scripts/test-recorder-module.py(编译生产 recorder.rs + native_audio_cue.rs + core audio.rs)→ exit 0,63 passed(含 7 个新增 recorder 测试)
  • node --test scripts/ci-macos-runner-contract.test.mjs → 1 passed;前端 tsc --noEmit → exit 0;mock-data 测试 1 passed
  • UI(Orca 内置浏览器,本地 dev/mock):桌面 zh-CN「设置 → 录音与输入」显示「播放回声抑制」,默认关闭、点击后开启;未证明真实音频/原生链路

CI 观测(评论时):大部分 job SUCCESS,macOS checks 仍在进行,native-capsule (macos-latest) 为 CANCELLED,mergeStateStatus=BLOCKED、reviewDecision=REVIEW_REQUIRED;CI 未全部完成,未作为通过依据。

P2(建议合并前处理)

P2-A|macOS 双工输出端点可能把“麦克风”当播放参考

  • 位置:openless-all/app/src-tauri/src/recorder.rs start_reference_stream(约 2535-2561);cpal 0.18.2 CoreAudio 设备路径
  • 触发:macOS 默认输出设备同时 supports_input()(USB 耳麦、音频接口、虚拟声卡等)且开启 AEC
  • 预期 vs 实际:预期参考流=系统播放 loopback;实际 cpal 对该设备按普通 Input 建流,参考信号=近端麦克风,且 reference_available=true 仍会误报可用
  • 影响:AEC 可能抵消用户自己的声音;macOS 主推路径在常见双工设备上错误
  • 证据级别:static(cpal 0.18.2 源码行号可核);introduced
  • 最小修法:只接受无输入通道的 render endpoint 或显式 tap;否则返回 capability=unavailable,而不是静默把麦克风当参考

P2-B|macOS 缺少系统音频捕获权限与系统版本门禁

  • 位置:openless-all/app/src-tauri/Info.plist、tauri.conf.json、src-tauri/src/permissions.rs、recorder.rs start_reference_stream
  • 预期 vs 实际:应具备 NSAudioCaptureUsageDescription、系统音频捕获权限探测与版本能力判断;实际三者均缺失,最低系统声明 14.0 而 cpal loopback 需要 macOS >14.6。失败时仅 log::warn 回退,UI 偏好仍显示开启
  • 影响:macOS 主路径可能长期静默无 AEC;14.0–14.5 首次调用的实际行为未验证
  • 证据级别:static(拒绝形态为 hypothesis);introduced
  • 最小修法:补 purpose string、权限探测、系统版本门禁;旧系统/未授权时 capability=unavailable 并给出原因

P2-C|Linux PulseAudio monitor 依赖本地化 description 匹配

  • 位置:openless-all/app/linux-egui/src/audio.rs select_playback_monitor_device(约 570-606);cpal PulseAudio 后端返回服务器本地化 info.description
  • 触发:非英文 locale 的 PulseAudio,或 monitor source 描述不含英文 monitor
  • 预期 vs 实际:预期按 source 名称(*.monitor)/设备类型选源;实际用 device.description() 子串匹配 "monitor",zh/ja/ko 环境匹配失败 → Unsupported → 静默无 AEC(PipeWire default_sink 英文路径不受影响)
  • 影响:主要非英文用户群 AEC 静默失效且无提示
  • 证据级别:static;introduced
  • 最小修法:优先匹配 info.name 后缀/设备 id,description 仅 fallback;失败写 capability/诊断

P2-D|参考样本按 ceil 多取,非整数时长组合会周期性/长期欠载并复位

  • 位置:openless-all/app/crates/openless-core/src/audio.rs ReferenceAudioBuffer::take_for_input(74-88)与 process_with_reference 的回退复位(约 447-472)
  • 实际:needed = ceil(input_frames*ref_rate/input_rate) 每次会多取小数样本;队列一旦 len < needed 立即清空整队,随后 canceller reset 并整段走旧路径。部分采样率/回调帧组合(如监听 48k + 麦克风 44.1k/512 帧)可反复触发
  • 影响:开关打开但 AEC 间歇或长期不工作(录音本身不丢)
  • 证据级别:static(算术确定,具体设备组合未真机复现);introduced
  • 最小修法:累积小数亏欠或四舍五入;欠载时仅 bypass、不清空整队;补采样率不匹配单测

P2-E|参考 FIFO 无时间戳/延迟对齐,滤波器跨度不足,溢出后滞后永久化

  • 位置:openless-all/app/crates/openless-core/src/audio.rs(参考缓冲,约 9、32-98、295)与 src-tauri/src/recorder.rs(参考流启动,约 1926-1961)
  • 实际:参考 FIFO 无时间戳、无延迟估计、无漂移补偿;滤波器仅 1024 taps=64ms,覆盖不了常见/蓝牙链路延迟;2s 缓冲溢出时静默丢最旧且滞后不会自愈。PR 自身的可行性文档也要求 timestamps/delay/bounded drift
  • 影响:蓝牙/高延迟/异速率场景 AEC 基本无效,且无 under/overflow 诊断
  • 证据级别:static(设备延迟 hypothesis);introduced
  • 最小修法:采集两路时间戳/延迟并按其对齐;启动时修剪 backlog;溢出时重置并追最新;补 under/overflow 计数

P2-F|单个非有限 mic 样本可污染 NLMS 系数,导致整段录音静默

  • 位置:openless-all/app/crates/openless-core/src/audio.rs(EchoCanceller::process 约 331-375、process_with_reference 约 452-457)
  • 路径:mic 重采样结果未清洗 non-finite(参考侧有清洗)→ 初始收敛期 estimate.abs()<0.01 允许自适应 → error=NaN 写入 coefficients(f32::clamp 保留 NaN)→ 此后每样本 error.is_finite() 失败输出 0.0 → 整段 PCM 全 0,而 UI 仍显示录音
  • 影响:正是验收禁止的“静默静音”;只有 reference 缺口触发 reset 才能恢复
  • 证据级别:代码路径 verified(算术与 NaN clamp 行为已核),触发条件 hypothesis(取决于设备是否输出非有限样本);introduced
  • 最小修法:清洗 mic 重采样结果的 non-finite;error 非有限时复位系数;补 NaN 注入测试

P2-G|AEC 能力/可用性没有产品信号,UI 用“桌面壳”当能力,失败静默降级

  • 位置:openless-all/app/src/pages/settings/RecordingInputSection.tsx:81,558;src/lib/types.ts 的 PlatformCapabilities 无 AEC 能力字段;src-tauri/src/recorder.rs 失败仅 warn 回退
  • 预期 vs 实际:UI 应依据 host capability 显示/禁用并说明原因;实际仅判断 platformCaps?.platform === 'desktop',macOS/Linux-ALSA/蓝牙等不可用路径仍显示“已开启”,免责声明只在 tooltip 且未提 macOS
  • 影响:用户无法区分真实生效与静默 fallback;与 P2-A/B/C/E 共同构成“开了但没消回声”的产品风险
  • 证据级别:static + UI 症状 verified(mock 页);introduced
  • 最小修法:core/native 增加 echoCancellationSupport { backend, available, reason },前端据此显示/禁用并给原因

P3(建议修复,不单独阻塞)

  • core:AEC 开启时 enhancement 的输入域改变且无测试;mid-session 回退只复位 canceller/重采样器、未复位 enhancer/normalizer;渲染回调 try_lock 失败静默丢整块且无计数;双讲检测用瞬时样本能量,过零附近易误判;AEC 主路径缺少集成/异常路径测试(仅直接测 EchoCanceller)。
  • native:音频回调内做 format!/日志/Vec 分配(实时安全风险);参考/麦克风时间戳未利用(见 P2-E);[DEBUG-ISSUE1223] 诊断探针留在生产路径(含 PID/session_id 日志)。
  • frontend:session-frozen 生效时机未在文案中告知;AEC 与「录音时静音」并存没有引导(语义待产品决策,非 bug);i18n 中文「回声抑制/回声消除」混用、法文把 best effort 译成 « expérimentale »;仓库无组件测试基座,本 PR 未补任何前端回归;首帧平台能力未知时该行后插入造成轻微布局跳动。

未测 / 需要真机或环境补齐(不视为通过)

  • 真机 AEC:Windows WASAPI loopback、macOS CoreAudio tap/TCC 授权、PipeWire/PulseAudio monitor、ALSA 回退、蓝牙/路由切换、双讲保真、长时漂移、CPU/功耗
  • src-tauri 完整 cargo test/打包;linux-egui 在 Linux 上的编译/运行;移动端 Android/iOS/Harmony;Swift/CoreML
  • 真实 IPC 持久化与英文/移动/窄屏 UI、刷新后的持久化(本轮 UI 仅 mock/dev、zh-CN 桌面)

建议

合并前至少处理 P2-A、P2-B、P2-C、P2-D、P2-F(或明确将 AEC 标为实验性并把不可用/降级状态显式暴露),P2-E 转为真机验收缺口并补诊断,P2-G 补 capability 通路与可见文案。修复后可再跑同一套只读审查(代码与测试基线这次是干净的)。

说明:以上来自本机只读自动 Review(分模块静态审查 + 指定测试 + mock UI),不含密钥/完整 patch,也未修改或合并代码。若某条与你的设计意图冲突(例如“best effort 允许静默回退”),欢迎指出取舍点,我可以按最新要求复核等级。

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

Labels

area:recording Recording area enhancement New feature or request experimental Experimental feature or change large-change Large or risky change needs-review Needs review needs-tests Needs tests P2 Medium priority platform:linux Linux-specific platform:macos macOS-specific platform:windows Windows-specific

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[audio] 支持可选的播放回声抑制,在保留系统声音的同时进行语音输入

3 participants