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 Method | Signal Type | Range |
|---|---|---|
::voct() | V/Oct pitch | ±10V |
::gate() | Gate | 0-5V |
::trigger() | Trigger | 0-5V |
::cv() | Unipolar CV | 0-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 Note | V/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
AtomicF64uses relaxed ordering—fine for audio- Updates are lock-free (no blocking)
- Read the latest value, never stale data