first commit
This commit is contained in:
101
docs/media/audio/audio-core/README.md
Normal file
101
docs/media/audio/audio-core/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# @suaveplan/audio-core - Universal audio data model and processing primitives: paged AudioDocument, non-destructive PlanEngine, block-level ProcessPipeline, and typed OpRegistry. Zero browser or Node dependencies.
|
||||
|
||||
Universal audio data model and processing primitives. Provides a paged `AudioDocument`, a non-destructive `PlanEngine`, a block-level `ProcessPipeline`, and a typed `OpRegistry` — all with zero browser or Node.js dependencies.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [analysis](./src/analysis/analysis-op.md) — `AnalysisOp` is the operator contract every `@suaveplan/audio-analysis-*` package implements: spectrum, beat-tracking,…
|
||||
- [analysis-op](./src/analysis/analysis-op.md) — `AnalysisOp` is the operator contract every `@suaveplan/audio-analysis-*` package implements: spectrum, beat-tracking,…
|
||||
- [document](./src/document/document.md) — `AudioDocument` is the primary paged audio document model in `@suaveplan/audio-core`. It represents a multi-channel aud…
|
||||
- [document](./src/document/document.md) — `AudioDocument` is the primary paged audio document model in `@suaveplan/audio-core`. It represents a multi-channel aud…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module centralises every error code, metadata description, and severity level that the `@suaveplan/audio-core` pac…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module centralises every error code, metadata description, and severity level that the `@suaveplan/audio-core` pac…
|
||||
- [page](./src/page/page.md) — This module provides the low-level building blocks for memory-efficient audio storage and retrieval inside an `AudioDoc…
|
||||
- [page](./src/page/page.md) — This module provides the low-level building blocks for memory-efficient audio storage and retrieval inside an `AudioDoc…
|
||||
- [plan-engine](./src/plan/plan-engine.md) — The `PlanEngine` class is the central coordinator for turning an `AudioDocument` edit list into an executable audio pla…
|
||||
- [segment](./src/plan/segment.md) — The segment helpers provide low-level geometric operations on `Segment` objects. A segment represents a contiguous mapp…
|
||||
- [block-processor](./src/process/block-processor.md) — `BlockProcessor` defines the standard lifecycle contract for audio processors that operate on fixed-size blocks of mult…
|
||||
- [process-pipeline](./src/process/process-pipeline.md) — `ProcessPipeline` executes a chain of audio editing operations on blocks of multi-channel PCM data. It bridges the decl…
|
||||
- [registry](./src/registry/op-registry.md) — `OpRegistry` is the central, in-memory catalog for all audio operations in `@suaveplan/audio-core`. It maps unique stri…
|
||||
- [crop](./src/registry/built-in/crop.md) — `registerCrop` adds the `crop` operation to an `OpRegistry`, making it available to the planning engine. The crop opera…
|
||||
- [dither](./src/registry/built-in/dither.md) — `registerDither` installs the `dither` processing operation into an `OpRegistry`. Dithering adds a small amount of pseu…
|
||||
- [fade](./src/registry/built-in/fade.md) — `registerFade` adds the `fade` operation to an `OpRegistry`. Fade applies a linear amplitude ramp across an entire proc…
|
||||
- [gain](./src/registry/built-in/gain.md) — `registerGain` installs the `gain` operation into an `OpRegistry`. It is the simplest block-level processor: every samp…
|
||||
- [insert](./src/registry/built-in/insert.md) — `registerInsert` adds the `insert` operation to an `OpRegistry`. Insert is a planning-stage edit that introduces silenc…
|
||||
- [mix](./src/registry/built-in/mix.md) — `registerMix` installs the `mix` operation into an `OpRegistry`. The mix processor adds a scaled copy of the input sign…
|
||||
- [normalize](./src/registry/built-in/normalize.md) — `registerNormalize` adds the `normalize` operation to an `OpRegistry`. Normalization scans an entire processing block t…
|
||||
- [pad](./src/registry/built-in/pad.md) — `registerPad` installs the `pad` operation into an `OpRegistry`. Pad is a planning function that extends the total time…
|
||||
- [pan](./src/registry/built-in/pan.md) — `registerPan` adds the `pan` operation to an `OpRegistry`. Pan is a block-level processor that adjusts the relative lou…
|
||||
- [remove](./src/registry/built-in/remove.md) — `registerRemove` installs the `remove` operation into an `OpRegistry`. Remove is a planning function that deletes sampl…
|
||||
- [repeat](./src/registry/built-in/repeat.md) — `registerRepeat` adds the `repeat` operation to an `OpRegistry`. Repeat is a planning function that duplicates the enti…
|
||||
- [resample](./src/registry/built-in/resample.md) — `registerResample` installs the `resample` operation into an `OpRegistry`. Resample is a planning function that changes…
|
||||
- [reverse](./src/registry/built-in/reverse.md) — `registerReverse` adds the `reverse` operation to an `OpRegistry`. Reverse is a planning function that flips the order…
|
||||
- [speed](./src/registry/built-in/speed.md) — `registerSpeed` installs the `speed` operation into an `OpRegistry`. Speed is a planning function that changes playback…
|
||||
- [stretch](./src/registry/built-in/stretch.md) — `registerStretch` adds the `stretch` operation to an `OpRegistry`. Stretch is a planning function that changes the dura…
|
||||
- [transform](./src/registry/built-in/transform.md) — `registerTransform` installs the `transform` operation into an `OpRegistry`. Transform is a block-level processor that…
|
||||
- [trim](./src/registry/built-in/trim.md) — `registerTrim` installs the `trim` operation into an `OpRegistry`. Trim is a planning function that shortens the audio…
|
||||
- [op-registry](./src/registry/op-registry.md) — `OpRegistry` is the central, in-memory catalog for all audio operations in `@suaveplan/audio-core`. It maps unique stri…
|
||||
- [render](./src/render/render.md) — The render module bridges the compiled plan produced by `PlanEngine` and the paged sample storage of an `AudioDocument`…
|
||||
- [render](./src/render/render.md) — The render module bridges the compiled plan produced by `PlanEngine` and the paged sample storage of an `AudioDocument`…
|
||||
- [stats](./src/stats/stats.md) — This module computes standard audio statistics across paged channel data. By scanning every sample in every page, it pr…
|
||||
- [stats](./src/stats/stats.md) — This module computes standard audio statistics across paged channel data. By scanning every sample in every page, it pr…
|
||||
- [stream](./src/stream/stream.md) — This module adapts the `@suaveplan/audio-core` processing pipeline to the Web Streams standard. It exports a factory fu…
|
||||
- [stream](./src/stream/stream.md) — This module adapts the `@suaveplan/audio-core` processing pipeline to the Web Streams standard. It exports a factory fu…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — The telemetry module is the single OpenTelemetry integration point for `@suaveplan/audio-core`. It exposes thin, lazy h…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — The telemetry module is the single OpenTelemetry integration point for `@suaveplan/audio-core`. It exposes thin, lazy h…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/audio-core
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { AudioDocument, OpRegistry, PlanEngine } from "@suaveplan/audio-core";
|
||||
|
||||
const doc = new AudioDocument({
|
||||
sampleRate: 48000,
|
||||
channels: 2,
|
||||
sourceLength: 48000,
|
||||
pages: [
|
||||
{
|
||||
startSample: 0,
|
||||
channelData: [new Float32Array(48000), new Float32Array(48000)],
|
||||
},
|
||||
],
|
||||
edits: [],
|
||||
});
|
||||
|
||||
// Apply a non-destructive trim edit
|
||||
doc.run({ type: "trim", opts: { start: 1000, end: 47000 } });
|
||||
|
||||
// Compile the plan
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const engine = new PlanEngine(registry);
|
||||
const plan = engine.compile(doc, true);
|
||||
|
||||
// Stream the result
|
||||
for await (const block of doc.stream()) {
|
||||
// block is Float32Array[] — one per channel
|
||||
}
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/document/document.md`](src/document/document.md) — `AudioDocument` class and JSON round-trip
|
||||
- [`src/page/page.md`](src/page/page.md) — Page primitives: `walkPages`, `copyPages`, `readPages`, allocation pool
|
||||
- [`src/plan/plan-engine.md`](src/plan/plan-engine.md) — `PlanEngine` for compiling and resolving edits
|
||||
- [`src/plan/segment.md`](src/plan/segment.md) — Segment overlap, merge, and split helpers
|
||||
- [`src/process/process-pipeline.md`](src/process/process-pipeline.md) — `ProcessPipeline` for block-level DSP
|
||||
- [`src/process/block-processor.md`](src/process/block-processor.md) — `BlockProcessor` interface and `NoopBlockProcessor`
|
||||
- [`src/registry/op-registry.md`](src/registry/op-registry.md) — `OpRegistry` for registering plan and process ops
|
||||
- [`src/registry/built-in/`](src/registry/built-in/) — Built-in operations: trim, crop, speed, stretch, reverse, insert, remove, repeat, pad, resample, gain, normalize, fade, pan, mix, dither, transform
|
||||
- [`src/render/render.md`](src/render/render.md) — `renderBlock` for copying source segments into output buffers
|
||||
- [`src/stats/stats.md`](src/stats/stats.md) — `computeStats` for peak, RMS, and DC offset
|
||||
- [`src/stream/stream.md`](src/stream/stream.md) — `createPlanTransformStream` for downstream transforms
|
||||
- [`src/error-codes.md`](src/error-codes.md) — Error codes and `AudioError` class
|
||||
|
||||
107
docs/media/audio/audio-core/src/analysis/analysis-op.md
Normal file
107
docs/media/audio/audio-core/src/analysis/analysis-op.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# AnalysisOp contract
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`AnalysisOp` is the operator contract every `@suaveplan/audio-analysis-*` package implements: spectrum, beat-tracking, loudness (BS.1770), pitch detection, and any future analyser. Each operator is a closed unit with a stable `name` + semver `version`, a Zod-validated config schema, and `run` / optional `runStream` methods that consume an `AudioDocument` (or `AnalysisFrame` iterator) and produce an `AnalysisResult`-extending value.
|
||||
|
||||
The `createAnalysisOp` builder wraps a raw operator spec with mandatory cross-cutting concerns — Zod boundary validation, OpenTelemetry span + counter + histogram instrumentation, AbortSignal-aware cancellation, and error wrapping via `AUDIO_CORE.ANALYSIS_*` codes. Analyser packages focus on the actual DSP; the builder enforces the contract.
|
||||
|
||||
## Features
|
||||
|
||||
- **Single contract** — every audio-analysis package implements one shape, so callers can compose analysers polymorphically.
|
||||
- **Zod boundary validation** — invalid configs reject at the `run` boundary with `AUDIO_CORE.ANALYSIS_INVALID_CONFIG` and the Zod issues attached.
|
||||
- **OpenTelemetry instrumentation** — every `run` emits a span `analysis.${name}` (with audio sample-rate + duration attributes), increments `audio_analysis_runs_total{analysis=NAME}`, and records `audio_analysis_duration_ms{analysis=NAME}`.
|
||||
- **AbortSignal cancellation** — pre-flight signal check before run; streaming runs check between yielded results.
|
||||
- **Error wrapping** — non-`AudioError` throws are wrapped as `AUDIO_CORE.ANALYSIS_RUN_FAILED` with the original error on `cause`.
|
||||
- **Streaming opt-in** — operators that supply a `runStream` automatically advertise `capabilities.supportsStream: true`; those that don't, don't.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import z from "zod";
|
||||
import { createAnalysisOp, type AnalysisResult } from "@suaveplan/audio-core/analysis";
|
||||
|
||||
interface RmsConfig { readonly windowSamples: number }
|
||||
interface RmsResult extends AnalysisResult { readonly rms: number }
|
||||
|
||||
const rmsOp = createAnalysisOp<RmsConfig, RmsResult>({
|
||||
name: "rms",
|
||||
version: "0.1.0",
|
||||
capabilities: { realtime: true, blocking: false },
|
||||
configSchema: z.object({ windowSamples: z.number().int().positive() }),
|
||||
run: async (doc, config) => {
|
||||
const ch0 = doc.read({ channels: [0] })[0]!;
|
||||
let sumSq = 0;
|
||||
for (const s of ch0) sumSq += s * s;
|
||||
return {
|
||||
analyserName: "rms",
|
||||
analyserVersion: "0.1.0",
|
||||
sampleRate: doc.sampleRate,
|
||||
durationSamples: doc.sourceLength,
|
||||
computedAt: Date.now(),
|
||||
rms: Math.sqrt(sumSq / ch0.length),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const result = await rmsOp.run(doc, { windowSamples: 1024 });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
const streamingOp = createAnalysisOp<{ binCount: number }, AnalysisResult & { spectrum: Float32Array }>({
|
||||
name: "spectrum",
|
||||
version: "1.0.0",
|
||||
capabilities: { realtime: false, blocking: true },
|
||||
configSchema: z.object({ binCount: z.union([z.literal(128), z.literal(256), z.literal(512)]) }),
|
||||
run: async (doc, config, opts) => ({
|
||||
analyserName: "spectrum",
|
||||
analyserVersion: "1.0.0",
|
||||
sampleRate: doc.sampleRate,
|
||||
durationSamples: doc.sourceLength,
|
||||
computedAt: Date.now(),
|
||||
spectrum: new Float32Array(config.binCount),
|
||||
}),
|
||||
runStream: async function* (source, config, opts) {
|
||||
for await (const frame of source) {
|
||||
if (opts.signal?.aborted) return;
|
||||
yield {
|
||||
analyserName: "spectrum",
|
||||
analyserVersion: "1.0.0",
|
||||
sampleRate: frame.sampleRate,
|
||||
durationSamples: frame.sampleCount,
|
||||
computedAt: Date.now(),
|
||||
spectrum: new Float32Array(config.binCount),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), 250);
|
||||
for await (const r of streamingOp.runStream!(frames, { binCount: 256 }, { signal: ctrl.signal })) {
|
||||
consume(r);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AnalysisOp<TConfig, TResult>` — operator contract; `name`, `version`, `capabilities`, `configSchema`, `run`, optional `runStream`.
|
||||
- `AnalysisResult` — base shape every result extends (`analyserName`, `analyserVersion`, `sampleRate`, `durationSamples`, `computedAt`).
|
||||
- `AnalysisFrame` — streaming window descriptor (`samples`, `sampleRate`, `channelCount`, `startSample`, `sampleCount`).
|
||||
- `AnalysisCapabilities` — `{ supportsStream, realtime, blocking }`; `supportsStream` is derived by the builder.
|
||||
- `AnalysisRunOpts` — `{ signal? }`.
|
||||
- `createAnalysisOp(spec)` — wraps a raw `AnalysisOpSpec` into an `AnalysisOp` with the cross-cutting concerns enforced.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Span attributes use the `analysis.*` and `audio.*` namespaces (OTel semantic-convention compatible).
|
||||
- The streaming wrapper threads the abort check between every yielded result so cancellation is observable at frame boundaries, not just at run start.
|
||||
- The builder is intentionally narrow — no plugin discovery, no retry, no batching — those compose at higher layers. The contract is what every analyser must obey.
|
||||
- Zod errors are surfaced under `context.issues` so callers can render structured validation feedback.
|
||||
- The `analysis.${name}.stream` span sits above the user's `runStream` generator so a thrown error inside the generator is captured even though the outer iterator may have been consumed lazily.
|
||||
262
docs/media/audio/audio-core/src/document/document.md
Normal file
262
docs/media/audio/audio-core/src/document/document.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# AudioDocument
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`AudioDocument` is the primary paged audio document model in `@suaveplan/audio-core`. It represents a multi-channel audio asset as a collection of pages, a list of edits, and an event bus, allowing non-destructive manipulation and streaming of large audio files without loading the entire waveform into contiguous memory.
|
||||
|
||||
## Features
|
||||
|
||||
- **Paged storage** — Audio data is kept in `AudioPage` objects so only the working set needs to reside in memory.
|
||||
- **Non-destructive edits** — Changes are appended to an `edits` array and compiled on demand by a `PlanEngine`.
|
||||
- **Event-driven architecture** — `on` and `off` methods expose `edit`, `undo`, and `plugin:error` events through an internal `EventBus`.
|
||||
- **Serialization** — `toJSON` and `fromJSON` enable round-trip persistence with plain `number[][]` arrays for transport.
|
||||
- **Streaming & block reads** — `stream` yields processed blocks lazily, while `read` returns a complete `Float32Array[]` for a given range.
|
||||
- **Undo support** — `undo` removes the most recent edits and invalidates cached length.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AudioDocument } from "@suaveplan/audio-core";
|
||||
|
||||
const doc = new AudioDocument({
|
||||
sampleRate: 48000,
|
||||
channels: 2,
|
||||
sourceLength: 0,
|
||||
pages: [],
|
||||
edits: [],
|
||||
});
|
||||
|
||||
// Append raw interleaved data.
|
||||
const interleaved = new Float32Array([0.1, -0.1, 0.2, -0.2]);
|
||||
doc.push(interleaved, { sampleRate: 48000, channels: 2, interleaved: true });
|
||||
|
||||
console.log(doc.duration); // seconds
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { AudioDocument } from "@suaveplan/audio-core";
|
||||
|
||||
const doc = new AudioDocument({
|
||||
id: "mix-001",
|
||||
sampleRate: 44100,
|
||||
channels: 2,
|
||||
sourceLength: 44100,
|
||||
pages: [
|
||||
{
|
||||
startSample: 0,
|
||||
channelData: [new Float32Array(44100), new Float32Array(44100)],
|
||||
},
|
||||
],
|
||||
edits: [],
|
||||
});
|
||||
|
||||
doc.on("edit", ({ edit, version }) => {
|
||||
console.log("Applied edit", edit, "at version", version);
|
||||
});
|
||||
|
||||
doc.run({ type: "gain", opts: { db: -6 } });
|
||||
|
||||
// Stream processed output in 1024-sample blocks.
|
||||
for await (const block of doc.stream({ startSample: 0, endSample: 44100 })) {
|
||||
// block is Float32Array[] per channel
|
||||
}
|
||||
|
||||
// Undo the gain change.
|
||||
doc.undo(1);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `constructor(opts: AudioDocumentOptions)`
|
||||
|
||||
Creates a new `AudioDocument`. The options object is validated by `AudioDocumentOptionsSchema`; on failure an `AudioError` with code `INVALID_EDIT` is thrown.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `opts` — `AudioDocumentOptions` containing `sampleRate`, `channels`, `sourceLength`, optional `pages`, `edits`, `source`, `metadata`, and `idGenerator`.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioError` — if Zod validation fails.
|
||||
|
||||
### `id: string`
|
||||
|
||||
Readonly document identifier. Defaults to `crypto.randomUUID()` unless supplied or overridden by `idGenerator`.
|
||||
|
||||
### `sampleRate: number`
|
||||
|
||||
Readonly sample rate in Hz.
|
||||
|
||||
### `channels: number`
|
||||
|
||||
Readonly channel count.
|
||||
|
||||
### `sourceLength: number`
|
||||
|
||||
Length of the originally loaded source in samples; increases when `push` appends new data.
|
||||
|
||||
### `pages: (AudioPage | null)[]`
|
||||
|
||||
The page array that stores channel data. Null entries represent gaps or deleted pages.
|
||||
|
||||
### `edits: AudioEdit[]`
|
||||
|
||||
The mutable list of non-destructive edits applied to the document.
|
||||
|
||||
### `version: number`
|
||||
|
||||
Incremented on every `run` or `undo` call.
|
||||
|
||||
### `stats: AudioStats | undefined`
|
||||
|
||||
Optional statistics computed for the source material.
|
||||
|
||||
### `source: string | undefined`
|
||||
|
||||
Optional URI or path that identifies the source file.
|
||||
|
||||
### `metadata: unknown | undefined`
|
||||
|
||||
Optional user-defined metadata.
|
||||
|
||||
### `length: number`
|
||||
|
||||
Getter that compiles the edit plan and returns the total sample count. The result is cached until `run`, `undo`, or `push` invalidates it.
|
||||
|
||||
### `duration: number`
|
||||
|
||||
Getter returning `length / sampleRate` in seconds.
|
||||
|
||||
### `run(...edits: AudioEdit[]): this`
|
||||
|
||||
Appends one or more edits after validating each with `AudioEditSchema`. Emits an `"edit"` event and invalidates the length cache.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `edits` — Spread array of `AudioEdit` objects.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- The same `AudioDocument` instance for chaining.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioError` — if any edit fails Zod validation (`INVALID_EDIT`).
|
||||
|
||||
### `undo(n = 1): AudioEdit | AudioEdit[] | null`
|
||||
|
||||
Removes the last `n` edits. Emits an `"undo"` event and invalidates the length cache.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `n` — Number of edits to remove (default `1`).
|
||||
|
||||
**Returns:**
|
||||
|
||||
- The removed edit, an array of removed edits, or `null` if nothing was removed.
|
||||
|
||||
### `clone(): AudioDocument`
|
||||
|
||||
Returns a deep copy with a new `id`, duplicated `Float32Array` channel data, and copied edits. The cloned document is independent of the original.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- A new `AudioDocument` instance.
|
||||
|
||||
### `toJSON(): AudioDocumentJSON`
|
||||
|
||||
Serialises the document to a JSON-compatible representation. Channel data is converted from `Float32Array` to `number[]`.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `AudioDocumentJSON` object.
|
||||
|
||||
### `static fromJSON(json: AudioDocumentJSON): AudioDocument`
|
||||
|
||||
Reconstructs an `AudioDocument` from JSON. Channel data is restored as `Float32Array` instances, and `version` and `stats` are restored so the reconstructed document's `toJSON()` is deep-equal to the original's — a full round-trip through `@suaveplan/serialize`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `json` — `AudioDocumentJSON` object.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- A new `AudioDocument` instance.
|
||||
|
||||
### `read(offset = 0, duration?: number, signal?: AbortSignal): Promise<Float32Array[]>`
|
||||
|
||||
Compiles the plan and reads contiguous samples across pages.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `offset` — Start sample (default `0`).
|
||||
- `duration` — Number of samples to read; defaults to the remaining length.
|
||||
- `signal` — Optional `AbortSignal` for cancellation.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- Promise resolving to an array of `Float32Array`, one per channel.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioError` — `STREAM_ABORTED` if the signal is already aborted, or `PAGE_OUT_OF_RANGE` if the request exceeds the compiled plan length.
|
||||
|
||||
### `stream(opts?: StreamOptions): AsyncGenerator<Float32Array[]>`
|
||||
|
||||
Lazily processes the document in blocks, running the compiled pipeline for each chunk.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `opts` — Optional `StreamOptions` with `blockSize`, `startSample`, `endSample`, and `signal`.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- Async generator yielding `Float32Array[]` blocks.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioError` — `STREAM_ABORTED` if the signal is aborted before or during streaming.
|
||||
|
||||
**Events:** A process op that throws during streaming does not abort the stream — the `ProcessPipeline`'s `plugin:error` event is bridged onto this document's event bus, so a `plugin:error` listener registered via `on` receives `{ plugin, error }` while the stream continues with that op degraded to pass-through.
|
||||
|
||||
### `push(data: Float32Array[] | Float32Array, fmt?: PCMFormat): this`
|
||||
|
||||
Appends new audio data to the document, automatically paginating into `PAGE_SIZE` chunks.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `data` — Either a planar array of `Float32Array` (one per channel) or a single interleaved `Float32Array`.
|
||||
- `fmt` — Required when `data` is interleaved; provides `sampleRate`, `channels`, and `interleaved` flag.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- The same `AudioDocument` instance for chaining.
|
||||
|
||||
### `stop(): this`
|
||||
|
||||
No-op placeholder for future transport control. Returns `this`.
|
||||
|
||||
### `on<K extends keyof AudioDocumentEventMap>(event: K, listener: (e: AudioDocumentEventMap[K]) => void): () => void`
|
||||
|
||||
Subscribes to document events. Returns an unsubscribe function.
|
||||
|
||||
### `off<K extends keyof AudioDocumentEventMap>(event: K, listener: (e: AudioDocumentEventMap[K]) => void): void`
|
||||
|
||||
Unsubscribes a specific listener.
|
||||
|
||||
### `emitPluginError(plugin: string, error: unknown): void`
|
||||
|
||||
Emits a `"plugin:error"` event through the internal event bus.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The class uses lazy evaluation for `length`: a `PlanEngine` compiles the edit list and the result is cached in `_lengthCache`. Any mutating operation (`run`, `undo`, `push`) resets the cache to `undefined`.
|
||||
- Constructor validation delegates to Zod schemas imported from `./document.schemas.js`. Validation failures throw `AudioError` so that invalid state cannot be created.
|
||||
- `stream` initialises a `ProcessPipeline` with the compiled plan and processes each block in-place, yielding new `Float32Array` outputs. This avoids allocating a single giant buffer for the entire document. It opens a `stream.read` OpenTelemetry span and increments the `audio_core_stream_blocks_total` counter per block; both are inert without a registered exporter. The pipeline's `plugin:error` events are bridged onto `_bus` for the lifetime of the generator and unbridged in a `finally` block.
|
||||
- The internal `_bus` is an `EventBus<AudioDocumentEventMap>` from `@suaveplan/events`, providing typed pub/sub without external dependencies on DOM or Node event APIs.
|
||||
115
docs/media/audio/audio-core/src/error-codes/error-codes.md
Normal file
115
docs/media/audio/audio-core/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Error Codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module centralises every error code, metadata description, and severity level that the `@suaveplan/audio-core` package can emit. By defining codes through the `@suaveplan/error` registry, consumers can rely on stable string identifiers instead of brittle message matching when handling failures. The module also exports a concrete `AudioError` class so that all audio-level exceptions share a single, recognisable type.
|
||||
|
||||
## Features
|
||||
|
||||
- **Typed error codes** — `AUDIO_CORE_CODES` provides strongly-typed constants such as `PLUGIN_NOT_FOUND` and `STREAM_ABORTED`.
|
||||
- **Per-code metadata** — `AUDIO_CORE_ERROR_METADATA` maps every code to a human-readable description and a severity of `"warning"` or `"error"`.
|
||||
- **Dedicated exception class** — `AudioError` extends `SuaveplanError` and carries the `AudioCoreErrorCode` discriminator.
|
||||
- **Idempotent registration** — `registerAudioCoreErrorCodes` may be called repeatedly without side effects because the metadata record is frozen at module load time.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AudioError, AUDIO_CORE_CODES } from "@suaveplan/audio-core";
|
||||
|
||||
function loadPlugin(name: string) {
|
||||
if (!registry.has(name)) {
|
||||
throw new AudioError(
|
||||
AUDIO_CORE_CODES.PLUGIN_NOT_FOUND,
|
||||
`Plugin "${name}" is not registered.`
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AudioError,
|
||||
AUDIO_CORE_CODES,
|
||||
AUDIO_CORE_ERROR_METADATA,
|
||||
} from "@suaveplan/audio-core";
|
||||
|
||||
function handleAudioError(err: unknown) {
|
||||
if (err instanceof AudioError) {
|
||||
const meta = AUDIO_CORE_ERROR_METADATA[err.code];
|
||||
console.error(`[${meta.severity}] ${meta.description}`);
|
||||
|
||||
if (err.code === AUDIO_CORE_CODES.STREAM_ABORTED) {
|
||||
// Warning-level; safe to continue.
|
||||
return "aborted";
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AUDIO_CORE_CODES`
|
||||
|
||||
A frozen record of error-code constants created via `defineCodes("AUDIO_CORE", [...])`.
|
||||
|
||||
- `PLUGIN_NOT_FOUND`
|
||||
- `INVALID_EDIT`
|
||||
- `STREAM_ABORTED`
|
||||
- `PAGE_OUT_OF_RANGE`
|
||||
- `PROCESS_ERROR`
|
||||
|
||||
### `AudioCoreErrorCode`
|
||||
|
||||
```ts
|
||||
type AudioCoreErrorCode = (typeof AUDIO_CORE_CODES)[keyof typeof AUDIO_CORE_CODES];
|
||||
```
|
||||
|
||||
The union of all values contained in `AUDIO_CORE_CODES`.
|
||||
|
||||
### `AudioCoreErrorMetadata`
|
||||
|
||||
```ts
|
||||
interface AudioCoreErrorMetadata {
|
||||
readonly description: string;
|
||||
readonly severity: "warning" | "error";
|
||||
}
|
||||
```
|
||||
|
||||
Describes the human-readable explanation and severity for a single error code.
|
||||
|
||||
### `AUDIO_CORE_ERROR_METADATA`
|
||||
|
||||
```ts
|
||||
const AUDIO_CORE_ERROR_METADATA: Readonly<Record<AudioCoreErrorCode, AudioCoreErrorMetadata>>
|
||||
```
|
||||
|
||||
Frozen lookup table that maps every `AudioCoreErrorCode` to its metadata. Useful for localising or logging errors without parsing exception messages.
|
||||
|
||||
### `AudioError`
|
||||
|
||||
```ts
|
||||
class AudioError extends SuaveplanError<AudioCoreErrorCode> {}
|
||||
```
|
||||
|
||||
The exception type thrown by `@suaveplan/audio-core`. Its `code` property is constrained to `AudioCoreErrorCode`, allowing consumers to branch on specific failure modes.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- This class is thrown, not caught internally.
|
||||
|
||||
### `registerAudioCoreErrorCodes(): void`
|
||||
|
||||
An idempotent hook that exists for compatibility with the `@suaveplan/error` registration lifecycle. The metadata is already frozen and exported, so the function body is empty. It is invoked automatically at module load time.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The module depends on `@suaveplan/error` for `defineCodes` and the base `SuaveplanError` class.
|
||||
- `AUDIO_CORE_ERROR_METADATA` is frozen with `Object.freeze` so that downstream code cannot accidentally mutate severity or descriptions at runtime.
|
||||
- `registerAudioCoreErrorCodes` is called immediately after definition, ensuring the codes are available as soon as the module is imported.
|
||||
141
docs/media/audio/audio-core/src/page/page.md
Normal file
141
docs/media/audio/audio-core/src/page/page.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Page Primitives
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module provides the low-level building blocks for memory-efficient audio storage and retrieval inside an `AudioDocument`. Audio data is stored in fixed-size pages rather than contiguous buffers, so the package can load, copy, and release multi-megabyte datasets without repeatedly allocating new `Float32Array` instances.
|
||||
|
||||
## Features
|
||||
|
||||
- **Object-pooled page allocation** — `allocatePage` and `releasePage` reuse `Float32Array[]` arrays keyed by channel count and page size.
|
||||
- **Page walking** — `walkPages` visits every page that overlaps a requested sample range, exposing offset and length information to a visitor callback.
|
||||
- **Channel-scoped copying** — `copyPages` flattens overlapping pages into a destination `Float32Array` for a single channel.
|
||||
- **Multi-channel reads** — `readPages` returns a planar `Float32Array[]` covering all channels for a given offset and duration.
|
||||
- **Fixed constants** — `PAGE_SIZE` (1,048,576 samples) and `BLOCK_SIZE` (1,024 samples) dictate the default page and processing granularity.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readPages } from "@suaveplan/audio-core";
|
||||
|
||||
// Read 4096 samples starting at sample 0 across all channels.
|
||||
const channels = readPages(document, 0, 4096);
|
||||
// channels[0] is Float32Array for left channel
|
||||
// channels[1] is Float32Array for right channel
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
allocatePage,
|
||||
releasePage,
|
||||
walkPages,
|
||||
copyPages,
|
||||
} from "@suaveplan/audio-core";
|
||||
|
||||
// Reuse a pooled page instead of allocating new memory.
|
||||
const page = allocatePage(2);
|
||||
// ...fill page[0] and page[1] with data...
|
||||
releasePage(page);
|
||||
|
||||
// Walk only the pages that intersect a 2048-sample window.
|
||||
walkPages(document, 0, 44100, 2048, (page, offset, length) => {
|
||||
console.log(`Page starts at ${page.startSample}, reading ${length} samples from offset ${offset}`);
|
||||
});
|
||||
|
||||
// Copy a single channel into an existing buffer.
|
||||
const target = new Float32Array(2048);
|
||||
copyPages(document, 0, 44100, 2048, target, 0);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PAGE_SIZE`
|
||||
|
||||
```ts
|
||||
const PAGE_SIZE = 1024 * 1024; // 1,048,576 samples
|
||||
```
|
||||
|
||||
The default number of samples per page. This constant is used by `allocatePage` when no explicit size is provided.
|
||||
|
||||
### `BLOCK_SIZE`
|
||||
|
||||
```ts
|
||||
const BLOCK_SIZE = 1024;
|
||||
```
|
||||
|
||||
The default processing block size. Stream and pipeline operations typically operate on multiples of this value.
|
||||
|
||||
### `allocatePage(channels: number, pageSize?: number): Float32Array[]`
|
||||
|
||||
Returns a planar array of `Float32Array` instances, one per channel, each of length `pageSize`. If a previously released page with the same dimensions exists in the internal pool, it is reused instead of allocating fresh memory.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `channels` — Number of channels.
|
||||
- `pageSize` — Length of each channel buffer; defaults to `PAGE_SIZE`.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `Float32Array[]` representing the page.
|
||||
|
||||
### `releasePage(page: Float32Array[]): void`
|
||||
|
||||
Stores the page in an internal `Map` keyed by `"${channels}:${pageSize}"` so that a later call to `allocatePage` with the same dimensions can reuse it. If the page has zero channels, the call returns early without storing.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `page` — A planar `Float32Array[]` to return to the pool.
|
||||
|
||||
### `walkPages(doc, _channel, startSample, count, visitor): void`
|
||||
|
||||
Iterates over the pages of `doc` and invokes `visitor` for every page that intersects the sample range `[startSample, startSample + count)`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `doc` — `AudioDocument` to inspect.
|
||||
- `_channel` — Channel index (reserved for future use; currently unused).
|
||||
- `startSample` — Inclusive start position in samples.
|
||||
- `count` — Number of samples to cover.
|
||||
- `visitor` — Callback receiving:
|
||||
- `page` — The intersecting `AudioPage`.
|
||||
- `offset` — Sample offset inside the page where the overlap begins.
|
||||
- `length` — Number of overlapping samples.
|
||||
|
||||
### `copyPages(doc, channel, startSample, count, target, targetOffset): void`
|
||||
|
||||
Copies samples from a single channel across all intersecting pages into a destination buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `doc` — `AudioDocument` source.
|
||||
- `channel` — Channel index to copy.
|
||||
- `startSample` — Inclusive start position in samples.
|
||||
- `count` — Number of samples to copy.
|
||||
- `target` — Destination `Float32Array`.
|
||||
- `targetOffset` — Write position inside `target`.
|
||||
|
||||
### `readPages(doc, offset?, duration?): Float32Array[]`
|
||||
|
||||
Reads all channels for a contiguous range by allocating a new `Float32Array` per channel and delegating to `copyPages` for each channel.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `doc` — `AudioDocument` source.
|
||||
- `offset` — Inclusive start position; defaults to `0`.
|
||||
- `duration` — Number of samples; defaults to `doc.length - offset`.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `Float32Array[]` with one array per channel.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The page pool is a module-level `Map<string, Float32Array[]>`. The key is derived from `poolKey`, which concatenates channel count and page size with a colon separator. This design is intentionally simple and does not enforce a maximum pool size.
|
||||
- `walkPages` skips `null` pages, allowing sparse documents where pages can be deleted or lazily loaded.
|
||||
- `readPages` always allocates new output buffers; it does not use the object pool. Callers that need to minimise allocations should use `copyPages` into pre-allocated buffers or reuse the buffers returned by `allocatePage`.
|
||||
- Because `PAGE_SIZE` is 1 MB of 32-bit floats (4 MB per channel), a two-channel page occupies 8 MB. The pooling strategy is most effective when documents share the same channel count and are processed in bursts.
|
||||
132
docs/media/audio/audio-core/src/plan/plan-engine.md
Normal file
132
docs/media/audio/audio-core/src/plan/plan-engine.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# plan-engine
|
||||
|
||||
## Purpose
|
||||
|
||||
The `PlanEngine` class is the central coordinator for turning an [`AudioDocument`](../document/document.md) edit list into an executable audio plan. It resolves each edit against the [`OpRegistry`](../registry/op-registry.md), applies any declared sample-rate or channel transforms, and produces a `CompiledPlan` that describes the final segment layout and the remaining pipeline of non-planning edits. After compilation, the engine can read the plan into memory as `Float32Array` blocks or stream it incrementally for large files.
|
||||
|
||||
## Features
|
||||
|
||||
- **Edit resolution** – Delegates edit expansion to the `OpRegistry`, allowing plugins to resolve macro edits into concrete operations or skip them entirely.
|
||||
- **Format tracking** – Applies `sr` and `ch` transforms registered on operations so the compiled plan reflects the final sample rate and channel count.
|
||||
- **Segment planning** – Invokes `plan` functions from the registry to remap source segments into destination segments.
|
||||
- **Synchronous reading** – `readPlan` copies audio pages into `Float32Array[]` for a given offset and duration.
|
||||
- **Async streaming** – `streamPlan` yields block-sized chunks with optional `AbortSignal` cancellation.
|
||||
- **Page pre-loading** – `ensurePages` is provided as an extension point for disk-backed documents to load required pages before reading.
|
||||
- **OpenTelemetry instrumentation** – `compile` opens a `plan.compile` span and records the `audio_core_edits_total` counter and `audio_core_compile_duration_ms` histogram; `streamPlan` opens a `stream.read` span and increments `audio_core_stream_blocks_total` per yielded block. All instrumentation is inert when no exporter is registered.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PlanEngine } from "@suaveplan/audio-core";
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = new OpRegistry();
|
||||
// …register operations…
|
||||
|
||||
const engine = new PlanEngine(registry);
|
||||
const plan = engine.compile(document);
|
||||
|
||||
// Read the entire compiled plan into memory
|
||||
const audio = engine.readPlan(document, plan);
|
||||
// audio is Float32Array[] — one array per channel
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PlanEngine } from "@suaveplan/audio-core";
|
||||
|
||||
const engine = new PlanEngine(registry);
|
||||
const plan = engine.compile(document, true); // final = true → limit = totalLen
|
||||
|
||||
// Stream with a custom block size and abort support
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
for await (const block of engine.streamPlan(document, plan, {
|
||||
blockSize: 2048,
|
||||
startSample: 0,
|
||||
endSample: plan.limit,
|
||||
signal: controller.signal,
|
||||
})) {
|
||||
// process block (Float32Array[] per channel)
|
||||
}
|
||||
|
||||
// Ensure pages are resident before random access
|
||||
await engine.ensurePages(document, plan, 0, 44100);
|
||||
const snippet = engine.readPlan(document, plan, 0, 44100);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class PlanEngine`
|
||||
|
||||
#### `constructor(registry: OpRegistry)`
|
||||
|
||||
Creates a new `PlanEngine` bound to the given operation registry.
|
||||
|
||||
- **Parameters:**
|
||||
- `registry` – `OpRegistry` instance that holds registered audio operations.
|
||||
|
||||
#### `compile(doc: AudioDocument, final?: boolean): CompiledPlan`
|
||||
|
||||
Compiles the document's edit list into a `CompiledPlan`.
|
||||
|
||||
- **Parameters:**
|
||||
- `doc` – The audio document to compile.
|
||||
- `final` – Optional. When `true`, sets `limit` to `totalLen`; otherwise `limit` is clamped to `doc.sourceLength`.
|
||||
- **Returns:** A `CompiledPlan` containing `segs`, `pipeline`, `totalLen`, `sampleRate`, and `limit`.
|
||||
- **Throws:**
|
||||
- `AudioError` with code `PLUGIN_NOT_FOUND` if an edit references an unknown operation type.
|
||||
|
||||
#### `readPlan(doc: AudioDocument, plan: CompiledPlan, offset?: number, duration?: number): Float32Array[]`
|
||||
|
||||
Reads a region of the compiled plan into per-channel `Float32Array` buffers.
|
||||
|
||||
- **Parameters:**
|
||||
- `doc` – The audio document containing source pages.
|
||||
- `plan` – The compiled plan produced by `compile`.
|
||||
- `offset` – Sample offset to start reading. Defaults to `0`.
|
||||
- `duration` – Number of samples to read. Defaults to the remaining length from `offset`.
|
||||
- **Returns:** Array of `Float32Array`, one per channel.
|
||||
- **Throws:**
|
||||
- `AudioError` with code `PAGE_OUT_OF_RANGE` if `offset + duration` exceeds `plan.totalLen`.
|
||||
|
||||
#### `streamPlan(doc: AudioDocument, plan: CompiledPlan, opts?: object): AsyncGenerator<Float32Array[]>`
|
||||
|
||||
Streams the compiled plan in block-sized chunks.
|
||||
|
||||
- **Parameters:**
|
||||
- `doc` – The audio document containing source pages.
|
||||
- `plan` – The compiled plan.
|
||||
- `opts` – Optional object with:
|
||||
- `blockSize?: number` – Samples per chunk. Defaults to `BLOCK_SIZE` (`1024`).
|
||||
- `startSample?: number` – First sample to stream. Defaults to `0`.
|
||||
- `endSample?: number` – Exclusive end sample. Defaults to `plan.totalLen`.
|
||||
- `signal?: AbortSignal` – Abort signal for early termination.
|
||||
- **Returns:** Async generator yielding `Float32Array[]` per chunk.
|
||||
- **Throws:**
|
||||
- `AudioError` with code `STREAM_ABORTED` if the signal is already aborted at call time or becomes aborted mid-flight.
|
||||
|
||||
#### `ensurePages(doc: AudioDocument, plan: CompiledPlan, offset?: number, duration?: number): Promise<void>`
|
||||
|
||||
Ensures that the pages required for the given range are loaded.
|
||||
|
||||
- **Parameters:**
|
||||
- `doc` – The audio document.
|
||||
- `plan` – The compiled plan.
|
||||
- `offset` – Sample offset. Defaults to `0`.
|
||||
- `duration` – Number of samples. Defaults to the remaining length.
|
||||
- **Returns:** Resolves when pages are guaranteed resident.
|
||||
- **Throws:**
|
||||
- `AudioError` with code `PAGE_OUT_OF_RANGE` if the range exceeds `plan.totalLen`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`PlanEngine` does not own the `AudioDocument`; it only reads from it. When an operation declares a sample-rate or channel transform, the engine clones the document and mutates the clone's metadata so downstream operations see the updated format. The actual audio data is never copied during compilation.
|
||||
|
||||
`ensurePages` is currently a no-op for in-memory documents because all pages are already resident. Subclasses or future document backends can override this behavior to load lazily-paged audio from disk or network storage.
|
||||
|
||||
The `compile` method evaluates `plan` functions in registry order. Each `plan` function receives the current segment list and a `PlanContext` and returns a new segment list. This allows operations to insert silence, stretch time, or remap source ranges without mutating the original document.
|
||||
|
||||
Telemetry is resolved lazily per call through `@opentelemetry/api`, so neither `compile` nor `streamPlan` produces any import-time side effect, and both are no-ops in the absence of a registered exporter. The `compile` span is ended in a `finally` block so the `audio_core_compile_duration_ms` histogram and `audio_core_edits_total` counter are recorded even when an unknown-op error is thrown.
|
||||
108
docs/media/audio/audio-core/src/plan/segment.md
Normal file
108
docs/media/audio/audio-core/src/plan/segment.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# segment
|
||||
|
||||
## Purpose
|
||||
|
||||
The segment helpers provide low-level geometric operations on [`Segment`](./plan-engine.types.ts) objects. A segment represents a contiguous mapping from a source sample range (`from`, `count`) to a destination sample range (`to`), optionally carrying a playback `rate` and interpolation hint. These utilities are used by the [`PlanEngine`](./plan-engine.ts) and by individual operation `plan` functions to split, merge, and validate segments during plan compilation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Split** – Divide a single segment into two segments at an arbitrary sample boundary.
|
||||
- **Overlap detection** – Determine whether two segments' destination ranges intersect.
|
||||
- **Merge** – Combine two adjacent, compatible segments into one contiguous segment.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
splitSegment,
|
||||
segmentsOverlap,
|
||||
mergeSegments,
|
||||
} from "@suaveplan/audio-core";
|
||||
|
||||
const seg = { from: 0, count: 1000, to: 0 };
|
||||
|
||||
const [head, tail] = splitSegment(seg, 400);
|
||||
// head → { from: 0, count: 400, to: 0 }
|
||||
// tail → { from: 400, count: 600, to: 400 }
|
||||
|
||||
const overlaps = segmentsOverlap(head, tail); // false
|
||||
const merged = mergeSegments(head, tail);
|
||||
// merged → { from: 0, count: 1000, to: 0 }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
splitSegment,
|
||||
segmentsOverlap,
|
||||
mergeSegments,
|
||||
} from "@suaveplan/audio-core";
|
||||
import type { Segment } from "@suaveplan/audio-core";
|
||||
|
||||
function truncateSegments(segs: Segment[], maxSample: number): Segment[] {
|
||||
const out: Segment[] = [];
|
||||
for (const seg of segs) {
|
||||
if (seg.to >= maxSample) continue;
|
||||
if (seg.to + seg.count <= maxSample) {
|
||||
out.push(seg);
|
||||
continue;
|
||||
}
|
||||
// The segment crosses the boundary — split and keep the first half
|
||||
const [keep, _drop] = splitSegment(seg, maxSample - seg.to);
|
||||
out.push(keep);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function coalesce(segs: Segment[]): Segment[] {
|
||||
const sorted = [...segs].sort((a, b) => a.to - b.to);
|
||||
const out: Segment[] = [];
|
||||
for (const seg of sorted) {
|
||||
const last = out[out.length - 1];
|
||||
if (last) {
|
||||
const merged = mergeSegments(last, seg);
|
||||
if (merged) {
|
||||
out[out.length - 1] = merged;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(seg);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `splitSegment(seg: Segment, at: number): [Segment, Segment]`
|
||||
|
||||
Splits a segment into two segments at the given destination-relative sample count.
|
||||
|
||||
- **Parameters:**
|
||||
- `seg` – The segment to split.
|
||||
- `at` – Number of destination samples to place in the first segment. Must be within `[0, seg.count]`.
|
||||
- **Returns:** A tuple `[first, second]` where `first.count === at` and `second.count === seg.count - at`.
|
||||
- **Throws:** No explicit throw; passing an out-of-range `at` will produce negative counts.
|
||||
|
||||
### `segmentsOverlap(a: Segment, b: Segment): boolean`
|
||||
|
||||
Returns `true` if the destination ranges of `a` and `b` overlap.
|
||||
|
||||
- **Parameters:**
|
||||
- `a` – First segment.
|
||||
- `b` – Second segment.
|
||||
- **Returns:** `true` when the intervals `[a.to, a.to + a.count)` and `[b.to, b.to + b.count)` intersect.
|
||||
|
||||
### `mergeSegments(a: Segment, b: Segment): Segment | null`
|
||||
|
||||
Attempts to merge two segments into a single segment.
|
||||
|
||||
- **Parameters:**
|
||||
- `a` – First segment.
|
||||
- `b` – Second segment.
|
||||
- **Returns:** A new `Segment` with combined `count` if `a` and `b` are adjacent, have contiguous source mappings, and share the same `rate`. Returns `null` if the segments cannot be merged.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Segments are plain objects; all helpers return new objects and never mutate the inputs. `mergeSegments` is commutative: it checks both `a` followed by `b` and `b` followed by `a`. The adjacency check requires that `from + count` and `to + count` line up exactly between the two segments, and that `rate` is identical. If either segment carries an optional `ref` or `interp` field, those properties are preserved from the first segment in the returned merged object via object spread, but the merge does not validate their compatibility.
|
||||
190
docs/media/audio/audio-core/src/process/block-processor.md
Normal file
190
docs/media/audio/audio-core/src/process/block-processor.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# BlockProcessor
|
||||
|
||||
## Purpose
|
||||
|
||||
`BlockProcessor` defines the standard lifecycle contract for audio processors that operate on fixed-size blocks of multi-channel PCM data. It provides a uniform interface for setup, processing, reset, and disposal, which allows the audio engine to treat every processor the same way regardless of its internal algorithm.
|
||||
|
||||
`NoopBlockProcessor` is a reference implementation of this interface that simply copies input samples to output without modification. It is useful as a placeholder, a fallback when a requested processor is unavailable, or as a base class for processors that only need to override a subset of the lifecycle methods.
|
||||
|
||||
## Features
|
||||
|
||||
- **Standardised lifecycle**: `setup`, `process`, `reset`, and `dispose` cover the full lifetime of a processor.
|
||||
- **Zero-cost fallback**: `NoopBlockProcessor` passes audio through unchanged with minimal overhead.
|
||||
- **Safe sparse handling**: Both the interface and the no-op implementation safely skip undefined channel arrays.
|
||||
- **Typed context**: `BlockContext` carries block offset, sample rate, channel count, and operation-specific options.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NoopBlockProcessor } from "@suaveplan/audio-core/process/block-processor";
|
||||
|
||||
const proc = new NoopBlockProcessor();
|
||||
|
||||
proc.setup({ sampleRate: 48000, channels: 2, blockSize: 1024 });
|
||||
|
||||
const input = [
|
||||
new Float32Array([1, 2, 3]),
|
||||
new Float32Array([4, 5, 6]),
|
||||
];
|
||||
const output = [new Float32Array(3), new Float32Array(3)];
|
||||
|
||||
proc.process(input, output, {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 2,
|
||||
opts: {},
|
||||
});
|
||||
|
||||
// output now contains a copy of input
|
||||
proc.dispose();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
BlockProcessor,
|
||||
NoopBlockProcessor,
|
||||
} from "@suaveplan/audio-core/process/block-processor";
|
||||
import type {
|
||||
BlockContext,
|
||||
ProcessorConfig,
|
||||
} from "@suaveplan/audio-core/process/block-processor.types";
|
||||
|
||||
class GainBlockProcessor implements BlockProcessor {
|
||||
readonly name = "gain";
|
||||
private gain = 1;
|
||||
|
||||
setup(_config: ProcessorConfig): void {
|
||||
// Configuration is available but gain is driven by opts
|
||||
}
|
||||
|
||||
process(
|
||||
input: Float32Array[],
|
||||
output: Float32Array[],
|
||||
ctx: BlockContext
|
||||
): void {
|
||||
const g = Number(ctx.opts.gain ?? 1);
|
||||
for (let ch = 0; ch < input.length; ch++) {
|
||||
const inCh = input[ch];
|
||||
const outCh = output[ch];
|
||||
if (outCh === undefined || inCh === undefined) continue;
|
||||
for (let i = 0; i < inCh.length; i++) {
|
||||
outCh[i] = inCh[i] * g;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.gain = 1;
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
const proc = new GainBlockProcessor();
|
||||
proc.setup({ sampleRate: 48000, channels: 1, blockSize: 128 });
|
||||
|
||||
const input = [new Float32Array([1, 2, 3])];
|
||||
const output = [new Float32Array(3)];
|
||||
proc.process(input, output, {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { gain: 2 },
|
||||
});
|
||||
// output[0] now contains [2, 4, 6]
|
||||
proc.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `interface BlockProcessor`
|
||||
|
||||
#### `readonly name: string`
|
||||
|
||||
- **Description**
|
||||
Human-readable identifier for the processor, used for logging and error reporting.
|
||||
|
||||
#### `setup(config: ProcessorConfig): void`
|
||||
|
||||
- **Parameters**
|
||||
- `config` — `ProcessorConfig` containing `sampleRate`, `channels`, and `blockSize`.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
Called once before the first block is processed, giving the processor a chance to allocate buffers or pre-compute constants.
|
||||
|
||||
#### `process(input: Float32Array[], output: Float32Array[], ctx: BlockContext): void`
|
||||
|
||||
- **Parameters**
|
||||
- `input` — Array of channel buffers containing the input samples.
|
||||
- `output` — Array of channel buffers that must receive the processed samples.
|
||||
- `ctx` — `BlockContext` with `blockOffsetSamples`, `sampleRate`, `channels`, and `opts`.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
Processes one block of audio. Implementations should write into the provided `output` buffers rather than allocating new arrays.
|
||||
|
||||
#### `reset(): void`
|
||||
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
Resets any internal state to its initial value. Called when the playback position jumps or the document changes.
|
||||
|
||||
#### `dispose(): void`
|
||||
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
Releases any resources held by the processor. After disposal the instance must not be used again.
|
||||
|
||||
### `class NoopBlockProcessor implements BlockProcessor`
|
||||
|
||||
#### `readonly name = "noop"`
|
||||
|
||||
- **Description**
|
||||
Fixed name string identifying the no-op processor.
|
||||
|
||||
#### `setup(_config: ProcessorConfig): void`
|
||||
|
||||
- **Parameters**
|
||||
- `_config` — Ignored.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
No-op.
|
||||
|
||||
#### `process(input: Float32Array[], output: Float32Array[], _ctx: BlockContext): void`
|
||||
|
||||
- **Parameters**
|
||||
- `input` — Source channel buffers.
|
||||
- `output` — Destination channel buffers.
|
||||
- `_ctx` — Ignored.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
Copies each defined input channel into the corresponding output channel using `Float32Array.set`.
|
||||
|
||||
#### `reset(): void`
|
||||
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
No-op.
|
||||
|
||||
#### `dispose(): void`
|
||||
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
No-op.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`BlockProcessor` intentionally separates `setup` from the constructor so that the same instance can be reused with different configurations. The `reset` method exists for scenarios where the transport seeks or the underlying document is replaced, allowing stateful processors such as compressors or delay lines to clear their history without being recreated.
|
||||
|
||||
`NoopBlockProcessor` iterates channels with a numeric loop and checks both `input[ch]` and `output[ch]` for `undefined` before calling `set`. This makes it safe to use with sparse channel arrays. Because the implementation does not allocate memory, it can be instantiated freely without performance concerns.
|
||||
|
||||
The `BlockContext.opts` field is typed as `Record<string, unknown>` to allow arbitrary parameters while still permitting downstream code to cast or validate values as needed. Processors that expect specific options should perform runtime checks rather than assuming a particular shape.
|
||||
126
docs/media/audio/audio-core/src/process/process-pipeline.md
Normal file
126
docs/media/audio/audio-core/src/process/process-pipeline.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# ProcessPipeline
|
||||
|
||||
## Purpose
|
||||
|
||||
`ProcessPipeline` executes a chain of audio editing operations on blocks of multi-channel PCM data. It bridges the declarative edit list stored in an `AudioDocument` with the imperative block-processing functions registered in an `OpRegistry`, making it possible to apply non-destructive edits during real-time or offline rendering.
|
||||
|
||||
The class is designed to operate on fixed-size blocks of samples. It initialises per-edit state, allows parameters to be updated without rebuilding that state, and processes each block by running every registered operation in sequence.
|
||||
|
||||
## Features
|
||||
|
||||
- **State initialisation** (`init`): Creates a `ProcessState` array from an `AudioEdit` pipeline, including per-channel scratch buffers.
|
||||
- **Hot parameter patching** (`patch`): Updates operation parameters in place without reallocating state, which is useful for live automation or undo/redo.
|
||||
- **Sequential block processing** (`process`): Runs each operation in order, passing the output of one step as the input to the next.
|
||||
- **Error isolation**: If an operation throws, the pipeline degrades that operation to pass-through, emits a typed `plugin:error` event carrying an `AudioError` (code `AUDIO_CORE.PROCESS_ERROR`), and continues running the remaining operations — a crash never propagates out of `process` or corrupts the stream.
|
||||
- **Structural-change crossfade**: When the pipeline's op set changes between blocks (detected via an op-name signature), the next ~20 ms of output is crossfaded from the dry input toward the processed signal to avoid clicks.
|
||||
- **Typed event surface** (`on` / `off`): Idempotent listener registration for the `plugin:error` topic, backed by `@suaveplan/events`.
|
||||
- **OpenTelemetry instrumentation**: Each `process` call opens a `process.block` span and records the `audio_core_process_duration_ms` histogram — inert when no exporter is registered.
|
||||
- **Multi-channel support**: Operates on arrays of `Float32Array`, one per channel, with safe handling of undefined or mismatched arrays.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core/registry/op-registry";
|
||||
import { ProcessPipeline } from "@suaveplan/audio-core/process/process-pipeline";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const pipeline = new ProcessPipeline(registry);
|
||||
|
||||
const edits = [{ type: "gain", opts: { gain: 2 } }];
|
||||
const state = pipeline.init(edits, 100, 48000, 1);
|
||||
|
||||
const input = [new Float32Array([1, 2, 3])];
|
||||
const output = [new Float32Array(3)];
|
||||
|
||||
pipeline.process(input, output, state, 0, 48000);
|
||||
// output[0] now contains [2, 4, 6]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core/registry/op-registry";
|
||||
import { ProcessPipeline } from "@suaveplan/audio-core/process/process-pipeline";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const pipeline = new ProcessPipeline(registry);
|
||||
|
||||
const edits = [
|
||||
{ type: "gain", opts: { gain: 1.5 } },
|
||||
{ type: "pan", opts: { balance: 0.2 } },
|
||||
];
|
||||
const state = pipeline.init(edits, 48000, 48000, 2);
|
||||
|
||||
// Later, update the gain without rebuilding state
|
||||
pipeline.patch(state, [
|
||||
{ type: "gain", opts: { gain: 2.0 } },
|
||||
{ type: "pan", opts: { balance: 0.2 } },
|
||||
]);
|
||||
|
||||
const input = [
|
||||
new Float32Array([1, 2, 3]),
|
||||
new Float32Array([4, 5, 6]),
|
||||
];
|
||||
const output = [new Float32Array(3), new Float32Array(3)];
|
||||
|
||||
pipeline.process(input, output, state, 0, 48000);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class ProcessPipeline`
|
||||
|
||||
#### `constructor(registry: OpRegistry)`
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` that resolves edit types to `AudioOpDescriptor` objects.
|
||||
- **Description**
|
||||
Creates a new pipeline bound to the given registry.
|
||||
|
||||
#### `init(pipeline: AudioEdit[], _totalDuration: number, _sampleRate: number, channels: number): ProcessState[]`
|
||||
|
||||
- **Parameters**
|
||||
- `pipeline` — Ordered array of audio edits to apply.
|
||||
- `_totalDuration` — Total duration in samples (currently reserved for future use).
|
||||
- `_sampleRate` — Sample rate in Hz (currently reserved for future use).
|
||||
- `channels` — Number of audio channels.
|
||||
- **Returns**
|
||||
- `ProcessState[]` — Array of state objects, one per edit, containing the resolved operation descriptor, parameter snapshot, and per-channel buffer arrays.
|
||||
- **Description**
|
||||
Builds the initial runtime state for the pipeline. If an edit type is not found in the registry, a fallback descriptor containing only the name is used.
|
||||
|
||||
#### `patch(state: ProcessState[], pipeline: AudioEdit[]): void`
|
||||
|
||||
- **Parameters**
|
||||
- `state` — Existing state array returned by `init`.
|
||||
- `pipeline` — New edit array whose parameters should overwrite those in `state`.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Description**
|
||||
Updates parameters in place for every index where both `state[i]` and `pipeline[i]` exist. Does not add or remove state entries, so the caller must call `init` again if the pipeline length changes.
|
||||
|
||||
#### `process(input: Float32Array[], output: Float32Array[], state: ProcessState[], blockOffsetSamples: number, sampleRate: number): Float32Array[]`
|
||||
|
||||
- **Parameters**
|
||||
- `input` — Array of channel buffers containing the input samples.
|
||||
- `output` — Array of channel buffers that will receive the processed samples.
|
||||
- `state` — Current pipeline state produced by `init` (and optionally `patch`).
|
||||
- `blockOffsetSamples` — Sample offset of the current block within the overall stream.
|
||||
- `sampleRate` — Sample rate in Hz.
|
||||
- **Returns**
|
||||
- `Float32Array[]` — The `output` array after processing.
|
||||
- **Description**
|
||||
Copies `input` into `output`, then iterates over `state` and invokes each operation's `process` function with a `BlockContext` that includes the operation's own parameters. Operations that lack a `process` function are skipped. A throwing operation does **not** propagate: it is restored to pass-through and reported via the `plugin:error` event. When the op-name signature differs from the previous block, the block is crossfaded from dry to wet.
|
||||
|
||||
#### `on(event, listener)` / `off(event, listener)`
|
||||
|
||||
- **Description**
|
||||
Register or remove a listener for the `plugin:error` topic (`{ plugin: string; error: AudioError }`). `on` returns an unsubscribe function; registration is idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The class keeps a private reference to the `OpRegistry` passed at construction time, plus a private `@suaveplan/events` `EventBus` for the `plugin:error` topic and crossfade bookkeeping (`_lastSignature`, `_crossfadeRemaining`, `_crossfadeTotal`). During `init`, it maps over the declarative `AudioEdit` array and resolves each type through the registry, producing a parallel array of mutable `ProcessState` objects. The `_totalDuration` and `_sampleRate` parameters are accepted for API stability but are not used internally today.
|
||||
|
||||
`patch` performs a bounded loop up to the minimum of `state.length` and `pipeline.length`, and it defensively checks for `undefined` entries on both sides. This makes it safe to use with sparse arrays.
|
||||
|
||||
In `process`, the input is first copied into the output buffers so that the pipeline starts from the dry signal. For each state entry, a temporary snapshot of the current output is taken. If the operation throws, the snapshot is copied back into `output`, a `plugin:error` event is emitted carrying an `AudioError` with code `AUDIO_CORE.PROCESS_ERROR`, and processing continues with the next operation — the failing op simply degrades to pass-through. The op-name signature is compared against the previous block; a difference arms a crossfade window of `round(sampleRate * 0.02)` samples, over which the output ramps from the dry input to the processed signal. The whole `process` body runs inside a `process.block` OpenTelemetry span with the `audio_core_process_duration_ms` histogram.
|
||||
78
docs/media/audio/audio-core/src/registry/built-in/crop.md
Normal file
78
docs/media/audio/audio-core/src/registry/built-in/crop.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# crop
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerCrop` adds the `crop` operation to an `OpRegistry`, making it available to the planning engine. The crop operation shortens the audio timeline by retaining only the samples that fall within a specified `[start, end)` range. Because it runs at the planning stage, it never rewrites PCM data; instead it recomputes segment boundaries so that the render stage reads the correct slices from the original source. This makes cropping fast and fully reversible as long as the original document is preserved.
|
||||
|
||||
## Features
|
||||
|
||||
- Sample-accurate, non-destructive cropping at the segment level
|
||||
- Preserves `ref`, `rate`, and `interp` fields on surviving segments
|
||||
- Correctly handles cuts that span multiple contiguous segments
|
||||
- Sensible defaults: `start` falls back to `0` and `end` falls back to `ctx.totalLen`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const crop = registry.get("crop")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { start: 12000, end: 36000 },
|
||||
};
|
||||
|
||||
const result = crop.plan!(segments, context);
|
||||
// result: [{ from: 12000, count: 24000, to: 12000 }]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const crop = registry.get("crop")!;
|
||||
|
||||
const segments = [
|
||||
{ from: 0, count: 10000, to: 0 },
|
||||
{ from: 0, count: 20000, to: 10000 },
|
||||
];
|
||||
|
||||
const context = {
|
||||
totalLen: 30000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 30000,
|
||||
opts: { start: 5000, end: 25000 },
|
||||
};
|
||||
|
||||
const result = crop.plan!(segments, context);
|
||||
// Splits across the original segment boundary
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerCrop(registry: OpRegistry): void`
|
||||
|
||||
Registers the `crop` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The underlying `plan` function is a `PlanFn` that receives `Segment[]` and `PlanContext`.
|
||||
- It walks segments in order, tracking cumulative sample positions. Segments that lie completely outside the crop window are discarded.
|
||||
- For a partially overlapped segment, the function computes the overlap start relative to the segment, shifts `seg.from` forward by that amount, reduces `seg.count` to the overlap length, and updates `seg.to` to maintain the output timeline.
|
||||
- The operation is order-dependent: cropping before another planning edit changes the timeline that the next edit sees.
|
||||
79
docs/media/audio/audio-core/src/registry/built-in/dither.md
Normal file
79
docs/media/audio/audio-core/src/registry/built-in/dither.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# dither
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerDither` installs the `dither` processing operation into an `OpRegistry`. Dithering adds a small amount of pseudo-random noise to each sample, which is a common technique for reducing quantization distortion when lowering bit depth or after heavy digital processing. The operation is applied at the block processing stage, meaning it mutates sample values directly rather than segment metadata.
|
||||
|
||||
## Features
|
||||
|
||||
- Triangular probability density function noise for gentle spectral shaping
|
||||
- Per-channel application with identical noise distribution
|
||||
- Configurable amplitude scaled relative to 16-bit full scale (`1 / 2^16`)
|
||||
- Gracefully skips undefined channels without throwing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const dither = registry.get("dither")!;
|
||||
|
||||
const input = [new Float32Array([0.1, 0.2, 0.3])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { amplitude: 1 },
|
||||
};
|
||||
|
||||
dither.process!(input, output, ctx);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const dither = registry.get("dither")!;
|
||||
const normalize = registry.get("normalize")!;
|
||||
|
||||
const input = [new Float32Array([0.5, 0.6, 0.7])];
|
||||
const temp = [new Float32Array(3)];
|
||||
const output = [new Float32Array(3)];
|
||||
|
||||
normalize.process!(input, temp, {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { target: 0.9 },
|
||||
});
|
||||
|
||||
dither.process!(temp, output, {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { amplitude: 0.5 },
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerDither(registry: OpRegistry): void`
|
||||
|
||||
Registers the `dither` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The noise generator uses two independent uniform random variables to approximate a triangular distribution: `(Math.random() - 0.5) * 2 + (Math.random() - 0.5) * 2`.
|
||||
- The final noise amplitude is multiplied by `amplitude / 65536`, so the default amplitude of `1` yields noise at roughly the 16-bit least-significant-bit level.
|
||||
- Because the operation is stateless, every block receives an independent noise seed; this is acceptable for dither but means the noise is not reproducible across runs.
|
||||
73
docs/media/audio/audio-core/src/registry/built-in/fade.md
Normal file
73
docs/media/audio/audio-core/src/registry/built-in/fade.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# fade
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerFade` adds the `fade` operation to an `OpRegistry`. Fade applies a linear amplitude ramp across an entire processing block, allowing smooth volume transitions such as fade-ins and fade-outs. Because it operates at the sample level inside `ProcessPipeline`, it can be chained with other block-level effects like gain or pan without leaving the render loop. This makes it ideal for cross-fades, ducking, and envelope shaping in real-time playback scenarios.
|
||||
|
||||
## Features
|
||||
|
||||
- Linear amplitude interpolation from `startGain` to `endGain`
|
||||
- Per-channel application with identical ramp timing
|
||||
- Safe handling of single-sample blocks (avoids division by zero)
|
||||
- Zero-allocation: writes directly into the provided output buffers
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const fade = registry.get("fade")!;
|
||||
|
||||
const input = [new Float32Array([1, 1, 1, 1])];
|
||||
const output = [new Float32Array(4)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { startGain: 0, endGain: 1 },
|
||||
};
|
||||
|
||||
fade.process!(input, output, ctx);
|
||||
// output approximately [0, 0.33, 0.66, 1]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const fadeOut = registry.get("fade")!;
|
||||
|
||||
const input = [new Float32Array([0.8, 0.8, 0.8, 0.8])];
|
||||
const output = [new Float32Array(4)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { startGain: 1, endGain: 0 },
|
||||
};
|
||||
|
||||
fadeOut.process!(input, output, ctx);
|
||||
// output approximately [0.8, 0.53, 0.27, 0]
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerFade(registry: OpRegistry): void`
|
||||
|
||||
Registers the `fade` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The interpolation parameter `t` is computed as `i / (len - 1)` for blocks longer than one sample; for single-sample blocks `t` is clamped to `0`.
|
||||
- The gain factor is `startGain + (endGain - startGain) * t`, which guarantees exact endpoint values when the block length is known.
|
||||
- Because the ramp is linear in amplitude (not decibels), it may sound slightly different from a logarithmic fade curve at extreme settings.
|
||||
73
docs/media/audio/audio-core/src/registry/built-in/gain.md
Normal file
73
docs/media/audio/audio-core/src/registry/built-in/gain.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# gain
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerGain` installs the `gain` operation into an `OpRegistry`. It is the simplest block-level processor: every sample in every channel is multiplied by a single scalar value. This makes it useful for both subtle level corrections and dramatic amplification, though consumers should be aware that it does not perform hard limiting or clipping detection. It is therefore the responsibility of the caller to ensure that the resulting samples remain within the expected floating-point range.
|
||||
|
||||
## Features
|
||||
|
||||
- Single scalar multiplication across all channels
|
||||
- Zero-allocation in-place or out-of-place processing
|
||||
- Default gain of `1` acts as a pass-through when no option is supplied
|
||||
- Skips undefined channels safely
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const gain = registry.get("gain")!;
|
||||
|
||||
const input = [new Float32Array([0.1, 0.2, 0.3])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { gain: 2 },
|
||||
};
|
||||
|
||||
gain.process!(input, output, ctx);
|
||||
// output: [0.2, 0.4, 0.6]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const gain = registry.get("gain")!;
|
||||
|
||||
const input = [new Float32Array([0.9, 0.95, 1.0])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { gain: 0.5 },
|
||||
};
|
||||
|
||||
gain.process!(input, output, ctx);
|
||||
// output: [0.45, 0.475, 0.5]
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerGain(registry: OpRegistry): void`
|
||||
|
||||
Registers the `gain` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The processor reads `ctx.opts["gain"]` and defaults to `1` using the nullish coalescing pattern `Number(ctx.opts["gain"] ?? 1)`.
|
||||
- It loops over every channel and every sample, writing `src[i] * gain` into the corresponding output buffer.
|
||||
- There is no saturation logic; if the result exceeds the `[-1, 1]` floating-point range, values will wrap or clip depending on the downstream consumer.
|
||||
78
docs/media/audio/audio-core/src/registry/built-in/insert.md
Normal file
78
docs/media/audio/audio-core/src/registry/built-in/insert.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# insert
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerInsert` adds the `insert` operation to an `OpRegistry`. Insert is a planning-stage edit that introduces silence into the timeline at an arbitrary sample position. It can split an existing segment when the insertion point falls inside it, or append a new silence segment when the point lies beyond the current end of the audio. This makes it useful for adding gaps, aligning tracks, or reserving space for later overdubs.
|
||||
|
||||
## Features
|
||||
|
||||
- Sample-accurate insertion at any position within or after the timeline
|
||||
- Splits existing segments automatically when the insertion point overlaps
|
||||
- Appends trailing silence when inserting past the end
|
||||
- Preserves `ref` and other metadata on the split segments
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const insert = registry.get("insert")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { at: 12000, length: 5000 },
|
||||
};
|
||||
|
||||
const result = insert.plan!(segments, context);
|
||||
// result contains three segments: before, silence, after
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const insert = registry.get("insert")!;
|
||||
|
||||
const segments = [
|
||||
{ from: 0, count: 10000, to: 0 },
|
||||
{ from: 0, count: 10000, to: 10000 },
|
||||
];
|
||||
|
||||
const context = {
|
||||
totalLen: 20000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 20000,
|
||||
opts: { at: 25000, length: 5000 },
|
||||
};
|
||||
|
||||
const result = insert.plan!(segments, context);
|
||||
// Appends silence at the end because 25000 is past the timeline
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerInsert(registry: OpRegistry): void`
|
||||
|
||||
Registers the `insert` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The underlying `plan` function is a `PlanFn` that iterates over segments while tracking cumulative positions.
|
||||
- When `segStart < at < segEnd`, the segment is broken into a `before` piece, a synthetic silence piece with `from: 0`, and an `after` piece whose `from` is shifted forward.
|
||||
- Segments that start at or after the insertion point have their `to` offset increased by the inserted length to maintain timeline alignment.
|
||||
- If the insertion point is at or past the end of the last segment, a trailing silence segment is pushed with `from: 0` and `to: pos`.
|
||||
73
docs/media/audio/audio-core/src/registry/built-in/mix.md
Normal file
73
docs/media/audio/audio-core/src/registry/built-in/mix.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# mix
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerMix` installs the `mix` operation into an `OpRegistry`. The mix processor adds a scaled copy of the input signal back onto itself, producing a wet/dry blend controlled by a single gain parameter. Unlike a cross-fade between two different sources, this operation works on a single buffer and is therefore useful for thickening a sound or creating a controlled boost without external inputs.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-parameter additive self-mix (`dst = src + src * gain`)
|
||||
- Per-channel application with independent buffer traversal
|
||||
- Default mix gain of `1` doubles the amplitude
|
||||
- Zero-allocation block processing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const mix = registry.get("mix")!;
|
||||
|
||||
const input = [new Float32Array([0.1, 0.2, 0.3])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { gain: 0.5 },
|
||||
};
|
||||
|
||||
mix.process!(input, output, ctx);
|
||||
// output: [0.15, 0.3, 0.45]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const mix = registry.get("mix")!;
|
||||
|
||||
const input = [new Float32Array([0.3, 0.3, 0.3])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { gain: -0.5 },
|
||||
};
|
||||
|
||||
mix.process!(input, output, ctx);
|
||||
// output: [0.15, 0.15, 0.15] (partial cancellation)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerMix(registry: OpRegistry): void`
|
||||
|
||||
Registers the `mix` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The formula `dst[i] = src[i] + src[i] * mixGain` is mathematically equivalent to `src[i] * (1 + mixGain)`.
|
||||
- Because there is no clamping, large positive mix gains can push samples outside the `[-1, 1]` range; consumers should follow this operation with a limiter or normalize if headroom is uncertain.
|
||||
- The operation is stateless and does not depend on block boundaries, so it can be split across arbitrary block sizes without artifacts.
|
||||
@@ -0,0 +1,72 @@
|
||||
# normalize
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerNormalize` adds the `normalize` operation to an `OpRegistry`. Normalization scans an entire processing block to find the peak absolute sample value, then scales every sample by a uniform factor so that the peak reaches a configurable target level. This is a common final-stage processor for ensuring consistent loudness across clips without manual gain adjustments.
|
||||
|
||||
## Features
|
||||
|
||||
- Two-pass block processing: peak detection followed by uniform scaling
|
||||
- Configurable target peak (default `1`, which corresponds to 0 dBFS)
|
||||
- Safe handling of silent blocks: if the peak is `0`, the gain factor defaults to `1`
|
||||
- Applies the same gain to every channel to preserve inter-channel balance
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const normalize = registry.get("normalize")!;
|
||||
|
||||
const input = [new Float32Array([0.25, 0.5, 0.75])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { target: 1 },
|
||||
};
|
||||
|
||||
normalize.process!(input, output, ctx);
|
||||
// output: [0.333, 0.666, 1.0]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const normalize = registry.get("normalize")!;
|
||||
|
||||
const input = [new Float32Array([0.1, 0.2, 0.3])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: { target: 0.891 }, // -1 dBFS headroom
|
||||
};
|
||||
|
||||
normalize.process!(input, output, ctx);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerNormalize(registry: OpRegistry): void`
|
||||
|
||||
Registers the `normalize` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The first pass computes `peak = max(abs(src[i]))` across every channel. The second pass multiplies each sample by `target / peak`.
|
||||
- When the block is completely silent (`peak === 0`), the gain is set to `1` to avoid division by zero.
|
||||
- Because normalization is block-local, separate blocks may receive different gain factors; for global normalization the entire audio should be rendered as a single block or the peak should be pre-computed and passed via the `gain` operation instead.
|
||||
74
docs/media/audio/audio-core/src/registry/built-in/pad.md
Normal file
74
docs/media/audio/audio-core/src/registry/built-in/pad.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# pad
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerPad` installs the `pad` operation into an `OpRegistry`. Pad is a planning function that extends the total timeline by prepending silence before the first segment, appending silence after the last segment, or both. It does not modify existing segment data; instead it inserts synthetic silence segments with `from: 0` so that the render engine produces zeros for the padded regions.
|
||||
|
||||
## Features
|
||||
|
||||
- Prepends silence when `start > 0`
|
||||
- Appends silence when `end` exceeds the current total length
|
||||
- Leaves existing segments untouched, preserving all metadata
|
||||
- Defaults to `start: 0` and `end: ctx.totalLen`, making no-op calls safe
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const pad = registry.get("pad")!;
|
||||
|
||||
const segments = [{ from: 0, count: 10000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 10000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 10000,
|
||||
opts: { start: 5000 },
|
||||
};
|
||||
|
||||
const result = pad.plan!(segments, context);
|
||||
// result: [{ from: 0, count: 5000, to: 0 }, { from: 0, count: 10000, to: 5000 }]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const pad = registry.get("pad")!;
|
||||
|
||||
const segments = [{ from: 0, count: 10000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 10000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 10000,
|
||||
opts: { start: 2000, end: 15000 },
|
||||
};
|
||||
|
||||
const result = pad.plan!(segments, context);
|
||||
// Prepends 2000 samples and appends 5000 samples
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerPad(registry: OpRegistry): void`
|
||||
|
||||
Registers the `pad` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plan function pushes a leading silence segment only when `start > 0`; the silence segment has `to: 0` so that rendering starts at sample zero.
|
||||
- Existing segments are copied with their `to` values shifted by the leading pad amount, ensuring that the render timeline remains contiguous.
|
||||
- A trailing silence segment is appended only when `end > pos`, where `pos` is the cumulative length after all original segments. The trailing segment uses `to: pos` so that it begins exactly where the original audio ends.
|
||||
- Because padding changes the total length, subsequent planning operations should be aware that `ctx.totalLen` may no longer match the original source length.
|
||||
79
docs/media/audio/audio-core/src/registry/built-in/pan.md
Normal file
79
docs/media/audio/audio-core/src/registry/built-in/pan.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# pan
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerPan` adds the `pan` operation to an `OpRegistry`. Pan is a block-level processor that adjusts the relative loudness of the left and right channels using a constant-power law, preserving perceived loudness as the stereo image moves. When the input is mono (fewer than two channels), the operation copies the input through unchanged so that downstream consumers do not need special casing.
|
||||
|
||||
## Features
|
||||
|
||||
- Constant-power panning via cosine/sine law for natural stereo positioning
|
||||
- Mono pass-through: single-channel input is copied without modification
|
||||
- Pan value `-1` is hard left, `0` is center, and `1` is hard right
|
||||
- Per-sample multiplication with pre-computed channel gains
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const pan = registry.get("pan")!;
|
||||
|
||||
const input = [
|
||||
new Float32Array([1, 1, 1]),
|
||||
new Float32Array([1, 1, 1]),
|
||||
];
|
||||
const output = [new Float32Array(3), new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 2,
|
||||
opts: { pan: -1 },
|
||||
};
|
||||
|
||||
pan.process!(input, output, ctx);
|
||||
// Left channel unchanged, right channel attenuated to near zero
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const pan = registry.get("pan")!;
|
||||
|
||||
const input = [
|
||||
new Float32Array([0.5, 0.5, 0.5]),
|
||||
new Float32Array([0.5, 0.5, 0.5]),
|
||||
];
|
||||
const output = [new Float32Array(3), new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 2,
|
||||
opts: { pan: 0 }, // center
|
||||
};
|
||||
|
||||
pan.process!(input, output, ctx);
|
||||
// Both channels scaled by ~0.707 (center pan)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerPan(registry: OpRegistry): void`
|
||||
|
||||
Registers the `pan` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The gain constants are computed as `cos(((pan + 1) * Math.PI) / 4)` for the left channel and `sin(((pan + 1) * Math.PI) / 4)` for the right channel.
|
||||
- At `pan = 0` (center), both gains evaluate to `Math.sqrt(2) / 2 ≈ 0.707`, which is the standard constant-power center attenuation.
|
||||
- The processor only touches the first two channels; any additional channels are ignored. If either left or right buffer is undefined, the function returns early.
|
||||
78
docs/media/audio/audio-core/src/registry/built-in/remove.md
Normal file
78
docs/media/audio/audio-core/src/registry/built-in/remove.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# remove
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerRemove` installs the `remove` operation into an `OpRegistry`. Remove is a planning function that deletes samples from the timeline by excising everything inside a `[start, end)` range and stitching the remaining segments back together. It is the conceptual inverse of `crop`: instead of keeping the interior, it discards the interior and preserves the exterior.
|
||||
|
||||
## Features
|
||||
|
||||
- Non-destructive removal at the segment level
|
||||
- Automatically splits partially overlapped segments into before/after pieces
|
||||
- Shifts `to` offsets for segments that follow the removed region
|
||||
- Defaults to `start: 0` and `end: ctx.totalLen`, removing the entire timeline if both are omitted
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const remove = registry.get("remove")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { start: 10000, end: 20000 },
|
||||
};
|
||||
|
||||
const result = remove.plan!(segments, context);
|
||||
// result: [{ from: 0, count: 10000, to: 0 }, { from: 20000, count: 28000, to: 10000 }]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const remove = registry.get("remove")!;
|
||||
|
||||
const segments = [
|
||||
{ from: 0, count: 15000, to: 0 },
|
||||
{ from: 0, count: 15000, to: 15000 },
|
||||
];
|
||||
|
||||
const context = {
|
||||
totalLen: 30000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 30000,
|
||||
opts: { start: 10000, end: 20000 },
|
||||
};
|
||||
|
||||
const result = remove.plan!(segments, context);
|
||||
// Removes across the boundary between the two original segments
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerRemove(registry: OpRegistry): void`
|
||||
|
||||
Registers the `remove` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plan iterates over segments, keeping those entirely before `start` or entirely after `end`.
|
||||
- For a segment that straddles the removal window, it produces a `before` piece (if any) and an `after` piece whose `from` is shifted to skip the removed samples and whose `to` is aligned to the new timeline position.
|
||||
- Because the operation shortens the total length, any subsequent planning edits see a smaller `ctx.totalLen` unless the removal is the final step.
|
||||
- The algorithm preserves segment order but may reduce the total segment count when entire segments fall inside the removal window.
|
||||
77
docs/media/audio/audio-core/src/registry/built-in/repeat.md
Normal file
77
docs/media/audio/audio-core/src/registry/built-in/repeat.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# repeat
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerRepeat` adds the `repeat` operation to an `OpRegistry`. Repeat is a planning function that duplicates the entire segment list a configurable number of times, recalculating `to` offsets so that the resulting timeline is contiguous. It is useful for creating loops, extending ambient beds, or previewing a short phrase multiple times without copying underlying audio data.
|
||||
|
||||
## Features
|
||||
|
||||
- Duplicates the full segment list non-destructively
|
||||
- Configurable repeat count with a default of `2`
|
||||
- Recalculates `to` offsets for every duplicated segment
|
||||
- Preserves `ref`, `rate`, and `interp` on each copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const repeat = registry.get("repeat")!;
|
||||
|
||||
const segments = [{ from: 0, count: 12000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 12000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 12000,
|
||||
opts: { times: 3 },
|
||||
};
|
||||
|
||||
const result = repeat.plan!(segments, context);
|
||||
// result contains three contiguous copies of the original segment
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const repeat = registry.get("repeat")!;
|
||||
|
||||
const segments = [
|
||||
{ from: 0, count: 5000, to: 0 },
|
||||
{ from: 0, count: 5000, to: 5000 },
|
||||
];
|
||||
|
||||
const context = {
|
||||
totalLen: 10000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 10000,
|
||||
opts: { times: 4 },
|
||||
};
|
||||
|
||||
const result = repeat.plan!(segments, context);
|
||||
// Four contiguous repetitions of the two-segment group
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerRepeat(registry: OpRegistry): void`
|
||||
|
||||
Registers the `repeat` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plan function loops `times` iterations, pushing every original segment into the output array with an updated `to` field.
|
||||
- The `to` offset for each repeated segment is computed as the cumulative sum of all preceding segment counts, ensuring no gaps or overlaps.
|
||||
- Because the original segments are shallow-copied with the spread operator, mutating the returned segments later will not affect the source array but will affect sibling copies if deep references are shared.
|
||||
@@ -0,0 +1,73 @@
|
||||
# resample
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerResample` installs the `resample` operation into an `OpRegistry`. Resample is a planning function that changes the target sample rate by scaling segment counts proportionally and updating the engine's internal sample rate tracker. It does not perform actual sample-rate conversion on PCM data; rather, it signals to the render pipeline that the output should be interpreted at a different rate, which is appropriate for integer-ratio conversions or rate-aware pipelines.
|
||||
|
||||
## Features
|
||||
|
||||
- Scales segment counts by the ratio of target sample rate to current sample rate
|
||||
- Updates the document sample rate via the `sr` transform function
|
||||
- Default target rate falls back to the current rate, resulting in a no-op
|
||||
- Works with any positive integer sample rate
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const resample = registry.get("resample")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { sampleRate: 44100 },
|
||||
};
|
||||
|
||||
const result = resample.plan!(segments, context);
|
||||
// count rounded to 48000 * (44100/48000) ≈ 44100
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const resample = registry.get("resample")!;
|
||||
|
||||
const segments = [{ from: 0, count: 96000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 96000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 96000,
|
||||
opts: { sampleRate: 96000 },
|
||||
};
|
||||
|
||||
const result = resample.plan!(segments, context);
|
||||
// Doubles the sample rate and halves the segment count proportionally
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerResample(registry: OpRegistry): void`
|
||||
|
||||
Registers the `resample` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `plan` function computes `ratio = targetRate / ctx.sampleRate` and rounds each segment's `count` with `Math.max(0, Math.round(seg.count * ratio))`.
|
||||
- The `sr` transform function returns `Number(opts["sampleRate"] ?? sr)`, so when no option is provided the current sample rate is preserved.
|
||||
- Because only segment counts are adjusted, the actual audio quality depends on the resampling algorithm used by the downstream renderer; this operation is purely a planning directive.
|
||||
79
docs/media/audio/audio-core/src/registry/built-in/reverse.md
Normal file
79
docs/media/audio/audio-core/src/registry/built-in/reverse.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# reverse
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerReverse` adds the `reverse` operation to an `OpRegistry`. Reverse is a planning function that flips the order of segments so that the last segment plays first and the first segment plays last. It then recomputes `to` offsets to create a contiguous timeline. Like all planning operations, it does not touch PCM buffers; it merely reorders metadata.
|
||||
|
||||
## Features
|
||||
|
||||
- Reverses segment order non-destructively
|
||||
- Recomputes `to` offsets for contiguous playback
|
||||
- Preserves all segment metadata including `ref` and `rate`
|
||||
- Requires no options
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const reverse = registry.get("reverse")!;
|
||||
|
||||
const segments = [
|
||||
{ from: 0, count: 10000, to: 0 },
|
||||
{ from: 0, count: 20000, to: 10000 },
|
||||
];
|
||||
|
||||
const context = {
|
||||
totalLen: 30000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 30000,
|
||||
opts: {},
|
||||
};
|
||||
|
||||
const result = reverse.plan!(segments, context);
|
||||
// Segments are now in opposite order with corrected to offsets
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const reverse = registry.get("reverse")!;
|
||||
const trim = registry.get("trim")!;
|
||||
|
||||
const segments = [{ from: 0, count: 50000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 50000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 50000,
|
||||
opts: { start: 10000, end: 40000 },
|
||||
};
|
||||
|
||||
const trimmed = trim.plan!(segments, context);
|
||||
const result = reverse.plan!(trimmed, { ...context, totalLen: 30000, length: 30000, opts: {} });
|
||||
// Reverses only the middle section after trimming
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerReverse(registry: OpRegistry): void`
|
||||
|
||||
Registers the `reverse` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plan clones the input segments with the spread operator, reverses the cloned array, and then performs a second pass to set `to` to the cumulative sum of all previous segment counts.
|
||||
- Because the operation works on segments rather than individual samples, the perceived reversal applies at the granularity of segments; if a single segment represents the entire audio, the render stage must still support reverse reads.
|
||||
- The function is stateless and does not inspect `ctx.opts`, so any supplied options are ignored.
|
||||
73
docs/media/audio/audio-core/src/registry/built-in/speed.md
Normal file
73
docs/media/audio/audio-core/src/registry/built-in/speed.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# speed
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerSpeed` installs the `speed` operation into an `OpRegistry`. Speed is a planning function that changes playback speed (and therefore pitch) by scaling each segment's playback rate and adjusting segment counts to reflect the new duration. It also updates the document sample rate so that downstream rendering produces the correct number of output samples for the new speed.
|
||||
|
||||
## Features
|
||||
|
||||
- Scales playback rate and segment duration in a single operation
|
||||
- Updates sample rate via the `sr` transform to keep render math consistent
|
||||
- Default rate of `1` produces a no-op
|
||||
- Supports any positive rate factor
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const speed = registry.get("speed")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { rate: 2 },
|
||||
};
|
||||
|
||||
const result = speed.plan!(segments, context);
|
||||
// count halved, rate doubled
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const speed = registry.get("speed")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { rate: 0.5 },
|
||||
};
|
||||
|
||||
const result = speed.plan!(segments, context);
|
||||
// count doubled, rate halved
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerSpeed(registry: OpRegistry): void`
|
||||
|
||||
Registers the `speed` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `plan` function multiplies `seg.rate` by the supplied rate and divides `seg.count` by the same factor, rounding to the nearest integer with `Math.max(0, Math.round(...))`.
|
||||
- The `sr` transform multiplies the current sample rate by the rate factor, which means the render pipeline will generate more or fewer samples per second as appropriate.
|
||||
- Because both rate and sample rate are adjusted, the pitch shift is proportional to the speed change; this operation does not provide pitch-preserving time stretching.
|
||||
77
docs/media/audio/audio-core/src/registry/built-in/stretch.md
Normal file
77
docs/media/audio/audio-core/src/registry/built-in/stretch.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# stretch
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerStretch` adds the `stretch` operation to an `OpRegistry`. Stretch is a planning function that changes the duration of the timeline by scaling segment counts while leaving playback rates and sample rates untouched. This makes it distinct from `speed`: stretch alters time without affecting pitch, assuming the downstream renderer performs the necessary interpolation or resampling. It is commonly used when aligning audio to video timelines or when creating slow-motion effects.
|
||||
|
||||
## Features
|
||||
|
||||
- Time-scales by a configurable factor (default `1`)
|
||||
- Leaves `rate` and `sampleRate` unchanged
|
||||
- Rounds segment counts to the nearest integer
|
||||
- Non-destructive: only metadata counts are modified
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const stretch = registry.get("stretch")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { factor: 2 },
|
||||
};
|
||||
|
||||
const result = stretch.plan!(segments, context);
|
||||
// count doubled to 96000
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const stretch = registry.get("stretch")!;
|
||||
|
||||
const segments = [
|
||||
{ from: 0, count: 10000, to: 0 },
|
||||
{ from: 0, count: 20000, to: 10000 },
|
||||
];
|
||||
|
||||
const context = {
|
||||
totalLen: 30000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 30000,
|
||||
opts: { factor: 1.5 },
|
||||
};
|
||||
|
||||
const result = stretch.plan!(segments, context);
|
||||
// counts scaled by 1.5 and rounded
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerStretch(registry: OpRegistry): void`
|
||||
|
||||
Registers the `stretch` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plan multiplies each segment's `count` by the factor and rounds with `Math.max(0, Math.round(...))`.
|
||||
- Unlike `speed`, there is no `sr` or `ch` transform, so the document sample rate and channel count remain unchanged.
|
||||
- Because segment counts change but playback rates do not, the render engine must compensate by reading samples at a different effective speed; this operation alone does not implement high-quality time-stretching algorithms.
|
||||
@@ -0,0 +1,76 @@
|
||||
# transform
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerTransform` installs the `transform` operation into an `OpRegistry`. Transform is a block-level processor that applies an arbitrary per-sample function to every channel. It is the escape hatch for custom effects that are not covered by the built-in operations: waveshapers, bit crushers, custom compressors, or any other sample-wise math. If no function is supplied, the operation copies input to output unchanged.
|
||||
|
||||
## Features
|
||||
|
||||
- Applies a user-provided `(sample: number) => number` function per sample
|
||||
- Runs on every channel independently
|
||||
- Pass-through when `fn` is omitted
|
||||
- Zero-allocation block processing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const transform = registry.get("transform")!;
|
||||
|
||||
const input = [new Float32Array([0.5, 1.2, -0.8])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: {
|
||||
fn: (s: number) => Math.max(-1, Math.min(1, s)), // hard clip
|
||||
},
|
||||
};
|
||||
|
||||
transform.process!(input, output, ctx);
|
||||
// output: [0.5, 1.0, -0.8]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const transform = registry.get("transform")!;
|
||||
|
||||
const input = [new Float32Array([0.1, 0.2, 0.3])];
|
||||
const output = [new Float32Array(3)];
|
||||
const ctx = {
|
||||
blockOffsetSamples: 0,
|
||||
sampleRate: 48000,
|
||||
channels: 1,
|
||||
opts: {
|
||||
fn: (s: number) => Math.sin(s * Math.PI), // sine waveshaper
|
||||
},
|
||||
};
|
||||
|
||||
transform.process!(input, output, ctx);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerTransform(registry: OpRegistry): void`
|
||||
|
||||
Registers the `transform` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The processor first checks whether `ctx.opts["fn"]` is a function. If it is missing, it copies each channel's input buffer into the corresponding output buffer with `dst.set(src)`.
|
||||
- When a function is present, it iterates over every sample and writes `fn(src[i])` into the output buffer. There is no clipping or range validation; the provided function is responsible for producing valid floating-point samples.
|
||||
- Because the function is invoked per sample, complex transforms can be CPU-intensive; for heavy processing it may be preferable to pre-compute lookup tables or use a dedicated processor.
|
||||
78
docs/media/audio/audio-core/src/registry/built-in/trim.md
Normal file
78
docs/media/audio/audio-core/src/registry/built-in/trim.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# trim
|
||||
|
||||
## Purpose
|
||||
|
||||
`registerTrim` installs the `trim` operation into an `OpRegistry`. Trim is a planning function that shortens the audio timeline by keeping only the samples that fall within a specified `[start, end)` range. It is functionally identical to `crop` in the current implementation, providing an alternative name that may feel more natural in editing contexts where "trim" is the conventional term for removing head and tail sections.
|
||||
|
||||
## Features
|
||||
|
||||
- Sample-accurate, non-destructive trimming at the segment level
|
||||
- Preserves `ref`, `rate`, and `interp` fields on surviving segments
|
||||
- Correctly handles trims that span multiple contiguous segments
|
||||
- Sensible defaults: `start` falls back to `0` and `end` falls back to `ctx.totalLen`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const trim = registry.get("trim")!;
|
||||
|
||||
const segments = [{ from: 0, count: 48000, to: 0 }];
|
||||
const context = {
|
||||
totalLen: 48000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 48000,
|
||||
opts: { start: 8000, end: 40000 },
|
||||
};
|
||||
|
||||
const result = trim.plan!(segments, context);
|
||||
// result: [{ from: 8000, count: 32000, to: 8000 }]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const trim = registry.get("trim")!;
|
||||
|
||||
const segments = [
|
||||
{ from: 0, count: 15000, to: 0 },
|
||||
{ from: 0, count: 15000, to: 15000 },
|
||||
];
|
||||
|
||||
const context = {
|
||||
totalLen: 30000,
|
||||
sampleRate: 48000,
|
||||
offset: 0,
|
||||
length: 30000,
|
||||
opts: { start: 5000, end: 25000 },
|
||||
};
|
||||
|
||||
const result = trim.plan!(segments, context);
|
||||
// Trims across the boundary between the two original segments
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerTrim(registry: OpRegistry): void`
|
||||
|
||||
Registers the `trim` audio operation into the supplied registry.
|
||||
|
||||
- **Parameters**
|
||||
- `registry` — `OpRegistry` instance that will own the operation.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Duplicate-name behavior is governed by the registry implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The underlying `plan` function is a `PlanFn` that receives `Segment[]` and `PlanContext`.
|
||||
- It walks segments in order, tracking cumulative sample positions. Segments that lie completely outside the trim window are discarded.
|
||||
- For a partially overlapped segment, the function computes the overlap start relative to the segment, shifts `seg.from` forward by that amount, reduces `seg.count` to the overlap length, and updates `seg.to` to maintain the output timeline.
|
||||
- Because `trim` and `crop` share the same implementation in the current version, consumers may use whichever name best matches their domain vocabulary.
|
||||
110
docs/media/audio/audio-core/src/registry/op-registry.md
Normal file
110
docs/media/audio/audio-core/src/registry/op-registry.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# OpRegistry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`OpRegistry` is the central, in-memory catalog for all audio operations in `@suaveplan/audio-core`. It maps unique string names to `AudioOpDescriptor` objects that encapsulate planning functions, processing functions, resolution logic, and channel or sample-rate transforms. By keeping the registry separate from the engine, consumers can add custom operations, remove built-ins they do not need, or swap implementations without modifying the core plan or process pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- **Register** any `AudioOpDescriptor` by name at runtime
|
||||
- **Unregister** operations to restrict the available surface area
|
||||
- **Lookup** a descriptor by name for use in `PlanEngine` or `ProcessPipeline`
|
||||
- **List** all registered descriptors in insertion order
|
||||
- **Factory method** `createWithDefaults` that pre-loads all 17 built-in operations
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = new OpRegistry();
|
||||
registry.register({
|
||||
name: "invert",
|
||||
process: (input, output) => {
|
||||
for (let ch = 0; ch < input.length; ch++) {
|
||||
const src = input[ch];
|
||||
const dst = output[ch];
|
||||
if (!src || !dst) continue;
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
dst[i] = -src[i]!;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const desc = registry.get("invert");
|
||||
console.log(desc?.name); // "invert"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const names = registry.list().map((d) => d.name);
|
||||
console.log(names); // ["trim", "crop", "speed", ...]
|
||||
|
||||
registry.unregister("dither");
|
||||
console.log(registry.get("dither")); // undefined
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class OpRegistry`
|
||||
|
||||
In-memory registry for `AudioOpDescriptor` instances.
|
||||
|
||||
#### `register(descriptor: AudioOpDescriptor): void`
|
||||
|
||||
Adds or overwrites an operation in the registry.
|
||||
|
||||
- **Parameters**
|
||||
- `descriptor` — `AudioOpDescriptor` containing at least a unique `name`.
|
||||
- **Returns**
|
||||
- `void`
|
||||
- **Throws**
|
||||
- Does not throw directly. Overwrites any existing entry with the same name.
|
||||
|
||||
#### `unregister(name: string): void`
|
||||
|
||||
Removes an operation from the registry.
|
||||
|
||||
- **Parameters**
|
||||
- `name` — operation name to remove.
|
||||
- **Returns**
|
||||
- `void`
|
||||
|
||||
#### `get(name: string): AudioOpDescriptor | undefined`
|
||||
|
||||
Retrieves a descriptor by name.
|
||||
|
||||
- **Parameters**
|
||||
- `name` — operation name to look up.
|
||||
- **Returns**
|
||||
- The matching `AudioOpDescriptor`, or `undefined` if the name is not present.
|
||||
|
||||
#### `list(): AudioOpDescriptor[]`
|
||||
|
||||
Returns every registered descriptor.
|
||||
|
||||
- **Returns**
|
||||
- An array of all descriptors. Order follows insertion order because the backing store is a `Map`.
|
||||
|
||||
#### `static createWithDefaults(): OpRegistry`
|
||||
|
||||
Factory that returns a new registry pre-populated with the built-in operations.
|
||||
|
||||
- **Returns**
|
||||
- A new `OpRegistry` instance containing `trim`, `crop`, `speed`, `stretch`, `reverse`, `insert`, `remove`, `repeat`, `pad`, `resample`, `gain`, `normalize`, `fade`, `pan`, `mix`, `dither`, and `transform`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry uses a private `Map<string, AudioOpDescriptor>` for storage, which guarantees `O(1)` lookups and preserves insertion order when iterating.
|
||||
- Because `register` overwrites on duplicate names, consumers can monkey-patch or upgrade an operation without creating a new registry.
|
||||
- `createWithDefaults` is the recommended entry point for most applications; it invokes every `registerXxx` function exported from `src/registry/built-in/`.
|
||||
- The class has no asynchronous methods and no runtime dependencies beyond the TypeScript types, so it can be instantiated in browsers, Node.js, or worker threads with identical behavior.
|
||||
66
docs/media/audio/audio-core/src/render/render.md
Normal file
66
docs/media/audio/audio-core/src/render/render.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Render
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The render module bridges the compiled plan produced by `PlanEngine` and the paged sample storage of an `AudioDocument`. It exposes a single function, `renderBlock`, which materialises a contiguous chunk of audio samples into planar `Float32Array` buffers, respecting the bounds of the compiled plan.
|
||||
|
||||
## Features
|
||||
|
||||
- **Plan-aware rendering** — Reads samples through the document’s page structure while honouring the total length declared by a `CompiledPlan`.
|
||||
- **Automatic bounds clamping** — Requests that exceed the plan length are silently truncated rather than throwing.
|
||||
- **Zero-allocation empty blocks** — When the requested offset is at or beyond the plan end, the function returns empty arrays without allocating sample memory.
|
||||
- **Delegated page reads** — Internally reuses `readPages` so that page traversal, overlap handling, and channel copying remain consistent with the rest of the package.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { renderBlock } from "@suaveplan/audio-core";
|
||||
|
||||
// Render 2048 samples starting at sample 0.
|
||||
const block = renderBlock(document, plan, 0, 2048);
|
||||
// block is Float32Array[] with one array per channel.
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { renderBlock } from "@suaveplan/audio-core";
|
||||
|
||||
// Render the final partial block, which may be shorter than BLOCK_SIZE.
|
||||
const blockSize = 1024;
|
||||
const offset = plan.totalLen - blockSize;
|
||||
const block = renderBlock(document, plan, offset, blockSize);
|
||||
|
||||
console.log(block[0].length); // <= blockSize
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `renderBlock(doc, plan, offset, duration): Float32Array[]`
|
||||
|
||||
Reads a contiguous range of samples from the document, clamped to the plan’s total length.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `doc` — `AudioDocument` containing the paged source data.
|
||||
- `plan` — `CompiledPlan` that defines `totalLen`, the authoritative sample count for the rendered output.
|
||||
- `offset` — Start sample index.
|
||||
- `duration` — Requested number of samples.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `Float32Array[]` with one array per channel. Each array’s length equals `min(duration, plan.totalLen - offset)`. If `offset` is at or beyond `plan.totalLen`, each array has length `0`.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- Does not throw for out-of-range offsets; instead returns empty buffers. Underlying page-read errors are possible if the document contains malformed pages.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `renderBlock` delegates directly to `readPages` after computing `count = Math.min(duration, plan.totalLen - offset)`. This centralises page-walking logic in a single module and avoids duplicating overlap-handling code.
|
||||
- The function does not interact with `ProcessPipeline`; it is intended for raw sample extraction. If you need to apply edits or effects, use `AudioDocument.prototype.stream` or `createPlanTransformStream` instead.
|
||||
- Because the return value is always a new array of `Float32Array` instances, repeated small calls to `renderBlock` will allocate frequently. For low-latency or real-time paths, prefer streaming APIs that reuse block buffers.
|
||||
72
docs/media/audio/audio-core/src/stats/stats.md
Normal file
72
docs/media/audio/audio-core/src/stats/stats.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Stats
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module computes standard audio statistics across paged channel data. By scanning every sample in every page, it produces peak amplitude, root-mean-square (RMS) level, and DC offset values that are useful for normalisation, metering, and quality checks.
|
||||
|
||||
## Features
|
||||
|
||||
- **Peak detection** — Reports the maximum absolute sample value.
|
||||
- **RMS calculation** — Computes the quadratic mean across all samples.
|
||||
- **DC offset** — Returns the arithmetic mean, revealing unwanted DC bias.
|
||||
- **Paged iteration** — Operates directly on `AudioPage` arrays without requiring a contiguous buffer.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { computeStats } from "@suaveplan/audio-core";
|
||||
|
||||
const pages = document.pages.filter((p) => p !== null);
|
||||
const stats = computeStats(pages);
|
||||
|
||||
console.log(stats.peak); // 0.0 – 1.0 (or higher if inter-sample peaks exist)
|
||||
console.log(stats.rms); // average loudness
|
||||
console.log(stats.dcOffset); // should be near 0.0 for clean recordings
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { computeStats } from "@suaveplan/audio-core";
|
||||
|
||||
// Compute stats for only the first 10 pages.
|
||||
const subset = document.pages.slice(0, 10).filter((p) => p !== null);
|
||||
const stats = computeStats(subset);
|
||||
|
||||
// Use RMS to drive a normalisation gain decision.
|
||||
const targetRms = 0.1;
|
||||
const gainDb = 20 * Math.log10(targetRms / (stats.rms || 1));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `computeStats(pages: AudioPage[]): ComputedStats`
|
||||
|
||||
Iterates every channel in every page, accumulating the absolute peak, sum of squares, and arithmetic sum. From these accumulators it derives RMS and DC offset.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `pages` — Array of `AudioPage` objects. Null entries should be filtered out by the caller; the function does not skip them internally.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `ComputedStats` object containing:
|
||||
- `peak: number` — Maximum absolute sample value.
|
||||
- `rms: number` — Root-mean-square of all samples (`sqrt(sumSq / count)`), or `0` when `count` is `0`.
|
||||
- `dcOffset: number` — Arithmetic mean (`sum / count`), or `0` when `count` is `0`.
|
||||
- `lufs?: number` — Optional field from the `ComputedStats` interface; not populated by this implementation.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- Does not throw under normal circumstances. If `pages` contains entries with malformed `channelData`, a runtime error may occur during array access.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The function performs a single linear pass over all samples, keeping three scalar accumulators (`peak`, `sumSq`, `sum`) and a sample counter. This gives it O(n) time complexity where n is the total number of samples across all pages and channels.
|
||||
- No intermediate buffers are allocated; the implementation reads directly from the `Float32Array` instances stored in each `AudioPage`.
|
||||
- The `ComputedStats` interface extends `AudioStats` and reserves an optional `lufs` field for future loudness measurement integrations. Current callers should not expect `lufs` to be present.
|
||||
- Because the scan is exhaustive, calling `computeStats` on a very large document can be CPU-intensive. For real-time metering, consider sampling a subset of pages or running the computation inside a worker.
|
||||
82
docs/media/audio/audio-core/src/stream/stream.md
Normal file
82
docs/media/audio/audio-core/src/stream/stream.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Stream
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module adapts the `@suaveplan/audio-core` processing pipeline to the Web Streams standard. It exports a factory function that builds a `TransformStream`, allowing audio data to flow through a `ProcessPipeline` as part of a larger streams graph, such as piping from a `ReadableStream` of decoded blocks into a media encoder or audio context.
|
||||
|
||||
## Features
|
||||
|
||||
- **Web Streams compatibility** — Returns a native `TransformStream<Float32Array[], Float32Array[]>` that can be used with `pipeThrough`.
|
||||
- **Plan-bound processing** — Initialises the `ProcessPipeline` from a `CompiledPlan` so that every chunk receives the correct effects, gain, and sample-rate logic.
|
||||
- **Stateful block tracking** — Maintains a running `blockOffset` so that time-dependent plugins receive accurate sample positions.
|
||||
- **Zero-copy output allocation** — Allocates a fresh `Float32Array` per channel for each chunk, leaving input arrays untouched.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createPlanTransformStream } from "@suaveplan/audio-core";
|
||||
|
||||
const transform = createPlanTransformStream(document, plan, registry);
|
||||
const reader = source.pipeThrough(transform).getReader();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
// value is Float32Array[] per channel
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createPlanTransformStream } from "@suaveplan/audio-core";
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const engine = new PlanEngine(registry);
|
||||
const compiled = engine.compile(document, false);
|
||||
|
||||
const transform = createPlanTransformStream(document, compiled, registry);
|
||||
|
||||
await source
|
||||
.pipeThrough(transform)
|
||||
.pipeTo(new WritableStream({
|
||||
write(block) {
|
||||
// Push each processed block to the audio worklet or encoder.
|
||||
worklet.port.postMessage(block, block.map((b) => b.buffer));
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createPlanTransformStream(doc, plan, registry): TransformStream<Float32Array[], Float32Array[]>`
|
||||
|
||||
Constructs a `TransformStream` that initialises a `ProcessPipeline` from the supplied plan and registry, then runs every incoming chunk through the pipeline.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `doc` — `AudioDocument` that provides channel count and context.
|
||||
- `plan` — `CompiledPlan` containing the pipeline definition, total length, and sample rate.
|
||||
- `registry` — `OpRegistry` that resolves operation types to processing functions.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- A `TransformStream<Float32Array[], Float32Array[]>`.
|
||||
- `writable` accepts planar `Float32Array[]` chunks.
|
||||
- `readable` emits processed planar `Float32Array[]` chunks of the same length.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- Does not throw during construction. Runtime errors during `transform` are propagated as stream errors. Plugin failures or invalid pipeline state will surface through the stream’s error handling mechanism.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `TransformStream` constructor captures `doc`, `plan`, and `registry` in its closure. Inside the `transform` callback, a new output `Float32Array` is allocated for every channel of the incoming chunk. This preserves the input data so that upstream streams can reuse their buffers if desired.
|
||||
- `blockOffset` is advanced by `input[0]?.length ?? 0` after each chunk. This assumes all chunks are uniform in length and that every channel array in a chunk has the same length. The pipeline relies on this offset for time-based calculations such as LFO phase or envelope position.
|
||||
- The `flush` method is empty; the readable side closes automatically when the writable side is closed or aborted. Consumers that need teardown logic (for example, flushing a reverb tail) should extend the pipeline or wrap the stream with additional handlers.
|
||||
- This module imports `ProcessPipeline` and `OpRegistry` from sibling modules, but it does not depend on DOM-specific APIs beyond the standard `TransformStream` constructor, so it works in modern browsers, Node 18+, and Deno.
|
||||
90
docs/media/audio/audio-core/src/telemetry/telemetry.md
Normal file
90
docs/media/audio/audio-core/src/telemetry/telemetry.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# telemetry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The telemetry module is the single OpenTelemetry integration point for
|
||||
`@suaveplan/audio-core`. It exposes thin, lazy helpers over `@opentelemetry/api`
|
||||
so the data-model and processing entry points — plan compilation, block
|
||||
processing, and streaming reads — can be traced and measured without scattering
|
||||
observability boilerplate through the codebase. Every helper resolves the active
|
||||
tracer or meter on each call, so instrumentation is completely inert when no
|
||||
exporter is registered and never runs as an import-time side effect.
|
||||
|
||||
## Features
|
||||
|
||||
- Named span constants for the three instrumented entry points: `plan.compile`,
|
||||
`process.block`, and `stream.read`.
|
||||
- Named metric constants for the two counters (`audio_core_edits_total`,
|
||||
`audio_core_stream_blocks_total`) and two histograms
|
||||
(`audio_core_compile_duration_ms`, `audio_core_process_duration_ms`).
|
||||
- `startSpan` returns a real `Span` the caller ends in a `finally` block.
|
||||
- `recordCompile`, `recordProcessBlock`, and `recordStreamBlock` push metric
|
||||
values through the globally-registered meter, falling back to the API's
|
||||
no-op meter when none is registered.
|
||||
- `now()` wraps `performance.now()` as the universal monotonic clock used for
|
||||
duration measurement — no Node or browser globals leak into callers.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { now, recordCompile, SPAN_PLAN_COMPILE, startSpan } from "./telemetry.js";
|
||||
|
||||
const span = startSpan(SPAN_PLAN_COMPILE);
|
||||
const started = now();
|
||||
try {
|
||||
// ...compile work...
|
||||
} finally {
|
||||
recordCompile(3, now() - started);
|
||||
span.end();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { recordStreamBlock, SPAN_STREAM_READ, startSpan } from "./telemetry.js";
|
||||
|
||||
async function* instrumentedStream(
|
||||
blocks: AsyncGenerator<Float32Array[]>
|
||||
): AsyncGenerator<Float32Array[]> {
|
||||
const span = startSpan(SPAN_STREAM_READ);
|
||||
try {
|
||||
for await (const block of blocks) {
|
||||
recordStreamBlock();
|
||||
yield block;
|
||||
}
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `startSpan(name: string): Span` — starts a span on the package tracer.
|
||||
- `recordCompile(editCount: number, durationMs: number): void` — increments
|
||||
`audio_core_edits_total` and records `audio_core_compile_duration_ms`.
|
||||
- `recordProcessBlock(durationMs: number): void` — records
|
||||
`audio_core_process_duration_ms`.
|
||||
- `recordStreamBlock(count?: number): void` — increments
|
||||
`audio_core_stream_blocks_total` (default `1`).
|
||||
- `now(): number` — monotonic millisecond timestamp via `performance.now()`.
|
||||
- Span name constants: `SPAN_PLAN_COMPILE`, `SPAN_PROCESS_BLOCK`,
|
||||
`SPAN_STREAM_READ`.
|
||||
- Metric name constants: `COUNTER_EDITS`, `COUNTER_STREAM_BLOCKS`,
|
||||
`HISTOGRAM_COMPILE_DURATION`, `HISTOGRAM_PROCESS_DURATION`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Helpers deliberately do **not** memoise the tracer or meter. `@opentelemetry/api`
|
||||
returns lightweight proxies that delegate to whichever provider is registered at
|
||||
call time, so re-resolving per call costs nothing meaningful and keeps the module
|
||||
correct when a provider is registered after the package was first imported —
|
||||
which is the normal order in host applications and in tests. Creating a counter
|
||||
or histogram by name is idempotent under a real SDK (instruments are deduplicated
|
||||
by name) and a no-op under the default API meter. Because nothing executes at
|
||||
module load, importing `@suaveplan/audio-core` mutates no global state.
|
||||
Reference in New Issue
Block a user