11 KiB
Capability Spec Delta: audio-sprite-packer (audio-sprite-packer)
Package: @suaveplan/audio-sprite-packer
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions.
ADDED Requirements
Requirement: Universal-tier purity
The package SHALL import zero browser-only APIs (window, document, navigator, AudioContext) and zero statically-imported Node-only APIs (node:fs, node:path, process, Buffer). All audio encode/decode operations SHALL be delegated to the injectable AudioAdapter. The package's dependencies SHALL be limited to @suaveplan/error, @suaveplan/types, @suaveplan/mime, @suaveplan/crypto-utils, tslib, and zod; @opentelemetry/api is the only permitted non-workspace runtime dependency.
Scenario: Built dist references no platform globals at module scope
- WHEN the built
dist/is grep'd for top-level imports - THEN it MUST NOT statically import
window,document,navigator,AudioContext,node:fs,node:path,process, orBuffer
Scenario: Tests pass under both runtimes
- WHEN the suite runs under
bun test(server) and under vitest (browser shim) - THEN both runs pass with identical results using
NullAudioAdapter
Verified by: tests/purity.test.ts::"dist statically imports no browser or Node globals" AND tests/purity.test.ts::"dependency set is limited to the declared workspace deps and zod"
Requirement: AudioAdapter plugin contract
The package SHALL export an AudioAdapter interface carrying supportedFormats, probe(data), decode(data), encode(samples, sampleRate, channels, format), stripSilence(samples, sampleRate, thresholdDb?), and normalise(samples). The package SHALL also export NullAudioAdapter implementing the interface: probe returns configurable fixture metadata, decode returns zeros, encode returns the input re-interpreted as bytes, stripSilence and normalise return the input unchanged.
Scenario: NullAudioAdapter probes a fixture
- WHEN
NullAudioAdapter.probeis called with any Uint8Array - THEN it returns
{ format: "wav", durationSeconds: 1, sampleRate: 44100, channels: 2 }
Scenario: NullAudioAdapter round-trips decode/encode
- WHEN
NullAudioAdapter.decodeis called and the result is passed toencode - THEN the returned bytes are byte-equal to the original input
Verified by: src/adapters/null-adapter/null-adapter.test.ts::"NullAudioAdapter.probe returns fixture metadata" AND src/adapters/null-adapter/null-adapter.test.ts::"decode/encode round-trips through NullAudioAdapter"
Requirement: AudioSpritePacker pack and unpack
The package SHALL export AudioSpritePacker with constructor options: { adapter: AudioAdapter; logger?: LoggerLike; clock?: Clock } and methods pack(clips, options?, signal?) and unpack(sprite, clipName). pack SHALL validate input is non-empty, probe and decode each clip via the adapter, optionally strip silence and normalise, append silence padding, encode the concatenated PCM, build a manifest with per-clip offsets, and return { data, manifest }. unpack SHALL look up the clip name in the manifest and return a Uint8Array slice of the sprite data at the recorded offset and byteLength.
Scenario: pack three clips and unpack each
- WHEN
packis called with threeAudioClipInputitems usingNullAudioAdapter - THEN the returned manifest has three entries with monotonically increasing offsets, and
unpackfor each name returns a Uint8Array whose byteLength matches the manifest entry
Scenario: unpack for unknown clip name throws
- WHEN
unpackis called with a clip name not present in the manifest - THEN it throws
AudioSpriteErrorwhosecode === "AUDIO_SPRITE.CLIP_NOT_FOUND"
Verified by: src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"pack three clips and unpack each by name" AND src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"unpack throws AUDIO_SPRITE_CLIP_NOT_FOUND for unknown name"
Requirement: Empty input rejection
pack called with an empty clips array SHALL throw AudioSpriteError with code AUDIO_SPRITE.EMPTY_INPUT before invoking the adapter.
Scenario: empty clips array throws EMPTY_INPUT
- WHEN
packis called with[] - THEN it throws
AudioSpriteErrorwhosecode === "AUDIO_SPRITE.EMPTY_INPUT"
Verified by: src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"pack throws EMPTY_INPUT for empty clips array"
Requirement: Manifest build and validation
The package SHALL export buildManifest(entries, packedBuffer) returning an AudioSpriteManifest with version: 1, format, sampleRate, channels, totalDurationSeconds, entries, and contentHash (SHA-256 of packedBuffer). It SHALL also export validateManifest(manifest) which checks version === 1, entries array non-empty, offsets non-overlapping, and throws AudioSpriteError with code AUDIO_SPRITE.INVALID_MANIFEST on any violation.
Scenario: buildManifest computes SHA-256 content hash
- WHEN
buildManifestis called with entries and a packed buffer - THEN the manifest
contentHashis a 64-character hex string matching the SHA-256 of the buffer
Scenario: validateManifest rejects overlapping offsets
- WHEN
validateManifestis called with entries whose byte ranges overlap - THEN it throws
AudioSpriteErrorwhosecode === "AUDIO_SPRITE.INVALID_MANIFEST"
Verified by: src/utils/manifest/manifest.test.ts::"buildManifest computes a SHA-256 content hash" AND src/utils/manifest/manifest.test.ts::"validateManifest rejects overlapping byte offsets"
Requirement: AbortSignal propagation
pack SHALL accept an optional AbortSignal. An already-aborted signal SHALL cause the method to throw AudioSpriteError with code AUDIO_SPRITE.ABORTED before any adapter call. A signal aborted mid-pack SHALL terminate at the next clip boundary, release any partial buffers, and throw the same error.
Scenario: pre-aborted signal short-circuits pack
- WHEN
packis called with an already-aborted signal - THEN it throws
AudioSpriteErrorwhosecode === "AUDIO_SPRITE.ABORTED"before the adapter is invoked
Verified by: src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"pack throws ABORTED for a pre-aborted signal"
Requirement: Zod boundary validation
Every public options object — PackOptions, AudioSpritePackerOptions, AudioClipInput — SHALL have a Zod schema, and every public entry point (pack, createAudioSpritePacker) SHALL validate its arguments against the schema before use. A validation failure SHALL throw AudioSpriteError with code AUDIO_SPRITE.INVALID_OPTIONS carrying the Zod issue list in context.
Scenario: invalid options are rejected at the boundary
- WHEN
packis called withsilencePaddingMs: -1 - THEN it throws
AudioSpriteErrorwhosecode === "AUDIO_SPRITE.INVALID_OPTIONS"with the Zod issues incontext
Verified by: src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"pack rejects invalid options with AUDIO_SPRITE_INVALID_OPTIONS"
Requirement: Structured error codes
The package SHALL register error codes via @suaveplan/error's defineCodes("AUDIO_SPRITE", [...]) yielding values AUDIO_SPRITE.UNSUPPORTED_FORMAT, AUDIO_SPRITE.DECODE_FAILED, AUDIO_SPRITE.ENCODE_FAILED, AUDIO_SPRITE.CLIP_NOT_FOUND, AUDIO_SPRITE.INVALID_OPTIONS, AUDIO_SPRITE.EMPTY_INPUT, AUDIO_SPRITE.INVALID_MANIFEST, and AUDIO_SPRITE.ABORTED. Every error thrown by the package SHALL be an AudioSpriteError extending SuaveplanError. Every registered code SHALL be reachable from a real call site exercised by tests.
Scenario: every registered code is thrown by a call site
- WHEN the test suite runs
- THEN each code in the registry is asserted thrown by at least one test
Verified by: src/error-codes.test.ts::"every AUDIO_SPRITE code is reachable from a call site"
Requirement: OpenTelemetry instrumentation
The package SHALL instrument pack via @opentelemetry/api: span audio_sprite.pack carrying clipCount and outputFormat attributes; counter audio_sprite.pack_total; histograms audio_sprite.pack_duration_ms and audio_sprite.compression_ratio. Instrumentation SHALL be a no-op when no exporter is registered and SHALL NOT run as an import-time side effect.
Scenario: instrumentation is inert without an exporter
- WHEN the package is imported and used with no OTel exporter registered
- THEN no error is thrown and no global state is mutated at import time
Scenario: a pack records span, counter, and histograms
- WHEN
packruns against a real in-memoryMeterProviderandTracerProvider - THEN one
audio_sprite.packspan is recorded,audio_sprite.pack_totalincrements by 1, and both histograms record one observation each
Verified by: src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"pack records OTel instruments when an exporter is registered" AND tests/purity.test.ts::"importing the package mutates no global state"
Requirement: Subpath exports per feature module
package.json SHALL declare one exports entry per feature module (./core/audio-sprite-packer/audio-sprite-packer, ./adapters/null-adapter/null-adapter, ./utils/manifest/manifest) in addition to the root . barrel, each resolving to a typed import condition. sideEffects SHALL be false.
Scenario: each feature module is independently importable
- WHEN a consumer imports
@suaveplan/audio-sprite-packer/adapters/null-adapter - THEN the import resolves to the NullAudioAdapter module without pulling the whole package barrel
Verified by: tests/exports.test.ts::"every feature module has a subpath export entry"
Requirement: Pack options silence padding and gain normalisation
PackOptions SHALL support silencePaddingMs (default 100), stripSilence (default true), and normalise (default false). When stripSilence is true, the adapter's stripSilence SHALL be called per clip before concatenation. When normalise is true, the adapter's normalise SHALL be called per clip. Silence padding SHALL append sampleRate × channels × silencePaddingMs / 1000 zero-valued float samples between clips.
Scenario: silence padding increases total duration
- WHEN two clips are packed with
silencePaddingMs: 100 - THEN the manifest
totalDurationSecondsis greater than the sum of the raw clip durations by approximately 0.1 seconds
Scenario: normalised clips have consistent peak amplitude
- WHEN
packis called withnormalise: trueand clips of varying amplitude - THEN each clip is passed to
adapter.normalisebefore encoding
Verified by: src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"silence padding increases total duration" AND src/core/audio-sprite-packer/audio-sprite-packer.test.ts::"normalise invokes adapter.normalise per clip"