Python’s chunk module parses IFF (Interchange File Format) chunk-based binary formats — the foundation of RIFF (WAV/AVI) and IFF/AIFF (AIFF/AIFF-C). import chunk. Create: c = chunk.Chunk(file_object, align=True, bigendian=True, inclheader=False). chunk size fields are 4 bytes big-endian by default (AIFF/IFF); set bigendian=False for RIFF/WAV little-endian. Attributes: .getname() → 4-byte bytes id (e.g., b'COMM', b'fmt '); .getsize() → int data size; .read(n) → bytes; .skip() — advances to end of chunk (including padding); .seek(pos, whence=0), .tell() — seek within chunk data; .close(). Chunks are padded to even byte boundaries (IFF convention). After reading/skipping a chunk, the file cursor is at the start of the next chunk. Walk a file by creating Chunk objects in a loop until EOFError. Nested chunks: RIFF/LIST and FORM/IFF containers have a 4-byte form type after the size field — read the first 4 bytes of the chunk data to get the form type, then loop creating child Chunk objects. Claude Code generates raw RIFF WAV parsers, AIFF chunk inspectors, IFF file analyzers, custom chunk extractors, and binary format debuggers.
CLAUDE.md for chunk
## chunk Stack
- Stdlib: import chunk, struct, io
- Open: c = chunk.Chunk(f, bigendian=False) # RIFF/WAV (LE size fields)
- c = chunk.Chunk(f, bigendian=True) # IFF/AIFF (BE size fields)
- Use: c.getname() c.getsize() c.read() c.skip()
- Walk: while True:
- try: c = chunk.Chunk(f, bigendian=False)
- except EOFError: break
- process(c); c.skip()
- Nested: read 4 bytes form-type after container, then loop child chunks
chunk IFF Chunk Parser Pipeline
# app/chunkutil.py — RIFF/WAV walk, AIFF walk, raw inspect, custom IFF builder
from __future__ import annotations
import chunk as _chunk
import io
import struct
from dataclasses import dataclass, field
from pathlib import Path
# ─────────────────────────────────────────────────────────────────────────────
# 1. Generic chunk walker
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class ChunkRecord:
name: bytes
size: int
offset: int # byte position in file where chunk DATA starts
data: bytes | None = field(default=None, repr=False)
def name_str(self) -> str:
return self.name.decode("latin-1", errors="replace").rstrip()
def __str__(self) -> str:
data_preview = ""
if self.data:
preview = self.data[:16].hex(" ")
data_preview = f" [{preview}{'...' if len(self.data) > 16 else ''}]"
return f"{self.name_str():<6s} size={self.size:8d} offset={self.offset}{data_preview}"
def walk_chunks(
f,
bigendian: bool = False,
read_data: bool = False,
max_data_bytes: int = 256,
) -> list[ChunkRecord]:
"""
Walk all top-level IFF chunks in a file-like object.
bigendian=False for RIFF (WAV/AVI), True for IFF/AIFF.
read_data=True captures up to max_data_bytes of each chunk's payload.
Example:
with open("audio.wav", "rb") as f:
for cr in walk_chunks(f, bigendian=False):
print(cr)
"""
records = []
while True:
try:
c = _chunk.Chunk(f, bigendian=bigendian)
except EOFError:
break
offset = f.tell()
if read_data:
data = c.read(min(c.getsize(), max_data_bytes))
else:
data = None
records.append(ChunkRecord(
name=c.getname(),
size=c.getsize(),
offset=offset,
data=data,
))
c.skip()
return records
def walk_chunks_from_bytes(data: bytes, bigendian: bool = False) -> list[ChunkRecord]:
"""
Walk all top-level chunks from an in-memory bytes object.
Example:
with open("audio.wav", "rb") as f:
records = walk_chunks_from_bytes(f.read())
"""
return walk_chunks(io.BytesIO(data), bigendian=bigendian)
# ─────────────────────────────────────────────────────────────────────────────
# 2. RIFF (WAV/AVI) parser — little-endian
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class RiffTree:
form_type: bytes
chunks: list[ChunkRecord]
nested: dict[bytes, list["RiffTree"]] # LIST/RIFF sub-containers
def find(self, name: bytes) -> ChunkRecord | None:
for c in self.chunks:
if c.name == name:
return c
return None
def __str__(self) -> str:
lines = [f"RIFF[{self.form_type.decode(errors='replace')}] "
f"{len(self.chunks)} chunks"]
for c in self.chunks:
lines.append(f" {c}")
return "\n".join(lines)
def parse_riff(path: str | Path) -> RiffTree:
"""
Parse a RIFF container file (WAV, AVI, etc.).
Returns a RiffTree with top-level chunks and nested LIST containers.
Example:
tree = parse_riff("audio.wav")
print(tree)
fmt_chunk = tree.find(b'fmt ')
if fmt_chunk:
print("fmt chunk data:", fmt_chunk.data.hex())
"""
with open(str(path), "rb") as f:
# RIFF header: 'RIFF' (4) + size (4 LE) + form_type (4)
riff_hdr = f.read(4)
if riff_hdr != b"RIFF":
raise ValueError(f"Not a RIFF file: {path}")
_size = struct.unpack("<I", f.read(4))[0]
form_type = f.read(4)
records: list[ChunkRecord] = []
nested: dict[bytes, list[RiffTree]] = {}
while True:
try:
c = _chunk.Chunk(f, bigendian=False)
except EOFError:
break
offset = f.tell()
cname = c.getname()
csize = c.getsize()
if cname in (b"LIST", b"RIFF"):
# Sub-container: read form_type, then recurse inline
sub_form = f.read(4)
sub_records = []
remaining = csize - 4
orig_pos = f.tell()
# Walk sub-chunks within remaining bytes
sub_f = io.BytesIO(f.read(remaining))
sub_chunks = walk_chunks(sub_f, bigendian=False, read_data=True)
sub_tree = RiffTree(form_type=sub_form, chunks=sub_chunks, nested={})
nested.setdefault(cname, []).append(sub_tree)
else:
data = c.read(min(csize, 256))
records.append(ChunkRecord(name=cname, size=csize,
offset=offset, data=data))
c.skip()
continue
c.skip() # skip past the LIST/RIFF chunk
return RiffTree(form_type=form_type, chunks=records, nested=nested)
# ─────────────────────────────────────────────────────────────────────────────
# 3. AIFF/IFF parser — big-endian
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class AiffChunkTree:
form_type: bytes
chunks: list[ChunkRecord]
def find(self, name: bytes) -> ChunkRecord | None:
for c in self.chunks:
if c.name == name:
return c
return None
def __str__(self) -> str:
lines = [f"FORM[{self.form_type.decode(errors='replace')}] "
f"{len(self.chunks)} chunks"]
for c in self.chunks:
lines.append(f" {c}")
return "\n".join(lines)
def parse_aiff_chunks(path: str | Path) -> AiffChunkTree:
"""
Parse an AIFF/AIFF-C file at the raw chunk level (big-endian IFF).
Returns an AiffChunkTree with all FORM sub-chunks.
Example:
tree = parse_aiff_chunks("track.aif")
print(tree)
comm = tree.find(b'COMM')
if comm:
# COMM chunk: nchannels(2) nframes(4) sampwidth(2) samplerate(10 80-bit)
nch, nfr, sw = struct.unpack(">hIh", comm.data[:8])
print(f" channels={nch} frames={nfr} sampwidth={sw}")
"""
with open(str(path), "rb") as f:
form_hdr = f.read(4)
if form_hdr != b"FORM":
raise ValueError(f"Not an IFF/AIFF file: {path}")
_size = struct.unpack(">I", f.read(4))[0]
form_type = f.read(4)
records = walk_chunks(f, bigendian=True, read_data=True)
return AiffChunkTree(form_type=form_type, chunks=records)
# ─────────────────────────────────────────────────────────────────────────────
# 4. WAV fmt chunk decoder
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class WavFmt:
audio_format: int # 1=PCM, 3=IEEE float, 6=ALAW, 7=ULAW, 65534=extensible
num_channels: int
sample_rate: int
byte_rate: int # SampleRate * NumChannels * BitsPerSample/8
block_align: int # NumChannels * BitsPerSample/8
bits_per_sample: int
@property
def format_name(self) -> str:
return {1: "PCM", 3: "IEEE_FLOAT", 6: "ALAW",
7: "ULAW", 65534: "EXTENSIBLE"}.get(self.audio_format, "UNKNOWN")
def __str__(self) -> str:
return (f"{self.format_name} {self.num_channels}ch "
f"{self.sample_rate}Hz {self.bits_per_sample}bit")
def decode_wav_fmt(data: bytes) -> WavFmt:
"""
Decode the raw bytes of a WAV 'fmt ' chunk into a WavFmt dataclass.
Example:
tree = parse_riff("audio.wav")
fmt_rec = tree.find(b'fmt ')
fmt = decode_wav_fmt(fmt_rec.data)
print(fmt)
"""
if len(data) < 16:
raise ValueError("fmt chunk too short")
af, nc, sr, br, ba, bps = struct.unpack("<HHIIHH", data[:16])
return WavFmt(audio_format=af, num_channels=nc, sample_rate=sr,
byte_rate=br, block_align=ba, bits_per_sample=bps)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Custom IFF writer
# ─────────────────────────────────────────────────────────────────────────────
def write_iff_chunk(f, name: bytes, data: bytes, bigendian: bool = True) -> None:
"""
Write a single IFF chunk to a file-like object.
Pads to even byte boundary (IFF convention).
Example:
buf = io.BytesIO()
write_iff_chunk(buf, b'TEST', b'hello')
"""
assert len(name) == 4, "Chunk name must be exactly 4 bytes"
size = len(data)
if bigendian:
f.write(name + struct.pack(">I", size))
else:
f.write(name + struct.pack("<I", size))
f.write(data)
if size % 2:
f.write(b"\x00") # pad to even boundary
def build_riff_wav(
pcm_le: bytes,
channels: int = 1,
sample_rate: int = 44100,
bits_per_sample: int = 16,
) -> bytes:
"""
Build a minimal RIFF WAV file from raw little-endian PCM bytes.
Returns the complete WAV file as bytes.
Example:
wav_bytes = build_riff_wav(pcm_le_bytes, channels=2, sample_rate=44100)
with open("out.wav", "wb") as f: f.write(wav_bytes)
"""
# fmt chunk
byte_rate = sample_rate * channels * bits_per_sample // 8
block_align = channels * bits_per_sample // 8
fmt_data = struct.pack("<HHIIHH",
1, # PCM
channels,
sample_rate,
byte_rate,
block_align,
bits_per_sample,
)
buf = io.BytesIO()
write_iff_chunk(buf, b"fmt ", fmt_data, bigendian=False)
write_iff_chunk(buf, b"data", pcm_le, bigendian=False)
inner = buf.getvalue()
# RIFF container: 'RIFF' + size + 'WAVE' + chunks
riff_size = 4 + len(inner) # 'WAVE' tag + all chunks
return b"RIFF" + struct.pack("<I", riff_size) + b"WAVE" + inner
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import math
import tempfile
print("=== chunk demo ===")
# ── build a minimal WAV in memory ─────────────────────────────────────────
print("\n--- build_riff_wav ---")
rate = 44100; dur = 0.5; freq = 440.0
n = int(rate * dur)
pcm_le = bytearray()
for i in range(n):
val = int(16000 * math.sin(2 * math.pi * freq * i / rate))
val = max(-32768, min(32767, val))
pcm_le.extend(struct.pack("<h", val))
wav_bytes = build_riff_wav(bytes(pcm_le), channels=1,
sample_rate=rate, bits_per_sample=16)
print(f" built {len(wav_bytes)} byte WAV in memory")
# ── walk_chunks_from_bytes on the WAV ─────────────────────────────────────
print("\n--- walk_chunks on in-memory WAV (skip RIFF header) ---")
# Skip the outer RIFF/WAVE header (12 bytes) before walking inner chunks
inner_bytes = wav_bytes[12:]
for cr in walk_chunks_from_bytes(inner_bytes, bigendian=False):
print(f" {cr}")
# ── decode fmt chunk ───────────────────────────────────────────────────────
print("\n--- decode_wav_fmt ---")
inner_recs = walk_chunks_from_bytes(inner_bytes, bigendian=False)
fmt_rec = next((r for r in inner_recs if r.name == b"fmt "), None)
if fmt_rec and fmt_rec.data:
fmt = decode_wav_fmt(fmt_rec.data)
print(f" {fmt}")
# ── write to file and parse_riff ──────────────────────────────────────────
with tempfile.TemporaryDirectory() as tmp:
wav_path = Path(tmp) / "tone.wav"
wav_path.write_bytes(wav_bytes)
print(f"\n--- parse_riff({wav_path.name}) ---")
tree = parse_riff(wav_path)
print(f" {tree}")
fmt_c = tree.find(b"fmt ")
if fmt_c and fmt_c.data:
print(f" fmt: {decode_wav_fmt(fmt_c.data)}")
data_c = tree.find(b"data")
if data_c:
print(f" data chunk: {data_c.size} bytes = "
f"{data_c.size / (2 * rate):.3f}s mono 16-bit")
# ── custom IFF chunk write ────────────────────────────────────────────────
print("\n--- write_iff_chunk (big-endian IFF) ---")
buf = io.BytesIO()
write_iff_chunk(buf, b"TEST", b"hello IFF", bigendian=True)
write_iff_chunk(buf, b"DATA", b"\x00\x01\x02\x03", bigendian=True)
buf.seek(0)
chunks = walk_chunks(buf, bigendian=True, read_data=True)
for c in chunks:
print(f" {c}")
print("\n=== done ===")
For the struct alternative — struct.unpack_from(fmt, data, offset) and struct.iter_unpack(fmt, data) let you parse binary formats at fixed offsets without the chunk-walking abstraction — use struct directly when you know the exact layout of a specific format (e.g., a single WAV fmt chunk) and don’t need to navigate a variable-length chunk tree; use chunk when you need to walk a file of unknown chunk layout, skip over unrecognized chunks, or build format-agnostic IFF/RIFF browsers that discover chunk names at runtime. For the wave and aifc alternatives — wave.open() and aifc.open() build on the same IFF chunk structure but give you a high-level audio API (getnchannels, readframes, writeframes) instead of raw bytes — use wave/aifc when you want audio semantics; use chunk when you need access below the audio API (inspecting LIST chunks inside RIFF, extracting nonstandard chunks, debugging malformed files, or implementing a new IFF-based format that neither wave nor aifc knows about). The Claude Skills 360 bundle includes chunk skill sets covering ChunkRecord / walk_chunks() / walk_chunks_from_bytes() generic walkers, RiffTree with parse_riff() RIFF/WAV container parser, AiffChunkTree with parse_aiff_chunks() IFF/AIFF parser, WavFmt with decode_wav_fmt() fmt decoder, and write_iff_chunk() / build_riff_wav() custom IFF writer. Start with the free tier to try IFF chunk patterns and chunk pipeline code generation.