Skip to content

Workbench

Workbench — Claude Context

Internal developer tool for loading, auditioning, and tweaking Touchstone recipes and synth voices. Not shipped to users. Depends on Horatio + Touchstone only (no Ariel, Enobarbus, or camera/Bluetooth entitlements). Stock SwiftUI, multi-platform (iOS + macOS).

Two modes: - Effects mode: Test effects chains (recipes) with signal generators or live input - Synth mode: Design voice graphs, play via MIDI keyboard, hear polyphonic output in real-time

Purpose

Build and test DSP effect chains (recipes) and synth voice topologies with real-time audio, parameter tweaking, visualization, and A/B comparison — without needing the full Prospero app or a camera.

Architecture

State

File Role
Models/WorkbenchState.swift @Observable central state: WorkbenchMode (.effects/.synth), recipe loading (effects + synth), synth recipe management (load/save/delete/revert), MIDI listener lifecycle, dual graph editing (editingRecipe + editingSynthRecipe), synth node operations (add/delete/edge), note binding management (add/remove/query), A/B comparison, parameter exposure, modulation management, user recipe persistence (separate directories)
Models/SignalType.swift 15 signal types in 6 sections: basic (sine/saw/square/noise/silence), tonal (gatedBurst/pwm/fm), transient (impulse/pluck/arpeggio), modulated (sweep/sampleAndHold), playback (sample), live (microphone)
Models/SynthesizedSample.swift Generates loopable buffers: chord (C major), drums (kick-snare), melody (C scale)
Models/NodeLayout.swift Node position for graph canvas
Models/RecipeDocument.swift FileDocument for recipe JSON export/import

Audio

File Role
Audio/WorkbenchAudioEngine.swift @Observable AVAudioEngine wrapper. Dual render path: effects mode (SignalGenerator → RecipeEngine L/R) and synth mode (MIDI → VoiceSynthEngine → stereo). Shared tail: auto-gain → brickwall limiter → waveform ring buffer → output. CPU metering, per-node profiling, live input (AVAudioSinkNode), recipe bypass, handleMIDIEvent() → SPSCRingBuffer
Audio/SignalGenerator.swift Phase-accumulator signal generation for all 15 types
Audio/MIDIInputListener.swift Persistent CoreMIDI listener: connects all sources, handles hot-plug, optional channel filter (omni default), forwards ParsedMIDIEvents to callback

Signal Chain (Effects Mode)

SignalGenerator / Live Input (AVAudioSinkNode)
        → Input Level (0 dBFS default)
        → RecipeEngine L/R (Touchstone graph processing)
        → Auto-Gain (off by default; RMS-based, -18 dBFS target, ±12 dB range)
        → BrickwallLimiter L/R (always on, -6 dBFS ceiling)
        → Feedback Mute (live + no headphones → silence)
        → Waveform ring buffer → AVAudioSourceNode

Signal Chain (Synth Mode)

MIDI Keyboard → MIDIInputListener (CoreMIDI)
        → SPSCRingBuffer<SynthCommand> (lock-free)
        → VoiceSynthEngine:
            Voice 0..N (RecipeEngine each) → ADSR envelope → stereo pan
            → Sum + polyGain
            → Post-mix effectsGraph L/R (optional, runs once on summed output)
        → Input Level → Auto-Gain → BrickwallLimiter
        → Waveform ring buffer → AVAudioSourceNode

Views (33 files)

File Role
ContentView.swift NavigationSplitView: mode-aware sidebar (RecipePickerView or SynthRecipePickerView) + detail (Inspector/Graph view modes) + VStack transport bar (keyboard + waveform + transport); synth mode detail content (recipe info, note bindings, polyphony controls)
Views/TransportView.swift Play/stop, mode picker (Effects/Synth), signal picker (effects) or MIDI status + voice count (synth), view mode picker, input/limiter controls, synth controls (polyphony stepper, glide slider, stereo spread, ADSR envelope sliders), bypass, auto-gain, CPU meter, live input, profiling
Views/RecipePickerView.swift Sidebar recipe list (effects mode), New/Clear/Edit actions
Views/SynthRecipePickerView.swift Sidebar synth recipe list (synth mode), New/Clear/Edit actions, node/binding counts
Views/RecipeInfoView.swift Recipe metadata display
Views/ParameterPanelView.swift Exposed parameter sliders
Views/WaveformView.swift Canvas waveform (green/black, 30fps)
Views/GraphEditorView.swift HSplitView: ZStack canvas + SelectedNodePanel (300px). Toolbar: add/modulate/info/A-B/save/export
Views/EdgeCanvasView.swift Bezier audio edges (solid) + modulation routes (dashed orange)
Views/NodeBoxView.swift Role-colored node with input/output ports, context menu
Views/ComponentBrowserView.swift Searchable component picker grouped by ComponentRole (69 components)
Views/AddModulationSheet.swift Modulation route creation (source/target/depth/curve/polarity)
Views/RecipeMetadataEditor.swift Recipe metadata fields
Views/NodeInspectorView.swift Per-node DisclosureGroup: parameters, bypass, timing
Views/NodeInspectorComponents.swift Shared: NodeVisualization (snapshot polling 15 Hz), NodeParameterSlider, ConnectionsSection
Views/SnapshotView.swift Live FDSPSnapshot display (15 Hz polling)
Views/PerformanceMeterView.swift CPU bar + peak marker + glitch count
Views/ValidationBannerView.swift Recipe validation status badges
Views/OnScreenKeyboardView.swift 2-octave on-screen piano keyboard (synth mode only): octave shift, velocity control, drag-to-play glissando, sends noteOn/noteOff via sendSynthCommand()

Visualizations (Views/Visualizations/)

File Components Visualized
TransferCurveView.swift Overdrive, Tape Saturation, Tube Warmth, Transformer Color
GainReductionView.swift Compressor, BrickwallLimiter, Sag
FrequencyResponseView.swift Biquad/SVF filters (Audio EQ Cookbook math)
StaircaseView.swift BitCrush (sine + quantized staircase)
WavetableMorphView.swift Oscillator waveform preview
AnalysisReadoutView.swift PitchReadout + OnsetReadout
ComponentVisualizationView.swift Dispatcher: component name → specialized viz, fallback to raw fields
SnapshotReader.swift SnapshotField model + byte-level FDSPSnapshot extraction

Key Implementation Details

  • Dual ContentView: Root ContentView.swift handles navigation; Views/ContentView.swift is the detail view (the root one is at the Workbench top level).
  • Runtime sample rate: _runtimeSampleRate negotiated from AVAudioSession (iOS) or engine output format (macOS). Used everywhere instead of hardcoded 44100.
  • Live input: AVAudioSinkNode → flat buffer → read by AVAudioSourceNode in same IO cycle. installTap was abandoned (iOS batches ~100ms regardless of hint).
  • @ObservationIgnored: All audio-thread state. Prefix _rt for audio-thread variables.
  • BrickwallLimiter: Separate L/R instances (FolioDSP kernels have internal state). Never bypassable.
  • Per-node profiling: RecipeEngine.profilingEnabled adds mach_absolute_time() per node. Separate code paths to avoid overhead when off.
  • User recipes: Saved to app support directory. Effects recipes in com.folioaudio.workbench/Recipes/, synth recipes in com.folioaudio.workbench/SynthRecipes/. User versions override bundled recipes with the same ID.
  • Synth mode: VoiceSynthEngine pre-allocates all voice RecipeEngine pairs at init. Optional post-mix effects graph (effectsGraph) runs once on summed stereo output. ADSR envelope from PolyphonyConfig (attackTime, decayTime, sustainLevel, releaseTime). Voice reset (resetAllNodes()) on fresh allocation/steal to clear old DSP state. MIDI events flow through MIDIInputListenerhandleMIDIEvent()SPSCRingBuffer<SynthCommand> → audio thread. Render callback branches on _rtVoiceSynthEngine != nil; when synthModeEnabled is true but engine is nil (e.g. all nodes deleted), outputs silence instead of falling through to effects-mode signal generator.
  • On-screen keyboard: 2-octave piano keyboard visible in synth mode when a recipe is loaded. Sends SynthCommand.noteOn/.noteOff directly via sendSynthCommand() — bypasses MIDIInputListener, same lock-free ring buffer path as hardware MIDI. Supports drag-to-play glissando, octave shift (0–7), velocity control.
  • Synth recipe editing: Separate editingSynthRecipe state with parallel graph operations (addSynthNode, addSynthEdge, deleteSynthNode). Graph editor auto-detects mode via workbenchMode. Supports both voice graph and effects graph editing — "Add Node" in synth mode prompts for target graph, effects nodes shown with orange border below a "POST-MIX EFFECTS" divider. NoteBinding editor in selected-node panel (synth mode only). MIDI binding badge on nodes with note bindings.
  • SynthRecipeLoader: Parallel to RecipeLoader — loads/saves SynthRecipe JSON. Bundled starter recipes in Packages/Touchstone/Sources/Touchstone/SynthRecipes/ (warm_pad, simple_lead, sub_bass).
  • Mode-aware sidebar: RecipePickerView for effects, SynthRecipePickerView for synth. Switched by workbenchMode.
  • MIDIInputListener: Persistent (unlike MIDILearnSession). Started/stopped with synth mode. Handles device hot-plug via MIDIClientCreateWithBlock notifications.

Rules

  • No Ariel or Enobarbus imports — Workbench is audio/recipe only (Horatio + Touchstone).
  • Recipe JSON parameter names must use exact FolioDSP bridge names (capitalized, with spaces, e.g. "Mod Rate", "Grain Size").
  • Always test with the brickwall limiter on — never bypass it, even in development.
  • When adding new visualizations, add the mapping in ComponentVisualizationView.swift's dispatcher.