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:
+15
-12
@@ -1,20 +1,17 @@
|
||||
"""Control-panel state: the data shape read from the Arduino over serial.
|
||||
|
||||
This is the serial-contract payload shared with sub-project 4 (firmware). It
|
||||
models the full panel from design §6/§7: the 7-way content dial, the four
|
||||
experience knobs (0..4), and the two intensity levels (volume, brightness).
|
||||
The wire framing itself is the separate 3<->4 serial contract; this module is
|
||||
the *decoded* form.
|
||||
models the full panel from the audio spec §3: a Visual toggle (on/off) + an
|
||||
Audio source selector (off/soundtrack/white_noise), the four experience knobs
|
||||
(0..4), and the two intensity levels (volume, brightness). The wire framing
|
||||
itself is the separate 3<->4 serial contract; this module is the *decoded* form.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, fields
|
||||
|
||||
# The seven positions of the content dial (design §6).
|
||||
CONTENT_POSITIONS = frozenset(
|
||||
{"off", "white_noise", "music", "audio_track", "video", "music_video", "audio_video"}
|
||||
)
|
||||
from player.audio import AUDIO_SOURCES, VISUAL_POSITIONS
|
||||
|
||||
KNOB_FIELDS = ("left", "right", "dark", "light", "volume", "brightness")
|
||||
KNOB_MIN = 0
|
||||
@@ -27,7 +24,8 @@ class ControlsError(ValueError):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Controls:
|
||||
content: str
|
||||
visual: str
|
||||
audio: str
|
||||
left: int
|
||||
right: int
|
||||
dark: int
|
||||
@@ -38,10 +36,15 @@ class Controls:
|
||||
|
||||
def validate_controls(c: Controls) -> None:
|
||||
"""Raise ControlsError if the payload is structurally invalid."""
|
||||
if c.content not in CONTENT_POSITIONS:
|
||||
if c.visual not in VISUAL_POSITIONS:
|
||||
raise ControlsError(
|
||||
f"invalid content position {c.content!r}; "
|
||||
f"expected one of {sorted(CONTENT_POSITIONS)}"
|
||||
f"invalid visual position {c.visual!r}; "
|
||||
f"expected one of {sorted(VISUAL_POSITIONS)}"
|
||||
)
|
||||
if c.audio not in AUDIO_SOURCES:
|
||||
raise ControlsError(
|
||||
f"invalid audio source {c.audio!r}; "
|
||||
f"expected one of {sorted(AUDIO_SOURCES)}"
|
||||
)
|
||||
for name in KNOB_FIELDS:
|
||||
value = getattr(c, name)
|
||||
|
||||
Reference in New Issue
Block a user