"""Synthetic speech for testing the voice detector. A glottal pulse train with jitter, shaped by three formant resonators and gated into syllables with pauses between phrases -- the structure a speech detector is supposed to key on, without needing a recorded voice sample. """ import numpy as np from scipy import signal as sps def synth_speech(seconds=4.0, fs=16000.0, pitch=120.0, seed=0, syllable_rate=4.0, pause_fraction=0.25): rng = np.random.default_rng(seed) n = int(seconds * fs) t = np.arange(n) / fs # Glottal excitation: pulse train with natural pitch drift and jitter. f0 = pitch * (1.0 + 0.06 * np.sin(2 * np.pi * 0.7 * t) + 0.02 * rng.standard_normal(n).cumsum() / np.sqrt(n)) phase = np.cumsum(2 * np.pi * f0 / fs) exc = np.zeros(n) pulses = np.flatnonzero(np.diff(np.floor(phase / (2 * np.pi))) > 0) exc[pulses] = 1.0 exc -= exc.mean() # Three formants, swept so the spectrum moves the way articulation does. out = np.zeros(n) for base, bw, amp in ((650.0, 90.0, 1.0), (1200.0, 110.0, 0.55), (2600.0, 160.0, 0.30)): sweep = base * (1.0 + 0.22 * np.sin(2 * np.pi * 1.9 * t + rng.uniform(0, 6.283))) # Piecewise-constant formant, cheap but spectrally honest. seg = int(fs * 0.04) y = np.zeros(n) for s in range(0, n - seg, seg): fc = float(np.mean(sweep[s:s + seg])) b, a = sps.iirpeak(min(fc, fs / 2 * 0.95) / (fs / 2), max(2.0, fc / bw)) y[s:s + seg] = sps.lfilter(b, a, exc[s:s + seg]) out += amp * y # Voiced/unvoiced: add fricative noise on some syllables. out += 0.05 * rng.standard_normal(n) * (np.abs(np.sin(2 * np.pi * 2.3 * t)) > 0.8) # Syllabic gating plus phrase pauses. syl = 0.5 + 0.5 * np.sin(2 * np.pi * syllable_rate * t) phrase = (np.sin(2 * np.pi * 0.35 * t) > (-1 + 2 * pause_fraction)).astype(float) phrase = sps.lfilter(np.ones(int(0.02 * fs)) / int(0.02 * fs), [1.0], phrase) out *= syl * phrase out /= max(np.abs(out).max(), 1e-9) return out * 0.7