Skip to content

feat(audio): music beds start at -18 dB and duck under the voice - #797

Merged
EtienneLescot merged 1 commit into
integration/demo-never-uglyfrom
feat/audio-music-ducking
Sep 26, 2026
Merged

EtienneLescot merged 1 commit into
integration/demo-never-uglyfrom
feat/audio-music-ducking

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Music no longer buries the voice. A new music bed starts at −18 dB with 1 s fades, and every bed dips 10 dB while someone speaks. Audit finding #6, part 2 of 2.

Stacked on #796. The first commit is #796; review only the last one. Merge #796 first, then this rebases to one commit.

What changed

  • Defaults at creation. createAudioTrack gives kind: "music" −18 dB, 1000 ms fade-in and fade-out; a voiceover stays at 0 dB (the export levels it). It is the one door every new track goes through. The schema's parse defaults are untouched.
  • Reset in the audio track inspector returns to those same defaults, not to 0 dB.
  • Ducking at export (mix_external_tracks). Voiceovers are summed first, so the voice is the recording plus every take. 10 ms RMS above −35 dBFS counts as speech. The dip starts 250 ms ahead (the export sees the whole voice), holds 500 ms across pauses, releases over 600 ms, as straight ramps in dB, interpolated per sample.
  • Ducking in the preview. Same depth, threshold, hold and release, driven by an AnalyserNode on the voice in the existing rAF loop.
  • Help text of the audio track pane says music dips while someone speaks (15 locales).

Existing projects

  • Levels are not rewritten. A bed saved at 0 dB stays at 0 dB; only new beds start at −18.
  • Ducking applies to every music bed at export, old ones included. Measured on an old 0 dB bed: same level in pauses, 10 dB lower under speech.

Decisions to confirm

Still differs between preview and export

  • The preview hears the voice as it plays, so its dip ends 250 ms into the first word instead of before it.
  • The preview's envelope is read once per frame, not per 10 ms block.

Related issue

Part of the Screen Studio design audit, finding #6.

Type of change

  • Feature
  • Enhancement

Release impact

  • Minor

Desktop impact

  • Not platform-specific

Rust changes stay in audio.rs, shared by the three pipelines. Only the Windows build ran here.

Testing

Measured through the native export (the addon's exportMulti, fed the scene built by buildSceneDescription). A −24 LUFS speech track notched at 3 kHz, a steady 3 kHz tone at −13 LUFS as the music, so a 3 kHz band-pass reads the bed alone.

Mix Programme Bed in pauses Bed under speech
Before, bed at 0 dB −13.2 LUFS (music louder than the voice) −19.5 dB −19.5 dB
After, same old 0 dB bed −15.0 LUFS, −1.5 dBTP −19.5 dB −28.7 to −29.5 dB
After, new bed (−18 dB, fades) −16.1 LUFS, −1.5 dBTP −37.5 dB −46.9 to −47.7 dB

Checks

  • cargo test -p openscreen-compositor --lib audio: 49 passed. New: dip ahead of the voice and recovery, no pumping in a 0.3 s breath, no dip on silence or on a boosted noise floor, a bed summed under the curve and a voiceover not.
  • npx tsc --noEmit, npx tsc -p tsconfig.test.json --noEmit: clean. npm run lint: clean (existing warnings). npm run i18n:check: passed.
  • Full npx vitest --run: 261/261 files. Two stale expectations (placed bed at 0 dB, fake AudioContext without createAnalyser) fixed and rerun green.
  • Not run: listening to the preview ducker in the app (unit-tested ballistics only).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Voice tracks are automatically balanced to a consistent loudness in preview and export.
    • Background music automatically lowers while speech is detected, then returns to its set level.
    • Music tracks now start with lower volume and gradual fades; voice tracks retain full volume with no fades by default.
    • Audio output uses a limiter to reduce harsh clipping.
  • Documentation
    • Updated audio settings guidance to explain loudness balancing and automatic music ducking.

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: ce19e40b-ae29-4261-88e3-a88574d5976c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: dcc24a53-b240-42b6-9830-5c04ca49af4b

📥 Commits

Reviewing files that changed from the base of the PR and between 218bdbb and aaec31c.

📒 Files selected for processing (34)
  • crates/compositor-view-napi/src/lib.rs
  • crates/compositor/src/audio.rs
  • crates/compositor/src/audio_jobs.rs
  • crates/compositor/src/scene.rs
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/native-bridge/services/compositorViewService.ts
  • electron/native/compositor-view/addon.d.ts
  • electron/preload.ts
  • src/components/ai-edition/RightPanes.audioTrack.test.tsx
  • src/components/ai-edition/RightPanes.tsx
  • src/components/ai-edition/VirtualPreview.audio.test.ts
  • src/components/ai-edition/VirtualPreview.playback.test.tsx
  • src/components/ai-edition/VirtualPreview.tsx
  • src/i18n/locales/ar/settings.json
  • src/i18n/locales/cs/settings.json
  • src/i18n/locales/de/settings.json
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/it/settings.json
  • src/i18n/locales/ja-JP/settings.json
  • src/i18n/locales/ko-KR/settings.json
  • src/i18n/locales/pt-BR/settings.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/tr/settings.json
  • src/i18n/locales/vi/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-TW/settings.json
  • src/lib/ai-edition/schema/index.test.ts
  • src/lib/ai-edition/schema/index.ts
  • src/lib/ai-edition/store/useTimeline.test.ts
  • src/native/sceneDescription.test.ts
  • src/native/sceneDescription.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The audio pipeline adds per-file loudness measurement and normalization, output peak limiting, and voice-responsive music ducking. Track kinds and kind-specific defaults are carried through scene data and editor controls. Electron exposes an asynchronous loudness-gain request, which the preview uses for recordings and voiceovers.

Changes

Audio processing and preview

Layer / File(s) Summary
Track kinds, defaults, and scene data
crates/compositor/src/scene.rs, src/lib/ai-edition/schema/index.ts, src/native/sceneDescription.ts, src/components/ai-edition/RightPanes.tsx, src/components/ai-edition/RightPanes.audioTrack.test.tsx, src/lib/ai-edition/schema/index.test.ts, src/lib/ai-edition/store/useTimeline.test.ts, src/native/sceneDescription.test.ts
Audio tracks carry a kind through scene serialization. New music tracks and inspector resets use −18 dB gain and one-second fades; voiceovers use 0 dB gain and no fades. Parsing stored tracks retains its existing defaults.
Loudness measurement and output limiting
crates/compositor/src/audio.rs, crates/compositor/src/audio_jobs.rs
The compositor measures gated per-file loudness, caches gains, applies normalization to decoded audio, and limits final peaks to −1.5 dBFS. Track sums remain unclipped before later processing.
Voiceover-first mixing and music ducking
crates/compositor/src/audio.rs
The compositor overlays voiceovers before music and applies a duck curve to music based on programme audio. Track placement, fades, and gain bounds remain part of the overlay path.
Loudness gain API bridge
crates/compositor-view-napi/src/lib.rs, electron/native/compositor-view/addon.d.ts, electron/native-bridge/services/compositorViewService.ts, electron/ipc/handlers.ts, electron/preload.ts, electron/electron-env.d.ts
An asynchronous native operation returns a file’s loudness gain. Electron exposes the operation through IPC and the preload API, and checks requested paths before calling the compositor service.
Preview gains, ducking, and help text
src/components/ai-edition/VirtualPreview.tsx, src/components/ai-edition/VirtualPreview.audio.test.ts, src/components/ai-edition/VirtualPreview.playback.test.tsx, src/i18n/locales/*/settings.json
The preview requests gains for recordings and voiceovers, routes voice through an analyser, and adjusts music gain during detected speech. Audio help text describes normalization and automatic music reduction across locales.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~50 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Preview as VirtualPreview
  participant Preload as Electron preload
  participant IPC as get-loudness-gain handler
  participant Service as CompositorViewService
  participant Addon as compositor-view addon
  participant Native as loudness_gain_db
  Preview->>Preload: getLoudnessGain(filePath)
  Preload->>IPC: invoke get-loudness-gain
  IPC->>Service: loudnessGainDb(filePath)
  Service->>Addon: loudnessGainDb(path)
  Addon->>Native: measure file loudness asynchronously
  Native-->>Addon: gain in dB
  Addon-->>Service: gain in dB
  Service-->>IPC: gain or null
  IPC-->>Preload: success and gainDb
  Preload-->>Preview: loudness gain result
Loading

Suggested reviewers: beetix, my-denia

Merge Risk: ⚪ Minimal · up to aaec3

The identified preview concerns do not block merging; proceed with normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to aaec3

Music ducking changes playback and export behavior, including for existing projects. The reviewed commit does not appear to add file-access authority or a new security boundary, but some inherited file-handling behavior remains uncertain.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The reviewed commit changes locally rendered and exported audio, not the set of renderer-approved filesystem paths. The inherited loudness operation runs in the desktop application’s native process on approved media.

Trust Boundaries and Controls

  • observed — The inherited path gate checks normalized path membership and a permitted media extension. Existing binary and audio-peaks handlers use that same gate; its directory check is lexical rather than a demonstrated canonical-file check.

Resilience and Maintainability Implications

  • observed — The preceding commit’s loudness cache coalesces concurrent requests for an identical path, size, and modification time, but has no eviction; measurement failure produces a neutral gain. Neither behavior is added by the reviewed commit.

Hardening Proposals

  • proposed — Assess bounded eviction and canonical file identity for the preceding commit’s measurement cache and approved-path policy separately; their current behavior is not an introduced finding against this stacked commit.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary user-facing changes: new music-bed defaults and voice-triggered ducking.
Description check ✅ Passed The description is complete and relevant. It covers the change, related audit finding, change type, release impact, platform impact, implementation details, compatibility behavior, and extensive testi…
Docstring Coverage ✅ Passed Docstring coverage is 83.82% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 68 functions across 19 files. (15 skipped: …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… the voice

Imported music landed at 0 dB with hard edges and sat at full level under
the narration. A new music track now starts at -18 dB with 1 s fades (the
inspector's reset returns to the same values); tracks already saved keep
the level their author set.

At export the music dips 10 dB while the voice (the recording plus every
voiceover) is above -35 dBFS, looking ahead 250 ms, holding 500 ms across
pauses and releasing over 600 ms. The preview runs the same ballistics on
a live analyser of the voice, without the look-ahead.
@EtienneLescot
EtienneLescot force-pushed the feat/audio-music-ducking branch from aaec31c to 9938f65 Compare September 26, 2026 08:25
@EtienneLescot
EtienneLescot changed the base branch from main to integration/demo-never-ugly September 26, 2026 08:25
@EtienneLescot
EtienneLescot merged commit c769d3e into integration/demo-never-ugly Sep 26, 2026
19 of 22 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.

1 participant