#!/usr/bin/env python3 """Generate the lab's audio fixtures. The Asterisk container ships with no sound files, and the design calls for *deterministic* audio: real hold music varies per call, so a classifier regression on the PSTN is indistinguishable from noise. These are synthesised from a fixed seed, so every run classifies identical input. Output is 8 kHz 16-bit mono signed-linear (.sln), which Asterisk plays without transcoding — the format is implied by the extension, so `Playback(lab-music)` finds `lab-music.sln`. python generate.py [outdir] """ import struct import sys from pathlib import Path import numpy as np RATE = 8000 # Asterisk's native rate for ulaw/alaw telephony def _write_sln(path: Path, samples: np.ndarray) -> None: """Write float samples in [-1, 1] as 16-bit signed little-endian PCM.""" clipped = np.clip(samples, -1.0, 1.0) pcm = (clipped * 32767).astype(" np.ndarray: """Sustained multi-harmonic tones — what the classifier must call MUSIC. A chord progression with stable pitch and strong harmonic structure. The steady spectrum across a long window is what distinguishes music from speech; this deliberately has no pauses. """ t = np.linspace(0, seconds, int(RATE * seconds), endpoint=False) # A-minor-ish progression, one chord per 2s bar. chords = [(220.0, 261.6, 329.6), (196.0, 246.9, 293.7), (174.6, 220.0, 261.6), (196.0, 246.9, 329.6)] out = np.zeros_like(t) bar = 2.0 for i, chord in enumerate(chords * int(np.ceil(seconds / (bar * len(chords))))): start, end = i * bar, (i + 1) * bar if start >= seconds: break mask = (t >= start) & (t < end) for j, freq in enumerate(chord): # Fundamental plus two harmonics, decaying — a plucked-string feel. for h, amp in ((1, 0.30), (2, 0.12), (3, 0.05)): out[mask] += amp / (j + 1) * np.sin(2 * np.pi * freq * h * t[mask]) # Gentle per-bar envelope so bars are distinguishable but never silent. env = 0.8 + 0.2 * np.sin(2 * np.pi * (t[mask] - start) / bar) out[mask] *= env return out * 0.45 def make_speech(seconds: float = 8.0, seed: int = 1337) -> np.ndarray: """Formant-like bursts with pauses — what the classifier must call SPEECH. Not real speech, but it carries the features the classifier keys on: a fundamental in the human range, shifting formants, and syllable-rate amplitude modulation with genuine silence between utterances. """ rng = np.random.default_rng(seed) t = np.linspace(0, seconds, int(RATE * seconds), endpoint=False) out = np.zeros_like(t) pos = 0.3 # leading pause while pos < seconds - 0.4: syl = rng.uniform(0.12, 0.28) # syllable length mask = (t >= pos) & (t < pos + syl) if mask.any(): local = t[mask] - pos f0 = rng.uniform(95, 165) # fundamental — adult speaking range # Two formants, swept slightly across the syllable. The ranges # deliberately avoid the DTMF bands (rows 697-941, columns # 1209-1633): a formant pair landing on both trips the Goertzel # detector and the whole utterance is classified as a keypress. f1 = rng.uniform(300, 620) + rng.uniform(-40, 40) * local / syl f2 = rng.uniform(1750, 2600) + rng.uniform(-120, 120) * local / syl sig = (0.50 * np.sin(2 * np.pi * f0 * local) + 0.30 * np.sin(2 * np.pi * f1 * local) + 0.18 * np.sin(2 * np.pi * f2 * local)) # Raised-cosine envelope: no clicks at syllable edges. sig *= np.sin(np.pi * local / syl) ** 0.6 out[mask] += sig # Inter-syllable gap; occasionally a longer between-word pause. pos += syl + (rng.uniform(0.25, 0.5) if rng.random() < 0.25 else rng.uniform(0.04, 0.12)) return out * 0.55 def make_silence(seconds: float = 5.0) -> np.ndarray: """Near-silence with a trace of noise — real lines are never digitally flat.""" rng = np.random.default_rng(4242) return rng.normal(0, 0.0006, int(RATE * seconds)) def main() -> None: outdir = Path(sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent) outdir.mkdir(parents=True, exist_ok=True) print(f"Generating lab audio into {outdir}/") _write_sln(outdir / "lab-music.sln", make_music()) _write_sln(outdir / "lab-speech.sln", make_speech()) _write_sln(outdir / "lab-silence.sln", make_silence()) print("Done. Deterministic: same bytes on every run.") if __name__ == "__main__": main()