Python’s audioop module performs sample-level operations on raw PCM audio buffers. import audioop. All functions take (fragment, width) where fragment is bytes and width is the sample byte width: 1 (8-bit unsigned), 2 (16-bit signed), or 4 (32-bit signed). Level: audioop.max(frag, width) → int; audioop.min(frag, width); audioop.minmax(frag, width) → (min, max); audioop.avg(frag, width) → int; audioop.rms(frag, width) → int (power). Gain: audioop.mul(frag, width, factor) → bytes (clip); audioop.bias(frag, width, bias) → bytes; audioop.lin2lin(frag, inwidth, outwidth) → bytes (bit depth conversion). Channels: audioop.tomono(frag, width, lfactor, rfactor) → bytes (stereo→mono); audioop.tostereo(frag, width, lfactor, rfactor) → bytes (mono→stereo). Codecs: audioop.lin2ulaw(frag, width) → bytes; audioop.ulaw2lin(data, width) → bytes; audioop.lin2alaw(frag, width); audioop.alaw2lin(data, width); audioop.lin2adpcm(frag, width, state) → (bytes, state); audioop.adpcm2lin(data, width, state) → (bytes, state). Rate: audioop.ratecv(frag, width, nchannels, inrate, outrate, state, weightA, weightB) → (bytes, state). Combine: audioop.add(frag1, frag2, width) → bytes (mix); audioop.cross(frag, width) → int (zero-crossing count). Note: deprecated 3.11, removed 3.13 — include compatibility guard. Claude Code generates audio normalizers, resampling pipelines, codec converters, stereo mixers, and PCM signal analyzers.
CLAUDE.md for audioop
## audioop Stack
- Stdlib: import audioop (deprecated 3.11, removed 3.13 — guard with try/except)
- Level: audioop.rms(frag, width) audioop.max(frag, width)
- Gain: audioop.mul(frag, width, factor) # clip to range
- Codec: audioop.lin2ulaw(frag, width) audioop.ulaw2lin(data, width)
- Rate: frag, state = audioop.ratecv(frag, w, ch, inrate, outrate, None)
- Mix: audioop.add(frag1, frag2, width) audioop.tomono(frag, width, 0.5, 0.5)
- Note: width = bytes per sample: 1=8bit, 2=16bit, 4=32bit
audioop PCM Signal Operations Pipeline
# app/audiooputil.py — level, gain, codec, resample, mix (with py3.13 fallback)
from __future__ import annotations
import array
import math
import struct
import wave
from dataclasses import dataclass
from pathlib import Path
# Guard for Python 3.13+ where audioop is removed
try:
import audioop as _audioop
_AUDIOOP_AVAILABLE = True
except ImportError:
_AUDIOOP_AVAILABLE = False
# ─────────────────────────────────────────────────────────────────────────────
# 1. Level measurement
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class LevelInfo:
peak: int # max absolute sample value
rms: int # root mean square level
minimum: int # minimum raw sample
maximum: int # maximum raw sample
width: int # bytes per sample
@property
def peak_db(self) -> float:
"""Peak level in dBFS (0 dBFS = full scale)."""
full_scale = (1 << (self.width * 8 - 1)) - 1
return 20 * math.log10(self.peak / full_scale) if self.peak > 0 else -math.inf
@property
def rms_db(self) -> float:
"""RMS level in dBFS."""
full_scale = (1 << (self.width * 8 - 1)) - 1
return 20 * math.log10(self.rms / full_scale) if self.rms > 0 else -math.inf
def __str__(self) -> str:
return (f"peak={self.peak} rms={self.rms} "
f"peak_db={self.peak_db:.1f}dBFS rms_db={self.rms_db:.1f}dBFS "
f"min={self.minimum} max={self.maximum}")
def measure_level(frag: bytes, width: int) -> LevelInfo:
"""
Measure peak, RMS, min, and max of a PCM buffer.
Example:
info = measure_level(pcm_bytes, width=2)
print(info)
"""
if _AUDIOOP_AVAILABLE:
peak = _audioop.max(frag, width)
rms = _audioop.rms(frag, width)
mn, mx = _audioop.minmax(frag, width)
else:
# Pure Python fallback
fmt = {1: "B", 2: "<h", 4: "<i"}[width]
step = width
unpacked = [struct.unpack_from(fmt, frag, i)[0]
for i in range(0, len(frag), step)]
if not unpacked:
return LevelInfo(0, 0, 0, 0, width)
mn = min(unpacked)
mx = max(unpacked)
peak = max(abs(mn), abs(mx))
rms = int(math.sqrt(sum(s * s for s in unpacked) / len(unpacked)))
return LevelInfo(peak=peak, rms=rms, minimum=mn, maximum=mx, width=width)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Gain / normalization
# ─────────────────────────────────────────────────────────────────────────────
def apply_gain(frag: bytes, width: int, factor: float) -> bytes:
"""
Multiply all samples by factor, clipping to the valid range.
Example:
quieter = apply_gain(pcm_bytes, 2, 0.5)
louder = apply_gain(pcm_bytes, 2, 2.0)
"""
if _AUDIOOP_AVAILABLE:
return _audioop.mul(frag, width, factor)
# Pure Python fallback (16-bit only)
fmt = "<h" if width == 2 else ">b"
out = bytearray()
lim = (1 << (width * 8 - 1)) - 1
for i in range(0, len(frag), width):
s = struct.unpack_from("<h", frag, i)[0]
v = max(-lim - 1, min(lim, int(s * factor)))
out.extend(struct.pack("<h", v))
return bytes(out)
def normalize(frag: bytes, width: int, target_peak: float = 0.9) -> bytes:
"""
Normalize a PCM buffer so its peak reaches target_peak fraction of full scale.
Does nothing if the buffer is silent.
Example:
normalized = normalize(pcm_bytes, width=2, target_peak=0.9)
"""
if _AUDIOOP_AVAILABLE:
peak = _audioop.max(frag, width)
else:
info = measure_level(frag, width)
peak = info.peak
if peak == 0:
return frag
full_scale = (1 << (width * 8 - 1)) - 1
factor = (full_scale * target_peak) / peak
return apply_gain(frag, width, factor)
def add_bias(frag: bytes, width: int, bias: int) -> bytes:
"""
Add a constant DC bias to each sample (useful for DC offset removal).
Example:
centered = add_bias(pcm_bytes, 2, -avg_sample_value)
"""
if _AUDIOOP_AVAILABLE:
return _audioop.bias(frag, width, bias)
out = bytearray()
lim = (1 << (width * 8 - 1)) - 1
for i in range(0, len(frag), width):
s = struct.unpack_from("<h", frag, i)[0]
v = max(-lim - 1, min(lim, s + bias))
out.extend(struct.pack("<h", v))
return bytes(out)
def change_bit_depth(frag: bytes, in_width: int, out_width: int) -> bytes:
"""
Convert PCM between bit depths (1, 2, or 4 bytes per sample).
Example:
pcm_24bit = change_bit_depth(pcm_16bit, in_width=2, out_width=3)
"""
if _AUDIOOP_AVAILABLE:
return _audioop.lin2lin(frag, in_width, out_width)
raise NotImplementedError("lin2lin fallback requires audioop")
# ─────────────────────────────────────────────────────────────────────────────
# 3. Channel conversion
# ─────────────────────────────────────────────────────────────────────────────
def stereo_to_mono(frag: bytes, width: int,
left_weight: float = 0.5, right_weight: float = 0.5) -> bytes:
"""
Mix stereo interleaved PCM to mono.
frag must be 2*width bytes per frame (L, R interleaved).
Example:
mono = stereo_to_mono(stereo_bytes, width=2)
"""
if _AUDIOOP_AVAILABLE:
return _audioop.tomono(frag, width, left_weight, right_weight)
# Pure Python (16-bit only)
out = bytearray()
for i in range(0, len(frag), width * 2):
l = struct.unpack_from("<h", frag, i)[0]
r = struct.unpack_from("<h", frag, i + width)[0]
v = max(-32768, min(32767, int(l * left_weight + r * right_weight)))
out.extend(struct.pack("<h", v))
return bytes(out)
def mono_to_stereo(frag: bytes, width: int,
left_weight: float = 1.0, right_weight: float = 1.0) -> bytes:
"""
Duplicate mono PCM to stereo interleaved, with independent channel scaling.
Example:
stereo = mono_to_stereo(mono_bytes, width=2)
"""
if _AUDIOOP_AVAILABLE:
return _audioop.tostereo(frag, width, left_weight, right_weight)
out = bytearray()
lim = (1 << (width * 8 - 1)) - 1
for i in range(0, len(frag), width):
s = struct.unpack_from("<h", frag, i)[0]
l = max(-lim - 1, min(lim, int(s * left_weight)))
r = max(-lim - 1, min(lim, int(s * right_weight)))
out.extend(struct.pack("<h", l))
out.extend(struct.pack("<h", r))
return bytes(out)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Codec conversion
# ─────────────────────────────────────────────────────────────────────────────
def to_ulaw(frag: bytes, width: int) -> bytes:
"""Encode linear PCM to G.711 µ-law (8-bit, 1 byte/sample)."""
if _AUDIOOP_AVAILABLE:
return _audioop.lin2ulaw(frag, width)
from .sunauutil import encode_ulaw # pure Python fallback from sunauutil
if width != 2:
raise ValueError("encode_ulaw fallback requires 16-bit PCM")
return encode_ulaw(frag)
def from_ulaw(data: bytes, width: int = 2) -> bytes:
"""Decode G.711 µ-law bytes to linear PCM at the given bit depth."""
if _AUDIOOP_AVAILABLE:
return _audioop.ulaw2lin(data, width)
from .sunauutil import decode_ulaw
pcm16 = decode_ulaw(data)
if width == 2:
return pcm16
return change_bit_depth(pcm16, 2, width)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Sample rate conversion
# ─────────────────────────────────────────────────────────────────────────────
def resample(
frag: bytes,
width: int,
nchannels: int,
in_rate: int,
out_rate: int,
) -> bytes:
"""
Resample PCM audio from in_rate to out_rate Hz.
Uses audioop.ratecv internally (linear interpolation).
Falls back to a simple linear resampler when audioop is unavailable.
Example:
pcm_48k = resample(pcm_44k, width=2, nchannels=2,
in_rate=44100, out_rate=48000)
"""
if _AUDIOOP_AVAILABLE:
result, _ = _audioop.ratecv(frag, width, nchannels,
in_rate, out_rate, None)
return result
# Pure Python linear interpolation fallback (mono 16-bit only)
if width != 2 or nchannels != 1:
raise NotImplementedError("Pure Python resample only supports mono 16-bit")
samples = array.array("h", frag)
n_out = int(len(samples) * out_rate / in_rate)
out = array.array("h")
for i in range(n_out):
src = i * in_rate / out_rate
idx = int(src)
frac = src - idx
a = samples[min(idx, len(samples) - 1)]
b = samples[min(idx + 1, len(samples) - 1)]
out.append(int(a + frac * (b - a)))
return out.tobytes()
# ─────────────────────────────────────────────────────────────────────────────
# 6. Mix / combine
# ─────────────────────────────────────────────────────────────────────────────
def mix(frag1: bytes, frag2: bytes, width: int,
gain1: float = 1.0, gain2: float = 1.0) -> bytes:
"""
Mix two PCM buffers sample-by-sample (same length and format required).
Applies per-channel gain before adding.
Example:
mixed = mix(voice_pcm, music_pcm, width=2, gain1=1.0, gain2=0.3)
"""
if gain1 != 1.0:
frag1 = apply_gain(frag1, width, gain1)
if gain2 != 1.0:
frag2 = apply_gain(frag2, width, gain2)
# Pad shorter buffer with silence
if len(frag1) < len(frag2):
frag1 = frag1 + bytes(len(frag2) - len(frag1))
elif len(frag2) < len(frag1):
frag2 = frag2 + bytes(len(frag1) - len(frag2))
if _AUDIOOP_AVAILABLE:
return _audioop.add(frag1, frag2, width)
# Pure Python fallback (16-bit)
out = bytearray()
for i in range(0, len(frag1), width):
a = struct.unpack_from("<h", frag1, i)[0]
b = struct.unpack_from("<h", frag2, i)[0]
v = max(-32768, min(32767, a + b))
out.extend(struct.pack("<h", v))
return bytes(out)
def zero_crossing_rate(frag: bytes, width: int) -> float:
"""
Return the fraction of samples that cross zero (0.0–1.0).
High ZCR indicates noise or high-frequency content; low ZCR indicates tonal signal.
Example:
zcr = zero_crossing_rate(pcm_bytes, 2)
print(f"ZCR: {zcr:.3f}")
"""
if _AUDIOOP_AVAILABLE:
crossings = _audioop.cross(frag, width)
n_samples = len(frag) // width
return crossings / n_samples if n_samples > 1 else 0.0
# Pure Python fallback
samples = array.array("h", frag)
if len(samples) < 2:
return 0.0
crossings = sum(1 for i in range(1, len(samples))
if (samples[i - 1] >= 0) != (samples[i] >= 0))
return crossings / len(samples)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== audioop demo ===")
if not _AUDIOOP_AVAILABLE:
print(" audioop not available (Python 3.13+); using pure Python fallbacks")
# ── synthesize two tones ──────────────────────────────────────────────────
rate = 44100; dur = 0.5
n = int(rate * dur)
a440 = array.array("h")
a880 = array.array("h")
for i in range(n):
v440 = int(16000 * math.sin(2 * math.pi * 440 * i / rate))
v880 = int(8000 * math.sin(2 * math.pi * 880 * i / rate))
a440.append(max(-32768, min(32767, v440)))
a880.append(max(-32768, min(32767, v880)))
pcm440 = a440.tobytes()
pcm880 = a880.tobytes()
# ── level measurement ─────────────────────────────────────────────────────
print("\n--- measure_level ---")
info = measure_level(pcm440, 2)
print(f" 440Hz: {info}")
info2 = measure_level(pcm880, 2)
print(f" 880Hz: {info2}")
# ── gain ──────────────────────────────────────────────────────────────────
print("\n--- apply_gain + normalize ---")
quiet = apply_gain(pcm440, 2, 0.25)
print(f" 0.25x gain peak: {measure_level(quiet, 2).peak}")
normed = normalize(pcm440, 2, 0.9)
print(f" normalized peak: {measure_level(normed, 2).peak}")
fs = 32767
print(f" 90% full-scale = {int(fs * 0.9)}")
# ── stereo ────────────────────────────────────────────────────────────────
print("\n--- mono↔stereo ---")
stereo = mono_to_stereo(pcm440, 2)
print(f" mono {len(pcm440)}B → stereo {len(stereo)}B")
back_mono = stereo_to_mono(stereo, 2)
print(f" stereo → mono {len(back_mono)}B")
lvl = measure_level(back_mono, 2)
print(f" back_mono level: {lvl.peak}")
# ── ULAW codec ────────────────────────────────────────────────────────────
print("\n--- ULAW encode/decode ---")
ulaw = to_ulaw(pcm440, 2)
pcm_back = from_ulaw(ulaw, 2)
print(f" PCM16: {len(pcm440)}B ULAW: {len(ulaw)}B ratio: {len(ulaw)/len(pcm440)*100:.0f}%")
orig_lvl = measure_level(pcm440, 2)
back_lvl = measure_level(pcm_back, 2)
print(f" original rms={orig_lvl.rms} decoded rms={back_lvl.rms}")
# ── resample ──────────────────────────────────────────────────────────────
print("\n--- resample 44100→48000 ---")
mono440 = a440.tobytes() # mono 16-bit
resampled = resample(mono440, 2, 1, 44100, 48000)
expected = int(len(mono440) * 48000 / 44100)
print(f" 44100Hz: {len(mono440)//2} samples "
f"48000Hz: {len(resampled)//2} samples "
f"expected≈{expected//2}")
# ── mix two tones ─────────────────────────────────────────────────────────
print("\n--- mix 440Hz + 880Hz ---")
mixed = mix(pcm440, pcm880, 2, gain1=1.0, gain2=0.5)
print(f" mixed peak={measure_level(mixed, 2).peak}")
# ── ZCR ───────────────────────────────────────────────────────────────────
print("\n--- zero_crossing_rate ---")
print(f" 440Hz ZCR={zero_crossing_rate(pcm440, 2):.4f} "
f"(expected ≈{2*440/rate:.4f})")
print(f" 880Hz ZCR={zero_crossing_rate(pcm880, 2):.4f} "
f"(expected ≈{2*880/rate:.4f})")
print("\n=== done ===")
For the soundfile (PyPI) alternative — soundfile.read(path) and soundfile.write(path, data, samplerate) wrap libsndfile and provide cross-platform reading and writing of WAV, FLAC, OGG, AIFF, and other formats along with resampling, channel conversion, and format normalization via a stable NumPy array API — use soundfile in any new code needing audio signal processing since it works on Python 3.13+ and offers far wider format support than audioop; use audioop sketches as reference for the signal operations (gain, RMS, rate conversion, µ-law codec) and then port to soundfile + numpy for production. For the array + struct alternative — array.array("h", pcm_bytes) and arithmetic on the resulting integers provide all the gain, normalize, and mix operations that audioop exposes, without the deprecated module dependency — combine array for storage with math.sqrt for RMS, list comprehensions for gain/clip, and struct.pack for ULAW encoding to replicate every audioop feature in stdlib-only Python 3.13 safe code. The Claude Skills 360 bundle includes audioop skill sets covering LevelInfo with measure_level() peak/RMS/dBFS, apply_gain()/normalize()/add_bias()/change_bit_depth() signal processing, stereo_to_mono()/mono_to_stereo() channel conversion, to_ulaw()/from_ulaw() G.711 codec, resample() rate conversion, mix()/zero_crossing_rate() combination analysis — all with pure Python fallbacks. Start with the free tier to try PCM signal processing patterns and audioop pipeline code generation.