swap pygame for miniaudio — zero system deps, pip install miniaudio
This commit is contained in:
parent
fdb9d4afc2
commit
b614114286
@ -9,7 +9,7 @@ When multiple files are listed, one is chosen at random each time the ambience a
|
||||
## Requirements
|
||||
|
||||
```bash
|
||||
pip install pygame yt-dlp
|
||||
pip install miniaudio yt-dlp
|
||||
```
|
||||
`ffmpeg` must also be installed on your system.
|
||||
|
||||
@ -79,4 +79,4 @@ The TUI status bar shows the current ambience:
|
||||
Dillion ❤ 10 │ 42 entries │ 3 todo │ 2026-06-24 │ ♫ tavern
|
||||
```
|
||||
|
||||
If pygame is not installed, the music icon is absent and no audio plays.
|
||||
If miniaudio is not installed, the status bar shows a hint and no audio plays.
|
||||
|
||||
112
tools/run.py
112
tools/run.py
@ -3,7 +3,7 @@
|
||||
run.py — The Chaos TTRPG Session Client
|
||||
|
||||
Layout: banner top | log (main) + character (right) | input bottom.
|
||||
Music: polls session/ambience.md, crossfades via pygame.mixer.
|
||||
Music: polls session/ambience.md, plays via miniaudio.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -18,12 +18,13 @@ from textual.containers import Horizontal, Vertical
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import Input, Static
|
||||
|
||||
# ── Optional pygame ───────────────────────────────────────
|
||||
# ── Optional miniaudio ────────────────────────────────────
|
||||
try:
|
||||
import pygame
|
||||
HAS_PYGAME = True
|
||||
import miniaudio
|
||||
HAS_AUDIO = True
|
||||
except ImportError:
|
||||
HAS_PYGAME = False
|
||||
HAS_AUDIO = False
|
||||
print("Note: miniaudio not installed — no ambience music. Install with: pip install miniaudio", file=sys.stderr)
|
||||
|
||||
|
||||
# ── Paths ────────────────────────────────────────────────
|
||||
@ -144,29 +145,19 @@ def parse_ambience_options():
|
||||
|
||||
|
||||
class AmbiencePlayer:
|
||||
"""Monitors ambience.md and crossfades background music."""
|
||||
"""Monitors ambience.md and plays background music via miniaudio."""
|
||||
|
||||
def __init__(self):
|
||||
self.current_ambience = 'silence'
|
||||
self._enabled = False
|
||||
self._last_mtime = 0
|
||||
self._options = {}
|
||||
self._init_pygame()
|
||||
self._device = None
|
||||
self._stream = None
|
||||
self.load_options()
|
||||
|
||||
def _init_pygame(self):
|
||||
if not HAS_PYGAME:
|
||||
return
|
||||
try:
|
||||
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
|
||||
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
|
||||
self._enabled = True
|
||||
except Exception:
|
||||
self._enabled = False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self._enabled
|
||||
return HAS_AUDIO
|
||||
|
||||
@property
|
||||
def ambience_name(self):
|
||||
@ -175,8 +166,17 @@ class AmbiencePlayer:
|
||||
def load_options(self):
|
||||
self._options = parse_ambience_options()
|
||||
|
||||
def _stop(self):
|
||||
if self._device:
|
||||
try:
|
||||
self._device.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._device = None
|
||||
self._stream = None
|
||||
|
||||
def poll(self):
|
||||
if not self._enabled:
|
||||
if not HAS_AUDIO:
|
||||
return
|
||||
try:
|
||||
mtime = os.path.getmtime(AMBIENCE_PATH)
|
||||
@ -195,23 +195,19 @@ class AmbiencePlayer:
|
||||
if name == self.current_ambience:
|
||||
return
|
||||
self.current_ambience = name
|
||||
self._stop()
|
||||
if name == 'silence' or name not in self._options:
|
||||
if pygame.mixer.music.get_busy():
|
||||
pygame.mixer.music.fadeout(2000)
|
||||
return
|
||||
tracks = self._options.get(name, [])
|
||||
# filter to existing files
|
||||
valid = [t for t in tracks if t.exists()]
|
||||
if not valid:
|
||||
return
|
||||
track = random.choice(valid)
|
||||
if pygame.mixer.music.get_busy():
|
||||
pygame.mixer.music.fadeout(2000)
|
||||
try:
|
||||
pygame.mixer.music.load(str(track))
|
||||
pygame.mixer.music.set_volume(0.4)
|
||||
pygame.mixer.music.play(loops=-1, fade_ms=2000)
|
||||
except pygame.error:
|
||||
self._stream = miniaudio.stream_file(str(track))
|
||||
self._device = miniaudio.PlaybackDevice()
|
||||
self._device.start(self._stream)
|
||||
except Exception:
|
||||
self.current_ambience = None
|
||||
|
||||
|
||||
@ -250,7 +246,9 @@ class StatusBar(AutoStatic):
|
||||
count = log_count()
|
||||
todo = len(read_todo())
|
||||
music = ""
|
||||
if app_ambience_player and app_ambience_player.available:
|
||||
if not HAS_AUDIO:
|
||||
music = " │ ♫ (install miniaudio)"
|
||||
elif app_ambience_player:
|
||||
name = app_ambience_player.ambience_name
|
||||
music = f" │ ♫ {name}"
|
||||
self.update(f"{char} │ {count} entries │ {todo} todo │ {TODAY}{music}")
|
||||
@ -295,10 +293,6 @@ class ChaosTUI(App):
|
||||
|
||||
#main {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#log-col {
|
||||
border-right: solid #3a3a3a;
|
||||
background: #111111;
|
||||
}
|
||||
#todo-header {
|
||||
@ -315,6 +309,20 @@ class ChaosTUI(App):
|
||||
height: 5;
|
||||
max-height: 5;
|
||||
}
|
||||
#char-header {
|
||||
background: #2d2d3a;
|
||||
color: #b0a0e0;
|
||||
text-style: bold;
|
||||
padding: 0 1;
|
||||
height: 1;
|
||||
}
|
||||
#char-content {
|
||||
background: #1e1e2a;
|
||||
padding: 0 1;
|
||||
color: #c0c0c0;
|
||||
height: 14;
|
||||
max-height: 14;
|
||||
}
|
||||
#log-header {
|
||||
background: #1d2d1d;
|
||||
color: #7dcd7d;
|
||||
@ -325,23 +333,7 @@ class ChaosTUI(App):
|
||||
#transcript {
|
||||
padding: 0 1;
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 36;
|
||||
min-width: 28;
|
||||
background: #181818;
|
||||
}
|
||||
#side-header {
|
||||
background: #2d2d3a;
|
||||
color: #b0a0e0;
|
||||
text-style: bold;
|
||||
padding: 0 1;
|
||||
height: 1;
|
||||
}
|
||||
#char-content {
|
||||
padding: 0 1;
|
||||
color: #c0c0c0;
|
||||
height: 1fr;
|
||||
}
|
||||
#status-bar {
|
||||
background: #222222;
|
||||
@ -367,16 +359,14 @@ class ChaosTUI(App):
|
||||
|
||||
def compose(self):
|
||||
yield Static(f"⚔ The Chaos ╎ {TODAY}", id="banner")
|
||||
with Horizontal(id="main"):
|
||||
with Vertical(id="log-col"):
|
||||
yield Static("TODO", id="todo-header")
|
||||
yield TodoPane(id="todo-content")
|
||||
yield Static("LOG", id="log-header")
|
||||
yield TranscriptPane(id="transcript")
|
||||
with Vertical(id="sidebar"):
|
||||
yield Static("CHARACTER", id="side-header")
|
||||
yield CharPane(id="char-content")
|
||||
yield StatusBar(id="status-bar")
|
||||
with Vertical(id="main"):
|
||||
yield Static("TODO", id="todo-header")
|
||||
yield TodoPane(id="todo-content")
|
||||
yield Static("CHARACTER", id="char-header")
|
||||
yield CharPane(id="char-content")
|
||||
yield Static("LOG", id="log-header")
|
||||
yield TranscriptPane(id="transcript")
|
||||
yield StatusBar(id="status-bar")
|
||||
with Horizontal(id="input-row"):
|
||||
self.input = Input(placeholder=" What do you do? (just type)", id="input")
|
||||
yield self.input
|
||||
|
||||
Loading…
Reference in New Issue
Block a user