Spaces:
Running
Running
| import os | |
| from typing import Optional | |
| import streamlit as st | |
| def _get_music_dir() -> str: | |
| return os.path.join(os.path.dirname(__file__), "assets", "audio", "music") | |
| def _get_effects_dir() -> str: | |
| return os.path.join(os.path.dirname(__file__), "assets", "audio", "effects") | |
| def get_audio_tracks() -> list[tuple[str, str]]: | |
| """Return list of (label, absolute_path) for .mp3 files in assets/audio/music.""" | |
| audio_dir = _get_music_dir() | |
| if not os.path.isdir(audio_dir): | |
| return [] | |
| tracks = [] | |
| for fname in os.listdir(audio_dir): | |
| if fname.lower().endswith('.mp3'): | |
| path = os.path.join(audio_dir, fname) | |
| # Use the filename without extension as the display name | |
| name = os.path.splitext(fname)[0] | |
| tracks.append((name, path)) | |
| return tracks | |
| def _load_audio_data_url(path: str) -> str: | |
| """Return a data: URL for the given audio file so the browser can play it.""" | |
| import base64, mimetypes | |
| mime, _ = mimetypes.guess_type(path) | |
| if not mime: | |
| # Default to mp3 to avoid blocked playback if unknown | |
| mime = "audio/mpeg" | |
| with open(path, "rb") as fp: | |
| encoded = base64.b64encode(fp.read()).decode("ascii") | |
| return f"data:{mime};base64,{encoded}" | |
| def _mount_background_audio(enabled: bool, src_data_url: Optional[str], volume: float, loop: bool = True) -> None: | |
| """Create/update a single hidden <audio> element in the top page and play/pause it. | |
| Args: | |
| enabled: Whether the background audio should be active. | |
| src_data_url: data: URL for the audio source. | |
| volume: 0.0–1.0 volume level. | |
| loop: Whether the audio should loop (default True). | |
| """ | |
| from streamlit.components.v1 import html as _html | |
| if not enabled or not src_data_url: | |
| _html( | |
| """ | |
| <script> | |
| (function(){ | |
| const doc = window.parent?.document || document; | |
| const el = doc.getElementById('bw-bg-audio'); | |
| if (el) { try { el.pause(); } catch(e){} } | |
| })(); | |
| </script> | |
| """, | |
| height=0, | |
| ) | |
| return | |
| # Clamp volume | |
| vol = max(0.0, min(1.0, float(volume))) | |
| should_loop = "true" if loop else "false" | |
| # Inject or update a single persistent audio element and make sure it starts after interaction if autoplay is blocked | |
| _html( | |
| f""" | |
| <script> | |
| (function(){{ | |
| const doc = window.parent?.document || document; | |
| let audio = doc.getElementById('bw-bg-audio'); | |
| if (!audio) {{ | |
| audio = doc.createElement('audio'); | |
| audio.id = 'bw-bg-audio'; | |
| audio.style.display = 'none'; | |
| doc.body.appendChild(audio); | |
| }} | |
| // Ensure loop is explicitly set every time, even if element already exists | |
| const shouldLoop = {should_loop}; | |
| audio.loop = shouldLoop; | |
| if (shouldLoop) {{ | |
| audio.setAttribute('loop', ''); | |
| }} else {{ | |
| audio.removeAttribute('loop'); | |
| }} | |
| audio.autoplay = true; | |
| audio.setAttribute('autoplay', ''); | |
| const newSrc = "{src_data_url}"; | |
| if (audio.src !== newSrc) {{ | |
| audio.src = newSrc; | |
| }} | |
| audio.muted = false; | |
| audio.volume = {vol:.3f}; | |
| const tryPlay = () => {{ | |
| const p = audio.play(); | |
| if (p && p.catch) {{ p.catch(() => {{ /* ignore autoplay block until user gesture */ }}); }} | |
| }}; | |
| tryPlay(); | |
| const unlock = () => {{ | |
| tryPlay(); | |
| }}; | |
| // Add once-only listeners to resume playback after first user interaction | |
| doc.addEventListener('pointerdown', unlock, {{ once: true }}); | |
| doc.addEventListener('keydown', unlock, {{ once: true }}); | |
| doc.addEventListener('touchstart', unlock, {{ once: true }}); | |
| }})(); | |
| </script> | |
| """, | |
| height=0, | |
| ) | |
| def _inject_audio_control_sync(): | |
| """Inject JS to sync volume and enable/disable state immediately.""" | |
| from streamlit.components.v1 import html as _html | |
| _html( | |
| ''' | |
| <script> | |
| (function(){ | |
| const doc = window.parent?.document || document; | |
| const audio = doc.getElementById('bw-bg-audio'); | |
| if (!audio) return; | |
| // Get values from Streamlit DOM | |
| const volInput = doc.querySelector('input[type="range"][aria-label="Volume"]'); | |
| const enableInput = doc.querySelector('input[type="checkbox"][aria-label="Enable music"]'); | |
| if (volInput) { | |
| volInput.addEventListener('input', function(){ | |
| audio.volume = parseFloat(this.value)/100; | |
| }); | |
| // Set initial volume | |
| audio.volume = parseFloat(volInput.value)/100; | |
| } | |
| if (enableInput) { | |
| enableInput.addEventListener('change', function(){ | |
| if (this.checked) { | |
| audio.muted = false; | |
| audio.play().catch(()=>{}); | |
| } else { | |
| audio.muted = true; | |
| audio.pause(); | |
| } | |
| }); | |
| // Set initial mute state | |
| if (enableInput.checked) { | |
| audio.muted = false; | |
| audio.play().catch(()=>{}); | |
| } else { | |
| audio.muted = true; | |
| audio.pause(); | |
| } | |
| } | |
| })(); | |
| </script> | |
| ''', | |
| height=0, | |
| ) | |
| # Sound effects functionality | |
| def get_sound_effect_files() -> dict[str, str]: | |
| """ | |
| Return dictionary of sound effect name -> absolute path. | |
| Prefers .mp3 files; falls back to .wav if no .mp3 is found. | |
| """ | |
| audio_dir = _get_effects_dir() | |
| if not os.path.isdir(audio_dir): | |
| return {} | |
| effect_names = [ | |
| "correct_guess", | |
| "incorrect_guess", | |
| "hit", | |
| "miss", | |
| "congratulations", | |
| ] | |
| def _find_effect_file(base: str) -> Optional[str]: | |
| # Prefer mp3, then wav for backward compatibility | |
| for ext in (".mp3", ".wav"): | |
| path = os.path.join(audio_dir, f"{base}{ext}") | |
| if os.path.exists(path): | |
| return path | |
| return None | |
| result: dict[str, str] = {} | |
| for name in effect_names: | |
| path = _find_effect_file(name) | |
| if path: | |
| result[name] = path | |
| return result | |
| def play_sound_effect(effect_name: str, volume: float = 0.5) -> None: | |
| """ | |
| Play a sound effect by name. | |
| Args: | |
| effect_name: One of 'correct_guess', 'incorrect_guess', 'hit', 'miss', 'congratulations' | |
| volume: Volume level (0.0 to 1.0) | |
| """ | |
| from streamlit.components.v1 import html as _html | |
| # Respect Enable Sound Effects setting from sidebar | |
| try: | |
| if not st.session_state.get("enable_sound_effects", True): | |
| return | |
| except Exception: | |
| pass | |
| sound_files = get_sound_effect_files() | |
| if effect_name not in sound_files: | |
| return # Sound file doesn't exist, silently skip | |
| sound_path = sound_files[effect_name] | |
| sound_data_url = _load_audio_data_url(sound_path) | |
| # Clamp volume | |
| vol = max(0.0, min(1.0, float(volume))) | |
| # Play sound effect using a unique audio element | |
| _html( | |
| f""" | |
| <script> | |
| (function(){{ | |
| const doc = window.parent?.document || document; | |
| const audio = doc.createElement('audio'); | |
| audio.src = "{sound_data_url}"; | |
| audio.volume = {vol:.3f}; | |
| audio.style.display = 'none'; | |
| doc.body.appendChild(audio); | |
| // Play and remove after playback | |
| audio.play().catch(e => console.error('Sound effect play error:', e)); | |
| audio.addEventListener('ended', () => {{ | |
| doc.body.removeChild(audio); | |
| }}); | |
| }})(); | |
| </script> | |
| """, | |
| height=0, | |
| ) |