118 lines
4.9 KiB
Markdown
118 lines
4.9 KiB
Markdown
# `codecs/aiff`
|
|
|
|
Native pure-JS AIFF (Audio Interchange File Format) decoder, decode-only.
|
|
|
|
## Purpose
|
|
|
|
AIFF is the big-endian counterpart to WAV used historically on Apple
|
|
hardware. The codec supports both the original AIFF container and the
|
|
later AIFC variant (which uses the same chunk structure but allows
|
|
non-PCM payloads — though this codec only decodes uncompressed integer
|
|
PCM).
|
|
|
|
Encoding is intentionally not supported: AIFF write is rarely useful
|
|
outside Apple's own tooling and the canonical archive format
|
|
is WAV/FLAC. Callers attempting to encode AIFF receive a clean
|
|
`AUDIO_CODECS.NOT_SUPPORTED` error rather than a silently-degraded
|
|
fallback.
|
|
|
|
## Features
|
|
|
|
- `canDecode(bytes)` — magic-byte sniff for `FORM` at byte 0 plus
|
|
`AIFF` or `AIFC` at byte 8; rejects buffers shorter than 12 bytes
|
|
or with non-matching magic.
|
|
- `decode(bytes)` — walks the AIFF chunk list for `COMM` and `SSND`,
|
|
decodes the 80-bit IEEE 754 extended-precision sample rate, and
|
|
returns a `DecodeResult` with normalised `Float32Array[]` PCM.
|
|
- 8-bit signed, 16-bit signed, 24-bit signed, and 32-bit signed PCM
|
|
in big-endian byte order are all decoded; the 80-bit extended-float
|
|
sample rate field is parsed in full so the codec round-trips any
|
|
sample rate AIFF can express, including the unusual ones used by old
|
|
samplers (37_800, 32_000, 24_000, etc).
|
|
- `encode` — throws `AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")`.
|
|
|
|
## Usage: Basic Example
|
|
|
|
```ts
|
|
import { AiffCodec } from "@suaveplan/audio-codecs/codecs/aiff";
|
|
|
|
const decoded = await AiffCodec.decode(aiffBytes);
|
|
console.log(decoded.sampleRate, decoded.channelData.length);
|
|
```
|
|
|
|
## Usage: Advanced Example
|
|
|
|
Detecting AIFF vs AIFC and falling through to a separate processor:
|
|
|
|
```ts
|
|
import { AiffCodec } from "@suaveplan/audio-codecs/codecs/aiff";
|
|
|
|
async function processAudio(bytes: Uint8Array): Promise<AudioDocument> {
|
|
if (AiffCodec.canDecode(bytes)) {
|
|
const result = await AiffCodec.decode(bytes);
|
|
return buildDocumentFromResult(result);
|
|
}
|
|
return processWithFallbackCodec(bytes);
|
|
}
|
|
|
|
// Direct registry registration:
|
|
import { CodecRegistry } from "@suaveplan/audio-codecs";
|
|
const registry = new CodecRegistry({ codecs: [AiffCodec] });
|
|
const doc = await registry.decode(aiffBytes);
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `AiffCodec: AudioCodec` — frozen constant; `name === "aiff"`,
|
|
`extensions === ["aiff", "aif", "aifc"]`, `mimeTypes === ["audio/aiff",
|
|
"audio/x-aiff"]`.
|
|
- `canDecode(bytes: Uint8Array): boolean`
|
|
- `decode(bytes: Uint8Array): Promise<DecodeResult>` — throws
|
|
`AudioCodecsError("AUDIO_CODECS.DECODE_FAILED")` for:
|
|
- buffer length < 12 (`too-short`),
|
|
- non-FORM magic (`bad-form`),
|
|
- non-AIFF/AIFC type (`bad-type`),
|
|
- COMM chunk size < 18 (`bad-comm-size`),
|
|
- missing COMM chunk (`no-common`),
|
|
- missing SSND chunk (`no-ssnd`),
|
|
- `sampleSize` = 0 (`unsupported-bits`),
|
|
- `sampleSize` > 32 (`unsupported-bytes-per-sample`).
|
|
- `encode(): Promise<Uint8Array>` — always throws
|
|
`AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")`.
|
|
- The optional streaming-factory methods are absent; `CodecRegistry`
|
|
honours that and throws `STREAM_ERROR` on attempted streaming
|
|
operations against this codec.
|
|
|
|
## Implementation Notes
|
|
|
|
- The 80-bit extended-precision IEEE 754 sample-rate field is decoded
|
|
by `parseIeee754Extended(bytes, off)` which reads 10 bytes (1 sign
|
|
bit + 15 exponent bits biased by 16383 + 64-bit explicit mantissa —
|
|
the AIFF format predates double-precision IEEE 754 and uses an
|
|
explicit-mantissa variant rather than the more common implicit-1
|
|
binary64). The function returns `0` when `off + 10 > bytes.byteLength`
|
|
so a truncated buffer produces a 0-Hz result rather than a thrown
|
|
error — the parent `decode` then fails with a meaningful chunk-level
|
|
error.
|
|
- AIFF chunk sizes are big-endian uint32; the chunk walker advances
|
|
`pos = body + size + (size & 1)` to honour the AIFF requirement that
|
|
chunks pad to even boundaries.
|
|
- The normalisation factor is `(1 << (sampleSize - 1)) >>> 0` — the
|
|
unsigned-shift handles the 32-bit case where `1 << 31` would
|
|
otherwise produce a negative number under signed arithmetic.
|
|
- The 24-bit decoder reads bytes in MSB-first order (AIFF is
|
|
big-endian) using one signed byte + two unsigned bytes to preserve
|
|
the sign bit without depending on the JavaScript runtime's
|
|
sign-extension semantics.
|
|
- `numSampleFrames` from COMM caps decoded output; the codec also
|
|
caps via `availableFrames = floor(ssndLength / frameBytes)` so a
|
|
truncated SSND chunk simply yields fewer frames rather than reading
|
|
past the buffer.
|
|
- No streaming factory exists because AIFF's `SSND` chunk is variably
|
|
positioned within the FORM block — implementing a single-pass
|
|
streaming decoder would require buffering the whole header section
|
|
anyway, which negates the streaming benefit. Callers needing
|
|
streaming PCM can pipe the AIFF through a full decode then push the
|
|
result via the registry's `encodeStream` into WAV (which is
|
|
streamable).
|