@molecule/api-audio-render
Utility · audio-render · API (Node) · v2.0.1 · Apache-2.0
Offline multi-track audio mixdown — renders an AudioSession to WAV/MP3/FLAC via FFmpeg, queue-driven.
npm install @molecule/api-audio-renderHow it works
@molecule/api-audio-render is a utility package for the API (Node) side (audio-render).
import { renderAudio, getRenderStatus } from '@molecule/api-audio-render'
const job = await renderAudio(
{
duration: 8,
channels: [
{
id: 'drums',
clips: [{ audioUrl: '/tmp/loop.wav', startTime: 0, duration: 8 }],
volume: 0.9,
},
{
id: 'lead',
clips: [{ audioUrl: '/tmp/lead.wav', startTime: 2, duration: 4 }],
pan: 0.3,
},
],
},
{ format: 'mp3', bitrate: '256k' },
)
// …later…
const status = getRenderStatus(job.id)Works with: @molecule/api-queue
Reference
Offline multi-track audio mixdown for molecule.dev — flagship use case is the music-daw app.
Same shape as @molecule/api-video-render: take a multi-track
AudioSession (channels with clips, per-channel volume / pan /
effects), enqueue a render job via the bonded queue provider, and
have a worker spawn ffmpeg to produce a single mixed-down audio file
(WAV, MP3, or FLAC).
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { renderAudio, getRenderStatus } from '@molecule/api-audio-render'
const job = await renderAudio(
{
duration: 8,
channels: [
{
id: 'drums',
clips: [{ audioUrl: '/tmp/loop.wav', startTime: 0, duration: 8 }],
volume: 0.9,
},
{
id: 'lead',
clips: [{ audioUrl: '/tmp/lead.wav', startTime: 2, duration: 4 }],
pan: 0.3,
},
],
},
{ format: 'mp3', bitrate: '256k' },
)
// …later…
const status = getRenderStatus(job.id)
// Worker side — typically a long-running process.
import { startAudioRenderWorker } from '@molecule/api-audio-render'
const stop = startAudioRenderWorker({
ffmpegPath: '/usr/local/bin/ffmpeg',
onJobComplete: (result) => console.log('rendered', result.jobId),
})
Type
utility
Installation
npm install @molecule/api-audio-render @molecule/api-queue
API
Interfaces
FfmpegCommand
Result of buildFfmpegArgs — the exact argv to spawn plus the
resolved filter graph (handy for diagnostics and tests).
interface FfmpegCommand {
/** Argument vector — element 0 is the first arg, NOT the binary name. */
args: string[]
/** The fully assembled `-filter_complex` script. */
filterGraph: string
/** Final output stream label fed into `-map`. */
outputStreamLabel: string
}
AudioRenderRequest
Minimal request shape consumed by the handlers.
interface AudioRenderRequest {
/** Parsed JSON body (POST). */
body?: unknown
/** Path parameters (`:id`). */
params?: Record<string, string | undefined>
}
AudioRenderResponse
Minimal response shape consumed by the handlers.
interface AudioRenderResponse {
setStatus: (status: number) => void
sendJson: (body: unknown) => void
}
AudioRenderRoutes
Bundle of route handlers returned by createAudioRenderRoutes.
interface AudioRenderRoutes {
/** `POST /render/audio` — enqueue a session. */
enqueue: (req: AudioRenderRequest, res: AudioRenderResponse) => Promise<void>
/** `GET /render/jobs/:id` — fetch a job's status. */
status: (req: AudioRenderRequest, res: AudioRenderResponse) => Promise<void>
/** `DELETE /render/jobs/:id` — request cancellation. */
cancel: (req: AudioRenderRequest, res: AudioRenderResponse) => Promise<void>
}
CreateAudioRenderRoutesOptions
Options for createAudioRenderRoutes.
interface CreateAudioRenderRoutesOptions {
/**
* Directory every clip `audioUrl` must live under. Relative paths resolve
* against it; URLs, `..` segments and paths outside it are rejected.
*/
mediaRoot: string
/** Directory rendered files are written to, as `<uuid>.<format>`. */
outputDir: string
/**
* Optional pre-flight validator. Throw to reject the enqueue request;
* the thrown error's `.message` becomes the JSON `error` field with
* HTTP 400.
*/
validate?: (session: AudioSession, options: AudioRenderOptions) => void | Promise<void>
}
AudioJobStore
Backend contract for render-job status. All operations are synchronous to
keep getRenderStatus / cancelRender synchronous (their public shape).
A backend shared across processes (to fix the split-process topology) must
therefore be a synchronous façade over the shared storage.
interface AudioJobStore {
/** Register a freshly-enqueued job. */
register(job: RenderJob): void
/** Look up a job by id, or `undefined` if unknown to this store. */
get(jobId: string): RenderJob | undefined
/** Merge a partial patch into an existing job. No-op (returns `undefined`) if unknown. */
update(jobId: string, patch: Partial<RenderJob>): RenderJob | undefined
/** Drop all jobs — primarily for tests. */
reset(): void
/** Snapshot of all registered job ids — for diagnostics and tests. */
ids(): string[]
}
AudioClip
A single audio clip placed on a AudioChannel timeline.
Time fields are seconds in the session's master timeline. The renderer
pads silence before the clip's startTime and trims to duration so
timeline placement is preserved exactly.
interface AudioClip {
/** Stable clip id — appears in error messages and progress events. */
id?: string
/**
* Source audio: a local filesystem path. An `http(s):` URL is only opened
* when the trusted caller sets `allowRemoteSources`; the HTTP routes accept
* only paths inside their configured `mediaRoot`. Strings containing
* control characters, a leading `-` or `..` segments are rejected.
*/
audioUrl: string
/** Seconds from session t=0 at which this clip should begin playing. */
startTime: number
/** Length of the clip's contribution to the mix, in seconds. */
duration: number
/**
* Optional offset into the source file (seconds). Defaults to 0 — the
* clip plays from the start of `audioUrl`.
*/
sourceOffset?: number
}
AudioEffect
A simple audio effect applied to a channel before mixdown.
The shape is intentionally minimal — providers translate kind into
the right ffmpeg -af filter. Unknown kinds are ignored at render time
(warned, not failed) so future effect types are forward-compatible.
interface AudioEffect {
/** Effect kind — `gain`, `lowpass`, `highpass`, `reverb`, etc. */
kind: string
/** Effect-specific parameters (numeric or string). */
params?: Record<string, number | string>
}
AudioChannel
A single mixer channel: an ordered list of clips plus per-channel volume / pan / effects.
interface AudioChannel {
/** Stable channel id — required because clips reference timing in seconds, not bars. */
id: string
/** Display label (caller-supplied, never localized by this package). */
label?: string
/** Clips placed on this channel's timeline. */
clips: AudioClip[]
/**
* Linear volume gain. `1` = unity, `0` = silent, `2` = +6 dB.
* Defaults to 1 when omitted.
*/
volume?: number
/**
* Stereo pan from `-1` (hard left) to `1` (hard right). `0` = center.
* Defaults to 0 when omitted.
*/
pan?: number
/** Whether this channel is muted in the final mix. Defaults to false. */
muted?: boolean
/** Optional per-channel effects, applied in array order before summing. */
effects?: AudioEffect[]
}
AudioSession
A complete multi-track session ready for mixdown.
The session is the single argument to renderAudio; it must be
fully self-describing — no implicit project state, no global mixer.
interface AudioSession {
/** Mixer channels. Empty arrays produce a silent track of `duration` seconds. */
channels: AudioChannel[]
/** Total session length in seconds (master timeline). */
duration: number
/** Optional master gain applied after summing the channels. Defaults to 1. */
masterVolume?: number
}
AudioRenderOptions
Options accepted by renderAudio.
interface AudioRenderOptions {
/** Output format. Defaults to `'mp3'`. */
format?: AudioRenderFormat
/** Output sample rate in Hz. Defaults to `44100`. */
sampleRate?: number
/** Output channel count: 1 = mono, 2 = stereo. Defaults to `2`. */
channels?: number
/**
* Output bitrate (e.g. `'192k'`). Honoured for `mp3`; ignored for
* uncompressed `wav` and lossless `flac`. Defaults to `'192k'` for mp3.
*/
bitrate?: string
/**
* Override the destination path. Defaults to a unique tmp path under the
* caller's `os.tmpdir()` with the format-appropriate extension. Trusted
* server code only — the HTTP routes ignore any client value and mint a
* path inside their configured `outputDir`.
*/
outputPath?: string
/**
* Override the queue name jobs are dispatched to. Defaults to
* `'audio-render'`. Trusted server code only — ignored by the HTTP routes.
*/
queueName?: string
/**
* Let ffmpeg open `http:`/`https:` clip sources. Defaults to `false`,
* which passes `-protocol_whitelist file` so only local files can be read.
* Trusted server code only — the HTTP routes never set it.
*/
allowRemoteSources?: boolean
}
RenderJob
The job descriptor returned by renderAudio and inspected by
getRenderStatus. Mutated in-place by the worker as state
advances; consumers should treat reads as a snapshot.
interface RenderJob {
/** Stable job id — opaque, generated by the queue provider. */
id: string
/** Current lifecycle state. */
status: AudioRenderJobStatus
/** Queue name the job was sent to. */
queueName: string
/** Resolved output path. */
outputPath: string
/** Resolved output format. */
format: AudioRenderFormat
/** Echo of the originating session — useful for re-rendering / diagnostics. */
session: AudioSession
/** Echo of the resolved options. */
options: Required<
Omit<AudioRenderOptions, 'outputPath' | 'queueName' | 'bitrate' | 'allowRemoteSources'>
> & {
bitrate?: string
}
/** When the job was enqueued. */
enqueuedAt: Date
/** Set when the worker begins processing. */
startedAt?: Date
/** Set when the worker finishes (success, failure, or cancellation). */
finishedAt?: Date
/** Populated when `status === 'failed'`. */
error?: string
}
AudioRenderJobPayload
Payload placed on the queue for the worker to consume.
interface AudioRenderJobPayload {
jobId: string
session: AudioSession
options: AudioRenderOptions
outputPath: string
format: AudioRenderFormat
}
ProcessAudioRenderJobOptions
Options for processAudioRenderJob.
interface ProcessAudioRenderJobOptions {
/**
* ffmpeg binary path. When omitted, resolves the `FFMPEG_PATH` environment
* variable, then falls back to `'ffmpeg'` (resolved on `$PATH`). See
* `resolveFfmpegPath`.
*/
ffmpegPath?: string
/** Override `child_process.spawn` for tests. */
spawn?: SpawnFunction
/** Maximum time (ms) to allow ffmpeg before killing it. Defaults to 600_000 (10 min). */
timeoutMs?: number
}
ProcessAudioRenderJobResult
Result of a single render attempt.
interface ProcessAudioRenderJobResult {
jobId: string
status: RenderJob['status']
outputPath?: string
exitCode?: number | null
error?: string
/** The exact argv passed to ffmpeg — useful for diagnostics in tests. */
ffmpegArgs: string[]
}
StartAudioRenderWorkerOptions
Options for startAudioRenderWorker.
interface StartAudioRenderWorkerOptions extends ProcessAudioRenderJobOptions {
/** Queue name to subscribe to. Defaults to `'audio-render'`. */
queueName?: string
/** Called whenever a job completes (success, failure, or cancellation). */
onJobComplete?: (result: ProcessAudioRenderJobResult) => void
}
Types
AudioRenderFormat
Supported output container/codec combinations.
wav— uncompressed PCM, default 16-bit signed (pcm_s16le).mp3— lossy MPEG-1 Layer III via libmp3lame.flac— lossless FLAC.
type AudioRenderFormat = 'wav' | 'mp3' | 'flac'
AudioRenderJobStatus
Lifecycle states a render job moves through.
queued → processing → (completed | failed | cancelled).
Cancellation requests on queued jobs short-circuit straight to
cancelled without ever entering processing.
type AudioRenderJobStatus = 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled'
SpawnFunction
Function signature compatible with child_process.spawn. Exposed as
the injection seam for tests.
type SpawnFunction = (
command: string,
args: readonly string[],
options?: SpawnOptions,
) => ChildProcess
Functions
sanitizeAudioPath(value, label)
Reject paths/URLs containing NUL, newline, or any other control
character, a leading - (ffmpeg would read it as an option), or a ..
path segment. Control characters can't appear in legitimate filesystem
paths or URLs; if one shows up it's almost certainly an injection attempt
or a bug upstream, and ffmpeg's filter-graph parser would mis-tokenize it.
function sanitizeAudioPath(value: unknown, label: string): string
value— The caller-supplied path or URL.label— Diagnostic label used in the thrown error.
Returns: The validated value, unchanged.
assertSafeChannelId(value)
Validate a channel id. Ids are embedded in filter-graph link labels, so
anything outside [A-Za-z0-9_-] could splice extra filters into the graph.
function assertSafeChannelId(value: unknown): string
value— The caller-supplied channel id.
Returns: The validated id, unchanged.
buildFfmpegArgs(session, options, outputPath)
Construct the full ffmpeg argv for an AudioSession.
function buildFfmpegArgs(
session: AudioSession,
options: Required<Pick<AudioRenderOptions, 'format' | 'sampleRate' | 'channels'>> & {
bitrate?: string
allowRemoteSources?: boolean
},
outputPath: string,
): FfmpegCommand
session— The multi-track session to render.options— Resolved render options (caller has already merged defaults).outputPath— Destination file path.
Returns: The argv plus the filter-graph string for diagnostics.
createAudioRenderRoutes(routeOptions)
Build the audio-render HTTP route handlers.
function createAudioRenderRoutes(routeOptions: CreateAudioRenderRoutesOptions): AudioRenderRoutes
routeOptions— Media root, output directory and optional validator.
Returns: A bundle of three async handlers.
createInMemoryAudioJobStore()
The default in-memory AudioJobStore, backed by a Map.
Process-local — a restart loses state.
function createInMemoryAudioJobStore(): AudioJobStore
Returns: A fresh in-memory store.
getAudioJobStore()
Return the active job store. Defaults to the in-memory store.
function getAudioJobStore(): AudioJobStore
Returns: The active AudioJobStore.
setAudioJobStore(store)
Replace the active job store. Wire a shared, cross-process backend here (in
BOTH the enqueuing process and the worker process) so getRenderStatus
observes the worker's transitions in a split-process queue topology. Pass
undefined to reset to a fresh in-memory store — primarily for tests.
function setAudioJobStore(store: AudioJobStore | undefined): void
store— The store to use, orundefinedto reset to in-memory.
registerJob(job)
Register a freshly-enqueued job in the active store.
function registerJob(job: RenderJob): void
job— The job descriptor returned byrenderAudio.
getJob(jobId)
Look up a job by id in the active store.
function getJob(jobId: string): RenderJob | undefined
jobId— The job id to retrieve.
Returns: The job, or undefined if no such id has been registered.
updateJob(jobId, patch)
Apply an in-place patch to a registered job. No-op if the id is unknown.
function updateJob(jobId: string, patch: Partial<RenderJob>): RenderJob | undefined
jobId— The job id to update.patch— Partial fields to merge into the job descriptor.
Returns: The updated job, or undefined if no such id was registered.
resetJobStore()
Drop all registered jobs — for tests.
function resetJobStore(): void
listJobIds()
Snapshot of all currently-registered job ids — for diagnostics and tests.
function listJobIds(): string[]
hasTraversalSegment(value)
Whether a path string contains a .. segment (with / or \ separators).
function hasTraversalSegment(value: string): boolean
value— The path to inspect.
Returns: true when any segment is exactly ...
resolveMediaSource(source, mediaRoot, label)
Resolve a client-supplied media source to an absolute path inside
mediaRoot. Relative paths are resolved against the root; absolute paths
are accepted only when they already sit inside it.
function resolveMediaSource(source: unknown, mediaRoot: string, label: string): string
source— The untrusted source string from the request.mediaRoot— The directory every source must live under.label— Field name used in error messages.
Returns: The absolute, confined path.
createOutputPath(outputDir, extension)
Mint a fresh output path inside outputDir from a random UUID.
function createOutputPath(outputDir: string, extension: string): string
outputDir— The directory rendered files are written to.extension— File extension without the dot (e.g.mp4).
Returns: An absolute output path no client input contributed to.
assertAllowedAudioFormat(value)
Validate an output format against the supported set.
function assertAllowedAudioFormat(value: unknown): AudioRenderFormat
value— The candidate format.
Returns: The format, typed.
renderAudio(session, options)
Enqueue an AudioSession for offline mixdown.
Does NOT block on rendering. Returns immediately with a RenderJob
whose status starts at 'queued'; poll getRenderStatus or
subscribe to the queue from a worker process to track progress.
function renderAudio(session: AudioSession, options?: AudioRenderOptions): Promise<RenderJob>
session— The multi-track session to render.options— Output format / sample rate / channel count / bitrate overrides. All fields optional — sensible mp3 defaults.
Returns: The freshly-enqueued render job.
getRenderStatus(jobId)
Look up a previously-enqueued render job by id.
Process-local. This reads the active job store (an in-memory Map by
default), which only reflects the worker's transitions when the worker runs
in the SAME process (true with the @molecule/api-queue-memory bond). With
a distributed queue bond and a separate worker process this returns the
stale queued snapshot forever — the worker's processing/completed/
failed updates land in the worker's own store. To make status observable
across processes, inject a shared backend via setAudioJobStore() in BOTH
processes, or persist durable status from the worker's onJobComplete
callback and read it from your own storage.
function getRenderStatus(jobId: string): RenderJob | undefined
jobId— The id returned inRenderJob.idfromrenderAudio.
Returns: The job as known to THIS process's store, or undefined.
cancelRender(jobId)
Mark a render job as cancelled.
Cancellation is cooperative: a job already in processing will be
marked but the worker decides whether to honour the flag (the bundled
worker checks before spawning ffmpeg and again on completion). Jobs in
a terminal state (completed, failed, cancelled) are not changed.
Process-local, same caveat as getRenderStatus: the flag is
written to this process's job store, so a worker in a separate process only
observes it when a shared store is injected via setAudioJobStore() in both
processes.
function cancelRender(jobId: string): boolean
jobId— The id returned inRenderJob.idfromrenderAudio.
Returns: true if the job was found and a cancellation flag applied, false if the id is unknown or the job is already terminal.
resolveFfmpegPath(explicit)
Resolve the ffmpeg binary path to spawn. Precedence: an explicit
ffmpegPath → the FFMPEG_PATH environment variable → 'ffmpeg'
(resolved on $PATH).
function resolveFfmpegPath(explicit?: string): string
explicit— An explicit path fromProcessAudioRenderJobOptions.
Returns: The binary path/name to spawn.
describeSpawnFailure(error, ffmpegPath)
Turn a spawn failure into a human-actionable message. A raw
spawn ffmpeg ENOENT gives no hint at the fix; for ENOENT this names the
resolved binary path and how to point at a real one. Any other error is
returned by its own .message.
function describeSpawnFailure(error: unknown, ffmpegPath: string): string
error— The error thrown/emitted bychild_process.spawn.ffmpegPath— The binary path that failed to spawn.
Returns: A clear, actionable error message string.
processAudioRenderJob(payload, processOptions)
Process a single render-job payload: build the ffmpeg argv, spawn ffmpeg, await exit, and update the job-store entry to match.
Intended to be wired into a queue subscription via
startAudioRenderWorker, but exported on its own so callers
with custom queue topologies can drive the work directly.
function processAudioRenderJob(
payload: AudioRenderJobPayload,
processOptions?: ProcessAudioRenderJobOptions,
): Promise<ProcessAudioRenderJobResult>
payload— The job payload received from the queue.processOptions— Optional spawn / timeout overrides.
Returns: A summary of the attempt suitable for logging.
startAudioRenderWorker(options)
Subscribe to the render queue and process every incoming job.
function startAudioRenderWorker(options?: StartAudioRenderWorkerOptions): () => void
options— Queue name + spawn / timeout overrides.
Returns: The unsubscribe function returned by subscribe().
Constants
DEFAULT_AUDIO_RENDER_QUEUE
Default queue name jobs are dispatched to.
const DEFAULT_AUDIO_RENDER_QUEUE: 'audio-render'
Injection Notes
Requirements
Peer dependencies:
@molecule/api-queue^1.0.1
Runtime Dependencies
@molecule/api-queue
Security: Every caller-controlled string (clip audioUrl, output
path) is checked against sanitizeAudioPath (no control characters,
no leading -, no .. segments) and every channel id against
assertSafeChannelId before it reaches ffmpeg's command line.
ffmpeg itself is invoked via child_process.spawn with an argv array —
never via a shell — with -nostdin, and every input is opened with
-protocol_whitelist file: a clip can only make ffmpeg fetch a URL when
trusted server code passes allowRemoteSources: true.
HTTP routes: untrusted input, authentication required.
createAudioRenderRoutes({ mediaRoot, outputDir }) requires both
directories and must be mounted behind authentication — its routes enqueue,
read and cancel any job by id. The client's outputPath, queueName and
allowRemoteSources are ignored, the output path is minted inside
outputDir from a random UUID, and every clip audioUrl must be a path
that resolves inside mediaRoot (URLs, .. segments and absolute paths
outside it are rejected with HTTP 400). outputPath, queueName and
allowRemoteSources on renderAudio are for trusted server code
only — never copy them from a request.
Resource intensity: ffmpeg can saturate CPU and IO for large sessions. The package is queue-driven on purpose so flagship apps can gate concurrency at the queue tier rather than in the request path.
ffmpeg is a runtime prerequisite, not a dependency. The worker spawns
whatever ffmpegPath points at — resolved from the ffmpegPath option, then
the FFMPEG_PATH env var, then ffmpeg on $PATH. Install it in the runtime
image (e.g. apt-get install -y ffmpeg) — it is NOT present in minimal
containers including the molecule.dev sandbox base image. When it's missing,
the job fails with a clear, actionable error naming the path and the fix
(install ffmpeg or set FFMPEG_PATH) on the job's error field — not a raw
spawn ffmpeg ENOENT.
Queue wiring: renderAudio dispatches through the abstract
@molecule/api-queue core, so a queue bond must be wired at startup —
bond('queue', provider) with @molecule/api-queue-memory (same-process
dev default), -redis, -rabbitmq, or -sqs — and
startAudioRenderWorker() must be running to consume jobs. Both throw
"Queue provider not configured. Call setProvider() first." otherwise.
Job status is process-local by default. getRenderStatus /
cancelRender read the active job store — an in-memory map by default — that
is shared between renderAudio and the worker ONLY when both run in the same
process (true with the memory queue bond). With a distributed queue bond and
a separate worker process, the enqueuing process never sees status
transitions. Fix it either way: inject a shared, cross-process backend via
setAudioJobStore() in BOTH processes (the store contract is synchronous, so
it must be a sync façade over the shared storage), OR persist durable status
from the worker's onJobComplete callback and read it from your own storage.
For HTTP exposure of enqueue/status/cancel, see createAudioRenderRoutes().
No locale bond: This package's surface is programmatic — no
user-visible strings are generated, so there's no companion
@molecule/api-locales-audio-render.