Files
arioch-assistant/assistant/audio.py
2026-04-07 22:06:36 +02:00

85 lines
2.6 KiB
Python

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