zonza/audio_level.py
Ralph Mayola 55c860c5b9 L'overlay devient une barre de dictee au lieu d'un cercle
Reprend le design demande par Ralph, d'apres une capture : barre sombre
arrondie posee en bas de l'ecran, et dedans, de gauche a droite, un bouton stop
rond rouge-orange a carre blanc, une forme d'onde en barres verticales dont la
traine retombe en pointilles, une icone de micro. Au-dessus, une bulle sombre a
pointe.

La forme d'onde demandait un HISTORIQUE de niveaux, la ou l'ancien cercle ne
lisait que le niveau courant : push_level et bar_heights sont ajoutes a
audio_level.py comme unites pures (le plus recent a gauche, les positions vides
ou silencieuses retombent sur un plancher — c'est lui qui donne les pointilles).

La bulle ne peut pas afficher le texte en direct comme la reference : Zonza
transcrit APRES l'arret, il n'y a pas de flux continu. Elle porte donc l'etape
en cours, et la fenetre reste affichee jusqu'au collage au lieu de disparaitre
pendant les secondes de calcul — l'ecran ne se vide plus sans explication.

Rendu verifie par capture d'ecran, pas seulement par les tests : le micro a ete
redessine apres coup (il sortait en tache blanche) et une marge basse ajoutee,
l'arrondi de la barre etant rogne par le bord de la fenetre.

45 tests passent. Le cdhash du stub est inchange, donc les autorisations
d'Accessibilite survivent a cette mise a jour (verifie : accordee = True).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 00:46:56 +02:00

56 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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.020.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]