Initial Commit
This commit is contained in:
54
openspec/archive/2026-05-19-add-audio-ir-loader/proposal.md
Normal file
54
openspec/archive/2026-05-19-add-audio-ir-loader/proposal.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Add `@suaveplan/audio-ir-loader`
|
||||
|
||||
**Package**: `@suaveplan/audio-ir-loader`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
|
||||
## Why
|
||||
|
||||
`audio-ir-core` owns the contract + registry. Content packages ship manifest entries + bytes. Between the two we need the pipeline: resolve the URI through `StorageUriResolver`, decode the bytes through the right `AudioDecoder`, normalize against the manifest's declared channels/sampleRate/gainDb, and hand back a frozen `ImpulseResponse`. That pipeline is `audio-ir-loader`. Hard prereq: `@suaveplan/audio-ir-core` (registry shape), `@suaveplan/storage-uri` (resolver), `@suaveplan/audio-decode-wav` (initial decoder). Future decoders plug into the same dispatch table.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `createIrLoader(options): IrLoader` — factory. Options: `{ registry: IrRegistry; resolver: StorageUriResolver; decoders: Record<string, AudioDecoder>; cache?: { maxBytes: number } }`. `decoders` maps MIME or extension to a decoder; the loader picks the right one for each entry.
|
||||
- ADDED: `IrLoader.load(id: string, options?: { signal?: AbortSignal }): Promise<ImpulseResponse>` — resolves the entry from the registry, reads bytes via `resolver.read(entry.uri, signal)`, dispatches to the right decoder, validates against manifest declarations, freezes, returns.
|
||||
- ADDED: `IrLoader.preload(ids: readonly string[], options?: { signal?: AbortSignal; concurrency?: number }): Promise<readonly ImpulseResponse[]>` — bulk load with bounded concurrency (default 4).
|
||||
- ADDED: `IrLoader.evict(id: string): boolean` — drops a cached ImpulseResponse. Returns whether it was cached.
|
||||
- ADDED: `IrLoader.cacheStats(): { entries: number; bytes: number; hits: number; misses: number }` — observability for callers that care.
|
||||
- ADDED: `AUDIO_IR_LOADER.*` codes via `defineCodes`: `DECODER_UNAVAILABLE`, `MANIFEST_MISMATCH`, `ABORTED`, `CACHE_OVERFLOW`, `DECODE_FAILED`, `RESOLVE_FAILED`.
|
||||
- ADDED: OTel — `audio_ir_loader.load_duration_ms{family, outcome}`, `audio_ir_loader.bytes_read_total{family}`, `audio_ir_loader.cache_hits_total{family}`, `audio_ir_loader.cache_misses_total{family}`, `audio_ir_loader.errors_total{code}`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- New decoder packages — each sibling decoder (`audio-decode-flac`, `audio-decode-opus`, …) lands in its own change.
|
||||
- Cross-process / network cache — in-process LRU only; future Redis-backed cache is a separate change.
|
||||
- Sample-rate conversion — manifest declares the expected rate; mismatch throws `MANIFEST_MISMATCH`. Future `@suaveplan/audio-resample` integration is a separate change.
|
||||
- Per-package `register()` calls — each content package owns its own registration; this loader consumes a populated registry, it does not populate it.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. The cross-cutting design lives at `openspec/changes/add-audio-ir-core/design.md`.
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| every spec requirement | Genesis-original I/O pipeline |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/loader/loader.test.ts` — happy path against a fake registry + fake resolver returning canned WAV bytes.
|
||||
- `src/loader/loader.spec-conformance.test.ts` — manifest-declaration mismatch tests; resolver error pass-through; decoder error pass-through; signal threading; cache hit/miss invariants.
|
||||
- `src/loader/preload.test.ts` — bulk-load concurrency bound; partial-failure aggregation.
|
||||
- `src/cache/cache.test.ts` — LRU eviction, `CACHE_OVERFLOW` when a single IR exceeds `maxBytes`.
|
||||
- `src/otel/otel.test.ts` — counter + histogram assertions with real in-memory `MeterProvider`.
|
||||
- `src/error-codes/error-codes.test.ts` — exhaustion.
|
||||
- `tests/integration/with-bundle-fallback.test.ts` — end-to-end: registry holds a `pool-outdoor-swimming-pool` entry whose URI is a `fallback://` URI; first candidate (`bundle://`) is missing from the temp dir; second candidate (`https://`) is served by a `node:http` fixture; loader resolves through the fallback walk and decodes successfully.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
No dependents yet. Verified by `bunx turbo ls --filter=...@suaveplan/audio-ir-loader` returning only the package itself.
|
||||
|
||||
First consumers will be application packages that compose `audio-ir-loader` with one or more `audio-ir-{family}-plugin-<name>` packages; each consumer lands in its own change.
|
||||
Reference in New Issue
Block a user