Files
2026-05-27 23:29:53 +01:00

441 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenSpec Change: enterprise-audio-core
- **Change ID**: enterprise-audio-core
- **Package**: `@suaveplan/audio-core`
- **Category**: universal
- **Target path**: `packages/audio/audio-core/`
- **Status**: PROPOSED
- **Date**: 2026-05-14
- **Design**: `./design.md` (also carries the cross-cutting `@suaveplan/audio-*` ecosystem architecture)
- **Spec delta**: `./specs/audio-core/spec.md`
---
## Summary
Deliver `@suaveplan/audio-core` on the genesis branch from scratch at enterprise quality. The package provides the universal audio data model and processing primitives for the genesis monorepo: a paged audio container (`AudioDocument`), non-destructive edit compilation (`PlanEngine`), block-level DSP pipeline (`ProcessPipeline`), and typed operation registry (`OpRegistry`). Zero browser or server dependencies — runs identically in Bun, Node.js, Deno, and browsers. This is the lowest-level audio package in the ecosystem; every other audio package depends on it.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/error` (`workspace:^`) | Base class for `AudioError`; `AUDIO_CORE.*` codes |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `IdGenerator`, `Rng` |
| `@suaveplan/events` (`workspace:^`) | Typed event bus for `AudioDocument` lifecycle events |
| `@suaveplan/streams` (`workspace:^`) | WHATWG `TransformStream` for audio stream transforms |
| `@suaveplan/serialize` (`workspace:^`) | Structured serialization for `AudioDocument.toJSON()` |
| `@suaveplan/audio-metadata` (`workspace:^`) | Supplies the `AudioMetadata` type carried on `AudioDocument.metadata` (type-only consumption) |
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on compile, process, and stream ops |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
---
## Public API
### Core types
```ts
class AudioError extends SuaveplanError
interface AudioDocumentOptions {
id?: string
sampleRate: number
channels: number
sourceLength: number
pages?: AudioPage[]
edits?: AudioEdit[]
source?: string | URL
metadata?: AudioMetadata
}
interface AudioPage {
readonly channelData: Float32Array[]
readonly startSample: number
}
interface AudioEdit {
type: string
opts: Record<string, unknown>
}
interface AudioStats {
peak: number
rms: number
lufs?: number
dcOffset: number
}
interface StreamOptions {
blockSize?: number
startSample?: number
endSample?: number
}
interface AudioDocumentEventMap {
'edit': (e: { edit: AudioEdit; version: number }) => void
'undo': (e: { edit: AudioEdit; version: number }) => void
'plugin:error': (e: { plugin: string; error: unknown }) => void
'page:loaded': (e: { page: AudioPage }) => void
}
class AudioDocument {
constructor(opts: AudioDocumentOptions)
readonly id: string
readonly pages: AudioPage[]
readonly sampleRate: number
readonly channels: number
readonly sourceLength: number
get length(): number
get duration(): number
edits: AudioEdit[]
version: number
stats?: AudioStats
source?: string | URL
metadata?: AudioMetadata
run(...edits: AudioEdit[]): this
undo(n?: number): AudioEdit | AudioEdit[] | null
clone(): AudioDocument
toJSON(): AudioDocumentJSON
read(offset?: number, duration?: number): Promise<Float32Array[]>
stream(opts?: StreamOptions): AsyncGenerator<Float32Array[]>
push(data: Float32Array[] | Float32Array, fmt?: PCMFormat): this
stop(): this
on<K extends keyof AudioDocumentEventMap>(event: K, listener: AudioDocumentEventMap[K]): void
off<K extends keyof AudioDocumentEventMap>(event: K, listener: AudioDocumentEventMap[K]): void
}
```
### Plan engine
```ts
interface CompiledPlan {
segs: Segment[]
pipeline: AudioEdit[]
totalLen: number
sampleRate: number
limit: number
}
class PlanEngine {
constructor(registry: OpRegistry)
compile(doc: AudioDocument, final?: boolean): CompiledPlan
readPlan(doc: AudioDocument, plan: CompiledPlan, offset?: number, duration?: number): Float32Array[]
streamPlan(doc: AudioDocument, plan: CompiledPlan, opts?: StreamOptions): AsyncGenerator<Float32Array[]>
ensurePages(doc: AudioDocument, plan: CompiledPlan, offset?: number, duration?: number): Promise<void>
}
interface Segment {
from: number // source sample start
count: number // source sample count
to: number // output sample start
rate?: number // playback rate (1 = normal)
ref?: AudioDocument | null // external source reference (null = silence)
interp?: Interpolator // custom resampler for non-unity rate
}
interface PlanContext {
totalLen: number
sampleRate: number
offset: number
length: number
opts: Record<string, unknown>
}
type PlanFn = (segs: Segment[], ctx: PlanContext) => Segment[]
type ResolveFn = (ctx: { stats?: AudioStats; sampleRate: number; channels: number; opts: Record<string, unknown> }) => AudioEdit[] | false | void
type SampleRateTransformFn = (sr: number, opts: Record<string, unknown>) => number | void
type ChannelTransformFn = (ch: number, opts: Record<string, unknown>) => number | void
```
### Process pipeline
```ts
interface ProcessState {
op: AudioOpDescriptor
params: Record<string, unknown>
buffers: Float32Array[]
}
interface BlockContext {
blockOffsetSamples: number
sampleRate: number
channels: number
}
class ProcessPipeline {
constructor(registry: OpRegistry)
init(pipeline: AudioEdit[], totalDuration: number, sampleRate: number, channels: number): ProcessState[]
patch(state: ProcessState[], pipeline: AudioEdit[]): void
process(
input: Float32Array[],
output: Float32Array[],
state: ProcessState[],
blockOffsetSamples: number,
sampleRate: number
): Float32Array[]
}
```
### Operation registry
```ts
interface AudioOpDescriptor {
readonly name: string
readonly params?: string[]
readonly resolve?: ResolveFn
readonly plan?: PlanFn
readonly process?: ProcessFn
readonly sr?: SampleRateTransformFn
readonly ch?: ChannelTransformFn
}
type ProcessFn = (
input: Float32Array[],
output: Float32Array[],
ctx: BlockContext
) => void
class OpRegistry {
register(descriptor: AudioOpDescriptor): void
unregister(name: string): void
get(name: string): AudioOpDescriptor | undefined
list(): AudioOpDescriptor[]
static createWithDefaults(): OpRegistry
}
```
### Block processor interface
```ts
interface ProcessorConfig {
sampleRate: number
channels: number
blockSize: number
}
interface BlockProcessor {
readonly name: string
setup(config: ProcessorConfig): void
process(input: Float32Array[], output: Float32Array[], ctx: BlockContext): void
reset(): void
dispose(): void
}
```
### Paged memory primitives
```ts
const PAGE_SIZE = 1024 * 1024 // samples (~23MB stereo @ 48kHz)
const BLOCK_SIZE = 1024 // samples per process block
function walkPages(
doc: AudioDocument,
channel: number,
startSample: number,
count: number,
visitor: (page: AudioPage, offset: number, length: number) => void
): void
function copyPages(
doc: AudioDocument,
channel: number,
startSample: number,
count: number,
target: Float32Array,
targetOffset: number
): void
function readPages(doc: AudioDocument, offset?: number, duration?: number): Float32Array[]
```
### Built-in operations
Plan ops: `trim`, `crop`, `speed`, `stretch`, `reverse`, `insert`, `remove`, `repeat`, `pad`, `resample`.
Process ops: `gain`, `normalize`, `fade`, `pan`, `mix`, `dither`, `transform`.
### Error codes
```ts
type AudioCoreErrorCode =
| 'AUDIO_PLUGIN_NOT_FOUND'
| 'AUDIO_INVALID_EDIT'
| 'AUDIO_STREAM_ABORTED'
| 'AUDIO_PAGE_OUT_OF_RANGE'
| 'AUDIO_PROCESS_ERROR'
const AUDIO_CORE_ERROR_METADATA: Readonly<Record<AudioCoreErrorCode, CodeMetadata>>
function registerAudioCoreErrorCodes(): void
```
### Zod schemas
```ts
AudioDocumentOptionsSchema
AudioPageSchema
AudioEditSchema
AudioStatsSchema
StreamOptionsSchema
CompiledPlanSchema
AudioOpDescriptorSchema
ProcessorConfigSchema
```
---
## New additions for genesis
1. **Zod boundary validation** — every public options object validated at entry.
2. **OTel instrumentation** — spans on `plan.compile`, `process.block`, `stream.read`; counters for `audio_core_edits_total`, `audio_core_stream_blocks_total`; histograms for `audio_core_compile_duration_ms`, `audio_core_process_duration_ms`.
3. **Error isolation** — crashing process ops emit `plugin:error` and pass input through unmodified without corrupting the stream.
4. **AbortSignal propagation** — threaded through `read()`, `stream()`, `ensurePages()`.
5. **Page pool allocator** — reusable `Float32Array` pages to reduce GC pressure during long streams.
6. **Property-based tests** — plan compilation is deterministic and reversible for identity edits; stream yields correct total sample count; clone produces equivalent `toJSON()`.
7. **Benchmark suite** — `plan.bench.ts` (100 edits × 1000 iterations), `stream.bench.ts` (5 min stereo through 10 ops), `process.bench.ts` (normalize + fade + pan over 1M samples).
---
## File layout
```text
packages/audio/audio-core/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
document/
document.ts
document.md
document.types.ts
document.test.ts
index.ts
page/
page.ts
page.md
page.types.ts
page.test.ts
index.ts
plan/
plan-engine.ts
plan-engine.md
plan-engine.types.ts
plan-engine.test.ts
segment.ts
segment.md
segment.types.ts
segment.test.ts
index.ts
process/
process-pipeline.ts
process-pipeline.md
process-pipeline.types.ts
process-pipeline.test.ts
block-processor.ts
block-processor.md
block-processor.types.ts
block-processor.test.ts
index.ts
registry/
op-registry.ts
op-registry.md
op-registry.types.ts
op-registry.test.ts
built-in/
trim.ts
crop.ts
speed.ts
stretch.ts
reverse.ts
insert.ts
remove.ts
repeat.ts
pad.ts
resample.ts
gain.ts
normalize.ts
fade.ts
pan.ts
mix.ts
dither.ts
transform.ts
index.ts
stream/
stream.ts
stream.md
stream.types.ts
stream.test.ts
index.ts
render/
render.ts
render.md
render.test.ts
index.ts
stats/
stats.ts
stats.md
stats.types.ts
stats.test.ts
index.ts
__benchmarks__/
plan.bench.ts
stream.bench.ts
process.bench.ts
tests/
integration.test.ts
```
---
## Impact
**Breaking changes**: none — fresh build, package does not yet exist on genesis.
**Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/streams`, `@suaveplan/serialize`, `@suaveplan/audio-metadata` published to Verdaccio.
**No dependents may import browser or server APIs through this package.**
### Affected dependents
`@suaveplan/audio-core` is the foundation of the audio family — every other audio package depends
on it. At the time of this change none of the dependents exist on genesis yet (they land in later
changes in this same batch), so there is no existing dependent surface to test or migrate. Each
dependent is created against this package's spec delta, not retrofitted.
- `@suaveplan/audio-codecs` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created against this spec in `enterprise-audio-codecs`)
- `@suaveplan/audio-analysis-beat` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-analysis-beat`)
- `@suaveplan/audio-analysis-pitch` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-analysis-pitch`)
- `@suaveplan/audio-analysis-loudness` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-analysis-loudness`)
- `@suaveplan/audio-analysis-spectrum` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-analysis-spectrum`)
- `@suaveplan/audio-timeline` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-timeline`)
- `@suaveplan/audio-transport` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-transport`)
- `@suaveplan/audio-engine-browser` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-engine-browser`)
- `@suaveplan/audio-engine-server` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-engine-server`)
- `@suaveplan/audio-plugins-fx` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-plugins-fx`)
- `@suaveplan/audio-controllers` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-controllers`)
- `@suaveplan/audio-cli` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-cli`)
Once landed, `@suaveplan/audio-core` is a foundation package: any subsequent change to its public
surface MUST run `bunx turbo test --filter=...@suaveplan/audio-core` per `openspec/AGENTS.md` §13
step 10.