From 8894d4c2f1a585cd90fffec1e57e6e46bace3190 Mon Sep 17 00:00:00 2001 From: josiahcoad Date: Sun, 12 Jul 2026 14:07:58 -0500 Subject: [PATCH] fix(export): mix all source audio tracks instead of only the first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/exporter/audioEncoder.ts | 372 +++++++++++++++--- .../exporter/multiTrackAudio.browser.test.ts | 55 +++ tests/fixtures/two-audio-tracks.mp4 | Bin 0 -> 23516 bytes 3 files changed, 379 insertions(+), 48 deletions(-) create mode 100644 src/lib/exporter/multiTrackAudio.browser.test.ts create mode 100644 tests/fixtures/two-audio-tracks.mp4 diff --git a/src/lib/exporter/audioEncoder.ts b/src/lib/exporter/audioEncoder.ts index b97aea1fd..ac33bd1f3 100644 --- a/src/lib/exporter/audioEncoder.ts +++ b/src/lib/exporter/audioEncoder.ts @@ -1,4 +1,4 @@ -import { WebDemuxer } from "web-demuxer"; +import { AVMediaType, type WebAVStream, WebDemuxer } from "web-demuxer"; import { MAX_NATIVE_PLAYBACK_RATE, type SpeedRegion, @@ -20,6 +20,10 @@ const SEEK_TIMEOUT_MS = 5_000; // AudioData slice size fed to the encoder from the offline time-stretch path. const OFFLINE_OUTPUT_FRAMES = 4096; const ENCODE_BACKPRESSURE_LIMIT = 20; +// Mixed multi-track audio is re-chunked into slices of this many frames +// (~21ms at 48kHz), matching the granularity at which the single-track path +// drops whole source chunks inside trim regions. +const MIX_SLICE_FRAMES = 1024; // Must match streamingDecoder's EPSILON_SEC so the offline audio segment lengths // use the same per-frame quantization as the video output. const VIDEO_EPSILON_SEC = 0.001; @@ -39,6 +43,26 @@ const EXPORT_AUDIO_CODECS: ExportAudioCodecCandidate[] = [ { encoderCodec: "opus", muxerCodec: "opus", label: "Opus" }, ]; +type MediaElementWithAudioTracks = HTMLMediaElement & { + audioTracks?: { + length: number; + [index: number]: { enabled: boolean }; + }; +}; + +// A multi-track source (e.g. system audio + microphone) plays only its default +// audio track; enable every track so the rendered timeline audio contains all +// of them. No-op when the audioTracks API is unavailable. +function enableAllMediaAudioTracks(media: HTMLMediaElement) { + const audioTracks = (media as MediaElementWithAudioTracks).audioTracks; + if (!audioTracks || audioTracks.length <= 1) { + return; + } + for (let index = 0; index < audioTracks.length; index += 1) { + audioTracks[index].enabled = true; + } +} + function averageChannels(sourcePlanes: Float32Array[], frame: number) { let mixed = 0; for (const plane of sourcePlanes) { @@ -293,64 +317,67 @@ export class AudioProcessor { readEndSec?: number, exportCodec?: ExportAudioCodec, ): Promise { - let audioConfig: AudioDecoderConfig; - try { - audioConfig = await demuxer.getDecoderConfig("audio"); - } catch { - console.warn("[AudioProcessor] No audio track found, skipping"); - return; - } - - const codecCheck = await AudioDecoder.isConfigSupported(audioConfig); - if (!codecCheck.supported) { - console.warn("[AudioProcessor] Audio codec not supported:", audioConfig.codec); - return; - } - - // Phase 1: decode, skipping trimmed regions. - const decodedFrames: AudioData[] = []; - - const decoder = new AudioDecoder({ - output: (data: AudioData) => decodedFrames.push(data), - error: (e: DOMException) => console.error("[AudioProcessor] Decode error:", e), - }); - decoder.configure(audioConfig); - const safeReadEndSec = typeof readEndSec === "number" && Number.isFinite(readEndSec) ? Math.max(0, readEndSec) : undefined; - const audioStream = - safeReadEndSec !== undefined - ? demuxer.read("audio", 0, safeReadEndSec) - : demuxer.read("audio"); - const reader = audioStream.getReader(); + // A recording can carry several audio tracks (the native capture helpers + // write system audio and the microphone as separate streams). The + // demuxer's default "audio" selection reads only the first track, which + // silently drops the rest — a mic-only recording would export just the + // silent system-audio track. Mix every decodable track instead. + let audioStreams: WebAVStream[] = []; try { - while (!this.cancelled) { - const { done, value: chunk } = await reader.read(); - if (done || !chunk) break; - - const timestampMs = chunk.timestamp / 1000; - if (this.isInTrimRegion(timestampMs, sortedTrims)) continue; + const allStreams = await demuxer.getAVStreams(); + audioStreams = allStreams.filter( + (stream) => stream.codec_type === AVMediaType.AVMEDIA_TYPE_AUDIO, + ); + } catch { + // Stream enumeration failed; fall back to the default-track path below. + } - decoder.decode(chunk); + let decodedFrames: AudioData[]; + let sampleRate: number; + let channels: number; - while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) { - await new Promise((resolve) => setTimeout(resolve, 1)); - } + if (audioStreams.length > 1) { + const mixed = await this.decodeAndMixAudioStreams( + demuxer, + audioStreams, + sortedTrims, + safeReadEndSec, + ); + if (!mixed) { + console.warn("[AudioProcessor] No decodable audio streams, skipping"); + return; } - } finally { + decodedFrames = mixed.frames; + sampleRate = mixed.sampleRate; + channels = mixed.numberOfChannels; + } else { + let audioConfig: AudioDecoderConfig; try { - await reader.cancel(); + audioConfig = await demuxer.getDecoderConfig("audio"); } catch { - /* reader already closed */ + console.warn("[AudioProcessor] No audio track found, skipping"); + return; } - } - if (decoder.state === "configured") { - await decoder.flush(); - decoder.close(); + const codecCheck = await AudioDecoder.isConfigSupported(audioConfig); + if (!codecCheck.supported) { + console.warn("[AudioProcessor] Audio codec not supported:", audioConfig.codec); + return; + } + + decodedFrames = await this.decodeDefaultAudioTrack( + demuxer, + audioConfig, + sortedTrims, + safeReadEndSec, + ); + sampleRate = audioConfig.sampleRate || 48000; + channels = audioConfig.numberOfChannels || 2; } if (this.cancelled || decodedFrames.length === 0) { @@ -368,8 +395,6 @@ export class AudioProcessor { error: (e: DOMException) => console.error("[AudioProcessor] Encode error:", e), }); - const sampleRate = audioConfig.sampleRate || 48000; - const channels = audioConfig.numberOfChannels || 2; const selectedCodec = exportCodec ?? (await AudioProcessor.selectSupportedExportCodec(sampleRate, channels)); if (!selectedCodec) { @@ -435,6 +460,256 @@ export class AudioProcessor { ); } + // Phase 1 of the trim-only path for single-track sources: decode the default + // audio track, skipping chunks inside trimmed regions. + private async decodeDefaultAudioTrack( + demuxer: WebDemuxer, + audioConfig: AudioDecoderConfig, + sortedTrims: TrimRegion[], + safeReadEndSec?: number, + ): Promise { + const decodedFrames: AudioData[] = []; + + const decoder = new AudioDecoder({ + output: (data: AudioData) => decodedFrames.push(data), + error: (e: DOMException) => console.error("[AudioProcessor] Decode error:", e), + }); + decoder.configure(audioConfig); + + const audioStream = + safeReadEndSec !== undefined + ? demuxer.read("audio", 0, safeReadEndSec) + : demuxer.read("audio"); + const reader = audioStream.getReader(); + + try { + while (!this.cancelled) { + const { done, value: chunk } = await reader.read(); + if (done || !chunk) break; + + const timestampMs = chunk.timestamp / 1000; + if (this.isInTrimRegion(timestampMs, sortedTrims)) continue; + + decoder.decode(chunk); + + while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) { + await new Promise((resolve) => setTimeout(resolve, 1)); + } + } + } finally { + try { + await reader.cancel(); + } catch { + /* reader already closed */ + } + } + + if (decoder.state === "configured") { + await decoder.flush(); + decoder.close(); + } + + return decodedFrames; + } + + /** + * Decode every audio stream in the source and sum them into one timeline. + * Returns the mix re-chunked into AudioData slices whose timestamps are + * source time (trim-region slices already dropped), so the caller's + * trim-offset/encode phase treats them exactly like single-track output. + */ + private async decodeAndMixAudioStreams( + demuxer: WebDemuxer, + streams: WebAVStream[], + sortedTrims: TrimRegion[], + safeReadEndSec?: number, + ): Promise<{ frames: AudioData[]; sampleRate: number; numberOfChannels: number } | null> { + const sampleRate = streams[0].sample_rate || 48000; + const numberOfChannels = Math.min( + 2, + Math.max(1, ...streams.map((stream) => stream.channels || 1)), + ); + + // Growable planar mix buffer in source time, one plane per channel. + let capacity = Math.max(sampleRate, Math.ceil((safeReadEndSec ?? 60) * sampleRate)); + let mixPlanes = Array.from({ length: numberOfChannels }, () => new Float32Array(capacity)); + let mixLength = 0; + let mixedStreams = 0; + + const ensureCapacity = (samples: number) => { + if (samples <= capacity) return; + while (capacity < samples) capacity *= 2; + mixPlanes = mixPlanes.map((plane) => { + const grown = new Float32Array(capacity); + grown.set(plane); + return grown; + }); + }; + + for (const stream of streams) { + if (this.cancelled) break; + + const config: AudioDecoderConfig = { + codec: stream.codec_string, + sampleRate: stream.sample_rate, + numberOfChannels: stream.channels, + ...(stream.extradata && stream.extradata.length > 0 + ? { description: stream.extradata } + : {}), + }; + let supported = false; + try { + supported = (await AudioDecoder.isConfigSupported(config)).supported ?? false; + } catch { + supported = false; + } + if (!supported) { + console.warn("[AudioProcessor] Skipping undecodable audio stream:", stream.codec_string); + continue; + } + + const pending: AudioData[] = []; + const decoder = new AudioDecoder({ + output: (data: AudioData) => pending.push(data), + error: (e: DOMException) => console.error("[AudioProcessor] Decode error:", e), + }); + decoder.configure(config); + + const drainPending = () => { + for (const data of pending) { + const frameCount = data.numberOfFrames; + const srcRate = data.sampleRate || stream.sample_rate || sampleRate; + const srcPlanes: Float32Array[] = []; + for (let c = 0; c < data.numberOfChannels; c++) { + const plane = new Float32Array(frameCount); + data.copyTo(plane, { format: "f32-planar", planeIndex: c }); + srcPlanes.push(plane); + } + const startSample = Math.round((data.timestamp / 1_000_000) * sampleRate); + data.close(); + if (startSample < 0 || frameCount === 0) { + // Negative-timestamp codec preroll carries priming samples, not + // timeline audio; the single-track path never plays it either. + continue; + } + + // Map source channels onto the mix's channel count. + let planes: Float32Array[]; + if (srcPlanes.length === numberOfChannels) { + planes = srcPlanes; + } else if (srcPlanes.length === 1) { + planes = Array.from({ length: numberOfChannels }, () => srcPlanes[0]); + } else { + const downmixed = downmixPlanarChannelsForExport(srcPlanes, numberOfChannels); + planes = []; + for (let c = 0; c < numberOfChannels; c++) { + planes.push(downmixed.subarray(c * frameCount, (c + 1) * frameCount)); + } + } + + const outCount = + srcRate === sampleRate ? frameCount : Math.floor((frameCount * sampleRate) / srcRate); + ensureCapacity(startSample + outCount); + for (let c = 0; c < numberOfChannels; c++) { + const mix = mixPlanes[c]; + const src = planes[c]; + if (srcRate === sampleRate) { + for (let i = 0; i < frameCount; i++) mix[startSample + i] += src[i]; + } else { + // Linear resample onto the mix's sample grid. + for (let i = 0; i < outCount; i++) { + const pos = (i * srcRate) / sampleRate; + const lo = Math.floor(pos); + const hi = Math.min(frameCount - 1, lo + 1); + const frac = pos - lo; + mix[startSample + i] += src[lo] * (1 - frac) + src[hi] * frac; + } + } + } + mixLength = Math.max(mixLength, startSample + outCount); + } + pending.length = 0; + }; + + const packetStream = demuxer.readAVPacket( + 0, + safeReadEndSec ?? 0, + AVMediaType.AVMEDIA_TYPE_AUDIO, + stream.index, + ); + const reader = packetStream.getReader(); + try { + while (!this.cancelled) { + const { done, value: packet } = await reader.read(); + if (done || !packet) break; + decoder.decode( + new EncodedAudioChunk({ + type: packet.keyframe === 1 ? "key" : "delta", + timestamp: Math.round(packet.timestamp * 1_000_000), + duration: Math.round(packet.duration * 1_000_000), + data: packet.data, + }), + ); + while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) { + await new Promise((resolve) => setTimeout(resolve, 1)); + } + drainPending(); + } + } finally { + try { + await reader.cancel(); + } catch { + /* reader already closed */ + } + } + + if (decoder.state === "configured") { + if (!this.cancelled) { + await decoder.flush(); + } + decoder.close(); + } + drainPending(); + mixedStreams += 1; + } + + if (this.cancelled || mixedStreams === 0 || mixLength === 0) { + return null; + } + + // Summing can clip where tracks overlap; hard-clamp to [-1, 1]. + for (const plane of mixPlanes) { + for (let i = 0; i < mixLength; i++) { + if (plane[i] > 1) plane[i] = 1; + else if (plane[i] < -1) plane[i] = -1; + } + } + + // Re-chunk the mix, dropping slices that start inside a trim region — the + // same chunk-level granularity the single-track decode applies. + const frames: AudioData[] = []; + for (let start = 0; start < mixLength; start += MIX_SLICE_FRAMES) { + const take = Math.min(MIX_SLICE_FRAMES, mixLength - start); + const timestampUs = Math.round((start / sampleRate) * 1_000_000); + if (this.isInTrimRegion(timestampUs / 1000, sortedTrims)) continue; + const data = new Float32Array(take * numberOfChannels); + for (let c = 0; c < numberOfChannels; c++) { + data.set(mixPlanes[c].subarray(start, start + take), c * take); + } + frames.push( + new AudioData({ + format: "f32-planar", + sampleRate, + numberOfFrames: take, + numberOfChannels, + timestamp: timestampUs, + data: data.buffer as ArrayBuffer, + }), + ); + } + return { frames, sampleRate, numberOfChannels }; + } + // Speed-aware path mirroring preview semantics (trim skipping + playbackRate). Relies on // browser media playback to preserve pitch and avoid the chipmunk effect. private async renderPitchPreservedTimelineAudio( @@ -460,6 +735,7 @@ export class AudioProcessor { if (this.cancelled) { throw new Error("Export cancelled"); } + enableAllMediaAudioTracks(media); const audioContext = new AudioContext(); const sourceNode = audioContext.createMediaElementSource(media); diff --git a/src/lib/exporter/multiTrackAudio.browser.test.ts b/src/lib/exporter/multiTrackAudio.browser.test.ts new file mode 100644 index 000000000..e0b1990c1 --- /dev/null +++ b/src/lib/exporter/multiTrackAudio.browser.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import twoTrackVideoUrl from "../../../tests/fixtures/two-audio-tracks.mp4?url"; +import { VideoExporter } from "./videoExporter"; + +describe("multi-track audio export (real browser)", () => { + // The fixture carries two audio tracks: a silent first track and a 440Hz + // tone second track — the shape the native capture helpers produce (system + // audio + microphone). Reading only the demuxer's default track exports + // pure silence (#silent-export regression). + it("mixes every audio track instead of exporting only the first", async () => { + const exporter = new VideoExporter({ + videoUrl: twoTrackVideoUrl, + width: 320, + height: 180, + frameRate: 15, + bitrate: 1_000_000, + wallpaper: "#1a1a2e", + // A zoom region forces the re-encode path; the source-copy fast path + // returns the source verbatim and never touches AudioProcessor. + zoomRegions: [ + { + id: "zoom-1", + startMs: 500, + endMs: 1500, + depth: 3, + customScale: 1.5, + focus: { cx: 0.5, cy: 0.5 }, + source: "auto" as const, + }, + ], + showShadow: false, + shadowIntensity: 0, + showBlur: false, + cropRegion: { x: 0, y: 0, width: 1, height: 1 }, + }); + + const result = await exporter.export(); + expect(result.success, result.error).toBe(true); + + const audioContext = new AudioContext(); + try { + const decoded = await audioContext.decodeAudioData(await result.blob!.arrayBuffer()); + let peak = 0; + for (let channel = 0; channel < decoded.numberOfChannels; channel++) { + const samples = decoded.getChannelData(channel); + for (let i = 0; i < samples.length; i++) { + peak = Math.max(peak, Math.abs(samples[i])); + } + } + expect(peak).toBeGreaterThan(0.1); + } finally { + await audioContext.close(); + } + }); +}); diff --git a/tests/fixtures/two-audio-tracks.mp4 b/tests/fixtures/two-audio-tracks.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..df479c1fe270ec1354ff8334dd7c3169dc99834c GIT binary patch literal 23516 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 z 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 VDmOJkVL}fj4#uIxrg)i5_!qz5(JlZ0 literal 0 HcmV?d00001