Skip to content

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 didSetdroneEngine.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.