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

11 KiB
Raw Blame History

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, or Buffer

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.probe is called with any Uint8Array
  • THEN it returns { format: "wav", durationSeconds: 1, sampleRate: 44100, channels: 2 }

Scenario: NullAudioAdapter round-trips decode/encode

  • WHEN NullAudioAdapter.decode is called and the result is passed to encode
  • 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 pack is called with three AudioClipInput items using NullAudioAdapter
  • THEN the returned manifest has three entries with monotonically increasing offsets, and unpack for each name returns a Uint8Array whose byteLength matches the manifest entry

Scenario: unpack for unknown clip name throws

  • WHEN unpack is called with a clip name not present in the manifest
  • THEN it throws AudioSpriteError whose code === "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 pack is called with []
  • THEN it throws AudioSpriteError whose code === "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 buildManifest is called with entries and a packed buffer
  • THEN the manifest contentHash is a 64-character hex string matching the SHA-256 of the buffer

Scenario: validateManifest rejects overlapping offsets

  • WHEN validateManifest is called with entries whose byte ranges overlap
  • THEN it throws AudioSpriteError whose code === "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 pack is called with an already-aborted signal
  • THEN it throws AudioSpriteError whose code === "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 pack is called with silencePaddingMs: -1
  • THEN it throws AudioSpriteError whose code === "AUDIO_SPRITE.INVALID_OPTIONS" with the Zod issues in context

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 pack runs against a real in-memory MeterProvider and TracerProvider
  • THEN one audio_sprite.pack span is recorded, audio_sprite.pack_total increments 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 totalDurationSeconds is greater than the sum of the raw clip durations by approximately 0.1 seconds

Scenario: normalised clips have consistent peak amplitude

  • WHEN pack is called with normalise: true and clips of varying amplitude
  • THEN each clip is passed to adapter.normalise before 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"