Files
human-experience-filter-art/player/controls.py
T
BenStullsBets 29c6d822bd 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>
2026-06-26 07:46:22 -07:00

72 lines
2.3 KiB
Python

"""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 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
from player.audio import AUDIO_SOURCES, VISUAL_POSITIONS
KNOB_FIELDS = ("left", "right", "dark", "light", "volume", "brightness")
KNOB_MIN = 0
KNOB_MAX = 4
class ControlsError(ValueError):
"""Raised when a Controls payload is structurally invalid."""
@dataclass(frozen=True)
class Controls:
visual: str
audio: str
left: int
right: int
dark: int
light: int
volume: int
brightness: int
def validate_controls(c: Controls) -> None:
"""Raise ControlsError if the payload is structurally invalid."""
if c.visual not in VISUAL_POSITIONS:
raise ControlsError(
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)
if isinstance(value, bool) or not isinstance(value, int):
raise ControlsError(f"knob {name} must be an int, got {value!r}")
if not (KNOB_MIN <= value <= KNOB_MAX):
raise ControlsError(
f"knob {name}={value} out of range {KNOB_MIN}..{KNOB_MAX}"
)
def parse_controls(data: dict) -> Controls:
"""Build a validated Controls from a decoded mapping, rejecting unknown or
missing keys."""
known = {f.name for f in fields(Controls)}
unknown = set(data) - known
if unknown:
raise ControlsError(f"unknown keys: {sorted(unknown)}")
missing = known - set(data)
if missing:
raise ControlsError(f"missing keys: {sorted(missing)}")
c = Controls(**data)
validate_controls(c)
return c