Recursive AI

WAVE, AIFF and CAF files

All onemedia docs

How RecursiveAI.OneMedia.Riff, .Aiff and .Caf read audio files made of one run of sample data. Specifications: Microsoft's RIFF and WAVE_FORMAT_EXTENSIBLE documentation, EBU Tech 3285 (BWF) and Tech 3306 (RF64/BW64), the iXML specification, Apple's AIFF/AIFF-C and Core Audio Format specifications, and the Wave64 description (the layout was checked against files written by ffmpeg and libsndfile, since Sony's PDF is no longer online). ExifTool's RIFF and AIFF tag pages are read as tables of facts.

Layers

LayerTypesNotes
Chunk structureChunkFile, ChunkNode, ChunkLayoutLittle-endian RIFF, RF64/BW64 (with ds64), Wave64 (GUID chunks, 8-byte alignment, sizes that include the 24-byte header) and big-endian IFF FORM. Lazy tree of ranges; unknown chunks are kept. CAF has its own tiny reader (CafReader, CafChunk).
Format modelWaveFormat (+ WaveCodecs), WaveChunkReader (cue/adtl, smpl, inst), AiffCommon, CafAudioDescription, CafPacketTableHeaderfmt incl. WAVE_FORMAT_EXTENSIBLE with channel mask and sub-format GUID; COMM with the 80-bit rate; desc, pakt, kuki, chan, info.
Neutral demuxWavDemuxer, AiffDemuxer, CafDemuxer (sync, seekable and forward-only), AudioFileAsyncDemuxer (ranged)One audio track, timescale = sample rate, every packet a keyframe.
TagsRiffTagReader → ChunkTagSetLIST INFO, bext v0–2, iXML, axml, cart, ID3 chunks (kept raw, and their frames read as id3v2 tags), AIFF NAME AUTH (c) ANNO COMT, CAF info.

Packets

PCM is cut into fixed packets with exact times (pts = frames before the packet, in units of 1/sample rate). The sizes are those the reference tool (ffprobe) uses, found by experiment on 219 files, so that packet lists can be compared byte for byte:

DataPacket
WAVE/W64 PCM, float, G.711the largest power of two of frames not above rate × 4096 / 44100 (500 Hz → 32, 8 kHz → 512, 44.1 kHz → 4096, 192 kHz → 16384), at least 1
AIFF/AIFF-C and CAF PCM4096 bytes rounded down to whole frames
MS ADPCM, IMA ADPCM (WAVE)as many whole blocks as fit the PCM frame count above, at least one
GSM 06.10one block (WAVE 65 bytes/320 frames; AIFF-C GSM 33 bytes/160 frames)
ima4 (AIFF-C, CAF)one 34-byte-per-channel block of 64 frames
MP3, MP2, AC-3, ADTS in WAVEone frame, found from its header; bytes between frames are skipped and reported (audio.resync)
CAF with a packet tablethe packet table's entries, read from the file as the demuxer goes (never held)
any other codecup to 4096 bytes (512 for G.726), each with its share of the stated frame count
  • The last packet takes every byte that is left, including a tail shorter than a frame or block (ffprobe delivers it; audio.partial-unit notes it).
  • AIFF: the sound data is what SSND holds after its offset field; the frame count in COMM is not used to cut it (aiff.frame-count notes a mismatch). A COMM of an AIFC form that stops after the 18 fixed bytes means no compression.
  • WAVE data of size 0 followed by sample data (a streaming writer that never came back) runs to the end of the file (wav.data-size-zero); a size of 0xFFFFFFFF, or an outer RIFF size of 0, means "to the end" as well (riff.unbounded-size).
  • A chunk that fits the file but ends after a too-small outer size is read in full (riff.size-exceeds-parent); libsndfile's Wave64 files do this.
  • CAF data with size −1 runs to the end of the file. Priming and remainder frames of the packet table are reported on the track, not applied.
  • fact counts are used for duration unless they differ from what the data holds by more than a factor of two (wav.fact-count).
  • Text: UTF-8 when valid, else Latin-1 (riff.text-latin1); INFO strings end at the first NUL.

Defects

riff.* (chunk structure): truncated-header, trailing, size-exceeds-file, size-exceeds-parent, unbounded-size, w64-size, ds64-short, bext-short, cart-short, smpl-short, smpl-loops, cue-count, ixml-large, ixml-xml, text-latin1. wav.*: no-data, block-align, data-size-zero, fact-count. aiff.*: no-data, frame-count, comt. caf.*: no-data, no-pakt, pakt-short, pakt-varint, size, size-exceeds-file, truncated-header. audio.*: partial-unit, resync, truncated-data. Every one carries the byte offset of the chunk or value. Typed exceptions (MediaFormatException) are raised only when no audio can be described: no fmt/COMM/desc, zero channels or rate.

Reading from ranged sources

WavFileFormat, AiffFileFormat and CafFileFormat implement OpenAsync. The synchronous reader runs over a cache of fetched ranges; a read of bytes not yet fetched stops the step, the range is fetched (16 KiB blocks for headers and tags, 64 KiB blocks for sample data, two blocks kept), and the step runs again. Opening a file therefore costs one request per distant chunk (typically 2–3: the head, and the tail where LIST/iXML/id3 chunks sit), and playing costs one request per 64 KiB; memory is constant. Tags (ReadMetadataAsync) fetch each tag chunk whole, bounded by MaxTagBytes.

Tags

Native systems: riff-info, bext, ixml, axml, cart, aiff, caf-info, id3-chunk (raw bytes with ranges, beside the id3v2 frames read from them; a chunk that holds no tag, or a tag longer than its chunk, is a defect and stays raw). The common view maps through TagMappingTable (generated page: tag-mapping.md). The BWF origination date and time is a capture-time candidate with the zone unknown (rank 35, below offset-bearing vendor times). iXML is read with XmlReader (no DTD, no entity expansion) and keeps its whole payload as the range.

Fidelity

Unedited input is never modified, and all chunks, known or not, stay reachable through Chunks with their ranges. Packets carry the source range of the bytes delivered. See the tables below for how packets, durations and tags compare with ffprobe, MediaInfo and ExifTool.

Differential results

Method: OneMedia.Oracles diff --family pcm against ffprobe 7.0.2 (static build), packet by packet (track, pts, dts, duration, size, file position, key flag, SHA-256 of the payload). Corpus: 65 files made by ffmpeg 7.0.2 from a sine input (PCM s8 to f64, extensible 5.1/7.1, µ-law, A-law, MS and IMA ADPCM, MP3 and AC-3 in WAVE, RF64, Wave64, AIFF, AIFF-C, CAF with PCM/ALAC/Opus/IMA4, tagged variants), 32 crafted files (tools/gen-pcm-fixtures.py: iXML, cart, smpl, cue, streaming sizes, damage) and 122 published samples (libsndfile test files, Core Audio samples, ffmpeg's sample collection).

ResultFiles
Packets equal ffprobe's, no exception neededthe rest
Equal after a documented exception (tools/OneMedia.Oracles/exceptions.tsv)see below
ffprobe reports no streams (we read or reject, no comparison)14
Not RIFF at all (NIST SPHERE files named .wav)3

Exceptions, each from an experiment on the worker:

  • CAF with a packet table: ffprobe prints N/A for the duration; we report the table's frames per packet.
  • Last packet of a block-coded WAVE stream: ffprobe prints N/A timestamps.
  • VOXware, TrueSpeech: ffprobe cuts them with its own frame parser (27 and 64-byte packets); we deliver 4096-byte packets (bytes identical).
  • OKI ADPCM, G.726 in WAVE and AIFF-C: the same data and packet bytes; ffprobe's parser counts more frames than the header implies.
  • Four libsndfile AIFF-C test files (Fnonull, Pmiscck, Poffset, Ptjunk): ffprobe delivers no packets (still none after removing the odd chunk, the FVER chunk or the SSND offset, so the cause is not one of those); the sound data is intact and we deliver it.
  • Files ffprobe rejects (ffprobe reports no streams): COMM missing, bad channel count or rate, pakt missing, chunk sizes past the file; we report a defect or raise a typed exception.
  • NIST SPHERE (sw1038t_short.wav, swb.wav, telephone.wav): ffprobe reads them; the container is not RIFF and not in scope.

Larger than 4 GB (generated, tools/corpus/generate.sh with LARGE=1): RF64 4.4 GB, Wave64 4.4 GB, CAF 4.4 GB, AIFF 2.3 GB. Packets equal ffprobe's on all four (2,179,689 packets), 0 bytes allocated per packet in steady state, peak working set 38–41 MB. Probe plus metadata read of the RF64 file: 20,480 bytes in 3 range requests (ffprobe: 1,032,192 B in 2; MediaInfo 344,064 B in 1); the same file read from disk: 4,216 B in 9 reads (ffprobe 884,816 B in 27; MediaInfo 1,048,576 B in 16; ExifTool 22,256 B in 5).

Tags against ExifTool 13.60 and MediaInfo 26.10 on 219 files (meta): see tags.md for the method. Differences are explained: MediaInfo's CAF durations are wrong for PCM (ffprobe and we agree); ExifTool computes ADPCM durations from the average data rate; both show mojibake for UTF-8 in INFO strings where we decode UTF-8; the id3 chunk of AIFF/WAVE was kept as raw bytes when this comparison was made; its frames are read now (artist, album, genre were "ours-missing" then). In the common view ID3 comes before LIST INFO. ATL.NET (7.19, bench tagcompare): 99 of 120 field comparisons equal; the others are ATL's file-name title fallback, its 0001-01-01 placeholder date, ID3 chunks, joined AIFF comments, and CAF comments it does not read.

Durations against ExifTool 13.60 and MediaInfo 26.10, field by field

meta --family pcm on the final build: 223 files; the Duration fact differs in 49 rows (ffprobe 7.0.2 format=duration added as a third opinion). Every row, grouped by cause:

CauseFilesOursExifToolMediaInfoffprobe
MediaInfo (and ExifTool, which prints nothing) misread CAF: they take the packet-table or data size without the frame size12 generated CAF PCM/IMA4 files, audio1, testcaf3, waterfall, oboe-*, caf-float*, caf-pcm32, over4gb.caf (e.g. caf-pcm_s16le-stereo48k ours 3.000)= ffprobenone0.085–1.396= ours
ExifTool takes ADPCM duration from the average data rate and ignores the block frame countwav-adpcm_ima_wav-*, wav-adpcm_ms-* (ours 3.000; ET 4.192 / 8.384), aif-ima-adpcm, dvi_adpcm (ET 0.774), o2_dvi, msadpcm, wav-ms-adpcm= ffprobewrongmostly = ours= ours
ExifTool/MediaInfo read the AIFF-C ima4 frame count from COMM, which counts blocks, not framesaifc-ima4-stereo (ours 3.001; both 0.047)= ffprobe0.0470.047= ours
Both tools add the encoder delay or count MPEG frames differentlywav-mp3 (3.025 vs 3.030), wav-ac3 (3.000 vs 3.008)frame walk = ffprobe+0.005+0.005= ours
GSM 06.10 in AIFF-C: ffprobe's duration is wrong (272 s, 5.88 s); ours agrees with both toolsaif-gsm610 (1.712), o2_gsm (0.040)1.712 / 0.0401.701 / 0.037same272.1 / 5.88
G.726 (4 bits per sample in the header): ours and MediaInfo use the header; ffprobe and ExifTool use 3 bitswav-g7210.8541.7090.8541.709
fact chunk says 1.0 s, data holds 12.5 stry1.wav12.485 after this round's fact check (was 1.000)104.064 (average-rate)1.00012.485
Stated frame count in COMM smaller than the sound dataaiff-count-short (the sound data decides: 0.250)0.2500.0630.0620.0625 (ffprobe plays the stated count when it is shorter)
Streaming sizesstream-ffffffff (ours 0.250 = ffprobe; ET 268435.456, MI 1.15e15 read the size literally), stream-zero (ours 0.250 = ffprobe, ET 0.253, MI 0.000)= ffprobewrongwrong= ours
Truncated datatruncated-data.wav (0.187 = MediaInfo = ffprobe; ExifTool trusts the header, 0.250)= ffprobe0.2500.187= ours
No usable data chunkhuge-chunk-size, no-data (ours 0, ExifTool 0.002–0.003, others nothing)0tinynonenone
Wave64 MS ADPCM: ffprobe prints N/A; our block arithmetic gives 1.723, MediaInfo 1.769w64-ms-adpcm1.723none1.769N/A
Wave64 files whose first outer size is too smallw64-alaw, w64-gsm610, w64-ima-adpcm, w64-pcmu8, w64-ulaw= ffprobenonewithin 0.04= ours
Ours is wrong: an AIFF-C whose COMM size omits the compression type bytes (digit0v0.aiff): the stray bytes shift the next chunk header, so no sound data is found and the duration is 0 (ffprobe, ExifTool and MediaInfo all give 0.560)digit0v0.aiff00.5600.5600.560
Garbage frame count (0xFFFFFFF6 frames)bad_length.aifc (ours 0; ET and MI 194,783 s; ffprobe rejects)0wrongwrongrejects

digit0v0.aiff is a known gap, not an exception: a resynchronising chunk search for AIFF (scanning for SSND after a chunk whose size runs past its parent) would fix it, and is left as a follow-up.

ATL.NET (final build): 119 of 120 field comparisons agree or are explained as above. The tagged-aiff.aiff Title difference is ATL's: the file's NAME chunk is 10 bytes, Tone test plus a NUL pad inside the size, and ATL keeps the NUL (Tone test\0) where we, ExifTool and MediaInfo end the string at the NUL.

Header comparison (infocheck) against ffprobe 7.0.2

225 files, 0 unexplained. Twenty differences are scoped in exceptions.tsv (fields info-duration, info-codec, info-open), each by the classes above: a COMM frame count smaller than the data (aiff-count-short); CAF packet tables and Wave64 MS ADPCM, where ffprobe prints no duration; ADPCM block arithmetic against ffprobe's size over average rate (aif-ima-adpcm); the libsndfile AIFF-C test files with odd chunks (Pmiscck, Poffset, Porder, Ptjunk) and the one with no sound chunk (Pnossnd); AIFF-C G722, which has no neutral codec id; damaged headers that we reject with a typed MediaFormatException while ffprobe lists a stream of zeroed parameters (aiff-no-comm, caf-no-desc, no-fmt, truncated-header, bad_chans, bad_srate, zero-channels); and the three NIST SPHERE files. ADPCM variants without a neutral id (OKI, G.726 little-endian in AIFF-C) are not compared. Estimates ffprobe makes from the file size for audio-only files are printed as audio-only-size-estimate.

Forward-only sources

WAVE, AIFF/AIFC and CAF open from a stream that cannot seek (Media.Open(Stream)) and give the packets a file gives. A CAF with variable-size packets (AAC, for one) needs its packet table, which the format places before the audio; the table is kept whole for a forward-only source, within the tag limit. A file with no table before the audio is refused with UnsupportedMediaException, where a seekable open reports the caf.no-pakt defect and delivers no packets.