Cortex-0 / src /data /audio_features.py
Frankenstein-Labs's picture
feat(audio): Phase 7 — encodeur audio from scratch
c40e77f verified
Raw History Blame Contribute Delete
3.94 kB
"""Extraction de mel-spectrogrammes **écrite à la main** (Phase 7).
Aucune bibliothèque d'audio pré-entraînée : STFT via `torch.fft`, banc de filtres
mel triangulaires construit à la main, puis log. Utilisé par l'encodeur audio.
Justification : docs/architecture.md §6.
"""
from __future__ import annotations
import math
import torch
def hann_window(n: int, device=None, dtype=torch.float32) -> torch.Tensor:
"""Fenêtre de Hann périodique (comme librosa/torch)."""
t = torch.arange(n, device=device, dtype=dtype)
return 0.5 - 0.5 * torch.cos(2 * math.pi * t / n)
def frame_signal(x: torch.Tensor, frame_length: int, hop_length: int,
center: bool = True) -> torch.Tensor:
"""Découpe ``(B, N)`` en trames ``(B, n_frames, frame_length)``.
``center=True`` ajoute un padding de réflexion de ``frame_length//2`` de chaque
côté (comme torch.stft), ce qui aligne les trames sur les échantillons.
"""
if x.dim() == 1:
x = x.unsqueeze(0)
if center:
pad = frame_length // 2
x = torch.nn.functional.pad(x.unsqueeze(1), (pad, pad), mode="reflect").squeeze(1)
n_frames = 1 + (x.size(1) - frame_length) // hop_length
idx = (torch.arange(n_frames, device=x.device) * hop_length).unsqueeze(1)
idx = idx + torch.arange(frame_length, device=x.device).unsqueeze(0)
return x[:, idx] # (B, n_frames, frame_length)
def stft_magnitude(x: torch.Tensor, n_fft: int, hop_length: int,
win_length: int | None = None) -> torch.Tensor:
"""Magnitude de la STFT ``(B, n_frames, n_fft//2 + 1)``."""
win_length = win_length or n_fft
window = hann_window(win_length, device=x.device, dtype=x.dtype)
frames = frame_signal(x, win_length, hop_length) * window
spec = torch.fft.rfft(frames, n=n_fft, dim=-1)
return spec.abs()
def hz_to_mel(f: torch.Tensor | float) -> torch.Tensor:
"""Échelle mel (formule de O'Shaughnessy)."""
f = torch.as_tensor(f, dtype=torch.float64)
return 2595.0 * torch.log10(1.0 + f / 700.0)
def mel_to_hz(m: torch.Tensor | float) -> torch.Tensor:
m = torch.as_tensor(m, dtype=torch.float64)
return 700.0 * (10.0 ** (m / 2595.0) - 1.0)
def mel_filterbank(sr: int, n_fft: int, n_mels: int, fmin: float = 0.0,
fmax: float | None = None) -> torch.Tensor:
"""Banc de filtres mel triangulaires ``(n_mels, n_fft//2 + 1)``."""
fmax = fmax if fmax is not None else sr / 2
n_freqs = n_fft // 2 + 1
mel_pts = torch.linspace(float(hz_to_mel(fmin)), float(hz_to_mel(fmax)), n_mels + 2)
hz_pts = mel_to_hz(mel_pts).to(torch.float32) # (n_mels+2,)
fft_freqs = torch.linspace(0, sr / 2, n_freqs) # (n_freqs,)
fb = torch.zeros(n_mels, n_freqs)
for i in range(n_mels):
left, center, right = hz_pts[i], hz_pts[i + 1], hz_pts[i + 2]
rising = (fft_freqs - left) / max(float(center - left), 1e-8)
falling = (right - fft_freqs) / max(float(right - center), 1e-8)
fb[i] = torch.clamp(torch.minimum(rising, falling), min=0.0)
return fb
class LogMelSpectrogram(torch.nn.Module):
"""Transforme une forme d'onde ``(B, N)`` en log-mel ``(B, 1, n_mels, n_frames)``."""
def __init__(self, sr: int = 16000, n_fft: int = 256, hop_length: int = 64,
n_mels: int = 64, fmin: float = 0.0, fmax: float | None = None):
super().__init__()
self.sr, self.n_fft, self.hop_length = sr, n_fft, hop_length
self.register_buffer("fb", mel_filterbank(sr, n_fft, n_mels, fmin, fmax))
def forward(self, waveform: torch.Tensor) -> torch.Tensor:
mag = stft_magnitude(waveform, self.n_fft, self.hop_length) # (B, T, F)
mel = torch.einsum("btf,mf->btm", mag, self.fb) # (B, T, n_mels)
log_mel = torch.log(mel.clamp(min=1e-6))
return log_mel.transpose(1, 2).unsqueeze(1) # (B, 1, n_mels, T)