Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Three-Layer Architecture

Quiver’s architecture bridges the gap between mathematical rigor and practical flexibility through three distinct layers.

graph TB
    subgraph "Layer 3: Patch Graph"
        L3A[Dynamic Topology]
        L3B[Runtime Patching]
        L3C[Type-Erased Interface]
    end

    subgraph "Layer 2: Port System"
        L2A[Signal Conventions]
        L2B[Port Definitions]
        L2C[Hardware Semantics]
    end

    subgraph "Layer 1: Typed Combinators"
        L1A[Arrow Composition]
        L1B[Compile-Time Types]
        L1C[Zero-Cost Abstractions]
    end

    L1A --> L2A
    L1B --> L2B
    L1C --> L2C
    L2A --> L3A
    L2B --> L3B
    L2C --> L3C

    style L1A fill:#4a9eff,color:#fff
    style L1B fill:#4a9eff,color:#fff
    style L1C fill:#4a9eff,color:#fff
    style L2A fill:#f9a826,color:#000
    style L2B fill:#f9a826,color:#000
    style L2C fill:#f9a826,color:#000
    style L3A fill:#50c878,color:#fff
    style L3B fill:#50c878,color:#fff
    style L3C fill:#50c878,color:#fff

Layer 1: Typed Combinators

The foundational layer provides Arrow-style functional composition with full compile-time type checking.

The Module Trait

pub trait Module: Send {
    type In;   // Input signal type
    type Out;  // Output signal type

    fn tick(&mut self, input: Self::In) -> Self::Out;
    fn process(&mut self, input: &[Self::In], output: &mut [Self::Out]);
    fn reset(&mut self);
    fn set_sample_rate(&mut self, sample_rate: f64);
}

The associated types In and Out enable compile-time verification that modules connect correctly.

Combinators

The ModuleExt trait provides composition operations:

CombinatorSignaturePurpose
chainA → B then B → C = A → CSequential composition
parallel(A → B) *** (C → D) = (A,C) → (B,D)Parallel processing
fanoutA → B and A → C = A → (B,C)Split input
first(A → B) on (A, X) = (B, X)Process first element
feedbackLoop with unit delayRecursion

Type Safety Example

// This compiles: types match
let synth = vco.chain(vcf).chain(vca);
// Vco: () → f64
// Svf: f64 → f64
// Vca: f64 → f64
// Result: () → f64 ✓

// This won't compile: type mismatch
let bad = vco.chain(stereo_module);
// Vco: () → f64
// StereoModule: (f64, f64) → (f64, f64)
// Error: expected f64, found (f64, f64) ✗

Layer 2: Port System

The middle layer adds hardware semantics through signal types and port definitions.

Signal Kinds

pub enum SignalKind {
    Audio,           // ±5V AC-coupled audio
    CvBipolar,       // ±5V control voltage
    CvUnipolar,      // 0-10V control voltage
    VoltPerOctave,   // 1V/Oct pitch standard
    Gate,            // 0V or +5V sustained
    Trigger,         // 0V or +5V brief pulse
    Clock,           // Regular timing pulses
}

Port Definitions

let spec = PortSpec::new()
    .with_input("in", PortDef::audio())
    .with_input("cutoff", PortDef::cv_unipolar().with_default(5.0))
    .with_output("lp", PortDef::audio())
    .with_output("hp", PortDef::audio());

The GraphModule Trait

Bridges typed modules to the graph:

pub trait GraphModule: Send {
    fn port_spec(&self) -> PortSpec;
    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues);
    fn reset(&mut self);
    fn set_sample_rate(&mut self, sample_rate: f64);
}

Layer 3: Patch Graph

The top layer provides runtime-configurable topology for maximum flexibility.

The Patch Container

pub struct Patch {
    nodes: SlotMap<NodeId, Box<dyn GraphModule>>,
    cables: Vec<Cable>,
    output_node: Option<NodeId>,
    processing_order: Vec<NodeId>,
}

Key Operations

// Add modules
let vco = patch.add("vco", Vco::new(44100.0));

// Connect ports
patch.connect(vco.out("saw"), vcf.in_("in"))?;

// Compile for processing
patch.compile()?;

// Process audio
let (left, right) = patch.tick();

Graph Processing

Compilation performs:

  1. Topological sort (Kahn’s algorithm)
  2. Cycle detection (no feedback without explicit delay)
  3. Signal validation (type checking with configurable strictness)

Layer Interaction

flowchart LR
    subgraph "Development Time"
        A[Define Module<br/>with types]
    end

    subgraph "Build Time"
        B[Implement<br/>GraphModule]
    end

    subgraph "Runtime"
        C[Add to Patch]
        D[Connect Ports]
        E[Compile & Run]
    end

    A --> B --> C --> D --> E

Example: Full Stack

// Layer 1: Typed module with compile-time checking
struct MyOsc {
    phase: f64,
    freq: f64,
}

impl Module for MyOsc {
    type In = f64;   // Frequency input
    type Out = f64;  // Audio output

    fn tick(&mut self, freq: f64) -> f64 {
        self.freq = freq;
        self.phase += freq / 44100.0;
        (self.phase * 2.0 * PI).sin() * 5.0
    }
}

// Layer 2: Port specification for graph integration
impl GraphModule for MyOsc {
    fn port_spec(&self) -> PortSpec {
        PortSpec::new()
            .with_input("freq", PortDef::cv_unipolar())
            .with_output("out", PortDef::audio())
    }

    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
        let freq = inputs.get("freq") * 20.0 + 20.0;  // 20-220 Hz
        let sample = <Self as Module>::tick(self, freq);
        outputs.set("out", sample);
    }
}

// Layer 3: Runtime patching
let osc = patch.add("osc", MyOsc { phase: 0.0, freq: 0.0 });
patch.connect(lfo.out("out"), osc.in_("freq"))?;

When to Use Each Layer

LayerUse When
Layer 1Building DSP algorithms with type safety
Layer 2Defining module interfaces for reuse
Layer 3Creating user-patchable synthesizers

The layers compose naturally—you can write a tight, typed DSP core and expose it through the graph system for flexible routing.