Cortex-0 / src /data /toy_audio.py
Frankenstein-Labs's picture
feat(audio): Phase 7 — encodeur audio from scratch
c40e77f verified
Raw History Blame Contribute Delete
3.08 kB
"""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)