Python’s sunau module reads and writes Sun AU (.au) and NeXT (.snd) audio files. import sunau. Open for reading: f = sunau.open(path, 'r'). Open for writing: f = sunau.open(path, 'w'). Reader attributes: .getnchannels() → int; .getsampwidth() → bytes per sample (1, 2, 3, or 4); .getframerate() → Hz; .getnframes() → total frames; .getcomptype() → 'ULAW', 'ALAW', or 'NONE'; .getcompname() → human-readable compression description; .getparams() → namedtuple(nchannels, sampwidth, framerate, nframes, comptype, compname). Read: .readframes(n) → bytes; .rewind(). Writer setup (before any writeframes): .setnchannels(n), .setsampwidth(n), .setframerate(n), .setcomptype(type, name) — defaults to 'NONE'/'not compressed'. Write: .writeframes(bytes). Close: .close() or use as context manager. AU files store headers in big-endian byte order; µ-law (ULAW) is 8-bit encoded speech-quality audio at 8000 Hz common in telephony. Note: sunau is deprecated in Python 3.11 and removed in 3.13 — include a compatibility guard. Claude Code generates AU readers, WAV↔AU converters, µ-law decoders, and multi-format audio inspectors.
CLAUDE.md for sunau
## sunau Stack
- Stdlib: import sunau (deprecated 3.11, removed 3.13 — guard with try/except)
- Read: with sunau.open(path, 'r') as f:
- params = f.getparams() # nchannels sampwidth framerate nframes
- raw = f.readframes(f.getnframes())
- Write: with sunau.open(path, 'w') as f:
- f.setnchannels(1); f.setsampwidth(2); f.setframerate(8000)
- f.writeframes(pcm_bytes)
- Note: AU is big-endian; ULAW compression is common in telephony .au files
sunau Sun AU Audio Pipeline
# app/sunauutil.py — read, write, inspect, convert, decode ULAW
from __future__ import annotations
import array
import io
import struct
import wave
from dataclasses import dataclass
from pathlib import Path
# Guard for Python 3.13+ where sunau is removed
try:
import sunau as _sunau
_SUNAU_AVAILABLE = True
except ImportError:
_SUNAU_AVAILABLE = False
# ─────────────────────────────────────────────────────────────────────────────
# 1. File inspection
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class AuInfo:
path: str
channels: int
sampwidth: int # bytes per sample
framerate: int # Hz
nframes: int
comptype: str # "ULAW", "ALAW", "NONE"
compname: str
duration: float # seconds
@property
def bit_depth(self) -> int:
return self.sampwidth * 8
def __str__(self) -> str:
return (
f"{Path(self.path).name}: "
f"{self.channels}ch {self.framerate}Hz "
f"{self.bit_depth}bit "
f"{self.comptype} "
f"{self.nframes}fr "
f"{self.duration:.3f}s"
)
def au_info(path: str | Path) -> AuInfo:
"""
Read metadata from a Sun AU file without decoding the audio data.
Example:
info = au_info("voice.au")
print(info)
"""
if not _SUNAU_AVAILABLE:
raise ImportError("sunau not available (Python 3.13+ removed it)")
with _sunau.open(str(path), "r") as f:
ch = f.getnchannels()
sw = f.getsampwidth()
rate = f.getframerate()
nfr = f.getnframes()
ct = f.getcomptype()
cn = f.getcompname()
duration = nfr / rate if rate else 0.0
return AuInfo(
path=str(path),
channels=ch,
sampwidth=sw,
framerate=rate,
nframes=nfr,
comptype=ct,
compname=cn,
duration=duration,
)
def read_au(path: str | Path) -> tuple[AuInfo, bytes]:
"""
Read an AU file, returning (AuInfo, raw_bytes).
Raw bytes are in the file's native encoding — use decode_ulaw() if ULAW.
Example:
info, raw = read_au("voice.au")
if info.comptype == "ULAW":
pcm = decode_ulaw(raw)
"""
if not _SUNAU_AVAILABLE:
raise ImportError("sunau not available")
with _sunau.open(str(path), "r") as f:
info_obj = au_info(path)
f.rewind()
raw = f.readframes(f.getnframes())
return info_obj, raw
# ─────────────────────────────────────────────────────────────────────────────
# 2. Write AU files
# ─────────────────────────────────────────────────────────────────────────────
def write_au(
path: str | Path,
pcm_bytes: bytes,
channels: int = 1,
sampwidth: int = 2,
framerate: int = 8000,
comptype: str = "NONE",
) -> None:
"""
Write raw PCM (or ULAW/ALAW) bytes to a Sun AU file.
Example:
write_au("output.au", pcm_bytes, channels=1, sampwidth=2, framerate=8000)
"""
if not _SUNAU_AVAILABLE:
raise ImportError("sunau not available")
compname = {"NONE": "not compressed", "ULAW": "CCITT G.711 u-law",
"ALAW": "CCITT G.711 A-law"}.get(comptype.upper(), "not compressed")
with _sunau.open(str(path), "w") as f:
f.setnchannels(channels)
f.setsampwidth(sampwidth)
f.setframerate(framerate)
f.setcomptype(comptype.upper(), compname)
f.writeframes(pcm_bytes)
# ─────────────────────────────────────────────────────────────────────────────
# 3. µ-law (ULAW) codec (pure Python — works even without sunau)
# ─────────────────────────────────────────────────────────────────────────────
# ITU-T G.711 µ-law (ULAW) decode table — 256 entries, output is 16-bit signed
_ULAW_TABLE: list[int] = []
def _build_ulaw_table() -> None:
"""Build the ULAW decode lookup table once at import time."""
for byte in range(256):
# Invert all bits (ULAW encoding inverts the byte)
b = ~byte & 0xFF
sign = b & 0x80
exp = (b >> 4) & 0x07
mant = b & 0x0F
val = ((mant << 3) + 132) << exp
val -= 132
_ULAW_TABLE.append(-val if sign else val)
_build_ulaw_table()
def decode_ulaw(data: bytes) -> bytes:
"""
Decode CCITT G.711 µ-law compressed bytes to 16-bit signed little-endian PCM.
Example:
info, ulaw_bytes = read_au("voice.au")
if info.comptype == "ULAW":
pcm16 = decode_ulaw(ulaw_bytes)
"""
out = array.array("h", (_ULAW_TABLE[b] for b in data))
return out.tobytes()
def encode_ulaw(pcm_bytes: bytes) -> bytes:
"""
Encode 16-bit signed little-endian PCM to CCITT G.711 µ-law bytes.
Input must be 16-bit PCM (2 bytes per sample).
Example:
ulaw = encode_ulaw(pcm16_bytes)
write_au("compressed.au", ulaw, comptype="ULAW", sampwidth=1)
"""
MU = 255
out = bytearray()
samples = array.array("h", pcm_bytes)
for sample in samples:
if sample < 0:
sign = 0x80
sample = -sample
else:
sign = 0
sample = min(sample, 32635)
sample += 132
# Find exponent
exp = 7
exp_mask = 0x4000
while exp > 0 and not (sample & exp_mask):
exp -= 1
exp_mask >>= 1
mantissa = (sample >> max(exp + 3, 0)) & 0x0F
encoded = ~(sign | (exp << 4) | mantissa) & 0xFF
out.append(encoded)
return bytes(out)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Format converters
# ─────────────────────────────────────────────────────────────────────────────
def au_to_wav(src: str | Path, dst: str | Path) -> AuInfo:
"""
Convert an AU file to WAV. Handles ULAW → PCM16 expansion automatically.
Returns AuInfo for the source file.
Example:
info = au_to_wav("voice.au", "voice.wav")
print(info)
"""
info, raw = read_au(src)
# Decode ULAW if needed
if info.comptype == "ULAW":
pcm = decode_ulaw(raw)
sampwidth = 2
else:
pcm = raw
sampwidth = info.sampwidth
with wave.open(str(dst), "wb") as wf:
wf.setnchannels(info.channels)
wf.setsampwidth(sampwidth)
wf.setframerate(info.framerate)
wf.writeframes(pcm)
return info
def wav_to_au(src: str | Path, dst: str | Path, comptype: str = "NONE") -> None:
"""
Convert a WAV file to AU format.
Set comptype="ULAW" to encode as µ-law (reduces to 8-bit telephony format).
Example:
wav_to_au("speech.wav", "speech.au", comptype="ULAW")
"""
if not _SUNAU_AVAILABLE:
raise ImportError("sunau not available")
with wave.open(str(src), "rb") as wf:
channels = wf.getnchannels()
sampwidth = wf.getsampwidth()
framerate = wf.getframerate()
pcm = wf.readframes(wf.getnframes())
if comptype.upper() == "ULAW":
# Encode PCM16 → ULAW (requires 16-bit input)
raw = encode_ulaw(pcm) if sampwidth == 2 else pcm
sampwidth = 1
else:
raw = pcm
write_au(dst, raw, channels=channels, sampwidth=sampwidth,
framerate=framerate, comptype=comptype.upper())
def au_from_bytes(data: bytes) -> tuple[AuInfo, bytes]:
"""
Parse an AU file from an in-memory bytes object.
Example:
with open("voice.au", "rb") as f:
data = f.read()
info, frames = au_from_bytes(data)
"""
if not _SUNAU_AVAILABLE:
raise ImportError("sunau not available")
buf = io.BytesIO(data)
with _sunau.open(buf, "r") as f:
nch = f.getnchannels()
sw = f.getsampwidth()
rate = f.getframerate()
nfr = f.getnframes()
ct = f.getcomptype()
cn = f.getcompname()
raw = f.readframes(nfr)
dur = nfr / rate if rate else 0.0
info = AuInfo(path="<bytes>", channels=nch, sampwidth=sw,
framerate=rate, nframes=nfr, comptype=ct,
compname=cn, duration=dur)
return info, raw
# ─────────────────────────────────────────────────────────────────────────────
# 5. Batch inspector
# ─────────────────────────────────────────────────────────────────────────────
def scan_au_files(directory: str | Path, recursive: bool = False) -> list[AuInfo]:
"""
Scan a directory for .au files and return AuInfo for each.
Example:
for info in scan_au_files("/usr/share/sounds"):
print(info)
"""
root = Path(directory)
pattern = "**/*.au" if recursive else "*.au"
results = []
for p in sorted(root.glob(pattern)):
try:
results.append(au_info(p))
except Exception:
pass
return results
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile
print("=== sunau demo ===")
if not _SUNAU_AVAILABLE:
print(" sunau not available (Python 3.13+)")
print(" Demonstrating ULAW codec only:")
# Synthesize a 440 Hz tone in 16-bit PCM
import math
rate = 8000; dur = 0.5
samples = array.array("h")
for i in range(int(rate * dur)):
v = int(10000 * math.sin(2 * math.pi * 440 * i / rate))
samples.append(max(-32768, min(32767, v)))
pcm = samples.tobytes()
ulaw = encode_ulaw(pcm)
back = decode_ulaw(ulaw)
print(f" PCM16 samples: {len(samples)}")
print(f" ULAW bytes: {len(ulaw)} ({len(ulaw)/len(pcm)*100:.0f}% of PCM)")
print(f" Decoded back: {len(back)//2} samples")
raise SystemExit(0)
with tempfile.TemporaryDirectory() as tmp:
# ── synthesize a short PCM tone ────────────────────────────────────────
import math
rate = 8000; dur = 1.0; freq = 440.0
samples = array.array("h")
for i in range(int(rate * dur)):
v = int(16000 * math.sin(2 * math.pi * freq * i / rate))
samples.append(max(-32768, min(32767, v)))
pcm16 = samples.tobytes()
# ── write AU (uncompressed) ────────────────────────────────────────────
au_path = Path(tmp) / "tone.au"
write_au(au_path, pcm16, channels=1, sampwidth=2, framerate=rate)
print(f"\n--- wrote {au_path.name} ({au_path.stat().st_size} bytes) ---")
# ── read back and inspect ──────────────────────────────────────────────
info, raw = read_au(au_path)
print(f"--- au_info: {info} ---")
# ── write ULAW AU ──────────────────────────────────────────────────────
ulaw_bytes = encode_ulaw(pcm16)
ulaw_path = Path(tmp) / "tone_ulaw.au"
write_au(ulaw_path, ulaw_bytes, channels=1, sampwidth=1,
framerate=rate, comptype="ULAW")
print(f"\n--- ULAW AU: {ulaw_path.stat().st_size} bytes "
f"({ulaw_path.stat().st_size / au_path.stat().st_size * 100:.0f}% of PCM) ---")
# ── round-trip au_to_wav ───────────────────────────────────────────────
wav_path = Path(tmp) / "tone.wav"
src_info = au_to_wav(au_path, wav_path)
print(f"\n--- au_to_wav ---")
print(f" source: {src_info}")
with wave.open(str(wav_path), "rb") as wf:
print(f" WAV: {wf.getnchannels()}ch {wf.getframerate()}Hz "
f"{wf.getsampwidth()*8}bit {wf.getnframes()}fr")
# ── au_from_bytes ──────────────────────────────────────────────────────
print("\n--- au_from_bytes ---")
raw_file = au_path.read_bytes()
mem_info, mem_frames = au_from_bytes(raw_file)
print(f" {mem_info}")
print(f" frames match: {raw == mem_frames}")
# ── ULAW encode/decode round-trip ──────────────────────────────────────
print("\n--- ULAW round-trip (first 8 samples) ---")
ulaw8 = encode_ulaw(pcm16[:16])
decoded = decode_ulaw(ulaw8)
orig_s = array.array("h", pcm16[:16])
dec_s = array.array("h", decoded)
for o, d in zip(orig_s, dec_s):
print(f" orig={o:6d} decoded={d:6d} err={abs(o-d):4d}")
print("\n=== done ===")
For the wave alternative — Python’s wave module reads and writes RIFF WAV files, which are the modern ubiquitous audio container format with uncompressed 16-bit PCM — use wave for all general-purpose audio I/O since WAV is universally supported; use sunau only when you specifically need to read .au/.snd files from legacy Unix workstations, NeXT systems, or telephony archives, or when you need µ-law ULAW encoding for 8-bit speech compression. For the audioop alternative — audioop.ulaw2lin(data, width) and audioop.lin2ulaw(data, width) convert between µ-law and linear PCM directly — note that both audioop and sunau are deprecated in Python 3.11 and removed in Python 3.13, so production code should use the pure-Python encode_ulaw()/decode_ulaw() implementations above or a library like soundfile (PyPI) which wraps libsndfile for ULAW/ALAW/FLAC/OGG support via a stable API. The Claude Skills 360 bundle includes sunau skill sets covering AuInfo with au_info()/read_au() inspection, write_au() writer, decode_ulaw()/encode_ulaw() pure-Python G.711 codec, au_to_wav()/wav_to_au()/au_from_bytes() converters, and scan_au_files() directory scanner. Start with the free tier to try AU audio patterns and sunau pipeline code generation.