# Annexe : Noyau - audio.py

## Rôle et utilité

Le script `core/audio.py` agit comme un traducteur entre l'interface Python de GrimOS et le serveur de son natif de Linux (généralement ALSA via les commandes `amixer`). Il permet à la barre des tâches de lire le volume actuel et de le modifier.

## Implémentation technique

- Utilisation de `subprocess` : Le fichier ne s'appuie sur aucune bibliothèque externe complexe (comme PyAudio ou PulseAudio-libs). Il exécute directement des commandes shell avec `subprocess.run()`.
- Lecture du volume : Pour connaître le volume actuel, il lance `amixer sget Master`. Il découpe ensuite la réponse texte (parsing) pour trouver une valeur en pourcentage (ex: `[50%]`) et l'état de la sourdine (`[on]` ou `[off]`).
- Modification du volume : Lorsqu'on ajuste la jauge, il convertit la valeur en commande `amixer sset Master X%`.
- Mode muet (Mute) : Il propose une fonction de bascule (`toggle_mute`) qui utilise la commande `amixer sset Master toggle`.

## Pistes de modification

- Support de PulseAudio/PipeWire : Si le système sous-jacent évoluait vers un serveur de son plus moderne, c'est ce fichier qu'il faudrait modifier pour utiliser `pactl` ou `wpctl` au lieu d'`amixer`.
- Sélecteur de sortie : Un développeur pourrait enrichir ce module pour lister les différentes cartes sons branchées (Haut-parleurs, Casque Bluetooth) via la commande `aplay -l` et permettre à l'utilisateur de choisir sa sortie audio depuis la barre des tâches.

## Code Source

    import subprocess
    import re
    import threading

    class AudioManager:
        def __init__(self, desktop):
            self.desktop = desktop
            self.control_name = self._find_master_control()

        def _find_master_control(self):
            """Tente de trouver le contrôle principal ALSA (Master, PCM, ou autre)."""
            try:
                # Chercher le contrôle 'Master' en premier
                res = subprocess.run(['amixer', 'sget', 'Master'], capture_output=True, text=True)
                if res.returncode == 0:
                    return 'Master'

                # Sinon, lister tous les contrôles scontrols
                res = subprocess.run(['amixer', 'scontrols'], capture_output=True, text=True)
                if "PCM" in res.stdout:
                    return "PCM"

                # Prendre le premier dispo
                match = re.search(r"Simple mixer control '([^']+)'", res.stdout)
                if match:
                    return match.group(1)
            except Exception:
                pass
            return 'Master' # Valeur par défaut de secours

        def get_volume(self):
            try:
                res = subprocess.run(['amixer', 'sget', self.control_name], capture_output=True, text=True)
                if res.returncode == 0:
                    # Chercher par ex: [50%]
                    match = re.search(r"\[(\d+)%\]", res.stdout)
                    if match:
                        return int(match.group(1))
            except Exception:
                pass
            return 0

        def set_volume(self, level):
            try:
                subprocess.run(['amixer', 'sset', self.control_name, f'{level}%', 'unmute'], capture_output=True)
                # Démuter spécifiquement les haut-parleurs au cas où
                subprocess.run(['amixer', 'sset', 'Speaker', 'unmute'], capture_output=True)
            except Exception:
                pass

        def test_audio(self):
            # speaker-test est bloquant, on le lance dans un thread
            def run_test():
                try:
                    # Test avec un son wav natif (court et reconnaissable : "Front Center")
                    # Si aplay ne trouve pas le fichier, speaker-test fera un bruit rose ou un bip.
                    res = subprocess.run(['aplay', '/usr/share/sounds/alsa/Front_Center.wav'], capture_output=True)
                    if res.returncode != 0:
                        # Fallback sur speaker-test
                        subprocess.run(['speaker-test', '-t', 'sine', '-f', '440', '-c', '1', '-l', '1'], timeout=2, capture_output=True)
                except Exception:
                    pass

            threading.Thread(target=run_test, daemon=True).start()