Skip to content

Commit 24f0abe

Browse files
committed
feat: autoplay
1 parent 4511252 commit 24f0abe

3 files changed

Lines changed: 89 additions & 21 deletions

File tree

src/audio_manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) 2025. All rights reserved.
22
"""Audio device management and playback functionality."""
33

4+
import time
45
from pathlib import Path
56

67
import numpy as np
@@ -167,7 +168,9 @@ def play_audio(self, audio_file: Path, *, blocking: bool = False) -> bool:
167168
sd.play(data, samplerate, device=self.output_device_id)
168169

169170
if blocking:
170-
sd.wait()
171+
# Use polling loop instead of sd.wait() to allow KeyboardInterrupt
172+
while sd.get_stream() and sd.get_stream().active:
173+
time.sleep(0.1)
171174
except (OSError, RuntimeError) as e:
172175
self.console.print(f"[red]Error:[/red] {e}")
173176
return False

src/cli.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) 2025. All rights reserved.
22
"""CLI interface using rich-click for beautiful output."""
33

4+
import contextlib
45
import sys
56

67
import rich_click as click
@@ -225,6 +226,25 @@ def volume(level: float | None) -> None:
225226
console.print("[green]✓[/green] Volume saved!")
226227

227228

229+
@cli.command()
230+
@click.option("--sequential", is_flag=True, help="Play sounds in alphabetical order instead of random.")
231+
def auto(sequential: bool) -> None: # noqa: FBT001
232+
"""Play all sounds randomly, one after another.
233+
234+
Each sound will play completely before the next one starts.
235+
Press Ctrl+C to stop playback.
236+
"""
237+
soundboard, _ = get_soundboard()
238+
239+
if not soundboard.sounds:
240+
console.print("[red]✗[/red] No sounds found.")
241+
console.print(f"[dim]Add audio files to: {soundboard.sounds_dir}[/dim]")
242+
sys.exit(1)
243+
244+
with contextlib.suppress(KeyboardInterrupt):
245+
soundboard.play_all_sounds(shuffle=not sequential)
246+
247+
228248
def _handle_play_sound(soundboard: Soundboard) -> None:
229249
"""Handle playing a sound by name."""
230250
sound_name = click.prompt("Enter sound name")
@@ -284,11 +304,18 @@ def _show_menu() -> None:
284304
console.print("6. List audio devices")
285305
console.print("7. Change output device")
286306
console.print("8. Adjust volume")
307+
console.print("9. Auto-play all sounds")
287308
console.print("0. Exit")
288309

289310

311+
def _handle_auto_play(soundboard: Soundboard) -> None:
312+
"""Handle auto-play all sounds."""
313+
with contextlib.suppress(KeyboardInterrupt):
314+
soundboard.play_all_sounds()
315+
316+
290317
@cli.command()
291-
def interactive() -> None: # noqa: C901
318+
def interactive() -> None:
292319
"""Launch interactive menu mode.
293320
294321
Provides a text-based menu for exploring and using the soundboard.
@@ -302,29 +329,30 @@ def interactive() -> None: # noqa: C901
302329

303330
soundboard.setup_default_hotkeys()
304331

332+
# Menu action dispatch table (lambdas needed for partial application)
333+
menu_actions = {
334+
"1": soundboard.list_sounds,
335+
"2": lambda: _handle_play_sound(soundboard),
336+
"3": soundboard.list_hotkeys,
337+
"4": lambda: _handle_hotkey_listener(soundboard),
338+
"5": soundboard.stop_sound,
339+
"6": audio_manager.print_devices,
340+
"7": lambda: _handle_change_device(audio_manager),
341+
"8": lambda: _handle_volume(audio_manager),
342+
"9": lambda: _handle_auto_play(soundboard),
343+
}
344+
305345
while True:
306346
_show_menu()
307347
choice = click.prompt("\nEnter your choice", type=str).strip()
308348

309-
if choice == "1":
310-
soundboard.list_sounds()
311-
elif choice == "2":
312-
_handle_play_sound(soundboard)
313-
elif choice == "3":
314-
soundboard.list_hotkeys()
315-
elif choice == "4":
316-
_handle_hotkey_listener(soundboard)
317-
elif choice == "5":
318-
soundboard.stop_sound()
319-
elif choice == "6":
320-
audio_manager.print_devices()
321-
elif choice == "7":
322-
_handle_change_device(audio_manager)
323-
elif choice == "8":
324-
_handle_volume(audio_manager)
325-
elif choice == "0":
349+
if choice == "0":
326350
console.print("\n[cyan]Goodbye! 👋[/cyan]")
327351
break
352+
353+
action = menu_actions.get(choice)
354+
if action:
355+
action()
328356
else:
329357
console.print("[red]Invalid choice.[/red]")
330358

src/soundboard.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) 2025. All rights reserved.
22
"""Soundboard with hotkey bindings for playing audio files."""
33

4+
import random
45
from pathlib import Path
56

67
from pynput import keyboard
@@ -140,22 +141,58 @@ def stop_listening(self) -> None:
140141
self.listener.stop()
141142
self.listener = None
142143

143-
def play_sound(self, sound_name: str) -> bool:
144+
def play_sound(self, sound_name: str, *, blocking: bool = False) -> bool:
144145
"""Manually play a sound by name.
145146
146147
Args:
147148
sound_name: Name of the sound to play
149+
blocking: If True, wait for playback to finish
148150
149151
Returns:
150152
True if playback started successfully
151153
152154
"""
153155
audio_file = self.sounds.get(sound_name)
154156
if audio_file:
155-
return self.audio_manager.play_audio(audio_file)
157+
return self.audio_manager.play_audio(audio_file, blocking=blocking)
156158
self.console.print(f"[red]✗[/red] Sound '{sound_name}' not found.")
157159
return False
158160

161+
def play_all_sounds(self, *, shuffle: bool = True) -> None:
162+
"""Play all sounds in random or sequential order.
163+
164+
Args:
165+
shuffle: If True, play sounds in random order. Default is True.
166+
167+
Raises:
168+
KeyboardInterrupt: When user presses Ctrl+C to stop playback.
169+
170+
"""
171+
if not self.sounds:
172+
self.console.print("[yellow]⚠[/yellow] No sounds to play.")
173+
return
174+
175+
sound_names = list(self.sounds.keys())
176+
if shuffle:
177+
random.shuffle(sound_names)
178+
else:
179+
sound_names = sorted(sound_names)
180+
181+
total = len(sound_names)
182+
mode = "randomly" if shuffle else "sequentially"
183+
184+
self.console.print(f"\n[bold cyan]Playing {total} sounds {mode}...[/bold cyan]")
185+
self.console.print("[dim]Press Ctrl+C to stop[/dim]\n")
186+
187+
try:
188+
for idx, sound_name in enumerate(sound_names, 1):
189+
self.console.print(f"[cyan][{idx}/{total}][/cyan] ", end="")
190+
self.play_sound(sound_name, blocking=True)
191+
except KeyboardInterrupt:
192+
self.console.print("\n[yellow]⏸[/yellow] Playback interrupted.")
193+
self.stop_sound()
194+
raise
195+
159196
def list_sounds(self) -> None:
160197
"""Print all available sounds in a formatted table."""
161198
if not self.sounds:

0 commit comments

Comments
 (0)