# `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 ```ts 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 ```ts 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` - `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` ```ts 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).