"""Audio encoding helper: float/int PCM waveform → compact streamable bytes. A provider that renders raw audio (MMS → 16 kHz float waveform) calls :func:`encode_waveform` to get ``(audio_bytes, mime)`` ready to store + serve. The pipeline never special-cases a codec — it just stores the bytes + mime. Preferred output is **Opus in an Ogg container** (``audio/ogg``) — ~24-32 kbps mono speech ≈ 3-4 KB/s, matching the contract's "keep clips small" note and the FE's ``Accept: audio/ogg`` preference. If the libsndfile build lacks the Opus subtype we fall back to Ogg/Vorbis (still ``audio/ogg``). A plain-WAV fallback exists only as a last resort so a render is never lost to an encoder gap. Opus mandates a 48 kHz sample rate; libsndfile resamples on write, but we also pre-handle the rate so duration math stays exact regardless of subtype. """ from __future__ import annotations import io from functools import lru_cache # Opus only accepts these container sample rates; 48000 is the canonical one. _OPUS_RATE = 48000 @lru_cache(maxsize=1) def _ogg_subtype() -> str | None: """Best Ogg subtype this libsndfile supports: 'OPUS' > 'VORBIS' > None.""" try: import soundfile as sf subs = sf.available_subtypes("OGG") if "OPUS" in subs: return "OPUS" if "VORBIS" in subs: return "VORBIS" except Exception: # noqa: BLE001 return None return None def duration_ms_for(num_frames: int, sample_rate: int) -> int: """Clip length in ms from frame count + sample rate (pre-encode, exact).""" if sample_rate <= 0: return 0 return int(round(num_frames * 1000.0 / sample_rate)) def encode_waveform(samples, sample_rate: int) -> tuple[bytes, str, int]: """Encode a 1-D PCM waveform to compact bytes. ``samples`` is a 1-D numpy float array (mono, typically in [-1, 1]) or any array soundfile accepts. Returns ``(audio_bytes, mime, duration_ms)``. Tries Opus/Ogg, then Vorbis/Ogg (both ``audio/ogg``), then WAV (``audio/wav`` — last resort so a clip is never lost). """ import numpy as np import soundfile as sf # Accept either a float waveform already in [-1, 1] (MMS) OR integer PCM # (Gemini / Google-Cloud TTS return 16-bit PCM). Integer input MUST be # normalized to float: casting int16 straight to float leaves values at # ±32768, which soundfile then clips to full-scale garbage — the "random # noise instead of speech" failure. Float input passes through untouched. arr = np.asarray(samples) if np.issubdtype(arr.dtype, np.integer): info = np.iinfo(arr.dtype) max_mag = float(max(abs(int(info.min)), int(info.max))) arr = (arr.astype("float32") / max_mag).reshape(-1) else: arr = arr.astype("float32").reshape(-1) duration_ms = duration_ms_for(arr.shape[0], sample_rate) subtype = _ogg_subtype() if subtype is not None: buf = io.BytesIO() try: # libsndfile writes Opus at 48 kHz; passing the true input rate lets # it resample correctly. Vorbis accepts the native rate directly. sf.write(buf, arr, sample_rate, format="OGG", subtype=subtype) return buf.getvalue(), "audio/ogg", duration_ms except Exception: # noqa: BLE001 — fall through to WAV pass # Last-resort uncompressed fallback (still playable by the FE