Claude Code for wave: Python WAV Audio File Reading and Writing — Claude Skills 360 Blog
Blog / AI / Claude Code for wave: Python WAV Audio File Reading and Writing
AI

Claude Code for wave: Python WAV Audio File Reading and Writing

Published: October 14, 2028
Read time: 5 min read
By: Claude Skills 360

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.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free