"""WAV read/write without scipy/soundfile (stdlib wave + numpy). Supports mono, multi-channel (e.g. Ambix 4/16/64), and float32/int16. """ from __future__ import annotations import struct import wave from pathlib import Path from typing import Tuple, Union import numpy as np PathLike = Union[str, Path] def read_wav(path: PathLike) -> Tuple[np.ndarray, int]: """Read a WAV file. Returns ------- audio : np.ndarray Shape (n_channels, n_samples), float64 in roughly [-1, 1]. sample_rate : int """ path = Path(path) with wave.open(str(path), "rb") as wf: nch = wf.getnchannels() sw = wf.getsampwidth() sr = wf.getframerate() nframes = wf.getnframes() raw = wf.readframes(nframes) if sw == 2: mono = np.frombuffer(raw, dtype=" 2^30-ish it's int; else might be float bits misread. # Prefer IEEE float if values look like floats when reinterpreted. arr_f = np.frombuffer(raw, dtype=" 1000: mono = arr_f else: mono = arr_i.astype(np.float64) / 2147483648.0 elif sw == 3: # 24-bit packed little-endian a = np.frombuffer(raw, dtype=np.uint8).reshape(-1, 3) vals = ( a[:, 0].astype(np.int32) | (a[:, 1].astype(np.int32) << 8) | (a[:, 2].astype(np.int32) << 16) ) vals = np.where(vals >= 0x800000, vals - 0x1000000, vals) mono = vals.astype(np.float64) / 8388608.0 elif sw == 1: mono = (np.frombuffer(raw, dtype=np.uint8).astype(np.float64) - 128.0) / 128.0 else: raise ValueError(f"unsupported sample width {sw}") if nch == 1: audio = mono.reshape(1, -1) else: audio = mono.reshape(-1, nch).T.copy() return audio, int(sr) def write_wav( path: PathLike, audio: np.ndarray, sample_rate: int, *, subtype: str = "pcm16", ) -> None: """Write WAV. audio shape (n_channels, n_samples) or (n_samples,).""" path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) a = np.asarray(audio, dtype=np.float64) if a.ndim == 1: a = a.reshape(1, -1) if a.ndim != 2: raise ValueError("audio must be (C,T) or (T,)") nch, n_samples = a.shape a = np.clip(a, -1.0, 1.0) if subtype == "pcm16": pcm = (a.T.reshape(-1) * 32767.0).astype(" np.ndarray: """Pad or truncate multi-channel audio to (max_order+1)**2 Ambix channels.""" nch = (max_order + 1) ** 2 a = np.asarray(audio, dtype=np.float64) if a.ndim == 1: a = a.reshape(1, -1) out = np.zeros((nch, a.shape[1]), dtype=np.float64) n = min(nch, a.shape[0]) out[:n] = a[:n] return out