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: source → destination 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.