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

Connect Modules

This guide covers the ways to connect modules in a Patch, from basic patching to attenuated and modulated cables. Every connection is made between two port references obtained from the NodeHandle that patch.add(...) returns.

Basic Connection

The fundamental operation connects an output port to an input port and returns a stable CableId:

let vco = patch.add("vco", Vco::new(44100.0));
let vcf = patch.add("vcf", Svf::new(44100.0));

let cable_id = patch.connect(vco.out("saw"), vcf.in_("in"))?;
  • vco.out("saw") — an output jack, returns a PortRef.
  • vcf.in_("in") — an input jack. Spelled in_() because in is a Rust keyword.

connect returns Result<CableId, PatchError>; the CableId stays valid even after other cables are removed, so hold onto it if you want to disconnect exactly this cable later.

Finding Port Names

Inspect a module’s PortSpec. inputs and outputs are Vec<PortDef>; each PortDef has an id, a name, and a kind (SignalKind):

let vco = Vco::new(44100.0);
let spec = vco.port_spec();

for port in &spec.inputs {
    println!("in  {}: {} ({:?})", port.id, port.name, port.kind);
}
for port in &spec.outputs {
    println!("out {}: {} ({:?})", port.id, port.name, port.kind);
}

Common port names:

ModuleInputsOutputs
Vcovoct, fm, pw, sync, fm_linsin, tri, saw, sqr
Svfin, cutoff, res, fm, keytrack, keytrack_amtlp, bp, hp, notch
Adsrgate, retrig, attack, decay, sustain, release, shapeenv, inv, eoc
Vcain, cv, response, gainout
StereoOutputleft, right(patch output)

See the Module Reference for the full list per module.

Connection with Attenuation

Scale a signal to 0–100% strength with connect_attenuated. The attenuation is clamped to 0.0..=1.0:

patch.connect_attenuated(
    lfo.out("sin"),
    vcf.in_("cutoff"),
    0.5, // 50% strength
)?;

Connection with Attenuation and Offset

For full attenuverter-plus-offset control, use connect_modulated:

patch.connect_modulated(
    lfo.out("sin"),
    vcf.in_("cutoff"),
    0.3,  // attenuation: -2.0..=2.0 (negative inverts, >1.0 amplifies)
    5.0,  // offset:      -10.0..=10.0 V, added after attenuation
)?;
  • attenuation is clamped to -2.0..=2.0. 1.0 is unity, 0.5 half strength, -1.0 inverts, 2.0 doubles (watch for clipping).
  • offset is clamped to -10.0..=10.0 volts and is added after attenuation. The example above shifts the LFO’s ±5 V swing to oscillate around +5 V.

Multiple Outputs (Mult)

One output can feed many inputs — connect it repeatedly, or use mult for a slice of destinations:

// The same gate triggers three envelopes:
patch.connect(gate.out("out"), env1.in_("gate"))?;
patch.connect(gate.out("out"), env2.in_("gate"))?;
patch.connect(gate.out("out"), env3.in_("gate"))?;

// Or in one call:
patch.mult(gate.out("out"), &[env1.in_("gate"), env2.in_("gate"), env3.in_("gate")])?;

Multiple Inputs (Summing)

Several cables into one input are summed, modeling how CVs mix at a hardware jack:

// Two LFOs combined on the filter cutoff:
patch.connect(lfo1.out("sin"), vcf.in_("cutoff"))?;
patch.connect(lfo2.out("tri"), vcf.in_("cutoff"))?;
// The cutoff input receives lfo1 + lfo2.

Normalled Inputs

Some modules declare normalled inputs in their port spec: when left unpatched, the input falls back to another port’s current value. For example, StereoOutput’s right input is normalled to left, so patching only left produces centered mono. Normalling is a property of the module definition (PortDef::normalled_to), resolved at compile time — you get the behavior automatically by leaving the input unpatched.

Validation Modes

Control how signal-kind mismatches are handled:

patch.set_validation_mode(ValidationMode::Strict); // error on mismatch
patch.set_validation_mode(ValidationMode::Warn);   // record a warning, allow it
patch.set_validation_mode(ValidationMode::None);   // no checking

The default is Warn, which flags questionable connections without blocking experimentation. In Strict mode an incompatible connection returns PatchError::SignalMismatch.

Disconnecting

Remove a specific cable by its CableId:

let cable_id = patch.connect(vco.out("saw"), vcf.in_("in"))?;
patch.disconnect(cable_id)?;

Or remove the cable between two specific ports:

patch.disconnect_ports(vco.out("saw"), vcf.in_("in"))?;

Error Handling

Connecting returns Result<CableId, PatchError>. PatchError is #[non_exhaustive], so a match needs a wildcard arm:

match patch.connect(a.out("x"), b.in_("y")) {
    Ok(cable_id) => println!("connected: {cable_id}"),
    Err(PatchError::InvalidPort { name, available, .. }) => {
        println!("no such port {name:?}; try one of {available:?}");
    }
    Err(PatchError::SignalMismatch { message, .. }) => {
        println!("signal mismatch: {message}");
    }
    Err(e) => println!("error: {e}"),
}

Note that feedback loops are not rejected at connect time — they surface as PatchError::CycleDetected from patch.compile() unless the loop passes through a cycle-breaking module such as UnitDelay or DelayLine.

Inspecting Connections

Query the current cables. cables() returns &[Cable], where each Cable has id, from, to, and optional attenuation / offset:

for cable in patch.cables() {
    println!("#{}: {:?} -> {:?}", cable.id, cable.from, cable.to);
}

Best Practices

  1. Name modules clearly"filter_lfo", not "lfo2".
  2. Keep Warn validation on during development to catch signal-kind mistakes.
  3. Check port specs when unsure of names, rather than guessing.
  4. Break feedback with a UnitDelay (or DelayLine) so compile() succeeds.
  5. Prefer attenuation over amplification to avoid clipping.