Spaces:
Running
Running
drenayaz commited on
Commit ·
15541f8
1
Parent(s): ecda75b
Replace pygame with AudioMixer using reachy_mini.media
Browse files- Custom AudioMixer class mixing music + SFX via push_audio_sample
- WAV files in 16k/ and 44k/ for wireless/lite compatibility
- Auto-detect sample rate from robot
- Add /stop_music endpoint for clean exit
- Replace pygame dependency with soundfile
- arcade/main.py +225 -64
- arcade/sounds/16k/boss.wav +3 -0
- arcade/sounds/16k/boss_hit.wav +3 -0
- arcade/sounds/16k/bounce.wav +3 -0
- arcade/sounds/16k/combo.wav +3 -0
- arcade/sounds/16k/death.wav +3 -0
- arcade/sounds/16k/duck_run.wav +3 -0
- arcade/sounds/16k/ennemy_death.wav +3 -0
- arcade/sounds/16k/flappy.wav +3 -0
- arcade/sounds/16k/fly.wav +3 -0
- arcade/sounds/16k/hit.wav +3 -0
- arcade/sounds/16k/jump.wav +3 -0
- arcade/sounds/16k/miss.wav +3 -0
- arcade/sounds/16k/perfect.wav +3 -0
- arcade/sounds/16k/phase.wav +3 -0
- arcade/sounds/16k/rythme.wav +3 -0
- arcade/sounds/16k/score.wav +3 -0
- arcade/sounds/16k/select.wav +3 -0
- arcade/sounds/16k/serve.wav +3 -0
- arcade/sounds/16k/shoot.wav +3 -0
- arcade/sounds/16k/spaceship.wav +3 -0
- arcade/sounds/16k/wave.wav +3 -0
- arcade/sounds/16k/win.wav +3 -0
- arcade/sounds/44k/boss.wav +3 -0
- arcade/sounds/44k/boss_hit.wav +3 -0
- arcade/sounds/44k/bounce.wav +3 -0
- arcade/sounds/44k/combo.wav +3 -0
- arcade/sounds/44k/death.wav +3 -0
- arcade/sounds/44k/duck_run.wav +3 -0
- arcade/sounds/44k/ennemy_death.wav +3 -0
- arcade/sounds/44k/flappy.wav +3 -0
- arcade/sounds/44k/fly.wav +3 -0
- arcade/sounds/44k/hit.wav +3 -0
- arcade/sounds/44k/jump.wav +3 -0
- arcade/sounds/44k/miss.wav +3 -0
- arcade/sounds/44k/perfect.wav +3 -0
- arcade/sounds/44k/phase.wav +3 -0
- arcade/sounds/44k/rythme.wav +3 -0
- arcade/sounds/44k/score.wav +3 -0
- arcade/sounds/44k/select.wav +3 -0
- arcade/sounds/44k/serve.wav +3 -0
- arcade/sounds/44k/shoot.wav +3 -0
- arcade/sounds/44k/spaceship.wav +3 -0
- arcade/sounds/44k/wave.wav +3 -0
- arcade/sounds/44k/win.wav +3 -0
- arcade/static/index.html +8 -7
- arcade/static/main.js +1 -1
- pyproject.toml +1 -1
arcade/main.py
CHANGED
|
@@ -10,15 +10,201 @@ import time
|
|
| 10 |
import json
|
| 11 |
import queue
|
| 12 |
|
|
|
|
|
|
|
| 13 |
import numpy as np
|
| 14 |
from scipy.spatial.transform import Rotation as R
|
| 15 |
-
import
|
| 16 |
from pydantic import BaseModel
|
| 17 |
from reachy_mini import ReachyMini, ReachyMiniApp
|
| 18 |
from fastapi import WebSocket, WebSocketDisconnect
|
| 19 |
from fastapi.responses import FileResponse
|
| 20 |
import asyncio
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# --- Configuration ---
|
| 23 |
USE_ESP32 = None # Set by web UI at startup
|
| 24 |
SERIAL_PORT = "/dev/ttyUSB0"
|
|
@@ -76,7 +262,7 @@ def save_scores(data: dict):
|
|
| 76 |
|
| 77 |
class Arcade(ReachyMiniApp):
|
| 78 |
custom_app_url: str | None = "http://0.0.0.0:8042"
|
| 79 |
-
request_media_backend: str | None = "
|
| 80 |
|
| 81 |
def __init__(self, *args, **kwargs):
|
| 82 |
super().__init__(*args, **kwargs)
|
|
@@ -139,87 +325,41 @@ class Arcade(ReachyMiniApp):
|
|
| 139 |
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
|
| 140 |
port = sys.argv[1] if len(sys.argv) > 1 else SERIAL_PORT
|
| 141 |
|
| 142 |
-
# --- Sound engine ---
|
| 143 |
-
sounds: dict[str, pygame.mixer.Sound] = {}
|
| 144 |
-
current_music: str | None = None
|
| 145 |
music_muted = False
|
| 146 |
sfx_muted = False
|
| 147 |
-
|
| 148 |
-
print("Init sons...")
|
| 149 |
-
audio_available = False
|
| 150 |
-
for attempt in range(10):
|
| 151 |
-
try:
|
| 152 |
-
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=1024)
|
| 153 |
-
pygame.mixer.set_num_channels(16)
|
| 154 |
-
audio_available = True
|
| 155 |
-
break
|
| 156 |
-
except pygame.error as e:
|
| 157 |
-
if attempt < 9:
|
| 158 |
-
print(f" Audio busy, retry {attempt + 1}/10...")
|
| 159 |
-
time.sleep(2)
|
| 160 |
-
else:
|
| 161 |
-
print(f" Audio non disponible: {e}")
|
| 162 |
-
print(" L'app fonctionnera sans son.")
|
| 163 |
-
|
| 164 |
-
if audio_available:
|
| 165 |
-
for event_name, file_name in SFX_MAP.items():
|
| 166 |
-
for ext in (".wav", ".ogg", ".mp3"):
|
| 167 |
-
path = os.path.join(SOUNDS_DIR, file_name + ext)
|
| 168 |
-
if os.path.exists(path):
|
| 169 |
-
try:
|
| 170 |
-
sounds[event_name] = pygame.mixer.Sound(path)
|
| 171 |
-
print(f" SFX: {event_name} -> {file_name}{ext}")
|
| 172 |
-
except Exception as e:
|
| 173 |
-
print(f" SFX: {event_name} ERREUR: {e}")
|
| 174 |
-
break
|
| 175 |
-
if event_name not in sounds:
|
| 176 |
-
print(f" SFX: {event_name} -> MANQUANT ({file_name}.*)")
|
| 177 |
-
print(f" {len(sounds)}/{len(SFX_MAP)} sons charges")
|
| 178 |
|
| 179 |
def play_sfx(name: str):
|
| 180 |
-
if
|
| 181 |
-
|
| 182 |
|
| 183 |
def start_music(game_name: str):
|
| 184 |
-
|
| 185 |
-
if not audio_available:
|
| 186 |
return
|
| 187 |
gn = game_name.lower()
|
| 188 |
if gn in MUSIC_MAP:
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
try:
|
| 194 |
-
pygame.mixer.music.load(path)
|
| 195 |
-
if not music_muted:
|
| 196 |
-
pygame.mixer.music.play(-1)
|
| 197 |
-
current_music = gn
|
| 198 |
-
print(f" [MUSIC] Playing: {music_name}{ext}")
|
| 199 |
-
except Exception as e:
|
| 200 |
-
print(f" [MUSIC] Erreur chargement {music_name}{ext}: {e}")
|
| 201 |
-
return
|
| 202 |
-
stop_music()
|
| 203 |
|
| 204 |
def stop_music():
|
| 205 |
-
|
| 206 |
-
if not audio_available:
|
| 207 |
return
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
print(" [MUSIC] Stopped")
|
| 211 |
-
current_music = None
|
| 212 |
|
| 213 |
def toggle_music():
|
| 214 |
nonlocal music_muted
|
| 215 |
-
if not
|
| 216 |
return
|
| 217 |
music_muted = not music_muted
|
| 218 |
if music_muted:
|
| 219 |
-
|
| 220 |
print(" [MUSIC] Muted")
|
| 221 |
else:
|
| 222 |
-
|
| 223 |
print(" [MUSIC] Unmuted")
|
| 224 |
|
| 225 |
def toggle_sfx():
|
|
@@ -405,6 +545,12 @@ class Arcade(ReachyMiniApp):
|
|
| 405 |
verified = ping_esp32(ser_ref[0])
|
| 406 |
return {"verified": verified}
|
| 407 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
# --- WebSocket ---
|
| 409 |
@self.settings_app.websocket("/ws")
|
| 410 |
async def ws_endpoint(websocket: WebSocket):
|
|
@@ -474,6 +620,20 @@ class Arcade(ReachyMiniApp):
|
|
| 474 |
# Send all scores to ESP32 so both sides start in sync
|
| 475 |
send_all_scores_to_esp()
|
| 476 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
# --- Calibration ---
|
| 478 |
print("Calibration... garde la tete droite")
|
| 479 |
roll_samples = []
|
|
@@ -574,7 +734,8 @@ class Arcade(ReachyMiniApp):
|
|
| 574 |
if spent < interval:
|
| 575 |
time.sleep(interval - spent)
|
| 576 |
finally:
|
| 577 |
-
|
|
|
|
| 578 |
if ser:
|
| 579 |
ser.close()
|
| 580 |
|
|
|
|
| 10 |
import json
|
| 11 |
import queue
|
| 12 |
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
import numpy as np
|
| 16 |
from scipy.spatial.transform import Rotation as R
|
| 17 |
+
import soundfile as sf
|
| 18 |
from pydantic import BaseModel
|
| 19 |
from reachy_mini import ReachyMini, ReachyMiniApp
|
| 20 |
from fastapi import WebSocket, WebSocketDisconnect
|
| 21 |
from fastapi.responses import FileResponse
|
| 22 |
import asyncio
|
| 23 |
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ============================================================
|
| 28 |
+
# AudioMixer — mixes music + SFX via reachy_mini.media
|
| 29 |
+
# ============================================================
|
| 30 |
+
class AudioMixer:
|
| 31 |
+
"""Single-threaded audio mixer using reachy_mini.media.push_audio_sample."""
|
| 32 |
+
|
| 33 |
+
CHUNK_SIZE = 1024
|
| 34 |
+
|
| 35 |
+
def __init__(self, reachy_mini: ReachyMini):
|
| 36 |
+
self.reachy_mini = reachy_mini
|
| 37 |
+
self._lock = threading.Lock()
|
| 38 |
+
self._stop_event = threading.Event()
|
| 39 |
+
self._thread: threading.Thread | None = None
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
self._samplerate = reachy_mini.media.get_output_audio_samplerate()
|
| 43 |
+
logger.info("Robot audio sample rate: %d Hz", self._samplerate)
|
| 44 |
+
except Exception:
|
| 45 |
+
self._samplerate = 44100
|
| 46 |
+
logger.warning("Could not detect sample rate, defaulting to 44100Hz")
|
| 47 |
+
|
| 48 |
+
if self._samplerate <= 22050:
|
| 49 |
+
self._audio_folder = os.path.join(SOUNDS_DIR, "16k")
|
| 50 |
+
else:
|
| 51 |
+
self._audio_folder = os.path.join(SOUNDS_DIR, "44k")
|
| 52 |
+
logger.info("Using audio folder: %s", self._audio_folder)
|
| 53 |
+
|
| 54 |
+
self._chunk_duration = self.CHUNK_SIZE / self._samplerate
|
| 55 |
+
self._sounds: dict[str, np.ndarray] = {}
|
| 56 |
+
self._music_data: np.ndarray | None = None
|
| 57 |
+
self._music_pos: int = 0
|
| 58 |
+
self._music_loop: bool = False
|
| 59 |
+
self._music_name: str | None = None
|
| 60 |
+
self._active_sounds: list[list] = []
|
| 61 |
+
|
| 62 |
+
def _load_audio(self, filepath: str) -> np.ndarray | None:
|
| 63 |
+
if not os.path.exists(filepath):
|
| 64 |
+
logger.warning("Audio file not found: %s", filepath)
|
| 65 |
+
return None
|
| 66 |
+
try:
|
| 67 |
+
data, sr = sf.read(filepath, dtype="float32")
|
| 68 |
+
if data.ndim > 1:
|
| 69 |
+
data = np.mean(data, axis=1)
|
| 70 |
+
return np.ascontiguousarray(data, dtype=np.float32)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.warning("Failed to load audio %s: %s", filepath, e)
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
def preload_sound(self, filename: str) -> bool:
|
| 76 |
+
if filename in self._sounds:
|
| 77 |
+
return True
|
| 78 |
+
data = self._load_audio(os.path.join(self._audio_folder, filename))
|
| 79 |
+
if data is not None:
|
| 80 |
+
with self._lock:
|
| 81 |
+
self._sounds[filename] = data
|
| 82 |
+
return True
|
| 83 |
+
return False
|
| 84 |
+
|
| 85 |
+
def play_sound(self, filename: str) -> bool:
|
| 86 |
+
with self._lock:
|
| 87 |
+
if filename not in self._sounds:
|
| 88 |
+
data = self._load_audio(os.path.join(self._audio_folder, filename))
|
| 89 |
+
if data is None:
|
| 90 |
+
return False
|
| 91 |
+
self._sounds[filename] = data
|
| 92 |
+
self._active_sounds.append([self._sounds[filename], 0])
|
| 93 |
+
return True
|
| 94 |
+
|
| 95 |
+
def play_music(self, filename: str, loop: bool = False) -> bool:
|
| 96 |
+
data = self._load_audio(os.path.join(self._audio_folder, filename))
|
| 97 |
+
if data is None:
|
| 98 |
+
return False
|
| 99 |
+
with self._lock:
|
| 100 |
+
self._music_data = data
|
| 101 |
+
self._music_pos = 0
|
| 102 |
+
self._music_loop = loop
|
| 103 |
+
self._music_name = filename
|
| 104 |
+
return True
|
| 105 |
+
|
| 106 |
+
def stop_music(self) -> None:
|
| 107 |
+
with self._lock:
|
| 108 |
+
self._music_data = None
|
| 109 |
+
self._music_pos = 0
|
| 110 |
+
self._music_name = None
|
| 111 |
+
|
| 112 |
+
def pause_music(self) -> None:
|
| 113 |
+
with self._lock:
|
| 114 |
+
self._music_paused_data = self._music_data
|
| 115 |
+
self._music_paused_pos = self._music_pos
|
| 116 |
+
self._music_data = None
|
| 117 |
+
|
| 118 |
+
def unpause_music(self) -> None:
|
| 119 |
+
with self._lock:
|
| 120 |
+
if hasattr(self, '_music_paused_data') and self._music_paused_data is not None:
|
| 121 |
+
self._music_data = self._music_paused_data
|
| 122 |
+
self._music_pos = self._music_paused_pos
|
| 123 |
+
self._music_paused_data = None
|
| 124 |
+
|
| 125 |
+
def start(self) -> None:
|
| 126 |
+
if self._thread is not None and self._thread.is_alive():
|
| 127 |
+
return
|
| 128 |
+
self._stop_event.clear()
|
| 129 |
+
self._thread = threading.Thread(target=self._mixer_loop, daemon=True)
|
| 130 |
+
self._thread.start()
|
| 131 |
+
logger.info("Audio mixer started")
|
| 132 |
+
|
| 133 |
+
def stop(self) -> None:
|
| 134 |
+
self._stop_event.set()
|
| 135 |
+
if self._thread is not None:
|
| 136 |
+
self._thread.join(timeout=2.0)
|
| 137 |
+
self._thread = None
|
| 138 |
+
try:
|
| 139 |
+
self.reachy_mini.media.stop_playing()
|
| 140 |
+
except Exception:
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
def _mixer_loop(self) -> None:
|
| 144 |
+
try:
|
| 145 |
+
self.reachy_mini.media.start_playing()
|
| 146 |
+
except Exception as e:
|
| 147 |
+
logger.error("Failed to start audio playback: %s", e)
|
| 148 |
+
return
|
| 149 |
+
|
| 150 |
+
start_time = time.time()
|
| 151 |
+
chunks_pushed = 0
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
while not self._stop_event.is_set():
|
| 155 |
+
output = np.zeros(self.CHUNK_SIZE, dtype=np.float32)
|
| 156 |
+
|
| 157 |
+
with self._lock:
|
| 158 |
+
# Mix music
|
| 159 |
+
if self._music_data is not None:
|
| 160 |
+
end = self._music_pos + self.CHUNK_SIZE
|
| 161 |
+
if end <= len(self._music_data):
|
| 162 |
+
output += self._music_data[self._music_pos:end]
|
| 163 |
+
self._music_pos = end
|
| 164 |
+
else:
|
| 165 |
+
remaining = len(self._music_data) - self._music_pos
|
| 166 |
+
if remaining > 0:
|
| 167 |
+
output[:remaining] += self._music_data[self._music_pos:]
|
| 168 |
+
if self._music_loop:
|
| 169 |
+
self._music_pos = self.CHUNK_SIZE - remaining
|
| 170 |
+
if self._music_pos > 0:
|
| 171 |
+
output[remaining:] += self._music_data[:self._music_pos]
|
| 172 |
+
else:
|
| 173 |
+
self._music_data = None
|
| 174 |
+
self._music_pos = 0
|
| 175 |
+
self._music_name = None
|
| 176 |
+
|
| 177 |
+
# Mix SFX
|
| 178 |
+
still_active = []
|
| 179 |
+
for entry in self._active_sounds:
|
| 180 |
+
data, pos = entry
|
| 181 |
+
end = pos + self.CHUNK_SIZE
|
| 182 |
+
if end <= len(data):
|
| 183 |
+
output += data[pos:end]
|
| 184 |
+
entry[1] = end
|
| 185 |
+
still_active.append(entry)
|
| 186 |
+
else:
|
| 187 |
+
remaining = len(data) - pos
|
| 188 |
+
if remaining > 0:
|
| 189 |
+
output[:remaining] += data[pos:]
|
| 190 |
+
self._active_sounds = still_active
|
| 191 |
+
|
| 192 |
+
np.clip(output, -1.0, 1.0, out=output)
|
| 193 |
+
self.reachy_mini.media.push_audio_sample(output)
|
| 194 |
+
chunks_pushed += 1
|
| 195 |
+
|
| 196 |
+
target_time = start_time + (chunks_pushed * self._chunk_duration)
|
| 197 |
+
sleep_time = target_time - time.time()
|
| 198 |
+
if sleep_time > 0:
|
| 199 |
+
time.sleep(sleep_time)
|
| 200 |
+
except Exception as e:
|
| 201 |
+
logger.error("Mixer loop error: %s", e)
|
| 202 |
+
finally:
|
| 203 |
+
try:
|
| 204 |
+
self.reachy_mini.media.stop_playing()
|
| 205 |
+
except Exception:
|
| 206 |
+
pass
|
| 207 |
+
|
| 208 |
# --- Configuration ---
|
| 209 |
USE_ESP32 = None # Set by web UI at startup
|
| 210 |
SERIAL_PORT = "/dev/ttyUSB0"
|
|
|
|
| 262 |
|
| 263 |
class Arcade(ReachyMiniApp):
|
| 264 |
custom_app_url: str | None = "http://0.0.0.0:8042"
|
| 265 |
+
request_media_backend: str | None = "gstreamer_no_video"
|
| 266 |
|
| 267 |
def __init__(self, *args, **kwargs):
|
| 268 |
super().__init__(*args, **kwargs)
|
|
|
|
| 325 |
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
|
| 326 |
port = sys.argv[1] if len(sys.argv) > 1 else SERIAL_PORT
|
| 327 |
|
| 328 |
+
# --- Sound engine (AudioMixer via reachy_mini.media) ---
|
|
|
|
|
|
|
| 329 |
music_muted = False
|
| 330 |
sfx_muted = False
|
| 331 |
+
mixer = None # initialized after mode selection
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
| 333 |
def play_sfx(name: str):
|
| 334 |
+
if mixer and not sfx_muted and name in SFX_MAP:
|
| 335 |
+
mixer.play_sound(SFX_MAP[name] + ".wav")
|
| 336 |
|
| 337 |
def start_music(game_name: str):
|
| 338 |
+
if not mixer:
|
|
|
|
| 339 |
return
|
| 340 |
gn = game_name.lower()
|
| 341 |
if gn in MUSIC_MAP:
|
| 342 |
+
wav_name = MUSIC_MAP[gn] + ".wav"
|
| 343 |
+
if not music_muted:
|
| 344 |
+
mixer.play_music(wav_name, loop=True)
|
| 345 |
+
print(f" [MUSIC] Playing: {wav_name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
def stop_music():
|
| 348 |
+
if not mixer:
|
|
|
|
| 349 |
return
|
| 350 |
+
mixer.stop_music()
|
| 351 |
+
print(" [MUSIC] Stopped")
|
|
|
|
|
|
|
| 352 |
|
| 353 |
def toggle_music():
|
| 354 |
nonlocal music_muted
|
| 355 |
+
if not mixer:
|
| 356 |
return
|
| 357 |
music_muted = not music_muted
|
| 358 |
if music_muted:
|
| 359 |
+
mixer.pause_music()
|
| 360 |
print(" [MUSIC] Muted")
|
| 361 |
else:
|
| 362 |
+
mixer.unpause_music()
|
| 363 |
print(" [MUSIC] Unmuted")
|
| 364 |
|
| 365 |
def toggle_sfx():
|
|
|
|
| 545 |
verified = ping_esp32(ser_ref[0])
|
| 546 |
return {"verified": verified}
|
| 547 |
|
| 548 |
+
@self.settings_app.post("/stop_music")
|
| 549 |
+
def api_stop_music():
|
| 550 |
+
"""Stop music playback."""
|
| 551 |
+
stop_music()
|
| 552 |
+
return {"ok": True}
|
| 553 |
+
|
| 554 |
# --- WebSocket ---
|
| 555 |
@self.settings_app.websocket("/ws")
|
| 556 |
async def ws_endpoint(websocket: WebSocket):
|
|
|
|
| 620 |
# Send all scores to ESP32 so both sides start in sync
|
| 621 |
send_all_scores_to_esp()
|
| 622 |
|
| 623 |
+
# --- Init audio mixer (after media is released by daemon) ---
|
| 624 |
+
print("Init sons...")
|
| 625 |
+
mixer = AudioMixer(reachy_mini)
|
| 626 |
+
loaded = 0
|
| 627 |
+
for event_name, file_name in SFX_MAP.items():
|
| 628 |
+
wav_name = file_name + ".wav"
|
| 629 |
+
if mixer.preload_sound(wav_name):
|
| 630 |
+
print(f" SFX: {event_name} -> {wav_name}")
|
| 631 |
+
loaded += 1
|
| 632 |
+
else:
|
| 633 |
+
print(f" SFX: {event_name} -> MANQUANT ({wav_name})")
|
| 634 |
+
print(f" {loaded}/{len(SFX_MAP)} sons charges")
|
| 635 |
+
mixer.start()
|
| 636 |
+
|
| 637 |
# --- Calibration ---
|
| 638 |
print("Calibration... garde la tete droite")
|
| 639 |
roll_samples = []
|
|
|
|
| 734 |
if spent < interval:
|
| 735 |
time.sleep(interval - spent)
|
| 736 |
finally:
|
| 737 |
+
if mixer:
|
| 738 |
+
mixer.stop()
|
| 739 |
if ser:
|
| 740 |
ser.close()
|
| 741 |
|
arcade/sounds/16k/boss.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:80d1a683c0d2c8f0a3aaf51626ccd8aa28b5c8f18bf53452d4b16f8b0a267a54
|
| 3 |
+
size 5537166
|
arcade/sounds/16k/boss_hit.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6752ea7bbe74fe9d3187cfa80db54aa476546f9d86d1767f1ef72d09b91fbf68
|
| 3 |
+
size 3244
|
arcade/sounds/16k/bounce.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:58dd3f4789c72d66ddbb46167b0eeee75fa71884b6d5b126e98a42674923e2dd
|
| 3 |
+
size 3244
|
arcade/sounds/16k/combo.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:827e6116e2a67c6499d8a703844f68a42011c2bca7c5dcef753234b1c7634629
|
| 3 |
+
size 9004
|
arcade/sounds/16k/death.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5b5fb4052ade0d786b2067e38d944fb39f3e7ad53a045e63fc96f543fb3d06c6
|
| 3 |
+
size 12582
|
arcade/sounds/16k/duck_run.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:af044b6f9fdf66725e87209b63e26d3434674ad19a3c2286b8d3f80da43d2f8f
|
| 3 |
+
size 2744364
|
arcade/sounds/16k/ennemy_death.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e0d68bc7e65268eb5953a9124181adb2d82a00b1f1eb66a1f536c0309f793dde
|
| 3 |
+
size 20106
|
arcade/sounds/16k/flappy.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:84f3fcee96d6911ce6910b76833b7d80f2fd8e6a8872fb12103cd1ef8f22b525
|
| 3 |
+
size 1043270
|
arcade/sounds/16k/fly.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37ab4edd6075a65b6e68eff6e46c8d40d3c41e8c2d242b694b8fe99eb2e0e5c9
|
| 3 |
+
size 163628
|
arcade/sounds/16k/hit.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:511331d46c8ce44071cd45c5cd371515a14c9d4e70d7bd4edf66e51a9832c7e8
|
| 3 |
+
size 1964
|
arcade/sounds/16k/jump.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5aa07d16bd403069711b2c542ebdb7e58f6dc94521d45f3ad5d9a95dca1ff42d
|
| 3 |
+
size 9238
|
arcade/sounds/16k/miss.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5a0456ddc6c34e54c3b0b247ff5c682883197a1e7df9f4df94a898bc60b0aabd
|
| 3 |
+
size 4842
|
arcade/sounds/16k/perfect.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f5731c5bcef49982a12e65c132585eab4ec5c736c9f07c6eeb490cb22a4099ea
|
| 3 |
+
size 5164
|
arcade/sounds/16k/phase.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:94857296a1d86a339f9e7f7ef5478afcc891d17a789741869915e9b0825928ef
|
| 3 |
+
size 12844
|
arcade/sounds/16k/rythme.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dc7163180cb81c46f874e7ccc3dbfb8368e3ff3c5d83477042702810c6965a59
|
| 3 |
+
size 1966124
|
arcade/sounds/16k/score.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5276b4b6583bcd622fcd624aee12c0b547d2011f16bae9e4de6a8668aa7b4b8d
|
| 3 |
+
size 6444
|
arcade/sounds/16k/select.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5da7adfbea5526404ba5e314c9077d71aa5e1633670a13e683a886e5801cee46
|
| 3 |
+
size 3882
|
arcade/sounds/16k/serve.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4e7cd1b2202ad326eb497a2a41fa4b4b5aea43729fe384be9b96e745857c9990
|
| 3 |
+
size 3884
|
arcade/sounds/16k/shoot.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7a92c7cc9bc71d0be4ed4fd3b5c4a84d73263430381f420fb74d43f4deb95137
|
| 3 |
+
size 2604
|
arcade/sounds/16k/spaceship.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8a1fab4ddc45f7f310272e91b76c5faeb6cbaa3de4ea1af20cb796084c625616
|
| 3 |
+
size 6078762
|
arcade/sounds/16k/wave.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b7d2415db968b0bb98743715b6949950f5079822ff7a3e1a77683bbd9620a888
|
| 3 |
+
size 11242
|
arcade/sounds/16k/win.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:177c1154924935fce635cc17c82b338b3a8524025be7ebd1433a279d48b4c181
|
| 3 |
+
size 17642
|
arcade/sounds/44k/boss.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:56ee028a6900a2f096eb37ea7ad964c3a99b82eff33519b51945ab5cdc20b93f
|
| 3 |
+
size 15261740
|
arcade/sounds/44k/boss_hit.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f60f45d49fb0a4eb53a098ee57837ef0c2e0bfa56815c22697a906fe1e1841be
|
| 3 |
+
size 8864
|
arcade/sounds/44k/bounce.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d3d1c0078b200342770c0b43d650b8968988fa00ca105d847b6fb6f8dc521d80
|
| 3 |
+
size 8864
|
arcade/sounds/44k/combo.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:43805be545b78a0f46f6d7caf010b5533a842fc54f6bee94e620a69b2b21405a
|
| 3 |
+
size 24740
|
arcade/sounds/44k/death.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3e9b7a3422665d4a5c5cb0df7e02dee7d88aecb95689037682bc233b9dff87fa
|
| 3 |
+
size 34604
|
arcade/sounds/44k/duck_run.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5d439c72171b79e22a0e836fb134cf2864b883611e6a6f43045bb4c0a388e483
|
| 3 |
+
size 7564076
|
arcade/sounds/44k/ennemy_death.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:85dc90b6b952c2d453dc5103c05fa06a225eab7762eb150b158dbd033434f743
|
| 3 |
+
size 55340
|
arcade/sounds/44k/flappy.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:388e1a892c1be85d0844944fb3907559aee5e93c67aaa8d5a3c38c1e6152caa2
|
| 3 |
+
size 2875436
|
arcade/sounds/44k/fly.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a7d8b49963394730b787b09e087ab992035b7db3b02dcd076ed41870a4813205
|
| 3 |
+
size 450922
|
arcade/sounds/44k/hit.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1d140554748ffed43b021790f0f09e96eaa32ad4556d443834a463cf43155ef6
|
| 3 |
+
size 5336
|
arcade/sounds/44k/jump.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a9530d0e1c660875dff0ea530d253726bdbffaff0cec624a3a6364d819bf57f5
|
| 3 |
+
size 25388
|
arcade/sounds/44k/miss.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3db5ebd68a9b33442cad8d682b12cb34d7ae9a23562ee59dd27c6ef31b88f7d0
|
| 3 |
+
size 13272
|
arcade/sounds/44k/perfect.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:622d842c8d4850fc0de1d1b697f8a04ca4cfc7e09c6a41f84995ae92b1c1fd17
|
| 3 |
+
size 14156
|
arcade/sounds/44k/phase.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:93369e58985b6985c44f1e71f7758798df757d7e8a3f9f4c5ca2ee272ef9917d
|
| 3 |
+
size 35324
|
arcade/sounds/44k/rythme.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9c7311785e30ce0f9c58cec2d0e53df4a5679f1d55a7fa12a3e4f989659bf799
|
| 3 |
+
size 5419052
|
arcade/sounds/44k/score.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fcd697de56e8e92e8767fe130b63476183b37bdc5b7e62f475d278e453296de4
|
| 3 |
+
size 17684
|
arcade/sounds/44k/select.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e0ccbd6673a11cb9afba1339fea9dde4060418fceca386de08b8430ed10d4a60
|
| 3 |
+
size 10624
|
arcade/sounds/44k/serve.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8586f543ceda5020a460c383f938e57ecdf4415c4c1e3a6c80b7b488884d2247
|
| 3 |
+
size 10628
|
arcade/sounds/44k/shoot.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6a96fb4a5017851544f44fedfc5715b8d48c9d6db6d30e741293141e3bdd3bdb
|
| 3 |
+
size 7100
|
arcade/sounds/44k/spaceship.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ce05d2a0525b013faa6f566974da88fe7c53db2f7548b21ae58ae72f7c31dfa2
|
| 3 |
+
size 16754514
|
arcade/sounds/44k/wave.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f2f613035beaa49786bc8ea5a42f141c99d9a234bf42fd317ca225e30b6ee6c5
|
| 3 |
+
size 30912
|
arcade/sounds/44k/win.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d11e4f84f31af78ad9d8e996d9b9c9da68d301d5a2897b8e29c2752ddafa65bc
|
| 3 |
+
size 48552
|
arcade/static/index.html
CHANGED
|
@@ -250,7 +250,8 @@
|
|
| 250 |
}
|
| 251 |
|
| 252 |
function backToLanding() {
|
| 253 |
-
|
|
|
|
| 254 |
}
|
| 255 |
|
| 256 |
function backToSetup() {
|
|
@@ -278,13 +279,13 @@
|
|
| 278 |
method: 'POST',
|
| 279 |
headers: {'Content-Type': 'application/json'},
|
| 280 |
body: JSON.stringify({use_esp32: false, port: ''})
|
| 281 |
-
}).then(() => {
|
| 282 |
-
modeSet = true;
|
| 283 |
-
window.ARCADE_CANVAS_ID = 'arcade-web';
|
| 284 |
-
const s = document.createElement('script');
|
| 285 |
-
s.src = '/static/main.js';
|
| 286 |
-
document.body.appendChild(s);
|
| 287 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
}
|
| 289 |
</script>
|
| 290 |
</body>
|
|
|
|
| 250 |
}
|
| 251 |
|
| 252 |
function backToLanding() {
|
| 253 |
+
fetch('/stop_music', {method: 'POST'});
|
| 254 |
+
setTimeout(() => window.location.reload(), 200);
|
| 255 |
}
|
| 256 |
|
| 257 |
function backToSetup() {
|
|
|
|
| 279 |
method: 'POST',
|
| 280 |
headers: {'Content-Type': 'application/json'},
|
| 281 |
body: JSON.stringify({use_esp32: false, port: ''})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
});
|
| 283 |
+
// Load game immediately (don't wait for /mode response — bridge may already be running)
|
| 284 |
+
modeSet = true;
|
| 285 |
+
window.ARCADE_CANVAS_ID = 'arcade-web';
|
| 286 |
+
const s = document.createElement('script');
|
| 287 |
+
s.src = '/static/main.js';
|
| 288 |
+
document.body.appendChild(s);
|
| 289 |
}
|
| 290 |
</script>
|
| 291 |
</body>
|
arcade/static/main.js
CHANGED
|
@@ -2988,4 +2988,4 @@ class ArcadeApp {
|
|
| 2988 |
}
|
| 2989 |
|
| 2990 |
// Boot — runs immediately (script is loaded dynamically after DOM is ready)
|
| 2991 |
-
new ArcadeApp();
|
|
|
|
| 2988 |
}
|
| 2989 |
|
| 2990 |
// Boot — runs immediately (script is loaded dynamically after DOM is ready)
|
| 2991 |
+
window._arcadeApp = new ArcadeApp();
|
pyproject.toml
CHANGED
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
| 13 |
"reachy-mini",
|
| 14 |
"scipy",
|
| 15 |
"pyserial",
|
| 16 |
-
"
|
| 17 |
"websockets",
|
| 18 |
]
|
| 19 |
keywords = ["reachy-mini-app"]
|
|
|
|
| 13 |
"reachy-mini",
|
| 14 |
"scipy",
|
| 15 |
"pyserial",
|
| 16 |
+
"soundfile",
|
| 17 |
"websockets",
|
| 18 |
]
|
| 19 |
keywords = ["reachy-mini-app"]
|