"""Streaming spool and WAV writer for direct speaker-layout output.""" from __future__ import annotations import struct from pathlib import Path import numpy as np WAVE_FORMAT_PCM = 0x0001 WAVE_FORMAT_IEEE_FLOAT = 0x0003 WAVE_FORMAT_EXTENSIBLE = 0xFFFE _PCM_GUID = bytes.fromhex("0100000000001000800000aa00389b71") _FLOAT_GUID = bytes.fromhex("0300000000001000800000aa00389b71") class SpeakerPcmSpool: """Temporary interleaved float32 store with float64 peak analysis.""" def __init__(self, path, sample_count, channel_count): self.path = Path(path) self.sample_count = int(sample_count) self.channel_count = int(channel_count) self.position = 0 self.peak = 0.0 self.clipped_values = 0 self.values = np.memmap( self.path, dtype=" self.sample_count: raise ValueError("speaker spool received more samples than allocated") if not np.all(np.isfinite(values)): raise ValueError("speaker renderer produced NaN or infinity") absolute = np.abs(values) if absolute.size: self.peak = max(self.peak, float(np.max(absolute))) self.clipped_values += int(np.count_nonzero(absolute > 1.0)) self.values[self.position:self.position + len(values)] = values.astype(np.float32) self.position += len(values) def finalize(self): if self.position != self.sample_count: raise ValueError( f"speaker spool has {self.position} samples, expected {self.sample_count}") self.values.flush() return self def close(self): values = self.values self.values = None del values def _fmt_chunk(channel_count, rate, sample_format): if sample_format == "float32": bits = 32 bytes_per_sample = 4 simple_tag = WAVE_FORMAT_IEEE_FLOAT guid = _FLOAT_GUID elif sample_format == "int24": bits = 24 bytes_per_sample = 3 simple_tag = WAVE_FORMAT_PCM guid = _PCM_GUID else: raise ValueError(f"unsupported speaker WAV format: {sample_format}") block_align = channel_count * bytes_per_sample byte_rate = rate * block_align if channel_count <= 2: body = struct.pack( " 0xFFFFFFFF if use_rf64: # RF64 + ds64 + fmt + data. file_size = 12 + 36 + 8 + len(fmt) + 8 + data_size stream.write(b"RF64") stream.write(struct.pack("> 8) & 0xFF packed[:, 2] = (unsigned >> 16) & 0xFF return packed.tobytes() def write_speaker_wav(path, pcm, sample_format, *, rate=48000, chunk_samples=262144): """Write an interleaved float32 array/memmap as float32 or PCM24 WAV.""" target = Path(path) values = np.asarray(pcm) if values.ndim != 2: raise ValueError(f"speaker PCM must be 2D, got {values.shape}") sample_count, channel_count = values.shape target.parent.mkdir(parents=True, exist_ok=True) with target.open("wb") as stream: info = _write_header( stream, channel_count, sample_count, int(rate), sample_format) for start in range(0, sample_count, int(chunk_samples)): block = np.asarray(values[start:start + chunk_samples], dtype="