fix(export): mix all source audio tracks instead of only the first#86
fix(export): mix all source audio tracks instead of only the first#86josiahcoad wants to merge 1 commit into
Conversation
The native capture helpers write system audio and the microphone as separate audio tracks in the recording. The export audio pipeline read only the demuxer's default (first) track, so a recording made with both system audio and mic enabled exported with just the system-audio track — pure silence when nothing was playing, dropping the user's voice entirely. - Trim-only path: enumerate all audio streams via getAVStreams(), decode each one (readAVPacket per stream index), and sum them into a single mixed timeline, re-chunked at the same granularity the single-track path uses for trim skipping. Falls back to the existing default-track path for single-track sources. - Pitch-preserved (media element) path: enable every audio track on the render element, mirroring what the editor preview already does. Adds a browser-mode regression test with a two-track fixture (silent first track + 440Hz tone second track) that exports through the real WebCodecs pipeline and asserts the result is audible; it fails with peak=0 on the previous code. Known remaining gap (intentionally out of scope): the source-copy fast path returns the source verbatim (all tracks preserved), and the offline WSOLA path (>16x speed) still reads the default track only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughChangesThe audio exporter now preserves multiple audio tracks in trim-only and speed-preserved export paths. Trim-only exports decode and mix audio streams with channel conversion, resampling, clamping, and trim-aware re-chunking. Browser coverage validates a two-track fixture. Multi-track audio export
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant VideoExporter
participant WebDemuxer
participant AudioDecoder
participant AudioEncoder
VideoExporter->>WebDemuxer: enumerate audio streams
WebDemuxer->>AudioDecoder: decode each stream
AudioDecoder->>AudioEncoder: provide decoded audio data
AudioEncoder->>AudioEncoder: mix, resample, clamp, and trim slices
AudioEncoder-->>VideoExporter: encode processed audio
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/lib/exporter/audioEncoder.ts (1)
641-674: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winA mid-stream decode error aborts the whole export and leaks
pendingframes.The stated intent is to “skip undecodable audio streams,” but only config-unsupported streams are skipped (Line 566). If a stream passes
isConfigSupportedyet a packet fails at runtime, the error callback logs while WebCodecs closes the decoder; the nextdecoder.decode(...)on Line 645 then throwsInvalidStateError, which propagates out ofdecodeAndMixAudioStreamsand fails the entire audio export — even for tracks that decoded cleanly. AnyAudioDatastill sitting inpendingat that point is also neverclose()d, leaking native resources.Consider isolating per-stream decode failures so one bad stream is dropped rather than aborting the mix.
🛡️ Sketch: contain per-stream failures
const reader = packetStream.getReader(); try { while (!this.cancelled) { const { done, value: packet } = await reader.read(); if (done || !packet) break; decoder.decode( new EncodedAudioChunk({ /* ... */ }), ); while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) { await new Promise((resolve) => setTimeout(resolve, 1)); } drainPending(); } + } catch (e) { + console.warn("[AudioProcessor] Skipping stream after decode error:", e); } finally { try { await reader.cancel(); } catch { /* reader already closed */ } + for (const d of pending) d.close(); + pending.length = 0; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/exporter/audioEncoder.ts` around lines 641 - 674, Update the per-stream decode flow in decodeAndMixAudioStreams around the reader loop and decoder.flush so runtime decode or flush failures are caught, logged, and cause only the current audio stream to be dropped rather than propagating to the overall export. Ensure all AudioData entries remaining in pending are closed when a stream fails, while preserving normal draining and cleanup for successful or cancelled streams.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/lib/exporter/audioEncoder.ts`:
- Around line 641-674: Update the per-stream decode flow in
decodeAndMixAudioStreams around the reader loop and decoder.flush so runtime
decode or flush failures are caught, logged, and cause only the current audio
stream to be dropped rather than propagating to the overall export. Ensure all
AudioData entries remaining in pending are closed when a stream fails, while
preserving normal draining and cleanup for successful or cancelled streams.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 900f75fd-369c-4f59-a00c-9e48fa77febd
⛔ Files ignored due to path filters (1)
tests/fixtures/two-audio-tracks.mp4is excluded by!**/*.mp4
📒 Files selected for processing (2)
src/lib/exporter/audioEncoder.tssrc/lib/exporter/multiTrackAudio.browser.test.ts
Problem
Recordings made with both system audio and microphone enabled export with no audible audio. The native capture helpers write system audio and the microphone as two separate audio tracks in the recording; the export pipeline reads only the demuxer's default (first) track — the system-audio track — which is pure silence when nothing was playing on the machine. The user's voice (track 2) is dropped entirely.
Reproduced on macOS 1.6.0: source recording has
a:0system audio at −91 dB (silence) anda:1mic at −15.9 dB peak; the exported MP4's audio is −91 dB throughout.Fix
getAVStreams(), decode each stream (readAVPacketper stream index), and sum them into one mixed timeline, hard-clamped to [-1, 1] and re-chunked at the same granularity the single-track path uses for trim skipping. Single-track sources take the existing path unchanged.VideoPlaybackalready does for the editor preview.Known gaps, intentionally out of scope and noted in the code: the source-copy fast path returns the source verbatim (all tracks preserved, but players will default to the silent first track), and the offline WSOLA path (segments >16× speed) still reads the default track only.
Testing
multiTrackAudio.browser.test.ts) with a two-track fixture — silent first track + 440 Hz tone second track, the exact shape the native helpers produce. It exports through the real WebCodecs pipeline and asserts the decoded result is audible. It fails with peak=0 on the previous code and passes with this change.tsc --noEmit,biome check, full unit suite (352 tests) and all browser-mode tests (7) pass.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests