Python’s wave module reads and writes uncompressed PCM WAV files — the standard lossless audio container. import wave. open: wf = wave.open("audio.wav", "rb") — read mode; wave.open("out.wav", "wb") — write mode; use as context manager. Wave_read: wf.getnchannels() → 1 (mono) or 2 (stereo); wf.getsampwidth() → bytes per sample (1=8-bit, 2=16-bit, 4=32-bit); wf.getframerate() → samples/sec (e.g. 44100, 48000); wf.getnframes() → total frames; wf.readframes(n) → bytes of raw interleaved PCM; wf.getparams() → namedtuple(nchannels, sampwidth, framerate, nframes, comptype, compname). Wave_write: wf.setnchannels(n); wf.setsampwidth(n); wf.setframerate(n); wf.writeframes(data: bytes); wf.setparams(params). Frame = one sample per channel; for stereo 16-bit, one frame = 4 bytes (L_lo, L_hi, R_lo, R_hi). Use struct.unpack_from("<h", data, offset) to read signed 16-bit samples. array.array("h", data) for bulk conversion. Only PCM (comptype "NONE") is supported. Claude Code generates tone synthesizers, audio analyzers, mixers, trimmers, and format converters.
CLAUDE.md for wave
## wave Stack
- Stdlib: import wave, struct, array
- Read: with wave.open("in.wav") as wf:
params = wf.getparams()
frames = wf.readframes(wf.getnframes())
- Write: with wave.open("out.wav", "wb") as wf:
wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(44100)
wf.writeframes(pcm_bytes)
- Samples: arr = array.array("h", pcm_bytes) # 16-bit signed ints
- Frame: n_channels * sampwidth bytes per frame
wave Audio Pipeline
# app/waveutil.py — read, write, synthesize, trim, mix, normalize, analyze
from __future__ import annotations
import array
import io
import math
import struct
import wave
from dataclasses import dataclass
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. WAV metadata
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class WavInfo:
channels: int
sampwidth: int # bytes per sample
framerate: int # samples per second
nframes: int
duration_s: float # seconds
@property
def bit_depth(self) -> int:
return self.sampwidth * 8
@property
def sample_max(self) -> int:
return (1 << (self.bit_depth - 1)) - 1
def __str__(self) -> str:
ch = "stereo" if self.channels == 2 else "mono" if self.channels == 1 else f"{self.channels}ch"
return (f"{ch} {self.bit_depth}-bit {self.framerate} Hz "
f"{self.nframes} frames {self.duration_s:.3f}s")
def wav_info(path: str | Path) -> WavInfo:
"""
Return metadata for a WAV file without reading the audio data.
Example:
info = wav_info("audio.wav")
print(info)
"""
with wave.open(str(path), "rb") as wf:
p = wf.getparams()
dur = p.nframes / p.framerate if p.framerate else 0.0
return WavInfo(
channels=p.nchannels,
sampwidth=p.sampwidth,
framerate=p.framerate,
nframes=p.nframes,
duration_s=dur,
)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Sample helpers: bytes ↔ int arrays
# ─────────────────────────────────────────────────────────────────────────────
def frames_to_samples(
raw: bytes,
sampwidth: int,
channels: int = 1,
) -> list[list[int]]:
"""
Convert raw PCM frame bytes to a list of channel sample arrays.
Returns [channel_0_samples, channel_1_samples, ...].
Example:
samples = frames_to_samples(raw, sampwidth=2, channels=2)
left, right = samples
"""
fmt = {1: "b", 2: "h", 4: "i"}.get(sampwidth)
if fmt is None:
raise ValueError(f"Unsupported sampwidth: {sampwidth}")
total = len(raw) // sampwidth
all_samples = array.array(fmt, raw)
return [
list(all_samples[ch::channels])
for ch in range(channels)
]
def samples_to_frames(
channels_data: list[list[int]],
sampwidth: int,
) -> bytes:
"""
Convert per-channel sample lists back to interleaved PCM frame bytes.
Example:
frames = samples_to_frames([left, right], sampwidth=2)
wf.writeframes(frames)
"""
fmt = {1: "b", 2: "h", 4: "i"}.get(sampwidth)
if fmt is None:
raise ValueError(f"Unsupported sampwidth: {sampwidth}")
n_channels = len(channels_data)
n_frames = len(channels_data[0])
interleaved = array.array(fmt)
for i in range(n_frames):
for ch in range(n_channels):
interleaved.append(channels_data[ch][i])
return interleaved.tobytes()
# ─────────────────────────────────────────────────────────────────────────────
# 3. Read / write helpers
# ─────────────────────────────────────────────────────────────────────────────
def read_wav(path: str | Path) -> tuple[WavInfo, bytes]:
"""
Read a WAV file and return (info, raw_pcm_bytes).
Example:
info, raw = read_wav("audio.wav")
samples = frames_to_samples(raw, info.sampwidth, info.channels)
"""
with wave.open(str(path), "rb") as wf:
p = wf.getparams()
raw = wf.readframes(p.nframes)
info = WavInfo(
channels=p.nchannels,
sampwidth=p.sampwidth,
framerate=p.framerate,
nframes=p.nframes,
duration_s=p.nframes / p.framerate if p.framerate else 0.0,
)
return info, raw
def write_wav(
path: str | Path,
raw: bytes,
channels: int = 1,
sampwidth: int = 2,
framerate: int = 44100,
) -> None:
"""
Write raw PCM bytes to a WAV file.
Example:
write_wav("out.wav", pcm_bytes, channels=1, sampwidth=2, framerate=44100)
"""
with wave.open(str(path), "wb") as wf:
wf.setnchannels(channels)
wf.setsampwidth(sampwidth)
wf.setframerate(framerate)
wf.writeframes(raw)
def copy_wav_params(src: str | Path, dst_path: str | Path, new_frames: bytes) -> None:
"""Copy WAV parameters from src and write new_frames to dst_path."""
with wave.open(str(src), "rb") as wf:
p = wf.getparams()
with wave.open(str(dst_path), "wb") as wf:
wf.setparams(p)
wf.writeframes(new_frames)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Signal processing helpers
# ─────────────────────────────────────────────────────────────────────────────
def normalize(samples: list[int], target_peak: float = 0.9) -> list[int]:
"""
Scale samples to a target peak amplitude (0.0–1.0).
Preserves zero-centered DC offset.
Example:
normalized = normalize(left_channel, target_peak=0.95)
"""
if not samples:
return samples
peak = max(abs(s) for s in samples)
if peak == 0:
return samples
# Determine max value for int type
max_val = max(abs(s) for s in samples)
# Guess bit depth from data
if max_val <= 127:
int_max = 127
elif max_val <= 32767:
int_max = 32767
else:
int_max = 2147483647
scale = target_peak * int_max / peak
return [max(-int_max - 1, min(int_max, round(s * scale))) for s in samples]
def trim_silence(
samples: list[int],
threshold: float = 0.01,
int_max: int = 32767,
) -> list[int]:
"""
Remove leading and trailing silence from a sample list.
threshold: amplitude fraction below which is considered silence.
Example:
trimmed = trim_silence(samples)
"""
thresh = int(threshold * int_max)
start = 0
while start < len(samples) and abs(samples[start]) <= thresh:
start += 1
end = len(samples)
while end > start and abs(samples[end - 1]) <= thresh:
end -= 1
return samples[start:end]
def mix_mono(
a: list[int],
b: list[int],
weight_a: float = 0.5,
weight_b: float = 0.5,
) -> list[int]:
"""
Mix two mono sample arrays by weighted addition.
Lengths are padded to match with zeros.
Example:
mixed = mix_mono(voice, music, weight_a=0.7, weight_b=0.3)
"""
n = max(len(a), len(b))
result: list[int] = []
for i in range(n):
s_a = a[i] if i < len(a) else 0
s_b = b[i] if i < len(b) else 0
mixed = s_a * weight_a + s_b * weight_b
result.append(int(mixed))
return result
# ─────────────────────────────────────────────────────────────────────────────
# 5. Tone synthesis
# ─────────────────────────────────────────────────────────────────────────────
def generate_sine(
frequency: float,
duration: float,
amplitude: float = 0.8,
framerate: int = 44100,
sampwidth: int = 2,
) -> bytes:
"""
Generate a sine wave as raw PCM bytes.
frequency: Hz (e.g. 440 for A4)
duration: seconds
amplitude: 0.0–1.0
Example:
raw = generate_sine(440, 1.0)
write_wav("a440.wav", raw)
"""
int_max = (1 << (sampwidth * 8 - 1)) - 1
n_frames = int(framerate * duration)
fmt = {1: "b", 2: "h", 4: "i"}[sampwidth]
samples = array.array(fmt)
for i in range(n_frames):
t = i / framerate
value = amplitude * int_max * math.sin(2 * math.pi * frequency * t)
samples.append(int(value))
return samples.tobytes()
def generate_dtmf(
digit: str,
duration: float = 0.2,
framerate: int = 44100,
sampwidth: int = 2,
) -> bytes:
"""
Generate DTMF tone bytes for a single phone keypad digit.
Example:
tone = generate_dtmf("5", duration=0.3)
write_wav("dtmf5.wav", tone)
"""
# DTMF frequency table
dtmf = {
"1": (697, 1209), "2": (697, 1336), "3": (697, 1477),
"4": (770, 1209), "5": (770, 1336), "6": (770, 1477),
"7": (852, 1209), "8": (852, 1336), "9": (852, 1477),
"*": (941, 1209), "0": (941, 1336), "#": (941, 1477),
}
if digit not in dtmf:
raise ValueError(f"Unknown DTMF digit: {digit!r}")
f1, f2 = dtmf[digit]
raw1 = generate_sine(f1, duration, amplitude=0.4, framerate=framerate, sampwidth=sampwidth)
raw2 = generate_sine(f2, duration, amplitude=0.4, framerate=framerate, sampwidth=sampwidth)
fmt = {1: "b", 2: "h", 4: "i"}[sampwidth]
a1 = array.array(fmt, raw1)
a2 = array.array(fmt, raw2)
result = array.array(fmt, [a + b for a, b in zip(a1, a2)])
return result.tobytes()
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile, os
print("=== wave demo ===")
with tempfile.TemporaryDirectory() as tmpdir:
sine_path = os.path.join(tmpdir, "sine_440.wav")
dtmf_path = os.path.join(tmpdir, "dtmf_5.wav")
norm_path = os.path.join(tmpdir, "normalized.wav")
mixed_path = os.path.join(tmpdir, "mixed.wav")
# ── generate_sine ──────────────────────────────────────────────────────
print("\n--- generate_sine (440 Hz, 0.5s) ---")
raw = generate_sine(440.0, 0.5, amplitude=0.6)
write_wav(sine_path, raw, channels=1, sampwidth=2, framerate=44100)
info = wav_info(sine_path)
print(f" {info}")
print(f" file size: {os.path.getsize(sine_path):,d} bytes")
# ── generate_dtmf ──────────────────────────────────────────────────────
print("\n--- generate_dtmf (digit '5') ---")
dtmf_raw = generate_dtmf("5", duration=0.3)
write_wav(dtmf_path, dtmf_raw)
print(f" {wav_info(dtmf_path)}")
# ── read_wav + frames_to_samples ───────────────────────────────────────
print("\n--- read_wav + frames_to_samples ---")
info, raw2 = read_wav(sine_path)
channels_data = frames_to_samples(raw2, info.sampwidth, info.channels)
mono = channels_data[0]
print(f" nframes={info.nframes} n_samples={len(mono)}")
print(f" sample min={min(mono)} max={max(mono)} abs_peak={max(abs(s) for s in mono)}")
# ── normalize ──────────────────────────────────────────────────────────
print("\n--- normalize ---")
normed = normalize(mono, target_peak=0.95)
peak_before = max(abs(s) for s in mono)
peak_after = max(abs(s) for s in normed)
print(f" peak before={peak_before} after={peak_after} (target ~{int(0.95*32767)})")
normed_bytes = samples_to_frames([normed], sampwidth=2)
write_wav(norm_path, normed_bytes)
print(f" wrote {norm_path}")
# ── mix_mono ───────────────────────────────────────────────────────────
print("\n--- mix_mono (440 Hz + 880 Hz) ---")
raw_a = generate_sine(440, 0.3, amplitude=0.4)
raw_b = generate_sine(880, 0.3, amplitude=0.4)
sA = frames_to_samples(raw_a, 2)[0]
sB = frames_to_samples(raw_b, 2)[0]
mixed = mix_mono(sA, sB)
write_wav(mixed_path, samples_to_frames([mixed], 2))
print(f" mixed {wav_info(mixed_path)}")
# ── trim_silence ───────────────────────────────────────────────────────
print("\n--- trim_silence ---")
padded = [0] * 100 + mono[:500] + [0] * 50
trimmed = trim_silence(padded)
print(f" padded={len(padded)} trimmed={len(trimmed)}")
print("\n=== done ===")
For the soundfile / sounddevice alternative — soundfile (PyPI) wraps libsndfile and supports FLAC, OGG Vorbis, AIFF, and 32/64-bit float WAV in addition to standard PCM WAV; sounddevice (PyPI) streams audio through PortAudio for live recording and playback — use these when you need multi-format support beyond PCM WAV, float sample arrays via NumPy, or real-time audio I/O; use wave for zero-dependency WAV reading/writing in environments where installing native extensions is not possible or when distributing a self-contained Python tool. For the audioop alternative — audioop (stdlib, removed in 3.13) provided low-level operations on raw audio bytes: audioop.rms(), audioop.ratecv() (sample-rate conversion), audioop.ulaw2lin() — if you need those operations on Python ≤ 3.12 use audioop; on Python 3.13+ implement equivalent operations with array and math as shown in the pipeline above, or switch to soundfile + NumPy. The Claude Skills 360 bundle includes wave skill sets covering WavInfo with wav_info(), frames_to_samples()/samples_to_frames() PCM converters, read_wav()/write_wav()/copy_wav_params() I/O helpers, normalize()/trim_silence()/mix_mono() signal processors, and generate_sine()/generate_dtmf() tone synthesizers. Start with the free tier to try audio processing patterns and wave pipeline code generation.