first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View 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)`.

View File

@@ -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.