Skip to content

Touchstone

Touchstone — Claude Context

DSP engine and effect graph package. Depends on Horatio (types) and FolioDSP (C++ kernels via FolioDSPBridge). Provides the recipe-based signal graph system, polyphonic synthesis engine, and all effect abstractions. Platforms: iOS 16+ / macOS 13+.

Architecture

Recipe (JSON) → RecipeLoader → RecipeEngine (mono RT-safe runtime)
                                    ↓
                          DSPComponent instances (FolioDSP kernels)
                          topologically sorted, serial or DAG
                          block-rate modulation applied
                                    ↓
                          Processed audio buffer

SynthRecipe (JSON) → SynthRecipeLoader → VoiceSynthEngine (polyphonic)
                                              ↓
                                    N × RecipeEngine (one per voice)
                                    ADSR envelope, glide, stereo pan
                                    optional post-mix effects graph
                                              ↓
                                    Summed stereo output

Files

Effect Protocols & Legacy

File Purpose
AudioEffect.swift Base protocol: id, isEnabled, parameters, setParameter(), getParameter()
AVNodeEffect.swift Tier 1 protocol for AVAudioNode-backed effects
InlineEffect.swift Tier 3 protocol for render-block effects: process(buffer:frameCount:sampleRate:)
FXChain.swift Manages serial AVNodeEffects in engine graph; stores/orders InlineEffects for render block
DelayEffect.swift Legacy Tier 1: wraps AVAudioUnitDelay (unused in Prospero — replaced by FolioDSP DelayLine)
ReverbEffect.swift Legacy Tier 1: wraps AVAudioUnitReverb (unused — replaced by FolioDSP FeedbackNetwork)
ToneEffect.swift Tier 3: one-pole LPF, cutoff maps exponentially 200 Hz–20 kHz
FuzzEffect.swift Tier 3: tanh saturation, drive 1×–20×, per-sample slew smoothing (rate 0.002)

DSP Component Bridge (FolioDSP)

File Purpose
DSPComponent.swift Wraps FolioDSP kernels as InlineEffect. Dual-path name resolution (registry ID + display name). Factory methods: makeReverb, makeDelay, makeTone, makeFuzz
MixedDSPComponent.swift Wet/dry mix wrapper around DSPComponent. Pre-allocated dry buffer, per-sample mix smoothing
BridgeAdapter.swift Low-level FolioDSPBridge adapter: instantiation, parameter set/get, snapshot read, reset
ComponentRegistry.swift Singleton cache of all FolioDSP components. Lookup by name/ID, filter by role/category
ComponentDescriptor.swift Value type describing a DSP component: id, name, role, category, parameters. Case-insensitive parameter lookup
ParameterDescriptor.swift Single parameter metadata: id, displayName, fullRange, defaultRange, defaultValue, unit, scale, smoothing, enumLabels
ComponentTypes.swift Enums: ParameterScale, ComponentRole (Processor/Generator/ControlSource/…), ComponentCategory, ComponentKind (69+ types)

Signal Graph

File Purpose
SignalGraph.swift DAG of nodes + edges. Topological sort (Kahn's algorithm), cycle detection, serial chain factory
GraphNode.swift Node: id, componentName, bypassed, initial parameters dict
GraphEdge.swift Directed edge: sourcedestination node ID

Recipe System

File Purpose
Recipe.swift JSON-serializable effect definition: graph + metadata + exposed parameters + modulations. Custom Codable for backward compat
ExposedParameter.swift User-facing parameter mapping: displayName, target nodeId/parameterName, min/max/default
RecipeEngine.swift Mono RT-safe runtime. Serial-chain fast path (no per-node buffers) and general DAG path (pre-allocated buffers, fan-in summing). Built-in Mixer/Crossfade node. Block-rate modulation. Per-node profiling via mach_absolute_time
RecipeLoader.swift JSON load/save. loadAll(from:) for batch loading. bundledRecipesURL via Bundle.module
RecipeError.swift Error enum: cyclicGraph, unknownComponent, unknownNode, noNodes, validationFailed
RecipeValidation.swift Validation types: severity, issue, result
RecipeValidator.swift Stateless validator: component resolution, parameter names, value ranges, modulation targets, structural integrity

Modulation

File Purpose
ModulationRoute.swift Connects modulator output → target parameter. sourceField for multi-output (ChaosAttractor x/y/z). Depth, curve, polarity. Caches baseValue to prevent drift
ModulationCurve.swift Curve enum: linear, exponential (x²), logarithmic (√x), sCurve (smoothstep)
ModulationPolarity.swift Polarity enum: bipolar (-1..1), unipolar (0..1)

Polyphonic Synthesis

File Purpose
SynthRecipe.swift Voice graph + optional effects graph + note bindings + polyphony config. Custom Codable with backward-compat defaults
VoiceSynthEngine.swift Polyphonic RT-safe synth. Pre-allocated voice pool, voice stealing (oldest/quietest/none), per-voice ADSR + glide + pitch bend + stereo pan. Optional post-mix effects (separate L/R RecipeEngine pair). Processes SynthCommand queue via SPSCRingBuffer
SynthRecipeLoader.swift JSON load/save for synth recipes. bundledSynthRecipesURL via Bundle.module

Real-Time Utilities

File Purpose
RTHelpers.swift @inlinable RT-safe helpers: rtOnePole() (smoothing), rtAsymmetricSlew() (rise/fall), rtSpikeGain() (duck/recover)
SPSCRingBuffer.swift Generic lock-free single-producer single-consumer ring buffer. OSAtomic head/tail pointers

Key Implementation Details

RecipeEngine Processing Paths

  • Serial chain (fast path): Linear graph + no modulation + no Mixer → reuse single buffer, no per-node allocation
  • General DAG (slow path): Branching/fan-in graphs → pre-allocated per-node buffers, topological sort, fan-in summing
  • Mixer/Crossfade: Recognized by componentName == "Mixer". Not a DSPComponent — balance managed by engine. Two-input blend: A*(1-balance) + B*balance
  • Modulator-only nodes: Generators with no audio edges are ticked for phase sync but excluded from audio output

Modulation

  • Block-rate: modulators advanced once per process call, not per sample
  • Multi-output sources (ChaosAttractor, KnotModulator) read x/y/z via FDSPSnapshot field
  • setExposedParameter updates modulation route base values to prevent drift

VoiceSynthEngine

  • Voice stealing calls resetAllNodes() — clears delay tails, filter state, LFO phase
  • Note retrigger re-triggers ADSR without resetting DSP state (legato)
  • Legato glide only when other notes active and legatoGlide=true
  • Note bindings pre-resolved at init → O(1) per-voice parameter injection
  • Equal-power stereo panning (cos/sin) per voice based on index and stereoSpread

Parameter Resolution

  • Two-path lookup in ComponentDescriptor: exact match on id OR display name (case-insensitive)
  • Recipe JSON must use FolioDSP bridge names (capitalized, with spaces: "Mod Rate", "Drive", "Size")
  • RecipeValidator mirrors the same two-path logic for consistency

Bundled Recipes

  • Effects (Sources/Touchstone/Recipes/): miranda, falstaff, puck, iago, miranda-modulated, ariel, caliban, hamlet, lady_macbeth, lear, mercutio, oberon, ophelia, prospero, the_witches, titiana, warm_bus
  • Synth (Sources/Touchstone/SynthRecipes/): warm_pad, simple_lead, sub_bass

Tests

cd Packages/Touchstone && swift test
Test file Coverage
SignalGraphTests Topological sort, cycle detection, serial chain factory, branching DAG
RecipeEngineTests Serial/graph processing, bypass, modulation, exposed parameters, snapshots, profiling
RecipeTests Codable round-trip, validation, backward-compatible decoding
RecipeLoaderTests JSON load/save, batch loading, bundled recipes
RecipeValidationTests Component resolution, parameter names, value ranges, Mixer validation
DSPComponentTests Factory methods, parameter set/get, snapshot reading, name resolution
ModulationRouteTests Curve application, polarity, multi-output field reading
RTHelpersTests One-pole smoothing, asymmetric slew, spike gain
ToneEffectTests Cutoff mapping, filtering, bypass
FuzzEffectTests Drive scaling, saturation, slew smoothing
FXChainTests Effect attachment, serial connection, enable/disable

Rules

  • Never call DSPComponent methods directly from the render callback — use the command buffer (SPSCRingBuffer).
  • Recipe JSON parameter names must exactly match FolioDSP bridge names.
  • resetAllNodes() on voice steal/allocation — prevents stale filter state bleeding between notes.
  • Legacy effects (DelayEffect, ReverbEffect) remain for API compatibility but are unused in production.
  • Adding a new ComponentKind case requires updating ComponentTypes + any switch statements in Workbench visualizations.