first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
# `renderOffline`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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`
```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).