Skip to content

Implement the voice-enhancement audio filter (#652) - #671

Merged
matteius merged 2 commits into
mainfrom
feat/voice-enhancement-filter-652
Oct 24, 2026
Merged

matteius merged 2 commits into
mainfrom
feat/voice-enhancement-filter-652

Conversation

@matteius

Copy link
Copy Markdown
Contributor

Closes #652.

Summary

The per-stream Voice Enhancement option (discussion #395) was saved and passed to the audio transcoder, but did nothing. With this PR it filters recorded audio while PCM is transcoded to AAC.

  • Filter chain: the first decoded frame lazily builds abuffer → highpass=f=100 ×2 → [lowpass=f=8000] → [afftdn=nr=12:nf=-40:tn=1] → aformat=s16:<rate> → abuffersink.
    • The filtered output goes straight into the encoder FIFO, skipping swr.
    • I used two highpass sections instead of one (24 dB/octave). A single section only took 50 Hz hum down about 12 dB; two give about −25 dB at 50 Hz and −19 dB at 60 Hz.
    • The lowpass is only added when the sample rate is above 16 kHz. 8 kHz G.711 already stops at 4 kHz.
  • Turning it on or off mid-recording: the next packet builds the graph, or drains and frees it, so no samples are lost. The final flush also drains the filter tail. The graph persists across segment rotations.
  • Errors: if the graph fails to build or run, it logs once and recording continues unfiltered. Recording never fails because of this.
  • When it's off: none of the new code runs; the audio path is the same as before.
  • Passthrough audio is not filtered. I documented this instead of forcing a transcode. AAC, MP3, Opus and AC-3 from the camera are stored as-is. Live audio is never filtered.
  • Detection recordings: previously only continuous recordings received the setting. Detection-triggered recordings now pass it through too.

Build and packaging

  • ENABLE_AUDIO_FILTERS (default ON when pkg-config finds libavfilter>=10). Without it the build still succeeds, and turning the option on logs that it is unavailable.
  • ENABLE_AUDIO_DENOISE controls afftdn, the CPU-heavy stage. It defaults OFF in the embedded profile.
  • Added libavfilter-dev to the Dockerfile, the CI workflows and the install docs. The Debian package now depends on libavfilter10 (trixie) or libavfilter11 (sid).
  • docs/BUILD.md and the README describe the feature and the passthrough limitation. The UI help text (en/fil/ko) no longer says "no effect yet".

Tests

New cases in tests/unit/test_audio_transcoder.c:

  • Hum attenuation: a 1 kHz tone plus an equally loud 50 Hz hum goes through the real transcoder and is decoded from AAC. Requires at least 20 dB less hum, no more than 6 dB lost on the tone, and an exact sample count.
  • All PCM formats with enhancement on: μ-law 8 kHz through 48 kHz stereo, with exact sample accounting.
  • Live toggling: on→off and off→on mid-stream without losing samples.

Validation (run locally against FFmpeg 8 / libavfilter 11):

  • Full ctest after building all targets, in a Debug build with ASan/UBSan: 156/156 passed, no leaks reported.
  • -DENABLE_AUDIO_FILTERS=OFF: compiles, does not link avfilter, tests pass, and the option logs that it is unavailable.
  • -DENABLE_AUDIO_DENOISE=OFF: chain runs without afftdn, tests pass.

Rollback

Revert this PR and the opt-in goes back to being a no-op.

🤖 Generated with Claude Code

The per-stream voice-enhancement opt-in (discussion #395) was stored and
plumbed to the audio transcoder but did nothing. Build a libavfilter graph
lazily from the first decoded frame and run PCM audio through it before
AAC encoding:

  abuffer -> highpass=f=100 x2 -> [lowpass=f=8000] -> [afftdn] ->
  aformat=s16:<rate> -> abuffersink

The filtered S16 output feeds the encoder FIFO directly, bypassing swr.
Toggling mid-recording builds the graph, or drains and frees it, on the
next packet; the final flush drains the filter tail. Build/run failures log
once and continue unfiltered. With the option off the existing path is
unchanged. Passthrough (AAC/MP3/Opus/AC-3) audio is not filtered.

- CMake: ENABLE_AUDIO_FILTERS (default ON when libavfilter is found) and
  ENABLE_AUDIO_DENOISE (afftdn; OFF in the embedded profile). Builds
  without libavfilter log that the option is unavailable.
- Detection-triggered recordings now pass the opt-in through too.
- Add libavfilter-dev to Dockerfile, CI workflows and install docs; add
  libavfilter10/11 to the Debian package dependencies.
- Update the UI help text (en/fil/ko) and document the feature in BUILD.md.
- Tests: hum attenuation (>=20 dB at 50 Hz, voice band within 6 dB),
  exact sample accounting across all PCM formats, and live toggling.

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

codecov Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.77998% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
tests/unit/test_audio_voice_filter_faults.c 99.39% 1 Missing ⚠️
tests/unit/test_udt_recording.c 99.81% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Unified detection thread coverage was effectively zero in CI: its two
real-code tests were not in the coverage job.

- test_udt_recording includes unified_detection_thread.c and drives real
  fixture packets plus synthetic G.711 through process_packet() against a
  temporary catalog with a healthy default storage target: detection-,
  external-trigger- and detection-stream-driven recording cycles with
  keep-alives, pre-buffer flush and start-time correction, per-codec audio
  setup (including the voice-enhancement opt-in), blocked placement, the
  stop fallback, multi-engine config and fan-out, the status/registry API,
  a real thread lifecycle against the fixture file, and shutdown cleanup.
- test_audio_voice_filter_faults wraps the libavfilter/FIFO calls to force
  every voice-enhancement failure path and checks recording continues
  unfiltered without losing samples.
- Run both, plus test_audio_voice_enhancement, test_detection_current_frame
  and test_detection_io_recovery, in the coverage and sanitizer jobs.

Locally: unified_detection_thread.c 26% -> 78.5% line coverage; all patch
lines in mp4_writer_utils.c and unified_detection_thread.c are covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matteius
matteius merged commit 800d5b5 into main Oct 24, 2026
14 checks passed
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.

[Agent] Implement the voice-enhancement audio filter (#395)

1 participant