"""Générateur d'audio synthétique jouet (Phase 7). Faute de téléchargement audio dans l'environnement de dev, on synthétise des **voyelles** par un modèle simple de formants (somme de sinus au F0 et aux fréquences de formants F1/F2), avec deux hauteurs (grave/aigu). Cela donne un jeu de 10 classes (5 voyelles × 2 hauteurs), suffisant pour valider un encodeur audio from scratch et une transcription de mots simples. Aucun modèle pré-entraîné : c'est du signal pur. """ from __future__ import annotations from dataclasses import dataclass import numpy as np #: Voyelles françaises et leurs deux premiers formants (Hz). VOWELS: dict[str, tuple[float, float]] = { "a": (800.0, 1200.0), "e": (500.0, 1800.0), "i": (300.0, 2300.0), "o": (500.0, 900.0), "u": (300.0, 700.0), } #: Hauteurs : grave / aigu (Hz du fondamental). PITCHES: dict[str, float] = {"grave": 100.0, "aigu": 200.0} SR = 16000 N_SAMPLES = 6144 # 0.384 s -> 96 trames avec hop 64 @dataclass class AudioItem: waveform: np.ndarray # (N_SAMPLES,) float32 vowel: str pitch: str @property def caption(self) -> str: return f"voyelle {self.vowel} {self.pitch}" def synth_vowel(vowel: str, pitch: str, sr: int = SR, n: int = N_SAMPLES, rng: np.random.Generator | None = None) -> np.ndarray: """Synthétise une voyelle : harmoniques du F0 pondérées par les formants.""" rng = rng or np.random.default_rng(0) f0 = PITCHES[pitch] * float(rng.uniform(0.95, 1.05)) f1, f2 = VOWELS[vowel] f1 *= float(rng.uniform(0.95, 1.05)) f2 *= float(rng.uniform(0.95, 1.05)) t = np.arange(n) / sr sig = np.zeros(n, dtype=np.float64) # Harmoniques du fondamental, amplitude = proximité aux formants. for h in range(1, 40): fh = h * f0 if fh > sr / 2: break amp = (np.exp(-((fh - f1) ** 2) / (2 * 120.0 ** 2)) + 0.7 * np.exp(-((fh - f2) ** 2) / (2 * 180.0 ** 2)) + 0.05) phase = float(rng.uniform(0, 2 * np.pi)) sig += amp * np.sin(2 * np.pi * fh * t + phase) sig += 0.02 * rng.standard_normal(n) # bruit de fond # Enveloppe douce (attaque/relâchement). env = np.ones(n) ramp = n // 10 env[:ramp] = np.linspace(0, 1, ramp) env[-ramp:] = np.linspace(1, 0, ramp) sig *= env sig /= (np.max(np.abs(sig)) + 1e-8) return (sig * float(rng.uniform(0.6, 1.0))).astype(np.float32) def generate_dataset(n: int, seed: int = 0) -> list[AudioItem]: """Génère ``n`` exemples répartis sur les 10 classes.""" rng = np.random.default_rng(seed) items: list[AudioItem] = [] classes = [(v, p) for v in VOWELS for p in PITCHES] for i in range(n): v, p = classes[i % len(classes)] items.append(AudioItem(synth_vowel(v, p, rng=rng), v, p)) rng.shuffle(items) return items def to_arrays(items: list[AudioItem]) -> np.ndarray: """Empile les formes d'onde en ``(N, N_SAMPLES)`` float32.""" return np.stack([it.waveform for it in items]).astype(np.float32)