Hecate
Hecate — Claude Context
Face-controlled chord synthesizer. Third app in the Prospero trilogy. Depends on Horatio + Enobarbus + Touchstone (no Ariel — has its own DroneSynthEngine). iOS only (requires TrueDepth camera for ARKit face tracking).
Concept
"Your face plays chords. Open your mouth to add the 5th and bass. Raise your eyebrows for the 3rd. Puff cheeks for the 7th. Purse lips for lush 7th+9th extensions. Head position sweeps the filter, shapes vowel formants, and dials resonance. Smile to brighten the chord with octave shimmer. A chord synth controlled entirely by your face."
Data Flow
ARTrackingSession (.face) → ARKitFaceTracker → @Published [FaceLandmarkInputSource]
→ Combine bridge → HecateState.faceSources
→ FaceMapping.defaults → normalize + route to:
→ ChordTone volumes → DroneSynthEngine.setToneVolume()
→ Bass volume → DroneSynthEngine.setBassVolume()
→ SynthParameter values → DroneSynthEngine.setParameter()
Architecture
Audio Layer
SPSCRingBuffer<T> — Generic lock-free SPSC ring buffer (256 slots, atomic head/tail)
DroneSynthEngine — @Observable, AVAudioSourceNode polyphonic chord synth:
- 6 tone banks (root, 5th, 3rd, 7th, 9th, octave), each with 3 detuned oscillators (waveform from VoicePreset)
- Sub-bass oscillator (follows root, 1 octave down)
- Bass voice (sweet-spot octave selection)
- Always-on drone (key root + 5th sine, harmonium style) — pinned to KEY root, not chord root; injected post-effects for clean pedal tone
- Tremolo LFO: sine/triangle/square/saw, BPM-synced subdivisions; applied post-effects (after reverb) so wet signal pumps; one-pole smoothed to soften sharp edges on square/saw
- 8-step arpeggiator cycling through active chord tones
- Breathing LFO (0.15 Hz, slow organic movement)
- Signal chain: ToneBanks → masterGain → faceLostFade → SVF Filter (LP) → Formant Filters (F1+F2 parallel bandpass, preset dry/F1/F2 mix) → Reverb (FeedbackNetwork) → Tremolo → Drone (post-effects injection) → Spike Protection → BrickwallLimiter
- Smile → octave shimmer (tone bank 5 amplitude, replaces chorus which was inaudible on phone speakers)
- Effects: StateVariableFilter, FeedbackNetwork, BrickwallLimiter — separate L/R instances via
makeEffectPair helper
- Face-lost handling: smooth gain fade (~100ms down, ~450ms up) instead of hard gate; also used for launch fade-in
- Spike protection: pre-limiter gain duck on extreme transients (fast duck ~0.5ms, slow recovery ~100ms)
@ObservationIgnored + _rt prefix for all audio-thread state
- Display timer @30 Hz snapshots audio state →
viz* properties for visualization
- Uses
rtOnePole from Touchstone RTHelpers for all per-sample smoothing
- All sound-shaping constants driven by
VoicePreset (via _rtPreset): waveform, detune, filter range/resonance, formant ranges/Q/mix, reverb params, sub/bass/drone amplitudes, brightness cap, breath rate
applyPreset() sends new preset via command buffer; drainCommands reconfigures DSP effects
- Limiter ceiling: -6 dBFS (hardcoded, not in preset)
Face Mapping Layer
FaceMapping — Declarative source→target mapping with normalization:
absFromCenter: uses distance from 0.5 (both head-turn directions map equally)
inputMin/inputMax: defines the sensitive range
invert: flips the mapping
- Per-tone max-accumulation: multiple face mappings targeting the same chord tone use
max() before sending to engine (prevents last-write-wins conflicts)
SoundPack System
SoundPack — bundles VoicePreset (sound) + PlasmaStyle (visuals) + metadata
VoicePreset — all sound-shaping constants (Codable): waveform, detune, filter, formants, reverb, sub, bass, drone, brightness, breath rate
PlasmaStyle — all visual constants (Codable): chord hues, particle behavior, strophalos attractor, bloom, sizing, color, vignette
VisualizationEngine enum (.plasma, future: .bars, .dotNetwork)
- Two built-in packs: Cathedral (triangle, warm, phone-speaker-friendly, cool blue plasma) and Coven (sawtooth, harmonic richness, violet plasma)
- Topology stays fixed across all packs — packs change character, not signal chain
- Constants that stay hardcoded (NOT in preset): slew rates, face-lost fade, spike protection, arp gate slew, limiter ceiling, ring buffer sizes
HecateState.currentPack with didSet → droneEngine.applyPreset()
- Settings → Sound section for pack selection
State
HecateState — @Observable, owns ARKitFaceTracker + ARTrackingSession + DroneSynthEngine
- Combine bridges from
@Published → @Observable
- Face source processing with per-mapping normalization and routing
faceLost → smooth fade to silence (face-controlled tones AND drone)
- Loading indicator: black overlay shown while
isTracking && !isRunning, with 100ms delay before startAudio() to let spinner render
- Meters default collapsed (
metersExpanded = false)
Visual Modes
- Selfie: TrackingSessionPreviewView (live camera feed)
- Plasma: Audio-reactive particle nebula (PlasmaFieldView)
Chord Voicing Mappings (Default)
| Expression |
Target |
| At rest |
Root drone |
| Open mouth (lower half) |
Add 5th |
| Mouth wide open (upper half) |
Bass note |
Raise eyebrows (browInnerUp) |
Add 3rd |
Puff cheeks (cheekPuff) |
Add 7th |
Purse lips (mouthPucker) |
Add 7th + 9th |
Sound Sculpting Mappings (Default)
| Source |
Parameter |
Range |
Mode |
| face.yaw (head turn) |
Filter cutoff |
0–0.25 |
absFromCenter |
| face.pitch (nod) |
Formant F2 (vowel shape) |
0–0.20 |
absFromCenter |
| face.roll (tilt) |
Filter resonance |
0–0.12 |
absFromCenter |
| face.smile |
Brightness (octave shimmer) |
0–1 |
linear |
| face.arkit.jawOpen |
Formant F1 (vowel openness) |
0–1 |
linear |
| face.smile |
Formant F2 (vowel shape) |
0–1 |
linear |
Visualization (PlasmaFieldView)
- 60 FPS via TimelineView, Canvas rendering
- Spawn runs in
onChange (guaranteed every frame), draw runs in Canvas
- Frame-rate-independent spawning using real elapsed
dt
- Occupancy-based spawn throttling prevents burst/gap cycle (tapers as pool approaches 80% capacity)
- Max 800 particles, 5-second lifetime, ~150/sec sustainable rate
- Spawn density driven by RMS energy + face-controlled tone amplitudes (excludes always-on root)
- No face detected (RMS near zero): sparse ambient trickle (~1/sec)
- Omnidirectional spawning (full 360° random angles)
- Per-particle sinusoidal wobble for organic lava-lamp motion
- Audio-reactive: tremolo phase → brightness/size pump, bass → center bloom, RMS → brightness, breath LFO → center drift + saturation shift, smile brightness → particle glow/size
- Chord hue palette: I=violet, ii=plum, iii=slate, IV=deep blue, V=indigo, vi=magenta, vii°=crimson
- Vignette with breath-modulated center
- Strophalos gravitational attractor: 3 gently curved arms (60° twist, π/3 from center to edge), 10-minute rotation period, particles pulled toward nearest arm (shape from density, not drawn)
- Background GPU protection: Canvas skips draw when
scenePhase != .active
DroneSynthEngine Visual Properties (@30 Hz)
| Property |
Source |
Used by |
vizToneAmplitudes[0–4] |
Tone bank current amplitudes × arp gates |
PlasmaField spawn density |
vizBassAmplitude |
Bass voice amplitude |
PlasmaField center bloom |
vizTremoloPhase |
BPM-synced LFO phase (0–1) |
PlasmaField brightness pump |
vizBreathPhase |
0.15 Hz organic LFO phase |
PlasmaField center drift |
vizChorusWidth |
Octave shimmer amplitude (smile) |
PlasmaField particle size/glow |
vizFilterSweep |
Normalized filter cutoff |
PlasmaField tangential stretch |
vizRmsEnergy |
Smoothed RMS (0.7×old + 0.3×new) |
PlasmaField brightness + spawn density |
waveformSamples |
256-sample ring buffer |
HUD mini waveform |
UI Layout
Top area (ModulatorMetersView, single header row):
HECATE title (CinzelDecorative-Black, kerning -1.3)
+ METERS toggle button (inline, right of title)
+ Visual mode icons (selfie/plasma, icon-only, right-aligned)
Collapsible meters (below header when expanded):
VOICING: JAW (ember), BROW (violet), CHEEK (cyan), PUCKER (green)
SCULPTING: TURN (violet), NOD (cyan), TILT (orange), SMILE (green, "Bright")
Center: Visual mode content (camera feed or plasma nebula)
Bottom (VStack, spacing: 0):
ChordBarView — key indicator + Nashville chord buttons (I–vii°)
HecateHUD — mini waveform, BPM stepper, ARP toggle,
sound mute, help(?), settings(⚙)
Design Language
- Core colors: charcoal (#1F1A24), violet (#A633D9), bone (#EBE6DA), ember (#F25933)
- Surface colors (dark → light): abyss (#0A090D), obsidian (#14111A), slate (#211C29), smoke (#2E2836)
- UI colors: darkPanel (obsidian @ 90%), border (white @ 12%), scrim (black @ 50%)
- Meter colors: meterVowel (#4DD9F2, warm cyan), meterReso (#F29933, warm orange), meterChorus (#66D173, soft green)
- All colors live in
DesignTokens.swift — no hardcoded colors elsewhere
- Display font: CinzelDecorative-Black (classical serif, used ONLY for "HECATE" title + "HECATE PRO")
- Body font: system rounded; Data font: system monospaced
- Vibe: Groovy, organic — distinct from Prospero's vintage brass/rosewood
- Strophalos motif: Gravitational attractor in the plasma, evoking Hecate's wheel
Tier Gating
TierConfig.debugOverride allows switching tiers in DEBUG builds (Settings → Debug section)
#if DEBUG defaults to .pro; release checks StoreManager.shared.isPro
- Free: C/F/Bb keys only, selfie mode + 15-sec plasma preview (soft toast), no arpeggiator (shows upgrade sheet)
- Pro: all 12 keys, unlimited plasma, arpeggiator
- StoreManager: single lifetime purchase (
hecate_one_time_purchase)
Files
| File |
Purpose |
HecateApp.swift |
@main entry |
ContentView.swift |
Root ZStack: visual mode, meters, HUD, mapping guide, loading indicator, plasma preview toast |
Audio/CommandRingBuffer.swift |
Re-exports SPSCRingBuffer<T> from Touchstone + SampleRingBuffer (waveform display) |
Audio/DroneSynthEngine.swift |
Chord synth engine: tone banks, filter, formants, reverb, tremolo, arp, drone, spike protection, viz snapshots, VoicePreset support |
Models/FaceMapping.swift |
Face→sound mapping definitions, NashvilleChord, MusicalKey, ChordTone, SynthParameter |
Models/HecateState.swift |
Central @Observable state, tracker + engine ownership, face processing, tier gating, SoundPack selection |
Models/VoicePreset.swift |
OscillatorWaveform enum + VoicePreset struct (all sound-shaping constants, Codable) |
Models/PlasmaStyle.swift |
PlasmaStyle struct (all visual constants for PlasmaFieldView, Codable) |
Models/SoundPack.swift |
SoundPack struct (VoicePreset + PlasmaStyle + metadata), VisualizationEngine enum |
Models/VisualMode.swift |
Visual mode enum (selfie, plasma) |
Views/ChordBarView.swift |
Nashville number chord picker |
Views/HecateHUD.swift |
Bottom bar: BPM stepper, ARP toggle, mute, help, settings |
Views/MappingGuideView.swift |
Full-screen onboarding overlay: voicing + sculpting mappings |
Views/MetronomeView.swift |
Visual beat indicator |
Views/ModulatorMetersView.swift |
Header row (HECATE + METERS toggle + mode icons) + collapsible meters |
Views/PlasmaFieldView.swift |
Audio-reactive plasma nebula with strophalos gravitational attractor |
Views/ProUpgradeView.swift |
Pro upgrade sheet: feature comparison + StoreKit purchase |
Views/SettingsView.swift |
Settings: sound pack, key, bass, drone, reverb, tremolo, debug tier |
DesignTokens.swift |
HecateTokens: colors (core, surfaces, meters), CinzelDecorative-Black font |
TierConfig.swift |
Tier gating + debug override + hasArp |
StoreManager.swift |
StoreKit 2: lifetime purchase |
Rules
- No Ariel import — Hecate has its own DroneSynthEngine.
- All audio-thread state uses
@ObservationIgnored + _rt prefix.
- Never block the render callback — use CommandRingBuffer for main→audio communication.
- Effects use separate L/R DSPComponent instances (FolioDSP kernels have internal state).
- BrickwallLimiter is always on, never bypassable.
- PlasmaFieldView: spawn in onChange (not Canvas closure) to prevent gaps.
- Drone bypasses effects chain — injected post-reverb as clean sine pedal.
- Tremolo depth didSet must also send rate (
updateTremoloRate()) to ensure both values are set.
- Always create feature branches — never commit directly to main.