Sound fields accept WAV/MP3/OGG/FLAC/M4A/AAC/AIFF/Opus. Browse dialog auto-plays each file on selection so you can preview before confirming. sound.py falls back to ffplay/gst-play-1.0 for formats not supported by pw-play/paplay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
57 lines
1.9 KiB
Python
57 lines
1.9 KiB
Python
"""Play short audio cues (a user WAV, or a built-in system sound) without blocking."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
_FREEDESKTOP = "/usr/share/sounds/freedesktop/stereo/{name}.oga"
|
|
|
|
# (player, extra_args, wav_only)
|
|
# Ordered: native PipeWire/PulseAudio first, then ffplay/gst as universal fallback.
|
|
_PLAYERS: list[tuple[str, list[str], bool]] = [
|
|
("pw-play", [], False),
|
|
("paplay", [], False),
|
|
("aplay", [], True), # WAV only
|
|
("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet"], False),
|
|
("gst-play-1.0", [], False),
|
|
]
|
|
_NATIVE_EXTS = {".wav", ".oga", ".ogg", ".flac"}
|
|
|
|
|
|
def play(path: str = "", *, fallback: str | None = None) -> "subprocess.Popen | None":
|
|
"""Play `path` (WAV/MP3/OGG/FLAC/…); fallback to a freedesktop system sound.
|
|
|
|
Returns the Popen object so callers can terminate a preview, or None.
|
|
"""
|
|
target = ""
|
|
if path:
|
|
expanded = os.path.expanduser(path)
|
|
if os.path.exists(expanded):
|
|
target = expanded
|
|
if not target and fallback:
|
|
fd = _FREEDESKTOP.format(name=fallback)
|
|
if os.path.exists(fd):
|
|
target = fd
|
|
if not target:
|
|
return None
|
|
|
|
ext = os.path.splitext(target)[1].lower()
|
|
for player, extra, wav_only in _PLAYERS:
|
|
if not shutil.which(player):
|
|
continue
|
|
if wav_only and ext != ".wav":
|
|
continue
|
|
if player == "paplay" and ext not in _NATIVE_EXTS:
|
|
continue
|
|
try:
|
|
proc = subprocess.Popen(
|
|
[player] + extra + [target],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
)
|
|
return proc
|
|
except OSError:
|
|
continue
|
|
return None
|