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)¶
- Save-to-Photos funktioniert, gibt aber kein Erfolgs-Feedback.
- Das Export-Sheet wirkt unprofessionell (karg, links-oben, plain-Text-Buttons, kein Preview).
- Nur PNG — gewünscht: Live Photo + Format-Wahl.
- 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ätzlichtransientBass/Mid/High: Float(oder eine parallele Triple). Helferlevel(_ band:) -> Floatundtransient(_ band:) -> Float.AudioInputpubliziert die erweitertenAudioBands(latestBands + meter wie gehabt).- Engine
RouteMode(Types.swift):public enum RouteMode: String, Codable, CaseIterable, Sendable { case level, transient }. AudioRoute(Preset.swift):+ var mode: RouteMode. Custominit(from:)(oderdecodeIfPresent ?? .level) damit fehlendesmodezu.leveldecodet (Back-Compat). Memberwise/public init mitmode: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),
])
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).CaptureConfigbekommtvar photoFormat: PhotoFormat = .png(genutzt beikind == .still). Live Photo impliziert eine kurze Motion-Dauer (fix ~2 s, kein User-Picker).
4.2 Service (CaptureService.swift)¶
captureStillverzweigt nachphotoFormat:- png → bestehender
encodePNG-Pfad. - heic → neuer
encodeHEIC(CGImageDestinationpublic.heic, q≈0.9; JPEG-Fallback wenn HEIC-Encoder fehlt, viaCGImageDestinationCopyTypeIdentifiers()-Check). - livePhoto → rendert einen ~2 s HEVC-
.mov(Live-Photo-Variante vonVideoClipRecorder) und greift den Key-Frame als HEIC-Still mit geteiltem Asset-Identifier; Ergebnis = (stillURL, videoURL). - Ergebnis-Typ erweitern:
CaptureResultträ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().uuidStringpro Capture.- In
begin()vorstartWriting():writer.metadata = [content.identifier=assetID]; zweiten.metadata-Input +AVAssetWriterInputMetadataAdaptor(still-image-time,CMMetadataFormatDescriptionCreateWithMetadataSpecifications) hinzufügen. HEVC +.moverzwingen (Live Photo nie.mp4). - Nach
startSession: einAVTimedMetadataGroup(still-image-time, Int80, 1-Frame-Range bei 50 % der Clip-Dauer) appenden. - Beim Key-Frame in
appendFramezusätzlich dessenCGImage(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— einperformChangesmitaddResource(.photo, still)undaddResource(.pairedVideo, video)(gleiche Asset-ID in den Dateien!).- Bestehender
saveToPhotosbleibt für PNG/HEIC/MOV. Beide gebenBoolzurü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
AVAssetImageGeneratorbzw. 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·AudioAnalyzerflux/baseline/envelope/peak → transient ·evaluateAudioFactorsMode-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);
evaluateAudioFactorsmit.transient-Route;applyDefaultAudioWiring(5 Routen, enabled/autoGain);PhotoFormat.fileExtension; AudioRoute decode mit/ohnemode(Back-Compat). - Integration (Host-GPU, wo möglich): HEIC-Encode eines Targets; Live-Photo-Build (AVAssetWriter mit den 3 Metadaten-Haken →
.movlä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.