tvOS T2b — Phone-Mic → ATV-Audioreaktivität (Design-Spec)¶
Datum: 2026-06-19
Status: Design abgenommen (Brainstorm), Implementation-Plan folgt
Vorgänger: T2a Editor-Remote (2026-06-16-tvos-t2a-editor-remote-design.md)
Plattform-Hinweis: „Phone" = iOS-Controller, iPhone oder iPad — der Remote-/Audio-Pfad ist
durchgängig #if os(iOS) (kein .phone-Gating); iPad hat Mikro + AVAudioEngine. Alles hier gilt
für beide identisch.
1. Ziel & Scope¶
Das ATV („Physarum Ambient", T1) hat kein Mikrofon und ist visuell-only. T2b macht den bereits verbundenen iOS-Editor-Remote (T2a) zur Audioquelle: das iOS-Gerät hört per Mic die Musik im Raum, berechnet die Audioreaktivität und streamt sie ans ATV, sodass das ATV-Visual auf den Sound reagiert — solange das Gerät als Remote verbunden ist und Audioreaktivität an ist.
In-Scope
- Neuer Wire-Case RemoteMessage.audioFactors(AudioFactorFrame) (phone→ATV, gleicher Bonjour-Kanal).
- iOS-Sendepfad: eigenes Throttle-Gate (~30 Hz), Faktor-Berechnung via bestehender pure evaluateAudioFactors.
- tvOS-Apply-Pfad: AmbientHost → AmbientController → tvOS-RenderCoordinator schreibt die Faktoren.
- Aktivierung gekoppelt an audioReactive.enabled + Verbindung; dezenter „streamt → TV"-Indikator.
- Neutralisierung bei Stop/Disconnect/Staleness; Verträglichkeit mit Crossfade/Auto-Cycle.
- Host-Tests + Geräte-Sign-off-Checkliste.
Out-of-Scope (bewusst) - Kein Mic auf dem ATV selbst (Hardware-Realität). - Keine Rückrichtung ATV→Phone (Protokoll bleibt unidirektional, Phone = Quelle). - Kein eigener „Audio-Sensor-only"-Modus ohne Editor-Remote (T2b lebt auf dem T2a-Kanal). - Web-/macOS-Remote unberührt.
2. Architektur-Entscheidung — fertige Faktoren senden (Option B)¶
Das iOS-Gerät besitzt das volle Audio-Hirn (Mic → AudioBands → Routes · Loudness-Response · AutoGain ·
Tuning → 10 Faktoren). Es streamt die fertig berechneten 10 Faktoren; das ATV schreibt sie direkt in
sim.audioFactors (6 Agent) + renderer.audioPostFactors (4 Post), die der tvOS-Render-Pfad ohnehin
jeden Frame liest (heute konstant neutral = 1.0, weil audioInput auf tvOS nil ist).
Warum B (statt rohe Bänder senden + ATV leitet ab):
- ATV bleibt dumm, kein Routes-/AutoGain-/Tuning-Sync; volles Phone-Feel landet 1:1 auf dem Bild (WYSIWYG).
- Die pure evaluateAudioFactors bleibt erhalten und getestet (läuft auf dem iOS-Gerät).
- Bandbreite trivial (10 floats @ 30 Hz).
- Bewusster Trade-off: das ATV ignoriert die Audio-Routes seines eigenen Presets. Hier korrekt — die
Factory-Presets haben Audio aus; die aktiven globalen Routes des Controllers sind die Wahrheit, und das
ATV spiegelt ohnehin dasselbe Preset.
3. Komponenten¶
3.1 Wire-Protokoll — RemoteMessage.audioFactors(AudioFactorFrame)¶
apple/PhysarumApp/Sources/Remote/RemoteMessage.swift
Neuer Case zur bestehenden Enum (.hello/.intent/.params). Codable erledigt das Framing —
keine Änderung an RemoteFraming (längen-präfigiertes JSON, ≤1 MB-Guard greift weiter).
struct AudioFactorFrame: Codable, Equatable {
// 6 Agent-Faktoren (== UniformInput.Audio Felder)
var moveSpeed, turnSpeed, deposit, sensorDistance, rhythmAmplitude, chaosStrength: Float
// 4 Post-Faktoren
var exposure, bloomIntensity, saturation, contrast: Float
}
AudioFactorFrame ist ein eigener Codable-Typ (nicht direkt UniformInput.Audio, da das ein
GPU-Uniform-Struct ist) mit Mapping zu/von AudioFactors.agentUniform + .postExposure/.postBloomIntensity/
.postSaturation/.postContrast. Default = alle 1.0 (neutral).
3.2 iOS-Sendepfad¶
apple/PhysarumApp/Sources/Remote/RemoteController.swift, RemoteSendGate.swift, App/RootView.swift
- Eigenes Gate: zweite
RemoteSendGate-Instanz mit ~30 Hz (minInterval ≈ 0.033), getrennt vom 20-Hz-Param-Gate. Kleiner Refactor:RemoteSendGatemuss die Rate per Instanz erlauben (heuteminParamInterval = 0.05hartkodiert). - Quelle der Faktoren (entkoppelt vom Render-Loop):
RemoteControllerruft auf seinem Gate-Timer die pureevaluateAudioFactors(bands: appState.audio.latestBands, reactive: appState.audioReactive, autoGain: &streamAutoGain, tuning: appState.audioTuning)und sendet das resultierendeAudioFactorFrameals.audioFactors.streamAutoGainist eine eigeneAutoGainState-Instanz im Controller. - Begründung: entkoppelt + isoliert host-testbar; funktioniert auch wenn das Preview gerade pausiert.
- Bekannte minimale Divergenz: die Stream-AutoGain-EMA läuft mit der Gate-Rate (30 Hz) statt der
Render-Rate (60 Hz) → leicht andere Envelope-Phase als das Phone-Preview. Visuell vernachlässigbar
(gleiche Routes/Bänder, langsamer EMA). Alternative für perfekte Parität (notiert, nicht gewählt):
RenderCoordinatorveröffentlicht die bereits berechnetenAudioFactors, Controller streamt diese. - Trigger: während verbunden und
audioReactive.enabledläuft der Stream; sonst gestoppt.
3.3 tvOS-Apply-Pfad¶
Remote/AmbientHost.swift → Ambient/AmbientController.swift → TV/TVPlayerView.swift →
Player/MetalView.swift → Player/RenderCoordinator.swift
AmbientHost.apply(_:to:)bekommt einen.audioFactors(frame)-Zweig →controller.receiveAudioFactors(frame)(Main-Queue-Kontrakt gilt weiter —RemoteLinkhopt schon auf Main).AmbientControllerhältlatestRemoteAudio: AudioFactorFrame?+ einen Empfangs-Tick (Staleness).- Der tvOS-Render-Pfad reicht diese Faktoren je Frame in
RenderCoordinator.applyAudio(). Dort gilt auf tvOS: frische Remote-Faktoren → insim.audioFactors+renderer.audioPostFactorsschreiben; sonst neutral.applyAudio()bleibt der einzige Frame-Write-Punkt dieser Faktoren (konsistent mit iOS/macOS).
3.4 Aktivierung (UX) — an audioReactive.enabled gekoppelt¶
Editor/AudioReactiveSection.swift, App/RootView.swift
Kein neuer Schalter. Verbunden + Audioreaktivität an → es streamt. Mic-Permission läuft über das bestehende Audio-Enable. Dezenter Indikator am Controller (z. B. in der Topbar/Audio-Sektion: „streamt → TV"), wenn aktiv verbunden + streamend.
3.5 Edge-Cases¶
- Sauberer Stop (Audio aus): das Phone sendet einen neutralen Schluss-Frame (
sendAudioFactorsNeutral) — smooth, da die Faktoren beienabled=falseohnehin neutral sind. - Disconnect:
AmbientController.resumeAutonomousCyclecleartremoteAudio→ snap-to-neutral + resumed den ruhigen autarken T1-Cycle (bestehenderhandleDisconnect/resumeAutonomousCycle). Nie eingefroren. - Staleness: kommt für > ~0,5 s kein Frame (stiller Drop/Wi-Fi-Hänger) →
tickcleartremoteAudio(neutral). - Stille: Faktoren entspannen sich von selbst (AutoGain + Transient-Envelopes), kein Sonderfall.
- Crossfade / Auto-Cycle: Faktoren sind struktur-unabhängige Multiplikatoren → wirken kontinuierlich,
unabhängig von
transitionDim/Reseed (hinter Schwarz unsichtbar, kein Eingriff nötig).
4. Plattform-Matrix¶
- iPhone + iPad: identisch (Quelle). Kein iPhone-spezifischer Code.
- macOS-Remote: out-of-scope (Remote ist heute iOS-only; eigener Follow-up wäre nötig).
- tvOS: Sink.
audioInputbleibtnil(kein lokales Mic); Remote-Faktoren sind der einzige Audio-Input.
5. Tests¶
Host-testbar (TDD):
- RemoteMessage JSON-Round-Trip inkl. neuem .audioFactors-Case (wie RemoteMessageTests).
- RemoteFraming mit dem neuen Case (Fragmentierung/Skip bleiben gültig).
- RemoteSendGate per-Instanz-Rate (30-Hz-Gate ≠ 20-Hz-Param-Gate; gemessen vom letzten Send).
- evaluateAudioFactors → AudioFactorFrame-Mapping (Bänder+Routes rein → erwartete 10 Faktoren raus).
- AmbientHost.apply(.audioFactors) → controller.receiveAudioFactors setzt latestRemoteAudio.
- tvOS-Apply: frische Faktoren → geschrieben; stale/cleared → neutral (Decay-Logik pure + getestet).
Geräte-Sign-off (Johannes, echte HW — nicht headless): - iPhone und iPad als Quelle: Musik im Raum → ATV-Visual reagiert; Feel matcht das Controller-Preview. - Toggle/Audio-aus + Disconnect → ATV wird ruhig (kein eingefrorener Faktor), T1 resumed. - Reaktion übersteht Preset-Wechsel/Crossfade auf dem ATV. - Latenz/Glätte bei 30 Hz akzeptabel (sonst auf 60 Hz heben).
6. Offene / verschobene Punkte¶
- Rate: Start mit 30 Hz (ATV-Tick-konform, geglättete Faktoren). 60 Hz als Tuning-Option falls Transienten zu weich wirken (Geräte-Entscheidung).
- AutoGain-Parität Stream vs. Preview: bewusst entkoppelt (minimale EMA-Divergenz). Perfekte Parität via „RenderCoordinator publiziert Faktoren" bleibt notierte Alternative.
- macOS-Remote (NWBrowser) als möglicher späterer Träger desselben Audio-Streams.
- Reconnect-Policy: wie T2a fail-fast (kein Auto-Retry); Stream resumed bei Neuverbindung.