"""Pont audio → animation : calcule un niveau de volume normalisé. Unité pure (numpy seulement) — aucune dépendance matérielle, testable sans micro. """ import numpy as np def compute_rms(frames): """Niveau RMS d'un bloc audio float32, normalisé et borné dans [0.0, 1.0]. frames : tableau numpy 1-D d'échantillons float32 (typiquement dans [-1, 1]). Retourne 0.0 pour un bloc vide ou silencieux. """ if frames.size == 0: return 0.0 rms = float(np.sqrt(np.mean(np.square(frames, dtype="float64")))) return min(1.0, max(0.0, rms)) def shape_level(rms, gain): """Met en forme un niveau RMS brut pour l'animation : amplifie et borne dans [0, 1]. Les RMS vocaux sont faibles (~0.02–0.15) ; appliqués tels quels, le cercle bouge à peine. On applique un gain puis une courbe en racine carrée : les niveaux faibles sont boostés (forte réactivité à voix normale) tout en restant bornés à 1.0 (pas de saturation visuelle). gain=0 → toujours 0 (aucune réaction). """ boosted = max(0.0, rms) * gain shaped = boosted ** 0.5 return min(1.0, max(0.0, shaped)) def push_level(history, level, capacity): """Empile un niveau EN TETE et renvoie un nouvel historique borne a `capacity`. L'historique recu n'est jamais modifie (le thread audio ecrit, le thread principal lit : rendre une nouvelle liste evite d'exposer un etat a moitie ecrit). Le niveau est borne dans [0, 1]. """ borne = min(1.0, max(0.0, float(level))) return [borne] + list(history)[: capacity - 1] def bar_heights(history, n_bars, floor): """Convertit un historique de niveaux en `n_bars` hauteurs dans [floor, 1]. Le plus recent est a l'indice 0 (gauche de la forme d'onde). Les positions sans donnee sont completees par `floor`, et les silences y retombent : c'est ce plancher qui se dessine en petit point et donne la traine pointillee. """ if n_bars < 1: raise ValueError("n_bars doit valoir au moins 1") retenus = list(history)[:n_bars] retenus += [0.0] * (n_bars - len(retenus)) return [max(floor, h) for h in retenus]