Zum Inhalt

Physarum Native — Device-Feedback Iteration Design Spec

Status: Approved · 2026-06-05 · author: Johannes (brainstormed with Claude) Builds on: the shipped Capture/Export + Audio-Reactivity features. Iterates on both from on-device testing feedback. Two independent parts: A — Audio Reactivity v2 (react to changes, default wiring) and B — Capture Polish (save feedback, sheet redesign, PNG/HEIC/Live Photo).

1. Kontext & Feedback (Gerät, 2026-06-05)

  1. Save-to-Photos funktioniert, gibt aber kein Erfolgs-Feedback.
  2. Das Export-Sheet wirkt unprofessionell (karg, links-oben, plain-Text-Buttons, kein Preview).
  3. Nur PNG — gewünscht: Live Photo + Format-Wahl.
  4. Audio reagiert auf Absolutwerte → wenig Effekt; soll auf Veränderungen reagieren; + ein Default-Wiring.

2. Festgezurrte Entscheidungen (Brainstorming 2026-06-05)

Achse Entscheidung
Audio-Modell Pro Band ein zusätzliches transient-Signal (half-wave-rectified Spectral-Flux → adaptive Baseline → Fast-Attack/Slow-Release-Envelope → Peak-normalisiert, 0..1, ruht bei 0, spitzt auf Onsets). Der bestehende level (geglätteter Absolutpegel) bleibt. Keine Renderer-/UniformInput-Änderung — factor = 1 + signal·depth·effGain unverändert, ruht aber bei transient neutral und punched.
Route-Mode AudioRoute bekommt mode: {level, transient}. Codable-Default .level (alte Preset-JSONs decoden unverändert) via custom decode; Editor-Default für neue Routes .transient (Nutzer will Punch).
Default-Wiring Button „Apply default reactivity" setzt enabled+autoGain+5 Routen (s. §3.4). Nicht-destruktiv, undo-pushend.
Save-Feedback Inline im Sheet (Button → Spinner → „✓ In Fotos gesichert", grün) + Haptik (iOS UINotificationFeedbackGenerator .success). Kein globales Toast-System.
Foto-Formate PNG · HEIC · Live Photo. Live Photo = HEIC-Still + HEVC-.mov + 3 Metadaten-Haken, paired-resource-Save. HEIC mit JPEG-Fallback (HEVC-Encoder gated; Simulator hat ihn ggf. nicht).
Sheet-Redesign Preview-Karten-Layout (App-Design: PhysButton/-Section, zentriert, Thumbnail der Aufnahme im fertig-Zustand).
Sequenzierung Inc 6a Audio v2 zuerst (höchster Nutzen) · Inc 6b Capture-Polish.

3. Teil A — Audio Reactivity v2

3.1 Analyzer: das transient-Signal (AudioAnalyzer.swift)

Den vorherigen Magnitude-Spektrum-Frame behalten. Pro Band (Bin-Ranges wie bandLevel): - Flux: fluxBand = Σ max(0, mag_t[k] − mag_{t−1}[k]) (im linearen Magnitude-Domain, vDSP: vsub→vthres(0)→sve über die Band-Bins). Optional einen kleinen per-Band Noise-Floor vor der Rektifikation abziehen. - Adaptive Baseline: dev = max(0, flux − slowEMA(flux)), slowEMA mit ~1–2 s-Fenster (per-Sekunde-Zeitkonstante → per-Hop-Koeffizient). - Envelope (asymmetrisch): env = dev > env ? dev : releaseCoef·env (sofortiger Attack, Release-τ ~150–400 ms). - Normalisierung: transient = clamp(env / max(runningPeak, peakFloor), 0, 1) (per-Band Rolling-Peak, langsamer Release — wie AutoGainState, aber per Band auf das Envelope). Konstanten als per-Sekunde-Zeitkonstanten ausdrücken, in per-Hop-Koeffizienten umgerechnet (entkoppelt von Buffer/Sample-Rate). Silence-Gate: Baseline einfrieren/abklingen wenn Band-RMS unter einem kleinen Floor (Raumrauschen soll die Baseline nicht aufblähen). Testbar: synthetische Spektrenfolge — steter Ton → transient bleibt ~0; plötzlicher Onset → transient spikt → klingt ab. (Der reine Flux-pro-Band + die Envelope-Schritte als isoliert testbare Funktionen.)

3.2 Datenmodell

  • AudioBands (AudioBands.swift): zusätzlich transientBass/Mid/High: Float (oder eine parallele Triple). Helfer level(_ band:) -> Float und transient(_ band:) -> Float.
  • AudioInput publiziert die erweiterten AudioBands (latestBands + meter wie gehabt).
  • Engine RouteMode (Types.swift): public enum RouteMode: String, Codable, CaseIterable, Sendable { case level, transient }.
  • AudioRoute (Preset.swift): + var mode: RouteMode. Custom init(from:) (oder decodeIfPresent ?? .level) damit fehlendes mode zu .level decodet (Back-Compat). Memberwise/public init mit mode: Default .level.

3.3 Evaluator (AudioFactors.swift)

Eine Zeile: let signal = route.mode == .transient ? bands.transient(route.source) : bands.level(route.source), dann f[route.target] += signal * route.depth * effGain. AutoGain/effGain unverändert (gilt für beide Modi). Parität-Tests entsprechend erweitern (transient-Route → erwarteter Faktor).

3.4 Default-Wiring (AppState.swift + UI)

applyDefaultAudioWiring(): pushUndo(); edited.audioReactive = AudioReactive(
  enabled: true, gain: 1, autoGain: true, routes: [
    AudioRoute(.bass, .transient, .bloomIntensity, +1.2),
    AudioRoute(.bass, .transient, .moveSpeed,      +0.8),
    AudioRoute(.high, .transient, .exposure,       +0.6),
    AudioRoute(.mid,  .transient, .chaosStrength,  +0.7),
    AudioRoute(.mid,  .level,     .saturation,     +0.4),
  ])
Button „Apply default reactivity" in AudioReactiveSection (z.B. neben dem Routes-Header). Setzt das obige (überschreibt vorhandene Routes — nach Bestätigung/oder einfach, da undo-bar).

3.5 Route-Editor (AudioReactiveSection.swift)

  • Je Route ein kompakter Mode-Picker (Level / Trans — segmented oder Menu) zusätzlich zu source/target/depth. Layout kompakt halten (die Row hat schon source+target-Picker + depth-Slider + Remove).
  • Neuer Route-Default: .transient.
  • (Optional, später) Meter zeigt Transient-Flashes; v1 behält den Level-Meter.

4. Teil B — Capture Polish

4.1 Format-Modell (CaptureConfig.swift)

  • enum PhotoFormat: String, CaseIterable { case png, heic, livePhoto } (label/fileExtension).
  • CaptureConfig bekommt var photoFormat: PhotoFormat = .png (genutzt bei kind == .still). Live Photo impliziert eine kurze Motion-Dauer (fix ~2 s, kein User-Picker).

4.2 Service (CaptureService.swift)

  • captureStill verzweigt nach photoFormat:
  • png → bestehender encodePNG-Pfad.
  • heic → neuer encodeHEIC (CGImageDestination public.heic, q≈0.9; JPEG-Fallback wenn HEIC-Encoder fehlt, via CGImageDestinationCopyTypeIdentifiers()-Check).
  • livePhoto → rendert einen ~2 s HEVC-.mov (Live-Photo-Variante von VideoClipRecorder) und greift den Key-Frame als HEIC-Still mit geteiltem Asset-Identifier; Ergebnis = (stillURL, videoURL).
  • Ergebnis-Typ erweitern: CaptureResult trägt den/die Output-URL(s) + Art (Still-Datei / Video / LivePhoto-Paar), damit Save/Share/Preview wissen, womit sie arbeiten.

4.3 Live-Photo-Pfad (VideoClipRecorder.swift, additiv)

  • assetID = UUID().uuidString pro Capture.
  • In begin() vor startWriting(): writer.metadata = [content.identifier=assetID]; zweiten .metadata-Input + AVAssetWriterInputMetadataAdaptor (still-image-time, CMMetadataFormatDescriptionCreateWithMetadataSpecifications) hinzufügen. HEVC + .mov erzwingen (Live Photo nie .mp4).
  • Nach startSession: ein AVTimedMetadataGroup (still-image-time, Int8 0, 1-Frame-Range bei 50 % der Clip-Dauer) appenden.
  • Beim Key-Frame in appendFrame zusätzlich dessen CGImage (BGRA-Readback) für den Still merken.
  • Order: startWriting → startSession → metadata-group → frames → markAsFinished(beide) → finishWriting.
  • HEIC-Still mit kCGImagePropertyMakerAppleDictionary["17"] = assetID.

4.4 Save + Feedback (CaptureSaver.swift)

  • saveLivePhoto(still:video:) async -> Bool — ein performChanges mit addResource(.photo, still) und addResource(.pairedVideo, video) (gleiche Asset-ID in den Dateien!).
  • Bestehender saveToPhotos bleibt für PNG/HEIC/MOV. Beide geben Bool zurück → das Sheet zeigt das Ergebnis inline.

4.5 Sheet-Redesign (CaptureSheet.swift)

  • Form: Segmented Foto/Video (PhysSection-Stil). Foto → Format-Picker (PNG/HEIC/Live Photo) + Auflösung. Video → Auflösung/Dauer/Codec. Prominenter Primär-PhysButton (ember) „Aufnehmen"/„Clip rendern".
  • Running: zentrierter Fortschritt + Cancel.
  • Done (Preview-Karte): Thumbnail der Aufnahme (Still-Datei laden; Video/LivePhoto → ein Frame via AVAssetImageGenerator bzw. der HEIC-Still), Dateiname dezent, PhysButton-Aktionen: „In Fotos sichern" (ember, primär) · „Share" · „Done" (ghost). Nach Sichern: Button → Spinner → „✓ In Fotos gesichert" (grün) + Haptik; bei Fehler „Konnte nicht sichern".
  • Failed: Fehlermeldung + „Nochmal".
  • Share: teilt das Primär-Artefakt (Still bzw. Video). Live-Photo-Full-Fidelity-Share ist out-of-scope (über Fotos teilen).

5. Increments (Scope in)

  • Inc 6a — Audio v2: RouteMode (engine) + AudioRoute.mode (back-compat decode) · AudioBands.transient · AudioAnalyzer flux/baseline/envelope/peak → transient · evaluateAudioFactors Mode-Branch · applyDefaultAudioWiring + Button · Route-Mode-Picker · Tests (transient-Pipeline, evaluator-Parität, default-wiring).
  • Inc 6b — Capture-Polish: PhotoFormat + Config · HEIC-Encode (+JPEG-Fallback) · Live-Photo-Recorder-Pfad + saveLivePhoto · Sheet-Redesign (Preview-Karte) + inline Save-Feedback + Haptik · Format-Picker · Tests (pure: Dateinamen/Format-Ext; Live-Photo-Metadaten build-/device-verifiziert).

6. Testing & Verifikation

  • Unit: transient-Pipeline (steter Ton→~0, Onset→Spike); evaluateAudioFactors mit .transient-Route; applyDefaultAudioWiring (5 Routen, enabled/autoGain); PhotoFormat.fileExtension; AudioRoute decode mit/ohne mode (Back-Compat).
  • Integration (Host-GPU, wo möglich): HEIC-Encode eines Targets; Live-Photo-Build (AVAssetWriter mit den 3 Metadaten-Haken → .mov lädt als AVAsset, hat die content.identifier + still-image-time-Track). Plus der bestehende Video-Integrationstest bleibt grün.
  • Gerät (Johannes): Audio mit „Apply default reactivity" → sichtbares Punchen auf den Beat (Kick→Bloom/Move, Hat→Exposure, Snare→Chaos); Save-Feedback erscheint; Live Photo landet in Fotos als Live Photo (animiert, nicht als Still+Video getrennt); HEIC/PNG korrekt; Sheet sieht professionell aus.
  • Gate: Engine swift test + App-Suite grün; macOS + iOS-Sim BUILD SUCCEEDED; aufs iPhone deployed.

7. Out-of-scope / Risiken

  • Onset-Trigger-Routes (diskrete „kick→flash"), SuperFlux, global tunbare Attack/Release-UI → später.
  • Live-Photo-Share in voller Treue (nur Save-to-Photos paired); Drag-reorder; Video-Thumbnail-Caching.
  • HEVC/HEIC-Encoder gated → Simulator hat ihn ggf. nicht: HEIC→JPEG-Fallback; Live Photo nur auf Gerät verifizierbar.
  • Asset-ID-Mismatch = stiller Split in Fotos → eine UUID durch alle drei Schreibstellen fädeln; still-image-time-Track (nicht top-level) ist Pflicht.
  • Transient-Tuning: zu schnell→Strobe, zu langsam→Plateau; Defaults aus der Recherche (Attack sofort, Release ~150–400 ms, Baseline ~1–2 s, Peak langsam).
  • Transient hängt am Hop-Rate (≈94 Hz Tap), nicht 60 fps Render — Koeffizienten als Zeitkonstanten halten.