import sys import subprocess from . import config def play_audio(pcm_bytes: bytes) -> None: """ Joue des bytes PCM bruts (S16LE, mono) via le lecteur système. - Linux : pipe direct vers aplay (aucun fichier temporaire) - macOS : pipe vers afplay via stdin (format AIFF/raw) - Windows: conversion via PowerShell (fallback) """ platform = sys.platform if platform.startswith("linux"): _play_pcm_aplay(pcm_bytes) elif platform == "darwin": _play_pcm_macos(pcm_bytes) elif platform == "win32": _play_pcm_windows(pcm_bytes) else: raise RuntimeError(f"Plateforme non supportée : {platform}") def _play_pcm_aplay(pcm_bytes: bytes) -> None: """Pipe WAV directement vers aplay (auto-détecte le format depuis le header).""" proc = subprocess.Popen( ["aplay", "-q", "-"], stdin=subprocess.PIPE, ) proc.communicate(pcm_bytes) if proc.returncode != 0: raise RuntimeError(f"aplay a échoué (code {proc.returncode})") def _play_pcm_macos(pcm_bytes: bytes) -> None: """Joue du WAV sur macOS via afplay (pipe stdin via fichier temporaire).""" import tempfile, os # afplay ne lit pas depuis stdin, on utilise un fichier temporaire with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: tmp.write(pcm_bytes) tmp_path = tmp.name try: subprocess.run(["afplay", tmp_path], check=True) finally: os.unlink(tmp_path) def _play_pcm_windows(pcm_bytes: bytes) -> None: import tempfile, os with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: tmp.write(_pcm_to_wav(pcm_bytes)) tmp_path = tmp.name try: subprocess.run( ["powershell", "-c", f'(New-Object Media.SoundPlayer "{tmp_path}").PlaySync()'], check=True, ) finally: os.unlink(tmp_path) def _pcm_to_wav(pcm_bytes: bytes) -> bytes: """Ajoute un header WAV minimal au PCM brut S16LE mono.""" import struct sample_rate = config.TTS_PCM_SAMPLE_RATE num_channels = 1 bits_per_sample = 16 byte_rate = sample_rate * num_channels * bits_per_sample // 8 block_align = num_channels * bits_per_sample // 8 data_size = len(pcm_bytes) header = struct.pack( "<4sI4s4sIHHIIHH4sI", b"RIFF", 36 + data_size, b"WAVE", b"fmt ", 16, 1, num_channels, sample_rate, byte_rate, block_align, bits_per_sample, b"data", data_size, ) return header + pcm_bytes def _command_exists(cmd: str) -> bool: return subprocess.run(["which", cmd], capture_output=True).returncode == 0