Zum Inhalt

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: RemoteSendGate muss die Rate per Instanz erlauben (heute minParamInterval = 0.05 hartkodiert).
  • Quelle der Faktoren (entkoppelt vom Render-Loop): RemoteController ruft auf seinem Gate-Timer die pure evaluateAudioFactors(bands: appState.audio.latestBands, reactive: appState.audioReactive, autoGain: &streamAutoGain, tuning: appState.audioTuning) und sendet das resultierende AudioFactorFrame als .audioFactors. streamAutoGain ist eine eigene AutoGainState-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): RenderCoordinator veröffentlicht die bereits berechneten AudioFactors, Controller streamt diese.
  • Trigger: während verbunden und audioReactive.enabled lä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 — RemoteLink hopt schon auf Main).
  • AmbientController hält latestRemoteAudio: AudioFactorFrame? + einen Empfangs-Tick (Staleness).
  • Der tvOS-Render-Pfad reicht diese Faktoren je Frame in RenderCoordinator.applyAudio(). Dort gilt auf tvOS: frische Remote-Faktoren → in sim.audioFactors + renderer.audioPostFactors schreiben; 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 bei enabled=false ohnehin neutral sind.
  • Disconnect: AmbientController.resumeAutonomousCycle cleart remoteAudio → snap-to-neutral + resumed den ruhigen autarken T1-Cycle (bestehender handleDisconnect/resumeAutonomousCycle). Nie eingefroren.
  • Staleness: kommt für > ~0,5 s kein Frame (stiller Drop/Wi-Fi-Hänger) → tick cleart remoteAudio (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. audioInput bleibt nil (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.