Python’s aifc module reads and writes AIFF (Audio Interchange File Format) and AIFF-C (compressed AIFF) files, the native audio format on Apple platforms and professional audio workstations. import aifc. Open for reading: f = aifc.open(path, 'r'). Open for writing: f = aifc.open(path, 'w'). Reader attributes: .getnchannels() → int; .getsampwidth() → bytes (1, 2, 3, or 4); .getframerate() → Hz; .getnframes() → total frames; .getcomptype() → b'NONE', b'sowt', b'ulaw', b'alaw', or custom 4-byte code; .getcompname() → human-readable description; .getparams() → namedtuple. Read: .readframes(n) → bytes (big-endian for AIFF, comp-dependent for AIFF-C); .rewind(). Writer setup: .setnchannels(n), .setsampwidth(n), .setframerate(n), .setcomptype(type, name) — defaults to b'NONE'/'not compressed'. Markers: .getmarkers() → list of (id, pos, name) tuples; .setmark(id, pos, name). Write: .writeframes(bytes). Close: .close() or context manager. AIFF uses big-endian PCM; AIFF-C sowt extension uses little-endian (compatible with Core Audio). Note: aifc is deprecated in Python 3.11 and removed in 3.13 — include compatibility guard. Claude Code generates AIFF readers, WAV↔AIFF converters, marker extractors, and multi-format audio inspectors.
CLAUDE.md for aifc
## aifc Stack
- Stdlib: import aifc (deprecated 3.11, removed 3.13 — guard with try/except)
- Read: with aifc.open(path, 'r') as f:
- params = f.getparams() # nchannels sampwidth framerate nframes
- raw = f.readframes(f.getnframes())
- marks = f.getmarkers() # [(id, pos, name), ...]
- Write: with aifc.open(path, 'w') as f:
- f.setnchannels(2); f.setsampwidth(2); f.setframerate(44100)
- f.setcomptype(b'NONE', b'not compressed')
- f.writeframes(pcm_bytes) # big-endian PCM for AIFF
- Note: AIFF = big-endian; AIFF-C sowt = little-endian (Core Audio)
aifc AIFF Audio Pipeline
# app/aifcutil.py — read, write, inspect, convert, swap endian, markers
from __future__ import annotations
import array
import io
import struct
import wave
from dataclasses import dataclass, field
from pathlib import Path
# Guard for Python 3.13+ where aifc is removed
try:
import aifc as _aifc
_AIFC_AVAILABLE = True
except ImportError:
_AIFC_AVAILABLE = False
# ─────────────────────────────────────────────────────────────────────────────
# 1. File inspection
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class AiffInfo:
path: str
channels: int
sampwidth: int # bytes per sample
framerate: int # Hz
nframes: int
comptype: bytes # b'NONE', b'sowt', b'ulaw', etc.
compname: bytes
markers: list[tuple[int, int, bytes]] # (id, pos, name)
duration: float
@property
def bit_depth(self) -> int:
return self.sampwidth * 8
@property
def is_compressed(self) -> bool:
return self.comptype not in (b'NONE', b'sowt')
@property
def is_little_endian(self) -> bool:
return self.comptype == b'sowt'
def __str__(self) -> str:
endian = "LE" if self.is_little_endian else "BE"
return (
f"{Path(self.path).name}: "
f"{self.channels}ch {self.framerate}Hz "
f"{self.bit_depth}bit-{endian} "
f"{self.comptype.decode(errors='replace')} "
f"{self.nframes}fr "
f"{self.duration:.3f}s "
f"markers={len(self.markers)}"
)
def aiff_info(path: str | Path) -> AiffInfo:
"""
Read AIFF/AIFF-C metadata without decoding audio data.
Example:
info = aiff_info("recording.aif")
print(info)
for mark in info.markers:
print(f" marker id={mark[0]} pos={mark[1]} name={mark[2]}")
"""
if not _AIFC_AVAILABLE:
raise ImportError("aifc not available (Python 3.13+ removed it)")
with _aifc.open(str(path), "r") as f:
ch = f.getnchannels()
sw = f.getsampwidth()
rate = f.getframerate()
nfr = f.getnframes()
ct = f.getcomptype()
cn = f.getcompname()
marks = f.getmarkers() or []
dur = nfr / rate if rate else 0.0
return AiffInfo(
path=str(path), channels=ch, sampwidth=sw,
framerate=rate, nframes=nfr, comptype=ct, compname=cn,
markers=list(marks), duration=dur,
)
def read_aiff(path: str | Path) -> tuple[AiffInfo, bytes]:
"""
Read an AIFF/AIFF-C file, returning (AiffInfo, raw_bytes).
Raw bytes are in the file's native endianness — use swap_endian() if needed.
Example:
info, raw = read_aiff("track.aif")
if info.is_little_endian:
pcm_be = swap_endian(raw, info.sampwidth)
"""
if not _AIFC_AVAILABLE:
raise ImportError("aifc not available")
with _aifc.open(str(path), "r") as f:
info = aiff_info(path)
f.rewind()
raw = f.readframes(f.getnframes())
return info, raw
# ─────────────────────────────────────────────────────────────────────────────
# 2. Write AIFF files
# ─────────────────────────────────────────────────────────────────────────────
def write_aiff(
path: str | Path,
pcm_bytes: bytes,
channels: int = 2,
sampwidth: int = 2,
framerate: int = 44100,
markers: list[tuple[int, int, str]] | None = None,
little_endian: bool = False,
) -> None:
"""
Write raw PCM bytes to an AIFF (big-endian) or AIFF-C sowt (little-endian) file.
Example:
write_aiff("output.aif", pcm_be_bytes, channels=2, framerate=44100)
write_aiff("output_le.aif", pcm_le_bytes, little_endian=True)
"""
if not _AIFC_AVAILABLE:
raise ImportError("aifc not available")
with _aifc.open(str(path), "w") as f:
f.setnchannels(channels)
f.setsampwidth(sampwidth)
f.setframerate(framerate)
if little_endian:
f.setcomptype(b"sowt", b"AIFF-C little-endian")
else:
f.setcomptype(b"NONE", b"not compressed")
if markers:
for mark_id, pos, name in markers:
f.setmark(mark_id, pos, name.encode() if isinstance(name, str) else name)
f.writeframes(pcm_bytes)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Endian swap helper
# ─────────────────────────────────────────────────────────────────────────────
def swap_endian(pcm: bytes, sampwidth: int) -> bytes:
"""
Swap byte order of 16-bit or 32-bit PCM samples.
Use when converting between AIFF (big-endian) and WAV/AIFF-C sowt (little-endian).
Example:
# AIFF big-endian → little-endian for WAV write
pcm_le = swap_endian(pcm_be, sampwidth=2)
"""
if sampwidth == 1:
return pcm # 8-bit is unsigned, no byte-swap needed
if sampwidth == 2:
fmt = f">{len(pcm)//2}h"
samples = struct.unpack(fmt, pcm)
return struct.pack(f"<{len(samples)}h", *samples)
if sampwidth == 3:
# 24-bit: swap 3-byte groups
out = bytearray()
for i in range(0, len(pcm), 3):
out.extend(pcm[i:i+3][::-1])
return bytes(out)
if sampwidth == 4:
fmt = f">{len(pcm)//4}i"
samples = struct.unpack(fmt, pcm)
return struct.pack(f"<{len(samples)}i", *samples)
raise ValueError(f"Unsupported sampwidth: {sampwidth}")
# ─────────────────────────────────────────────────────────────────────────────
# 4. Format converters
# ─────────────────────────────────────────────────────────────────────────────
def aiff_to_wav(src: str | Path, dst: str | Path) -> AiffInfo:
"""
Convert an AIFF/AIFF-C file to WAV.
Automatically swaps byte order from big-endian AIFF to little-endian WAV.
Returns AiffInfo for the source file.
Example:
info = aiff_to_wav("track.aif", "track.wav")
"""
info, raw = read_aiff(src)
# WAV wants little-endian; swap if AIFF big-endian
if info.sampwidth > 1 and not info.is_little_endian:
pcm_le = swap_endian(raw, info.sampwidth)
else:
pcm_le = raw
with wave.open(str(dst), "wb") as wf:
wf.setnchannels(info.channels)
wf.setsampwidth(info.sampwidth)
wf.setframerate(info.framerate)
wf.writeframes(pcm_le)
return info
def wav_to_aiff(src: str | Path, dst: str | Path, little_endian: bool = False) -> None:
"""
Convert a WAV file to AIFF. By default writes big-endian AIFF.
Set little_endian=True to produce AIFF-C sowt (compatible with Core Audio).
Example:
wav_to_aiff("speech.wav", "speech.aif")
wav_to_aiff("speech.wav", "speech_le.aif", little_endian=True)
"""
if not _AIFC_AVAILABLE:
raise ImportError("aifc not available")
with wave.open(str(src), "rb") as wf:
channels = wf.getnchannels()
sampwidth = wf.getsampwidth()
framerate = wf.getframerate()
pcm_le = wf.readframes(wf.getnframes())
# WAV is little-endian; AIFF wants big-endian
if not little_endian and sampwidth > 1:
pcm = swap_endian(pcm_le, sampwidth)
else:
pcm = pcm_le
write_aiff(dst, pcm, channels=channels, sampwidth=sampwidth,
framerate=framerate, little_endian=little_endian)
def aiff_from_bytes(data: bytes) -> tuple[AiffInfo, bytes]:
"""
Parse an AIFF/AIFF-C file from an in-memory bytes object.
Example:
with open("recording.aif", "rb") as f:
data = f.read()
info, frames = aiff_from_bytes(data)
"""
if not _AIFC_AVAILABLE:
raise ImportError("aifc not available")
buf = io.BytesIO(data)
with _aifc.open(buf, "r") as f:
nch = f.getnchannels()
sw = f.getsampwidth()
rate = f.getframerate()
nfr = f.getnframes()
ct = f.getcomptype()
cn = f.getcompname()
marks = f.getmarkers() or []
raw = f.readframes(nfr)
dur = nfr / rate if rate else 0.0
info = AiffInfo(path="<bytes>", channels=nch, sampwidth=sw,
framerate=rate, nframes=nfr, comptype=ct,
compname=cn, markers=list(marks), duration=dur)
return info, raw
# ─────────────────────────────────────────────────────────────────────────────
# 5. Marker utilities
# ─────────────────────────────────────────────────────────────────────────────
def extract_markers(path: str | Path) -> list[tuple[int, float, str]]:
"""
Extract AIFF markers as (id, time_seconds, name) tuples.
Returns empty list if no markers present.
Example:
for mark_id, t, name in extract_markers("session.aif"):
print(f" marker {mark_id} @ {t:.3f}s: {name}")
"""
info = aiff_info(path)
result = []
rate = info.framerate or 1
for mark_id, pos, name in info.markers:
t = pos / rate
name_str = name.decode(errors="replace") if isinstance(name, bytes) else name
result.append((mark_id, t, name_str))
return result
def add_markers_to_aiff(
src: str | Path,
dst: str | Path,
markers: list[tuple[int, float, str]],
) -> None:
"""
Copy an AIFF file and add new time-based markers (seconds).
Example:
add_markers_to_aiff("raw.aif", "marked.aif",
[(1, 2.5, "verse"), (2, 45.0, "chorus")])
"""
info, raw = read_aiff(src)
int_marks = [(mid, int(t * info.framerate), name)
for mid, t, name in markers]
write_aiff(dst, raw, channels=info.channels,
sampwidth=info.sampwidth, framerate=info.framerate,
markers=int_marks, little_endian=info.is_little_endian)
def scan_aiff_files(directory: str | Path, recursive: bool = False) -> list[AiffInfo]:
"""
Scan a directory for .aif and .aiff files, returning AiffInfo for each.
Example:
for info in scan_aiff_files("/Music/sessions", recursive=True):
print(info)
"""
root = Path(directory)
results = []
patterns = ["**/*.aif", "**/*.aiff"] if recursive else ["*.aif", "*.aiff"]
for pattern in patterns:
for p in sorted(root.glob(pattern)):
try:
results.append(aiff_info(p))
except Exception:
pass
return results
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import math
import tempfile
print("=== aifc demo ===")
if not _AIFC_AVAILABLE:
print(" aifc not available (Python 3.13+)")
print(" Demonstrating swap_endian only:")
be_samples = struct.pack(">4h", 100, -100, 200, -200)
le_samples = swap_endian(be_samples, 2)
back = swap_endian(le_samples, 2)
print(f" BE: {list(struct.unpack('>4h', be_samples))}")
print(f" LE: {list(struct.unpack('<4h', le_samples))}")
print(f" roundtrip: {be_samples == back}")
raise SystemExit(0)
with tempfile.TemporaryDirectory() as tmp:
# ── synthesize 440 Hz stereo PCM (big-endian for AIFF) ────────────────
rate = 44100; dur = 0.5; freq = 440.0
n = int(rate * dur)
be_bytes = bytearray()
for i in range(n):
val = int(16000 * math.sin(2 * math.pi * freq * i / rate))
val = max(-32768, min(32767, val))
be_bytes.extend(struct.pack(">h", val)) # big-endian L
be_bytes.extend(struct.pack(">h", val)) # big-endian R (same)
pcm_be = bytes(be_bytes)
# ── write AIFF ────────────────────────────────────────────────────────
aif_path = Path(tmp) / "tone.aif"
write_aiff(aif_path, pcm_be, channels=2, sampwidth=2, framerate=rate,
markers=[(1, n // 4, "quarter"), (2, n // 2, "half")])
print(f"\n--- wrote {aif_path.name} ({aif_path.stat().st_size} bytes) ---")
# ── read + inspect ────────────────────────────────────────────────────
info, raw = read_aiff(aif_path)
print(f"--- aiff_info: {info} ---")
# ── markers ───────────────────────────────────────────────────────────
print("\n--- extract_markers ---")
for mid, t, name in extract_markers(aif_path):
print(f" id={mid} t={t:.3f}s name={name!r}")
# ── endian swap ───────────────────────────────────────────────────────
print("\n--- swap_endian round-trip (first 4 samples) ---")
le = swap_endian(pcm_be[:16], 2)
back = swap_endian(le, 2)
print(f" BE→LE→BE match: {pcm_be[:16] == back}")
# ── aiff_to_wav ───────────────────────────────────────────────────────
wav_path = Path(tmp) / "tone.wav"
src_info = aiff_to_wav(aif_path, wav_path)
print(f"\n--- aiff_to_wav ---")
with wave.open(str(wav_path), "rb") as wf:
print(f" source: {src_info}")
print(f" WAV: {wf.getnchannels()}ch {wf.getframerate()}Hz "
f"{wf.getsampwidth()*8}bit {wf.getnframes()}fr")
# ── wav_to_aiff round-trip ────────────────────────────────────────────
aif2_path = Path(tmp) / "tone_roundtrip.aif"
wav_to_aiff(wav_path, aif2_path)
info2, raw2 = read_aiff(aif2_path)
print(f"\n--- wav_to_aiff: {info2} ---")
# Compare frames (should be identical after double swap)
print(f" frames match original: {pcm_be == raw2}")
# ── aiff_from_bytes ───────────────────────────────────────────────────
print("\n--- aiff_from_bytes ---")
file_bytes = aif_path.read_bytes()
mem_info, mem_raw = aiff_from_bytes(file_bytes)
print(f" {mem_info}")
print(f" frames match: {raw == mem_raw}")
print("\n=== done ===")
For the wave alternative — wave reads and writes RIFF WAV files (little-endian PCM), which is the universal audio interchange format supported by every DAW, audio library, and operating system — use wave for all modern audio I/O; use aifc only when reading .aif/.aiff files from Apple macOS, professional audio workstations, or archives that use AIFF’s big-endian PCM or AIFF-C compression, keeping in mind that aifc is removed in Python 3.13 so production code needs a fallback to soundfile (PyPI) or pure struct-based parsing. For the chunk alternative — Python’s chunk module parses generic IFF chunk-based binary formats (the basis of both RIFF/WAV and IFF/AIFF) at the raw block level — use chunk when you need to inspect or extract individual IFF chunks from an AIFF file without using aifc’s higher-level API, or when dealing with non-standard AIFF extensions that aifc ignores; use aifc for the standard get/set/readframes/writeframes interface. The Claude Skills 360 bundle includes aifc skill sets covering AiffInfo with aiff_info()/read_aiff() inspection, write_aiff() writer with marker support, swap_endian() byte-order conversion, aiff_to_wav()/wav_to_aiff()/aiff_from_bytes() converters, and extract_markers()/add_markers_to_aiff()/scan_aiff_files() marker utilities. Start with the free tier to try AIFF audio patterns and aifc pipeline code generation.