29c6d822bd
- player/audio.py: pure resolve_visual / resolve_audio_url / resolve_audio (§4/§6) - retire player/content.py (7-way table) + its test - player/controls.py: serial contract content -> visual + audio - player/state.py: Playback carries video:bool + audio_source:str - simulator/app.py: /api/alteration validates visual+audio, returns render.video + server-resolved render.audio.url from altitude_index; NOISE_URL constant - tests reframed; full suite 285 passed, 2 skipped Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.2 KiB
Python
55 lines
2.2 KiB
Python
"""Resolve the orthogonal Visual × Audio controls into render outputs (audio spec
|
||
§4/§6). The single source of truth for: whether the projector shows video, and
|
||
which audio url plays for a given (audio source, current altitude).
|
||
|
||
Replaces player/content.py's 7-way bundled table. White-noise is a global,
|
||
altitude-independent bed; soundtrack follows the single Altitude dial; off is
|
||
silence. `music` is a reserved, deferred source — not in v1."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
|
||
VISUAL_POSITIONS = frozenset({"on", "off"})
|
||
# v1 audio sources. "music" is reserved/deferred (no assets) and intentionally absent.
|
||
AUDIO_SOURCES = frozenset({"off", "soundtrack", "white_noise"})
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AudioResolution:
|
||
source: str
|
||
url: str | None
|
||
altitude_coupled: bool
|
||
|
||
|
||
def resolve_visual(position: str) -> bool:
|
||
"""Visual on/off → whether the renderer shows video (vs fade-to-black)."""
|
||
if position not in VISUAL_POSITIONS:
|
||
raise ValueError(
|
||
f"unknown visual position {position!r}; expected one of {sorted(VISUAL_POSITIONS)}"
|
||
)
|
||
return position == "on"
|
||
|
||
|
||
def resolve_audio_url(source: str, *, scale_audio: str, noise_url: str) -> str | None:
|
||
"""The audio url for (source, current altitude): soundtrack → the current
|
||
scale's asset (or None if none authored); white_noise → the global bed; off →
|
||
None. `scale_audio` is the ring scale's `audio` path (relative to /media/audio/)."""
|
||
if source not in AUDIO_SOURCES:
|
||
raise ValueError(
|
||
f"unknown audio source {source!r}; expected one of {sorted(AUDIO_SOURCES)}"
|
||
)
|
||
if source == "off":
|
||
return None
|
||
if source == "white_noise":
|
||
return noise_url
|
||
# soundtrack — couple to the current altitude's asset
|
||
return f"/media/audio/{scale_audio}" if scale_audio else None
|
||
|
||
|
||
def resolve_audio(source: str, *, scale_audio: str, noise_url: str) -> AudioResolution:
|
||
"""The full render.audio view: source, resolved url, and whether it follows the
|
||
Altitude dial (true only for soundtrack)."""
|
||
url = resolve_audio_url(source, scale_audio=scale_audio, noise_url=noise_url)
|
||
return AudioResolution(source=source, url=url, altitude_coupled=(source == "soundtrack"))
|