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

Use External MIDI

Connect your Quiver patches to MIDI keyboards, controllers, and DAWs.

The AtomicF64 Bridge

MIDI events arrive on a separate thread. Use AtomicF64 for thread-safe communication:

use std::sync::Arc;
use quiver::prelude::*;

// Create shared atomic values
let pitch = Arc::new(AtomicF64::new(0.0));  // V/Oct
let gate = Arc::new(AtomicF64::new(0.0));   // Gate signal
let mod_wheel = Arc::new(AtomicF64::new(0.0)); // CC1

// Clone for MIDI thread
let pitch_midi = Arc::clone(&pitch);
let gate_midi = Arc::clone(&gate);

ExternalInput Module

Inject atomic values into your patch:

let pitch_in = patch.add("pitch", ExternalInput::voct(Arc::clone(&pitch)));
let gate_in = patch.add("gate", ExternalInput::gate(Arc::clone(&gate)));

patch.connect(pitch_in.out("out"), vco.in_("voct"))?;
patch.connect(gate_in.out("out"), env.in_("gate"))?;

ExternalInput variants:

Factory MethodSignal TypeRange
::voct()V/Oct pitch±10V
::gate()Gate0-5V
::trigger()Trigger0-5V
::cv()Unipolar CV0-10V
::cv_bipolar()Bipolar CV±5V

MIDI to V/Oct Conversion

Standard conversion:

fn midi_note_to_voct(note: u8) -> f64 {
    (note as f64 - 60.0) / 12.0
}

// In your MIDI handler:
pitch_midi.set(midi_note_to_voct(note_number));
MIDI NoteV/Oct
36 (C2)-2.0V
48 (C3)-1.0V
60 (C4)0.0V
72 (C5)+1.0V

MidiState Helper

For more complete MIDI handling:

let midi_state = MidiState::new();

// In MIDI callback:
midi_state.note_on(60, 100);  // Note 60, velocity 100
midi_state.note_off(60);
midi_state.control_change(1, 64);  // Mod wheel to 50%

// Read current state:
let current_voct = midi_state.voct();
let current_gate = midi_state.gate();
let mod_value = midi_state.cc(1);

Example: Complete MIDI Integration

//! How-To: MIDI Input Integration
//!
//! Demonstrates connecting external MIDI to a Quiver patch using AtomicF64
//! for thread-safe communication between MIDI and audio threads.
//!
//! Run with: cargo run --example howto_midi

use quiver::prelude::*;
use std::sync::Arc;

fn main() {
    let sample_rate = 44100.0;

    // Thread-safe communication channels
    let pitch_cv = Arc::new(AtomicF64::new(0.0)); // V/Oct
    let gate_cv = Arc::new(AtomicF64::new(0.0)); // Gate
    let velocity_cv = Arc::new(AtomicF64::new(5.0)); // Velocity (0-10V)
    let mod_wheel_cv = Arc::new(AtomicF64::new(0.0)); // CC1 modulation

    // Create patch
    let mut patch = Patch::new(sample_rate);

    // External inputs
    let pitch = patch.add("midi_pitch", ExternalInput::voct(Arc::clone(&pitch_cv)));
    let gate = patch.add("midi_gate", ExternalInput::gate(Arc::clone(&gate_cv)));
    let velocity = patch.add("midi_vel", ExternalInput::cv(Arc::clone(&velocity_cv)));
    let mod_wheel = patch.add("mod_wheel", ExternalInput::cv(Arc::clone(&mod_wheel_cv)));

    // Synth voice
    let vco = patch.add("vco", Vco::new(sample_rate));
    let vcf = patch.add("vcf", Svf::new(sample_rate));
    let vca = patch.add("vca", Vca::new());
    let env = patch.add("env", Adsr::new(sample_rate));
    let output = patch.add("output", StereoOutput::new());

    // MIDI → synth connections
    patch.connect(pitch.out("out"), vco.in_("voct")).unwrap();
    patch.connect(gate.out("out"), env.in_("gate")).unwrap();

    // Audio chain
    patch.connect(vco.out("saw"), vcf.in_("in")).unwrap();
    patch.connect(vcf.out("lp"), vca.in_("in")).unwrap();
    patch.connect(vca.out("out"), output.in_("left")).unwrap();
    patch.connect(vca.out("out"), output.in_("right")).unwrap();

    // Modulation routing
    patch.connect(env.out("env"), vcf.in_("cutoff")).unwrap();
    patch.connect(env.out("env"), vca.in_("cv")).unwrap();
    patch.connect(mod_wheel.out("out"), vcf.in_("fm")).unwrap(); // Mod wheel → filter

    patch.set_output(output.id());
    patch.compile().unwrap();

    println!("=== MIDI Integration Demo ===\n");

    // Simulate MIDI events (in real app, these come from MIDI callback)
    fn midi_note_to_voct(note: u8) -> f64 {
        (note as f64 - 60.0) / 12.0
    }

    fn midi_velocity_to_cv(velocity: u8) -> f64 {
        velocity as f64 / 127.0 * 10.0
    }

    fn midi_cc_to_cv(value: u8) -> f64 {
        value as f64 / 127.0 * 10.0
    }

    // Simulate playing a C4 note
    println!("Simulating MIDI Note On: C4 (60), velocity 100");
    pitch_cv.set(midi_note_to_voct(60));
    velocity_cv.set(midi_velocity_to_cv(100));
    gate_cv.set(5.0); // Gate high

    // Process some samples during note
    let attack_samples = (sample_rate * 0.3) as usize;
    for _ in 0..attack_samples {
        patch.tick();
    }
    println!("  Attack phase processed ({} samples)", attack_samples);

    // Simulate mod wheel movement
    println!("\nSimulating CC1 (Mod Wheel): 64");
    mod_wheel_cv.set(midi_cc_to_cv(64));

    // More processing
    for _ in 0..(sample_rate * 0.2) as usize {
        patch.tick();
    }

    // Simulate note off
    println!("\nSimulating MIDI Note Off");
    gate_cv.set(0.0); // Gate low

    // Process release
    let release_samples = (sample_rate * 0.5) as usize;
    for _ in 0..release_samples {
        patch.tick();
    }
    println!("  Release phase processed ({} samples)", release_samples);

    // Play a chord (demonstrating polyphony would need PolyPatch)
    println!("\n--- Playing ascending notes ---");
    for (note, name) in [(60, "C4"), (64, "E4"), (67, "G4"), (72, "C5")] {
        // Note on
        pitch_cv.set(midi_note_to_voct(note));
        gate_cv.set(5.0);

        // Play for 200ms
        let mut peak = 0.0_f64;
        for _ in 0..(sample_rate * 0.2) as usize {
            let (left, _) = patch.tick();
            peak = peak.max(left.abs());
        }

        // Note off
        gate_cv.set(0.0);
        for _ in 0..(sample_rate * 0.1) as usize {
            patch.tick();
        }

        println!(
            "  {} (MIDI {}): V/Oct = {:.3}V, peak = {:.2}V",
            name,
            note,
            midi_note_to_voct(note),
            peak
        );
    }

    println!("\nMIDI integration complete.");
    println!("In a real application:");
    println!("  1. Create AtomicF64 values for each MIDI parameter");
    println!("  2. Update them from your MIDI callback");
    println!("  3. The audio thread reads the latest values each tick");
}

Gate vs Trigger

sequenceDiagram
    participant K as Keyboard
    participant G as Gate
    participant T as Trigger

    K->>G: Key Down
    G->>G: Goes HIGH (+5V)
    T->>T: Brief pulse (5ms)

    Note over G: Stays HIGH while held

    K->>G: Key Up
    G->>G: Goes LOW (0V)
  • Gate: Stays high while key held (for sustain)
  • Trigger: Brief pulse at note start (for percussion)

Velocity Mapping

Convert MIDI velocity (0-127) to CV:

fn velocity_to_cv(velocity: u8) -> f64 {
    velocity as f64 / 127.0 * 10.0  // 0-10V range
}

Route to VCA for dynamics:

let velocity_in = patch.add("vel", ExternalInput::cv(vel_atomic));
patch.connect(velocity_in.out("out"), vca.in_("cv"))?;

Pitch Bend

Pitch bend is typically ±2 semitones:

fn pitch_bend_to_voct(bend: i16) -> f64 {
    // bend: -8192 to +8191
    // Result: ±2 semitones = ±(2/12) V = ±0.167V
    (bend as f64 / 8192.0) * (2.0 / 12.0)
}

Sum with note pitch:

let total_pitch = note_voct + bend_voct;
pitch_atomic.set(total_pitch);

Thread Safety Notes

  • AtomicF64 uses relaxed ordering—fine for audio
  • Updates are lock-free (no blocking)
  • Read the latest value, never stale data