feat(audio): Visual×Audio Python contract — resolver, controls, state, API

- 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>
This commit is contained in:
BenStullsBets
2026-06-26 07:46:22 -07:00
parent 1f9b2a0bec
commit 29c6d822bd
10 changed files with 274 additions and 150 deletions
+14 -8
View File
@@ -20,7 +20,7 @@ from typing import Callable, Optional
from hef.selection import Coordinate
from player.alteration import RenderPlan, plan_alteration
from player.content import ContentResolution, resolve_content
from player.audio import resolve_visual
from player.controls import Controls
@@ -34,11 +34,14 @@ class TransitionKind:
@dataclass(frozen=True)
class Playback:
"""What should currently be playing."""
"""What should currently be playing. Visual and audio are orthogonal (audio
spec §2): `video` is whether the projector shows footage, `audio_source` is
the independent audio dial (off / soundtrack / white_noise)."""
clip_id: Optional[str] # None = black walls
plan: Optional[RenderPlan] # None when black
content: ContentResolution
video: bool # whether footage is shown (Visual on/off)
audio_source: str # the Audio dial source (off/soundtrack/white_noise)
volume: int
brightness: int
@@ -58,7 +61,8 @@ def _first(library):
_BLACK = Playback(
clip_id=None,
plan=None,
content=ContentResolution("none", False),
video=False,
audio_source="off",
volume=0,
brightness=0,
)
@@ -78,12 +82,13 @@ class Player:
self._current = _BLACK
def _resolve(self, controls: Controls) -> Playback:
content = resolve_content(controls.content)
if not content.video:
video = resolve_visual(controls.visual)
if not video:
return Playback(
clip_id=None,
plan=None,
content=content,
video=False,
audio_source=controls.audio,
volume=controls.volume,
brightness=controls.brightness,
)
@@ -92,7 +97,8 @@ class Player:
return Playback(
clip_id=clip.id,
plan=plan_alteration(coord),
content=content,
video=True,
audio_source=controls.audio,
volume=controls.volume,
brightness=controls.brightness,
)