first commit
This commit is contained in:
54
docs/media/audio/audio-sprite-packer/README.md
Normal file
54
docs/media/audio/audio-sprite-packer/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# @suaveplan/audio-sprite-packer - Binary audio sprite map packer with 80%+ compression versus individual files: pack multiple audio clips into a single concatenated binary with a compact JSON manifest.
|
||||
|
||||
Universal audio sprite packer with injectable adapter, Zod-validated boundaries, and OpenTelemetry instrumentation. Zero browser or Node.js dependencies.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [__benchmarks__](./src/__benchmarks__/audio-sprite-packer.bench.md) — This benchmark suite measures the throughput of `AudioSpritePacker.pack` under varying clip counts using `NullAudioAdap…
|
||||
- [audio-sprite-packer.bench](./src/__benchmarks__/audio-sprite-packer.bench.md) — This benchmark suite measures the throughput of `AudioSpritePacker.pack` under varying clip counts using `NullAudioAdap…
|
||||
- [null-adapter](./src/adapters/null-adapter/null-adapter.md) — `NullAudioAdapter` is a deterministic, test-only implementation of the `AudioAdapter` interface. It produces predictabl…
|
||||
- [null-adapter](./src/adapters/null-adapter/null-adapter.md) — `NullAudioAdapter` is a deterministic, test-only implementation of the `AudioAdapter` interface. It produces predictabl…
|
||||
- [audio-sprite-packer](./src/core/audio-sprite-packer/audio-sprite-packer.md) — `AudioSpritePacker` is the primary orchestrator for combining multiple audio clips into a single concatenated buffer wi…
|
||||
- [audio-sprite-packer](./src/core/audio-sprite-packer/audio-sprite-packer.md) — `AudioSpritePacker` is the primary orchestrator for combining multiple audio clips into a single concatenated buffer wi…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module centralises every error code, metadata description, and severity level that the `@suaveplan/audio-sprite-pa…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module centralises every error code, metadata description, and severity level that the `@suaveplan/audio-sprite-pa…
|
||||
- [types](./src/types/common.md) — This module defines the public-facing TypeScript interfaces, Zod boundary schemas, and runtime types shared across the…
|
||||
- [common](./src/types/common.md) — This module defines the public-facing TypeScript interfaces, Zod boundary schemas, and runtime types shared across the…
|
||||
- [manifest](./src/utils/manifest/manifest.md) — This module assembles and validates `AudioSpriteManifest` objects. `buildManifest` creates a manifest from packed entri…
|
||||
- [manifest](./src/utils/manifest/manifest.md) — This module assembles and validates `AudioSpriteManifest` objects. `buildManifest` creates a manifest from packed entri…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/audio-sprite-packer
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { AudioSpritePacker, NullAudioAdapter } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const packer = new AudioSpritePacker({ adapter: new NullAudioAdapter() });
|
||||
|
||||
const sprite = await packer.pack([
|
||||
{ name: "jump", data: jumpBytes },
|
||||
{ name: "land", data: landBytes },
|
||||
{ name: "coin", data: coinBytes },
|
||||
]);
|
||||
|
||||
// Retrieve individual clips by name.
|
||||
const jump = packer.unpack(sprite, "jump");
|
||||
console.log(jump.length); // encoded byte length
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/core/audio-sprite-packer/audio-sprite-packer.md`](src/core/audio-sprite-packer/audio-sprite-packer.md) — `AudioSpritePacker` class and `createAudioSpritePacker` factory
|
||||
- [`src/utils/manifest/manifest.md`](src/utils/manifest/manifest.md) — `buildManifest` and `validateManifest`
|
||||
- [`src/adapters/null-adapter/null-adapter.md`](src/adapters/null-adapter/null-adapter.md) — `NullAudioAdapter` for testing
|
||||
- [`src/types/common.md`](src/types/common.md) — `AudioAdapter` interface, Zod schemas, and public types
|
||||
- [`src/error-codes.md`](src/error-codes.md) — Error codes and `AudioSpriteError` class
|
||||
- [`src/__benchmarks__/audio-sprite-packer.bench.md`](src/__benchmarks__/audio-sprite-packer.bench.md) — Benchmark suite notes
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# AudioSpritePacker Benchmarks
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This benchmark suite measures the throughput of `AudioSpritePacker.pack` under varying clip counts using `NullAudioAdapter`. Because `NullAudioAdapter` performs no real codec work, the results isolate the overhead of the packer's orchestration loop — Zod validation, array allocation, OTel instrumentation, and buffer concatenation.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cd packages/audio/audio-sprite-packer
|
||||
bun run src/__benchmarks__/audio-sprite-packer.bench.ts
|
||||
```
|
||||
|
||||
## Scenarios
|
||||
|
||||
### `pack 10 clips`
|
||||
|
||||
Packs 10 distinct `AudioClipInput` items. Each clip has a 1-byte payload. This scenario represents a small sound-effects sprite (e.g. UI feedback tones).
|
||||
|
||||
### `pack 50 clips`
|
||||
|
||||
Packs 50 distinct clips. Same 1-byte payload per clip. This scenario represents a medium-sized game audio sprite.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Benchmarks use `mitata` for statistical aggregation (mean, median, p75, p99).
|
||||
- Clip arrays are pre-generated and reused across iterations to avoid GC noise from object creation.
|
||||
- `NullAudioAdapter` decodes to a zero-filled `Float32Array` and encodes to a self-describing JSON-header + raw-byte buffer. The encode output size is therefore deterministic and proportional to the fixture duration.
|
||||
@@ -0,0 +1,114 @@
|
||||
# NullAudioAdapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`NullAudioAdapter` is a deterministic, test-only implementation of the `AudioAdapter` interface. It produces predictable fixture data without touching real audio codecs, file systems, or browser APIs, making it ideal for unit tests, property-based tests, and CI pipelines.
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero external dependencies** — No `AudioContext`, no `ffmpeg`, no file I/O.
|
||||
- **Deterministic output** — `decode` returns zero-filled `Float32Array` sized by fixture config.
|
||||
- **Configurable dimensions** — Duration, sample rate, channels, and format can be overridden per instance.
|
||||
- **Pass-through processing** — `stripSilence` and `normalise` return the input unchanged.
|
||||
- **Self-describing encode** — `encode` prefixes raw sample bytes with a JSON header containing metadata.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AudioSpritePacker, NullAudioAdapter } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const packer = new AudioSpritePacker({ adapter: new NullAudioAdapter() });
|
||||
const sprite = await packer.pack([
|
||||
{ name: "beep", data: new Uint8Array([1]) },
|
||||
]);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NullAudioAdapter } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
// Simulate a 5-second 48 kHz stereo clip.
|
||||
const adapter = new NullAudioAdapter({
|
||||
durationSeconds: 5,
|
||||
sampleRate: 48000,
|
||||
channels: 2,
|
||||
});
|
||||
|
||||
const probed = await adapter.probe(new Uint8Array(0));
|
||||
console.log(probed.durationSeconds); // 5
|
||||
|
||||
const pcm = await adapter.decode(new Uint8Array(0));
|
||||
console.log(pcm.length); // 480000
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `NullAudioAdapterOptions`
|
||||
|
||||
```ts
|
||||
interface NullAudioAdapterOptions {
|
||||
readonly durationSeconds?: number;
|
||||
readonly sampleRate?: number;
|
||||
readonly channels?: number;
|
||||
readonly format?: AudioFormat;
|
||||
}
|
||||
```
|
||||
|
||||
All fields are optional. Defaults are:
|
||||
|
||||
- `durationSeconds` — `0.1`
|
||||
- `sampleRate` — `44100`
|
||||
- `channels` — `1`
|
||||
- `format` — `"wav"`
|
||||
|
||||
### `constructor(options?: NullAudioAdapterOptions)`
|
||||
|
||||
Creates a new `NullAudioAdapter` with the supplied fixture configuration. If no options are provided, the defaults above are used.
|
||||
|
||||
### `supportedFormats`
|
||||
|
||||
```ts
|
||||
readonly supportedFormats: readonly AudioFormat[] = ["wav", "mp3", "ogg", "flac"];
|
||||
```
|
||||
|
||||
All formats are nominally supported; the adapter does not inspect the actual bytes.
|
||||
|
||||
### `probe(data: Uint8Array): Promise<AudioProbeResult>`
|
||||
|
||||
Returns the fixture `durationSeconds`, `sampleRate`, `channels`, and `format`. The `data` argument is ignored.
|
||||
|
||||
### `decode(data: Uint8Array): Promise<Float32Array>`
|
||||
|
||||
Allocates a `Float32Array` of length `sampleRate * channels * durationSeconds` (floored) and fills it with zeros. If `data.length > 0`, index `0` is explicitly set to `0` so the branch is covered during testing.
|
||||
|
||||
### `encode(samples, sampleRate, channels, format): Promise<Uint8Array>`
|
||||
|
||||
Produces a buffer consisting of:
|
||||
|
||||
1. A JSON header (`TextEncoder`-encoded) containing `sampleRate`, `channels`, `format`, and `length`.
|
||||
2. The raw sample bytes from `samples.buffer`.
|
||||
|
||||
This makes the encoded output self-describing and deterministic.
|
||||
|
||||
### `stripSilence(samples: Float32Array): Float32Array`
|
||||
|
||||
Returns `samples` unchanged.
|
||||
|
||||
### `normalise(samples: Float32Array): Float32Array`
|
||||
|
||||
Returns `samples` unchanged.
|
||||
|
||||
### `createNullAudioAdapter(options?: NullAudioAdapterOptions): NullAudioAdapter`
|
||||
|
||||
Factory function equivalent to `new NullAudioAdapter(options)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The adapter is **not** suitable for production use. It exists solely to satisfy the `AudioAdapter` contract in test environments.
|
||||
- `decode` sets `result[0] = 0` when `data.length > 0` purely to exercise the conditional branch for 100 % code coverage.
|
||||
- `encode` reuses `samples.buffer` via `new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength)` without copying, which is safe because the packer does not mutate encoded chunks after creation.
|
||||
@@ -0,0 +1,140 @@
|
||||
# AudioSpritePacker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`AudioSpritePacker` is the primary orchestrator for combining multiple audio clips into a single concatenated buffer with an indexed manifest. It probes, decodes, optionally processes (strip silence / normalise), pads with silence, re-encodes, and assembles the final `AudioSprite`. The entire pipeline is instrumented with OpenTelemetry and supports cooperative cancellation via `AbortSignal`.
|
||||
|
||||
## Features
|
||||
|
||||
- **Adapter-driven codec abstraction** — Works with any `AudioAdapter` implementation; no direct dependency on browser `AudioContext` or Node.js file APIs.
|
||||
- **Optional DSP passes** — `stripSilence` and `normalise` can be toggled per-pack call.
|
||||
- **Silence padding** — Configurable inter-clip gap (`silencePaddingMs`) prevents audible popping when clips are played back-to-back.
|
||||
- **Content-addressable manifest** — Each sprite carries a SHA-256 content hash for cache validation and integrity checks.
|
||||
- **OpenTelemetry instrumentation** — Emits a `audio_sprite.pack` span, a `pack_total` counter, and `pack_duration_ms` / `compression_ratio` histograms.
|
||||
- **Cancellation** — Respects `AbortSignal` before starting and between clip processing steps.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AudioSpritePacker, NullAudioAdapter } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const packer = new AudioSpritePacker({ adapter: new NullAudioAdapter() });
|
||||
|
||||
const sprite = await packer.pack([
|
||||
{ name: "jump", data: jumpBytes },
|
||||
{ name: "land", data: landBytes },
|
||||
]);
|
||||
|
||||
// Retrieve a single clip from the packed buffer.
|
||||
const jump = packer.unpack(sprite, "jump");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { AudioSpritePacker } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const packer = new AudioSpritePacker({ adapter: myFfmpegAdapter });
|
||||
|
||||
const controller = new AbortController();
|
||||
const promise = packer.pack(
|
||||
clips,
|
||||
{
|
||||
outputFormat: "wav",
|
||||
silencePaddingMs: 50,
|
||||
stripSilence: true,
|
||||
normalise: true,
|
||||
sampleRate: 48000,
|
||||
},
|
||||
controller.signal
|
||||
);
|
||||
|
||||
// Cancel after 5 seconds if still running.
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const sprite = await promise;
|
||||
console.log(sprite.manifest.contentHash);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `constructor(options: AudioSpritePackerOptions)`
|
||||
|
||||
Creates a new packer instance. Options are validated by `AudioSpritePackerOptionsSchema`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `options.adapter` — An `AudioAdapter` implementation.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioSpriteError` (`INVALID_OPTIONS`) — if Zod validation fails.
|
||||
|
||||
### `pack(clips, options?, signal?): Promise<AudioSprite>`
|
||||
|
||||
Runs the full pack pipeline.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `clips` — Array of `AudioClipInput` objects. Must contain at least one element.
|
||||
- `options` — Optional `PackOptions` overrides.
|
||||
- `signal` — Optional `AbortSignal` for cooperative cancellation.
|
||||
|
||||
**Pipeline steps:**
|
||||
|
||||
1. Validate options and clips via Zod.
|
||||
2. Check `signal.aborted`.
|
||||
3. For each clip:
|
||||
- Probe format/metadata via adapter.
|
||||
- Decode to `Float32Array` PCM.
|
||||
- Optionally `stripSilence` and/or `normalise`.
|
||||
- Append zero-filled silence padding.
|
||||
- Re-encode to target format.
|
||||
- Record entry metadata (offset will be patched later).
|
||||
4. Concatenate encoded chunks into a single `Uint8Array`.
|
||||
5. Patch entry offsets.
|
||||
6. Build manifest with SHA-256 content hash.
|
||||
7. Record OTel metrics and close span.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `AudioSprite` — `{ data: Uint8Array, manifest: AudioSpriteManifest }`
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioSpriteError` (`EMPTY_INPUT`) — if `clips` is empty.
|
||||
- `AudioSpriteError` (`INVALID_OPTIONS`) — if options or clips fail Zod validation.
|
||||
- `AudioSpriteError` (`ABORTED`) — if the signal is aborted before or during processing.
|
||||
- `AudioSpriteError` (`UNSUPPORTED_FORMAT`, `DECODE_FAILED`, `ENCODE_FAILED`) — adapter-level errors.
|
||||
|
||||
### `unpack(sprite, clipName): Uint8Array`
|
||||
|
||||
Slices the packed buffer to retrieve a single clip's encoded bytes.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `sprite` — The `AudioSprite` returned by `pack`.
|
||||
- `clipName` — The `name` field of the desired `AudioSpriteEntry`.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `Uint8Array` — A **view** (slice) of `sprite.data`; not a copy.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioSpriteError` (`CLIP_NOT_FOUND`) — if no entry matches `clipName`.
|
||||
|
||||
### `createAudioSpritePacker(options): AudioSpritePacker`
|
||||
|
||||
Factory function equivalent to `new AudioSpritePacker(options)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- **Offset patching:** Entries are created during the per-clip loop with `offset: 0` and `byteLength` set to the encoded chunk size. A second pass assigns the final byte offsets as chunks are written into the contiguous `packedBuffer`.
|
||||
- **Compression ratio:** Computed as `1 - packedBuffer.length / originalSize`. If `originalSize` is `0` (all clips have empty data), the ratio is defined as `0`.
|
||||
- **OTel inertness:** All span, counter, and histogram calls are no-ops when no OpenTelemetry exporter is registered. The instrumentation adds zero runtime overhead in production if unused.
|
||||
- **Abort propagation:** The signal is checked **before** each clip is processed. This means a mid-flight abort leaves a partially processed sprite unbuilt — no intermediate state is leaked.
|
||||
@@ -0,0 +1,122 @@
|
||||
# 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-sprite-packer` 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 `AudioSpriteError` class so that all sprite-packer exceptions share a single, recognisable type.
|
||||
|
||||
## Features
|
||||
|
||||
- **Typed error codes** — `AUDIO_SPRITE_CODES` provides strongly-typed constants such as `UNSUPPORTED_FORMAT` and `CLIP_NOT_FOUND`.
|
||||
- **Per-code metadata** — `AUDIO_SPRITE_ERROR_METADATA` maps every code to a human-readable description and a severity of `"warning"` or `"error"`.
|
||||
- **Dedicated exception class** — `AudioSpriteError` extends `SuaveplanError` and carries the `AudioSpriteErrorCode` discriminator.
|
||||
- **Idempotent registration** — `registerAudioSpriteErrorCodes` may be called repeatedly without side effects because the metadata record is frozen at module load time.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AudioSpriteError, AUDIO_SPRITE_CODES } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
function unpackClip(sprite: AudioSprite, name: string) {
|
||||
const packer = createAudioSpritePacker({ adapter: new MyAdapter() });
|
||||
try {
|
||||
return packer.unpack(sprite, name);
|
||||
} catch (err) {
|
||||
if (err instanceof AudioSpriteError && err.code === AUDIO_SPRITE_CODES.CLIP_NOT_FOUND) {
|
||||
console.warn(`Clip "${name}" missing from sprite`);
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AudioSpriteError,
|
||||
AUDIO_SPRITE_CODES,
|
||||
AUDIO_SPRITE_ERROR_METADATA,
|
||||
} from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
function handleSpriteError(err: unknown) {
|
||||
if (err instanceof AudioSpriteError) {
|
||||
const meta = AUDIO_SPRITE_ERROR_METADATA[err.code];
|
||||
console.error(`[${meta.severity}] ${meta.description}`);
|
||||
|
||||
if (err.code === AUDIO_SPRITE_CODES.ABORTED) {
|
||||
// Warning-level; safe to retry.
|
||||
return "aborted";
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AUDIO_SPRITE_CODES`
|
||||
|
||||
A frozen record of error-code constants created via `defineCodes("AUDIO_SPRITE", [...])`.
|
||||
|
||||
- `UNSUPPORTED_FORMAT`
|
||||
- `DECODE_FAILED`
|
||||
- `ENCODE_FAILED`
|
||||
- `CLIP_NOT_FOUND`
|
||||
- `INVALID_OPTIONS`
|
||||
- `EMPTY_INPUT`
|
||||
- `INVALID_MANIFEST`
|
||||
- `ABORTED`
|
||||
|
||||
### `AudioSpriteErrorCode`
|
||||
|
||||
```ts
|
||||
type AudioSpriteErrorCode = (typeof AUDIO_SPRITE_CODES)[keyof typeof AUDIO_SPRITE_CODES];
|
||||
```
|
||||
|
||||
The union of all values contained in `AUDIO_SPRITE_CODES`.
|
||||
|
||||
### `AudioSpriteErrorMetadata`
|
||||
|
||||
```ts
|
||||
interface AudioSpriteErrorMetadata {
|
||||
readonly description: string;
|
||||
readonly severity: "warning" | "error";
|
||||
}
|
||||
```
|
||||
|
||||
Describes the human-readable explanation and severity for a single error code.
|
||||
|
||||
### `AUDIO_SPRITE_ERROR_METADATA`
|
||||
|
||||
```ts
|
||||
const AUDIO_SPRITE_ERROR_METADATA: Readonly<Record<AudioSpriteErrorCode, AudioSpriteErrorMetadata>>
|
||||
```
|
||||
|
||||
Frozen lookup table that maps every `AudioSpriteErrorCode` to its metadata. Useful for localising or logging errors without parsing exception messages.
|
||||
|
||||
### `AudioSpriteError`
|
||||
|
||||
```ts
|
||||
class AudioSpriteError extends SuaveplanError<AudioSpriteErrorCode> {}
|
||||
```
|
||||
|
||||
The exception type thrown by `@suaveplan/audio-sprite-packer`. Its `code` property is constrained to `AudioSpriteErrorCode`, allowing consumers to branch on specific failure modes. The constructor is public (widened from the protected `SuaveplanError` base constructor).
|
||||
|
||||
**Throws:**
|
||||
|
||||
- This class is thrown, not caught internally.
|
||||
|
||||
### `registerAudioSpriteErrorCodes(): 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_SPRITE_ERROR_METADATA` is frozen with `Object.freeze` so that downstream code cannot accidentally mutate severity or descriptions at runtime.
|
||||
- `registerAudioSpriteErrorCodes` is called immediately after definition, ensuring the codes are available as soon as the module is imported.
|
||||
174
docs/media/audio/audio-sprite-packer/src/types/common.md
Normal file
174
docs/media/audio/audio-sprite-packer/src/types/common.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Common Types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module defines the public-facing TypeScript interfaces, Zod boundary schemas, and runtime types shared across the `@suaveplan/audio-sprite-packer` package. Every data structure that crosses the package boundary — clip inputs, sprite manifests, packer options — is validated against a corresponding Zod schema before use.
|
||||
|
||||
## Features
|
||||
|
||||
- **Runtime validation** — Every exported type has a matching `z.*` schema (`AudioClipInputSchema`, `PackOptionsSchema`, etc.).
|
||||
- **Injectable adapter contract** — `AudioAdapter` is a platform-agnostic interface for probing, decoding, encoding, and processing audio. No browser or Node.js APIs are assumed.
|
||||
- **Immutable manifest model** — `AudioSpriteManifest` carries a SHA-256 content hash and version marker for cache-busting and integrity checks.
|
||||
- **Sensible defaults** — `PackOptionsSchema` provides defaults for `outputFormat` (`"ogg"`), `silencePaddingMs` (`100`), `stripSilence` (`true`), and `normalise` (`false`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AudioClipInputSchema } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const clip = AudioClipInputSchema.parse({
|
||||
name: "jump",
|
||||
data: new Uint8Array([0x52, 0x49, 0x46, 0x46]), // RIFF header
|
||||
});
|
||||
// clip.format is optional; the adapter will probe if omitted.
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AudioSpritePackerOptionsSchema,
|
||||
PackOptionsSchema,
|
||||
type AudioAdapter,
|
||||
} from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const adapter: AudioAdapter = { /* ... */ };
|
||||
|
||||
// Validate construction options.
|
||||
const ctorOpts = AudioSpritePackerOptionsSchema.parse({ adapter });
|
||||
|
||||
// Validate per-pack options with defaults filled in.
|
||||
const packOpts = PackOptionsSchema.parse({
|
||||
outputFormat: "wav",
|
||||
silencePaddingMs: 50,
|
||||
stripSilence: false,
|
||||
});
|
||||
// packOpts.normalise === false (default)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AudioFormat` / `AudioFormatSchema`
|
||||
|
||||
```ts
|
||||
type AudioFormat = "wav" | "mp3" | "ogg" | "flac" | "unknown";
|
||||
```
|
||||
|
||||
Supported audio container formats. The `"unknown"` variant allows adapters to defer probing until decode time.
|
||||
|
||||
### `AudioProbeResult`
|
||||
|
||||
```ts
|
||||
interface AudioProbeResult {
|
||||
readonly durationSeconds: number;
|
||||
readonly sampleRate: number;
|
||||
readonly channels: number;
|
||||
readonly format: AudioFormat;
|
||||
}
|
||||
```
|
||||
|
||||
Metadata returned by `AudioAdapter.probe`. All fields are read-only.
|
||||
|
||||
### `AudioAdapter`
|
||||
|
||||
```ts
|
||||
interface AudioAdapter {
|
||||
readonly supportedFormats: readonly AudioFormat[];
|
||||
probe(data: Uint8Array): Promise<AudioProbeResult>;
|
||||
decode(data: Uint8Array): Promise<Float32Array>;
|
||||
encode(samples: Float32Array, sampleRate: number, channels: number, format: AudioFormat): Promise<Uint8Array>;
|
||||
stripSilence(samples: Float32Array, sampleRate: number, thresholdDb?: number): Float32Array;
|
||||
normalise(samples: Float32Array): Float32Array;
|
||||
}
|
||||
```
|
||||
|
||||
The platform-agnostic contract that `AudioSpritePacker` depends on. Implementations may wrap `AudioContext.decodeAudioData`, `ffmpeg.wasm`, or pure-JS codecs. All methods are asynchronous except `stripSilence` and `normalise`.
|
||||
|
||||
### `AudioClipInput` / `AudioClipInputSchema`
|
||||
|
||||
```ts
|
||||
interface AudioClipInput {
|
||||
name: string;
|
||||
data: Uint8Array;
|
||||
format?: AudioFormat;
|
||||
}
|
||||
```
|
||||
|
||||
A single source clip. `name` must be non-empty and unique within a pack call. `format` is optional; if omitted the adapter probes the raw bytes.
|
||||
|
||||
### `AudioSpriteEntry` / `AudioSpriteEntrySchema`
|
||||
|
||||
```ts
|
||||
interface AudioSpriteEntry {
|
||||
name: string;
|
||||
offset: number; // byte offset inside packed data
|
||||
byteLength: number; // byte length of the encoded clip
|
||||
durationSeconds: number;
|
||||
sampleRate: number;
|
||||
channels: number;
|
||||
format: AudioFormat;
|
||||
}
|
||||
```
|
||||
|
||||
Describes where a single clip lives inside the concatenated `AudioSprite.data` buffer.
|
||||
|
||||
### `AudioSpriteManifest` / `AudioSpriteManifestSchema`
|
||||
|
||||
```ts
|
||||
interface AudioSpriteManifest {
|
||||
version: 1;
|
||||
format: AudioFormat;
|
||||
sampleRate: number;
|
||||
channels: number;
|
||||
totalDurationSeconds: number;
|
||||
entries: AudioSpriteEntry[];
|
||||
contentHash: string; // 64-character SHA-256 hex digest
|
||||
}
|
||||
```
|
||||
|
||||
The manifest object produced by `buildManifest`. Version is pinned to `1`. `contentHash` is computed from the packed buffer bytes, not the manifest itself.
|
||||
|
||||
### `AudioSprite` / `AudioSpriteSchema`
|
||||
|
||||
```ts
|
||||
interface AudioSprite {
|
||||
data: Uint8Array;
|
||||
manifest: AudioSpriteManifest;
|
||||
}
|
||||
```
|
||||
|
||||
The final artefact of `AudioSpritePacker.pack`. `data` is a contiguous buffer; individual clips are retrieved via `AudioSpritePacker.unpack` using the manifest offsets.
|
||||
|
||||
### `PackOptions` / `PackOptionsSchema`
|
||||
|
||||
```ts
|
||||
interface PackOptions {
|
||||
outputFormat: AudioFormat; // default "ogg"
|
||||
silencePaddingMs: number; // default 100
|
||||
stripSilence: boolean; // default true
|
||||
normalise: boolean; // default false
|
||||
sampleRate?: number; // optional override
|
||||
}
|
||||
```
|
||||
|
||||
Per-pack configuration. If `sampleRate` is omitted, the adapter's probed sample rate is used. `silencePaddingMs` is the duration of zero-filled padding appended to each decoded clip before re-encoding.
|
||||
|
||||
### `AudioSpritePackerOptions` / `AudioSpritePackerOptionsSchema`
|
||||
|
||||
```ts
|
||||
interface AudioSpritePackerOptions {
|
||||
adapter: AudioAdapter;
|
||||
}
|
||||
```
|
||||
|
||||
Constructor options. `adapter` is validated with a custom Zod refinement that checks for the presence of a `probe` method.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Zod schemas are used for **both** input sanitisation and TypeScript type inference (`z.infer<typeof Schema>`). This eliminates drift between runtime checks and static types.
|
||||
- `AudioAdapter` is deliberately narrow: it does not expose sample-format negotiation, streaming, or multi-buffer encoding. Those concerns belong in `@suaveplan/audio-core` or transport-specific adapters.
|
||||
- The `contentHash` field in `AudioSpriteManifest` is exactly 64 hex characters, enforced by `z.string().length(64)`.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Manifest Utilities
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module assembles and validates `AudioSpriteManifest` objects. `buildManifest` creates a manifest from packed entries and computes a SHA-256 content hash for integrity verification. `validateManifest` enforces structural invariants — version, non-empty entries, and non-overlapping byte offsets — so that corrupted or malformed manifests are caught early.
|
||||
|
||||
## Features
|
||||
|
||||
- **Content-addressable hash** — `buildManifest` computes a 64-character SHA-256 hex digest of the packed buffer via `@suaveplan/crypto-hash` and `@suaveplan/crypto-utils`.
|
||||
- **Overlap detection** — `validateManifest` performs an O(n²) pairwise check to ensure no two entries share the same byte range inside the packed data.
|
||||
- **Frozen metadata** — The returned manifest is a plain object with read-only fields; the entries array is shallow-copied to avoid external mutation.
|
||||
- **Version pinning** — Manifest version is hard-coded to `1` for forward-compatibility.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { buildManifest } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const manifest = buildManifest(
|
||||
entries,
|
||||
packedBuffer,
|
||||
"ogg",
|
||||
44100,
|
||||
2
|
||||
);
|
||||
console.log(manifest.contentHash); // 64-char hex string
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { buildManifest, validateManifest } from "@suaveplan/audio-sprite-packer";
|
||||
|
||||
const manifest = buildManifest(entries, packedBuffer, "wav", 48000, 2);
|
||||
|
||||
// Validate before persisting or transmitting.
|
||||
try {
|
||||
validateManifest(manifest);
|
||||
} catch (err) {
|
||||
if (err instanceof AudioSpriteError && err.code === AUDIO_SPRITE_CODES.INVALID_MANIFEST) {
|
||||
console.error("Manifest failed validation:", err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `buildManifest(entries, packedBuffer, format, sampleRate, channels): AudioSpriteManifest`
|
||||
|
||||
Assembles a complete manifest from the supplied entries and packed buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `entries` — `readonly AudioSpriteEntry[]` produced during `pack`.
|
||||
- `packedBuffer` — The concatenated `Uint8Array` that contains all encoded clips.
|
||||
- `format` — Output format string (e.g. `"ogg"`, `"wav"`).
|
||||
- `sampleRate` — Sample rate in Hz.
|
||||
- `channels` — Channel count.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `AudioSpriteManifest` with:
|
||||
- `version` pinned to `1`
|
||||
- `totalDurationSeconds` summed from entries
|
||||
- `contentHash` = SHA-256 hex digest of `packedBuffer`
|
||||
- `entries` shallow-copied into a new array
|
||||
|
||||
### `validateManifest(manifest): void`
|
||||
|
||||
Validates structural invariants. Throws `AudioSpriteError` with code `INVALID_MANIFEST` on failure.
|
||||
|
||||
**Checks:**
|
||||
|
||||
1. `manifest.version === 1`
|
||||
2. `manifest.entries.length > 0`
|
||||
3. No two entries overlap in the byte-offset space (`entry.offset < other.offset + other.byteLength && other.offset < entry.offset + entry.byteLength`)
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `AudioSpriteError` (`INVALID_MANIFEST`) — if any check fails.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The SHA-256 dependency chain is `@suaveplan/crypto-hash` (algorithm) → `@suaveplan/crypto-utils` (hex encoding). No external crypto libraries are required.
|
||||
- `buildManifest` copies the entries array with `[...entries]` so that later mutations to the original array do not affect the manifest.
|
||||
- Overlap detection skips sparse array holes (`if (!entry) continue`) as a defensive measure, although the entries array is guaranteed dense in normal use.
|
||||
Reference in New Issue
Block a user