4.9 KiB
4.9 KiB
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 forFORMat byte 0 plusAIFForAIFCat byte 8; rejects buffers shorter than 12 bytes or with non-matching magic.decode(bytes)— walks the AIFF chunk list forCOMMandSSND, decodes the 80-bit IEEE 754 extended-precision sample rate, and returns aDecodeResultwith normalisedFloat32Array[]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— throwsAudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED").
Usage: Basic Example
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:
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): booleandecode(bytes: Uint8Array): Promise<DecodeResult>— throwsAudioCodecsError("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).
- buffer length < 12 (
encode(): Promise<Uint8Array>— always throwsAudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED").- The optional streaming-factory methods are absent;
CodecRegistryhonours that and throwsSTREAM_ERRORon 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 returns0whenoff + 10 > bytes.byteLengthso a truncated buffer produces a 0-Hz result rather than a thrown error — the parentdecodethen 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 where1 << 31would 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.
numSampleFramesfrom COMM caps decoded output; the codec also caps viaavailableFrames = 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
SSNDchunk 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'sencodeStreaminto WAV (which is streamable).