Files
skills/docs/media/audio/audio-engine-browser/src/offline/offline-render.md
2026-05-29 08:20:56 +01:00

3.6 KiB

renderOffline

(no submodules)

Purpose

renderOffline is the bridge to non-realtime export. It wires a BrowserTimelineAdapter to an OfflineAudioContext, attaches the adapter to the supplied TimelineEngine, drives playback from 0, and resolves with the rendered AudioBuffer. Because the adapter accepts an injected context, offline render reuses the exact same rendering code path as realtime playback — the "tests" and the "export feature" share one seam.

Failure modes are uniform: pre-flight checks (bad duration, bad sample rate, pre-aborted signal), context construction failure, and startRendering() rejection all raise BrowserAudioError(OFFLINE_RENDER_FAILED) with the underlying error attached as cause.

Features

  • Pre-flight validation of duration (> 0, finite) and sampleRate (> 0, finite).
  • AbortSignal honoured at the boundary.
  • Optional contextFactory for tests that cannot construct a real OfflineAudioContext (e.g. happy-dom); production callers omit it and the function dynamically imports standardized-audio-context.
  • OTel span (engine_browser.offline_render) and duration histogram (audio_engine_browser_offline_render_duration_ms).
  • Disposes the adapter in finally so a render failure does not leak resources.

Usage: Basic Example

import { renderOffline } from "@suaveplan/audio-engine-browser/offline/offline-render";

const buffer = await renderOffline(timeline, 2, 48000);
console.log(buffer.length, buffer.sampleRate, buffer.numberOfChannels);

Usage: Advanced Example

import { renderOffline } from "@suaveplan/audio-engine-browser/offline/offline-render";

const buffer = await renderOffline(timeline, 4, 44100, {
    signal: controller.signal,
    contextFactory: ({ length, sampleRate, channels }) =>
        new MyDeterministicOfflineContext({ length, sampleRate, channels }),
});

// buffer.length === Math.ceil(4 * 44100)

API Reference

renderOffline(timeline, duration, sampleRate, options?) → Promise<AudioBuffer>

  • timeline — the TimelineEngine to drive.
  • duration — seconds; must be finite and positive.
  • sampleRate — Hz; must be finite and positive.
  • options.signal — optional AbortSignal; an already-aborted signal short-circuits before any context work.
  • options.contextFactory — optional injected factory. Production omits this; tests provide one.

Returns the AudioBuffer from OfflineAudioContext.startRendering.

RenderOfflineOptions

interface RenderOfflineOptions {
    signal?: AbortSignal;
    contextFactory?: (args: {
        length: number;
        sampleRate: number;
        channels: number;
    }) => OfflineContextLike;
}

Implementation Notes

  • Length is computed as Math.ceil(duration * sampleRate) so a 2-second render at 48 kHz produces exactly 96 000 frames.
  • The dynamic import("standardized-audio-context") keeps the shim out of the import graph when contextFactory is always supplied (e.g. in the test suite).
  • The adapter is disposed in finally. The injected OfflineAudioContext is NOT closed by the adapter because the adapter sees it as injected; the caller owns the context lifetime if they passed one in via the factory. The function does not call close() itself either, because OfflineAudioContext.startRendering resolves to a single buffer and the spec considers the context spent at that point.
  • The duration histogram is recorded in finally so both success and failure produce a sample. Failure samples are useful for SLO monitoring (the percentile of failed renders).