/** * audio-mix-plan.ts — PURE multi-layer audio mix-plan helper. * * Composes project audio inputs (music, narration, dialogue, sfx) into the * {@link AudioLayer}[] shape consumed by {@link buildMultiLayerMixArgs} in * stitch.ts. No filesystem access, no network calls, no side-effects. * * stitch.ts's `AudioLayer.role` union natively includes `'sfx'`, so this module * re-exports the base type directly — `'sfx'` layers produced here are mixed in * (never used as a ducking sidechain) by `buildMultiLayerMixArgs`. */ import type { AudioLayer as BaseAudioLayer } from './stitch.js'; /** * Re-export of stitch.ts's {@link AudioLayer} (whose `role` union already * includes `'sfx'`). Output of {@link buildAudioLayers} is therefore directly * assignable to {@link buildMultiLayerMixArgs}'s `layers` parameter. */ export type AudioLayer = BaseAudioLayer; // --------------------------------------------------------------------------- // Public inputs / option shapes // --------------------------------------------------------------------------- /** * All optional audio paths for a single project mix. * Absent fields are simply skipped when building the layer list. */ export interface AudioMixInputs { /** Path to background-music bed (looped at low volume). */ musicPath?: string; /** Path to voice-over narration track. */ narrationPath?: string; /** Paths to on-screen dialogue tracks (one entry per track). */ dialoguePaths?: string[]; /** Paths to sound-effect tracks. */ sfxPaths?: string[]; } export interface AudioMixPlanOptions { /** * Volume for music layers (0..1). * Passed through to {@link BuildMultiLayerMixOptions}.musicVolume. * Default: 0.05 (matches stitch.ts DEFAULT_MUSIC_VOLUME). */ musicVolume?: number; /** * Volume for sfx layers (0..1). * Default: 0.6. */ sfxVolume?: number; /** * When true, music will be sidechained under narration/dialogue. * Forwarded to {@link BuildMultiLayerMixOptions}.duckMusicUnderVoice. * Default: false. */ duckMusicUnderVoice?: boolean; } // --------------------------------------------------------------------------- // buildAudioLayers // --------------------------------------------------------------------------- /** * Convert a flat {@link AudioMixInputs} map into the ordered {@link AudioLayer}[] * that {@link buildMultiLayerMixArgs} consumes. * * Layer ordering: music → narration → dialogue (in order) → sfx (in order). * Absent inputs produce no layers. Deterministic and side-effect-free. */ export function buildAudioLayers( inputs: AudioMixInputs, opts: AudioMixPlanOptions = {}, ): AudioLayer[] { const { musicVolume = 0.05, sfxVolume = 0.6 } = opts; const layers: AudioLayer[] = []; if (inputs.musicPath) { layers.push({ trackPath: inputs.musicPath, role: 'music', volume: musicVolume, loop: true, }); } if (inputs.narrationPath) { layers.push({ trackPath: inputs.narrationPath, role: 'narration', }); } for (const dp of inputs.dialoguePaths ?? []) { layers.push({ trackPath: dp, role: 'dialogue', }); } for (const sp of inputs.sfxPaths ?? []) { layers.push({ trackPath: sp, role: 'sfx', volume: sfxVolume, }); } return layers; } // --------------------------------------------------------------------------- // planMultiLayerMix // --------------------------------------------------------------------------- export interface MultiLayerMixPlan { /** Ordered layers ready to pass to buildMultiLayerMixArgs. */ layers: AudioLayer[]; /** * Whether music ducking is requested. * Pass as BuildMultiLayerMixOptions.duckMusicUnderVoice. */ duckMusicUnderVoice: boolean; } /** * Convenience wrapper that returns both the layer list and the ducking flag * so a future assemble-wiring is a single destructure. * * ```ts * const { layers, duckMusicUnderVoice } = planMultiLayerMix(videoPath, inputs, opts); * const args = buildMultiLayerMixArgs(videoPath, layers, outputPath, { * duckMusicUnderVoice, * musicVolume: opts?.musicVolume, * }); * ``` * * **Not wired into assemble.ts** — called only by future callers. */ export function planMultiLayerMix( _videoPath: string, inputs: AudioMixInputs, opts: AudioMixPlanOptions = {}, ): MultiLayerMixPlan { return { layers: buildAudioLayers(inputs, opts), duckMusicUnderVoice: opts.duckMusicUnderVoice ?? false, }; }