Audio-Feel-Überarbeitung + globale Audio-Config — Design-Spec¶
Datum: 2026-06-07 · Status: Design (genehmigt im Brainstorming, vor Spec-Review) Auslöser: Johannes' Geräte-Test der Audio-Reaktivität auf dem iPhone JK. Scope: Spec 1 von 2. Die audio-reaktive Interaktionsmatrix (Audio → Species-Anziehung/Abstoßung) ist bewusst ausgegliedert → eigenes Spec 2 (
…-audio-matrix-reactivity-design.md), weil sie Engine-Anbindung + neue Faktor-Semantik braucht.
1. Kontext & Motivation¶
Drei Beobachtungen am Gerät:
- Bänder ungleich ausgesteuert. Der Input-Meter zeigt bei High kaum, bei Mid wenig, bei Bass am meisten Ausschlag. Ursache (verifiziert im Code):
bandLevelmittelt dB über feste Hz-Bänder (Bass 20–250, Mid 250–4000, High 4000–16000). Musik hat ein abfallendes Spektrum (Bass trägt physikalisch mehr Energie), und das sehr breite High-Band mittelt über viele Bins nahe dem Rauschboden → niedriger Schnitt. Eine lineare „gleich große Bins"-Aufteilung würde das verschlimmern. - Reaktivität gefühlt zu zahm. Der vorhandene Gain-Slider (0.1×–10×) wirkt zwar bereits multiplikativ auch bei Auto-Gain an (
effGain = gain · autoGainMul), aber durch die bass-lastige Aussteuerung bleibt die Reaktion von Mid/High schwach. - Audio-Config wird beim Preset-Wechsel überschrieben.
audioReactivelebt inPresetParams;AppState.select(id:)machtedited = preset.params→ die gesamte Audio-Verdrahtung wird durch die des neuen Presets ersetzt. Verhindert das Durchtesten eines Audio-Setups über mehrere Looks.
Zusätzlich vorhandene, hier mit-gelöste Feel-Risiken (aus früherem Spec offen):
- Kein Silence-Gate → bei Stille zieht Auto-Gain bis ×20 hoch, Grundrauschen kann zappeln.
- Low-Loudness-Over-Boost → der lautheits-abhängige autoGainMul wird auch auf den (bereits per-Band-normalisierten) Transient-Pfad multipliziert → leise Tracks können über-flashen.
2. Ziele / Nicht-Ziele¶
Ziele
- Bass/Mid/High schlagen ähnlich aus — im Meter und in den level-Routes.
- Insgesamt kräftigere, aber dynamik-erhaltende Reaktion: leise = sanft, laut = voll, Stille = ruhig.
- Audio-Config bleibt über Preset-Wechsel/Undo/Randomize hinweg erhalten (global).
- Transient nicht mehr lautheits-über-geboostet.
Nicht-Ziele - Audio → Interaktionsmatrix (→ Spec 2). - System-/Tab-Audio (bleibt Mic-only). - Web-Parität beim Audio (wird bewusst aufgegeben, siehe §6). - Änderungen am GPU-Uniform-Layout oder an Shadern (nicht nötig).
3. Design-Überblick — zwei Pegel statt einem¶
Die zentrale Idee: rohen Pegel (für die Lautheits-Stufe) und per-Band-normalisierten Pegel (für Balance + schnelle Dynamik) trennen.
AudioAnalyzer.analyze() (Audio-Thread, pro FFT-Frame)
rawBass/Mid/High (dB→0..1, wie bisher)
│
├──> loudness = max(raw) ── roher Gesamtpegel
│
└──> Per-Band-AGC: je Band träger running-peak ── Balance + Dynamik
normBand = clamp(rawBand / max(peakBand, floor), 0, 1)
transient* (unverändert; schon per-Band peak-normalisiert)
│
AudioBands{ bass/mid/high = normBand, loudness, transient* }
│
├──> meterLevels (UI-Meter zeigt normBand → ausgeglichen)
│
└──> evaluateAudioFactors() (Render-Loop, pro Frame)
Loudness-Response resp = smoothstep(gateLo, gateHi, loudnessEnv)
effGain = gain · (autoGain ? resp : 1)
pro Route: factor += signal(normBand|transient) · depth · effGain
Warum die schnelle Dynamik erhalten bleibt: Weil der per-Band-Peak träge ist (Decay ~Sekunden), folgt normBand der schnellen Musik (leise Stelle → klein gegen den noch hohen Peak, laute Stelle → ~1). Identisches Prinzip wie der bereits funktionierende Transient-Peak.
Warum Auto-Gain seinen Sinn behält (umgedeutet): Die per-Band-Normalisierung macht den klassischen kompensierenden Auto-Gain (leise hochziehen) redundant — er würde leise sogar doppelt boosten. Stattdessen wird er zur Loudness-Response: einer sanften 0..1-Kurve auf der rohen Lautheit, die bei Stille → 0 (ruhig) und ab normaler Lautstärke → 1 (voll) geht. Das ersetzt das alte autoGainMul und das fehlende Silence-Gate in einem Konzept. Toggle „Auto-gain" aus → resp = 1 (immer volle, ungedämpfte Reaktion).
4. Teil A — Per-Band-Normalisierung + Loudness-Response¶
4.1 AudioBands.swift¶
bass/mid/hightragen künftig die normalisierten Werte (was Meter + level-Routes sehen).- Neues Feld
var loudness: Float = 0— roher Gesamtpegel (max(rawBass,rawMid,rawHigh)). peak(computedmax(bass,mid,high)) wird inevaluateAudioFactorsdurchloudnessersetzt;peakkann bleiben (von normalisierten Bändern) oder entfallen — Implementierungsdetail.level(_:)gibt damit automatisch den normalisierten Wert zurück;transient(_:)unverändert.
4.2 AudioAnalyzer.swift¶
- Rohe Band-Level wie bisher berechnen (
bandLevelbleibt unangetastet). loudness = max(rawBass, rawMid, rawHigh)setzen.- Per-Band-Level-AGC analog zur vorhandenen Transient-Peak-Mechanik (
BandT): pro Band ein träger running-peaklevelPeak, UpdatelevelPeak = max(rawBand, peakDecay · levelPeak), dannnormBand = clamp(rawBand / max(levelPeak, levelPeakFloor), 0, 1). EMA-Glättung (smoothing) wie bisher auf das normalisierte Ergebnis anwenden (weicher an den Rails). - Die
transient*-Berechnung bleibt exakt wie sie ist.
4.3 AudioFactors.swift¶
AutoGainState→ trägt denloudnessEnv(EMA der rohenloudness).- Neue Loudness-Response:
resp = smoothstep(gateLo, gateHi, loudnessEnv)(Helfer falls nötig:clamp((x-lo)/(hi-lo),0,1)mitt·t·(3-2t)). effGain = reactive.gain · (reactive.autoGain ? resp : 1).- Routen-Auswertung:
signal = route.mode == .transient ? bands.transient(src) : bands.level(src)(level = normBand);f[target] += signal · depth · effGain. Kein separater autoGainMul auf Transient mehr — beide Pfade teileneffGain.
4.4 Konstanten (Startwerte — begründet, am Gerät zu justieren)¶
| Konstante | Startwert | Begründung |
|---|---|---|
levelPeakDecay (τ) |
~2.5 s | Träge genug für Dynamik-Erhalt, schnell genug um Track-Wechsel zu folgen (Transient nutzt 3.0 s). |
levelPeakFloor |
0.05 | Verhindert Division-Explosion bei sehr leisen Bändern (Transient nutzt 0.02; höher = ruhiger bei leise). |
loudnessEnv-Glättung |
~150 ms | Folgt Passagen-Dynamik, glättet aber Einzel-Frames. |
gateLo |
0.04 | Unterhalb ≈ Rauschboden → resp 0 → Stille bleibt ruhig. |
gateHi |
0.22 | Ab „normaler Zimmerlautstärke" volle Reaktion. |
gain-Default |
1.0 | Master-Headroom; bei systematisch zu zahmer Reaktion am Gerät anheben. Range bleibt 0.1–10. |
Diese sechs Werte sind die Tuning-Schrauben. Erwartung: Johannes justiert
gateLo/gateHi(Stille-/Voll-Schwelle) undgain(Gesamt-Punch) am Gerät; ich passe die Defaults nach seinem Feedback an.
4.5 UI¶
- Toggle-Label bleibt „Auto-gain" (vertraut, konzeptuell weiterhin „automatische Anpassung an die Lautheit"). Der zugehörige Hilfetext (
ParamHelp) wird auf die neue Semantik aktualisiert: „Passt die Gesamt-Reaktion an die Lautstärke an — leise sanft, laut voll, Stille ruhig. Aus = immer volle Reaktion." (Falls im Review gewünscht: Umbenennung in „Loudness" / „Dynamik".) - Meter unverändert in der Mechanik (zeigt
meterLevels.bass/mid/high), jetzt ausgeglichen.
5. Teil B — Globale Audio-Config¶
5.1 Scope-Verschiebung¶
- Neue Quelle der Wahrheit:
AppState.audioReactive: AudioReactive(global, nicht Teil vonedited/PresetParams). AudioReactiveSectionliest/schreibtstate.audioReactivestattstate.edited.audioReactive.RenderCoordinator.applyAudio()lieststate.audioReactive.select(id:), Undo/Redo, Randomize/Mutate/Morph (AppState+Exploration) berührenaudioReactivenicht mehr → Config überlebt Look-Wechsel.setAudioEnabled(_:)-Logik (auto-on-enable der 5 Default-Routen bei leerer Liste) bleibt identisch, nur auf dem globalen Feld.
5.2 Persistenz¶
audioReactivewird global persistiert (App-Support-JSON über den vorhandenen Store-Mechanismus oder@AppStorage-kodiert) und beim App-Start geladen; Default =enabled:false, gain:1, autoGain:true, routes:[](auto-on-enable füllt beim ersten Enable).
5.3 Undo¶
- Audio-Config ist nicht Teil der Preset-Undo-History (sie ist nicht mehr in
edited). DiepushUndo()-Aufrufe inAudioReactiveSection(autoGain/gain/routes) entfallen. Zurücksetzen erfolgt über den vorhandenen „Default"-Button (stellt die 5 Default-Routen wieder her). (YAGNI: kein separates Audio-Undo.)
5.4 Migration / JSON-Kompatibilität¶
PresetParams.audioReactivebleibt im Struct (das Feld nur verschieben statt entfernen) → alle bestehenden Preset-JSONs und Custom-Presets decodieren unverändert. Es wird lediglich nicht mehr als Live-Quelle gelesen;AppState.audioReactive(global) ist die einzige Quelle der Wahrheit.- Custom-Presets, die künftig gespeichert werden, brauchen das Feld nicht mehr; alte behalten es harmlos.
- Kein Datenverlust: Built-in-Presets haben
enabled:false, routes:[], der globale Default ist identisch.
6. Web-Parität (bewusste Abweichung)¶
Der native Audio-Pfad war ein paritätstreuer Port von computeAudioFactors. Diese Überarbeitung weicht bewusst ab (Per-Band-AGC, Loudness-Response statt kompensierendem Auto-Gain, globale Config). Die Web-App (Codeberg Pages, eingefroren) bleibt unverändert. Begründung: native ist die Zukunft (Pivot), das Feel-Tuning erfordert die Freiheit. Dokumentiert hier, damit später kein „Rückportieren" stillschweigend erwartet wird.
7. Tests¶
AudioAnalyzerTests (neu/erweitert)
- Per-Band-Balance: synthetisches Spektrum mit starkem Bass + schwachem High → nach Einschwingen liefern normBass und normHigh ähnliche Größenordnung (während raw stark divergiert).
- loudness == max(rawBass,rawMid,rawHigh).
- Stille (Null-Samples) → alle normBand → ~0, loudness → ~0.
- Dynamik-Erhalt: lauter Block → leiser Block (bei trägem Peak) → normBand fällt sichtbar (nicht sofort auf 1 geklemmt).
AudioFactorsTests (erweitert)
- Loudness-Response-Kurve: loudnessEnv < gateLo → resp ≈ 0 (Faktoren neutral trotz Routen); > gateHi → resp ≈ 1 (volle Wirkung); Monotonie dazwischen.
- Transient-Entkopplung: bei konstant niedriger loudness wird ein Transient-Route nicht über effGain hochgeboostet (kein Low-Loudness-Over-Boost).
- autoGain == false → resp wirkt nicht (effGain == gain).
- gain skaliert beide Pfade gemeinsam.
AudioWiringTests / AppState (erweitert)
- select(id:) lässt audioReactive unverändert (global): Setup setzen → Preset wechseln → Routen/gain/autoGain identisch.
- setAudioEnabled auto-on-enable funktioniert auf dem globalen Feld (leer → 5 Routen + autoGain; bestehend → unverändert; disable → Routen bleiben).
- Persistenz-Roundtrip (encode/decode der globalen Config).
8. Offene / justierbare Punkte (fürs Review)¶
- Startwerte §4.4 (besonders
gateLo/gateHi,gain-Default) — Geräte-Tuning. - UI-Label „Auto-gain" behalten vs. umbenennen (§4.5).
peak-Property inAudioBandsbehalten oder entfernen (kosmetisch).
9. Bewusst NICHT in diesem Spec¶
- Audio → Interaktionsmatrix (Self-Cohesion / Inter-Affinity, additive Modulation, Engine-
audioMatrixDelta, eigener UI-Unterbereich) → Spec 2.
10. Umsetzungs-Notiz (2026-06-08) — Per-Band-Balance verschoben¶
Bei der Umsetzung hat eine Messreihe gezeigt, dass die Per-Band-Normalisierung (§4.1/§4.2) nicht wie angenommen sauber balanciert: Die FFT-Bandextraktion trennt schlecht (dB-Mittel über sehr unterschiedlich breite Bänder + Fensterleckage → ein 8-kHz-Sinus liest mid=0.69 > high=0.58, ein reiner Bass-Ton füllt alle Bänder auf 0.5–1.0). Per-Band-AGC verstärkt darauf jedes Band über dem Floor auf Vollausschlag — ein Bass-Ton würde High-Routes triggern. Das ist mit Sinus-Unit-Tests nicht validierbar und braucht Geräte-Tuning auf echter Musik bzw. eine überarbeitete Extraktion (fester perzeptueller Boost statt adaptiver AGC, oder andere Bandkanten).
Entscheidung (mit Johannes): Band-Balance entkoppeln → eigene gerätegetunte Runde. Umgesetzt aus diesem Spec: Loudness-Response (§4.3) + globale Audio-Config (§5). AudioBands.loudness (roher Max) ist drin; die Bänder bleiben vorerst roh (kein Per-Band-AGC). Messdaten + Optionen für die Balance-Runde liegen in den Session-Notizen.