Polyphonic Patches
So far we’ve built monophonic (single-voice) patches. Real keyboards need polyphony—multiple simultaneous notes. Quiver provides a complete voice allocation system.
flowchart TB
MIDI[MIDI Input] --> VA[Voice<br/>Allocator]
VA --> V1[Voice 1]
VA --> V2[Voice 2]
VA --> V3[Voice 3]
VA --> VN[Voice N]
V1 --> MIX[Mixer]
V2 --> MIX
V3 --> MIX
VN --> MIX
MIX --> OUT[Output]
Voice Allocation
When a new note arrives and all voices are busy, which voice should be “stolen”?
| Strategy | Description | Best For |
|---|---|---|
| RoundRobin | Steal oldest voice | Even wear |
| QuietestSteal | Steal softest voice | Minimal artifacts |
| OldestSteal | Steal note held longest | Predictable |
| NoSteal | Ignore new notes | Pad sounds |
| HighestPriority | High notes steal low | Melodies |
| LowestPriority | Low notes steal high | Bass lines |
Voice States
Each voice has a lifecycle:
stateDiagram-v2
[*] --> Free
Free --> Active: Note On
Active --> Releasing: Note Off
Releasing --> Free: Release Complete
Active --> Active: Retrigger
Releasing --> Active: Retrigger
Building a Polyphonic Patch
//! Tutorial: Polyphonic Patches
//!
//! Demonstrates voice allocation for playing multiple simultaneous notes.
//! This is essential for keyboard-style synthesizers.
//!
//! Run with: cargo run --example tutorial_polyphony
use quiver::prelude::*;
fn main() {
let num_voices = 4;
println!("=== Polyphony Demo ===\n");
println!("Simulating a {}-voice polyphonic synthesizer\n", num_voices);
// Create a voice allocator
let mut allocator = VoiceAllocator::new(num_voices);
// Helper to convert MIDI note to V/Oct
fn midi_to_voct(note: u8) -> f64 {
(note as f64 - 60.0) / 12.0
}
fn note_name(note: u8) -> String {
let names = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
let octave = (note / 12) as i32 - 1;
format!("{}{}", names[(note % 12) as usize], octave)
}
// Simulate playing a chord: C4, E4, G4, B4 (Cmaj7)
let chord = [60u8, 64, 67, 71]; // C4, E4, G4, B4
println!("Playing Cmaj7 chord:");
for ¬e in &chord {
if let Some(voice_idx) = allocator.note_on(note, 0.8) {
println!(
" {} (MIDI {}) → Voice {}, V/Oct = {:.3}V",
note_name(note),
note,
voice_idx,
midi_to_voct(note)
);
} else {
println!(
" {} (MIDI {}) → No voice available!",
note_name(note),
note
);
}
}
// Show voice states
println!("\nVoice states after chord:");
for i in 0..num_voices {
if let Some(voice) = allocator.voice(i) {
match voice.state {
VoiceState::Active => {
if let Some(note) = voice.note {
println!(
" Voice {}: Active, playing {} (V/Oct: {:.3}V)",
i,
note_name(note),
voice.voct
);
}
}
VoiceState::Free => println!(" Voice {}: Free", i),
VoiceState::Releasing => println!(" Voice {}: Releasing", i),
}
}
}
// Now try to play another note - will steal!
println!("\nPlaying D5 (MIDI 74) - all voices busy, must steal:");
if let Some(stolen_voice) = allocator.note_on(74, 0.9) {
println!(
" D5 assigned to Voice {} (stolen from previous note)",
stolen_voice
);
} else {
println!(" D5 could not be allocated (NoSteal mode)");
}
// Show updated states
println!("\nVoice states after steal:");
for i in 0..num_voices {
if let Some(voice) = allocator.voice(i) {
match voice.state {
VoiceState::Active => {
if let Some(note) = voice.note {
println!(
" Voice {}: Active, playing {} (V/Oct: {:.3}V)",
i,
note_name(note),
voice.voct
);
}
}
VoiceState::Free => println!(" Voice {}: Free", i),
VoiceState::Releasing => println!(" Voice {}: Releasing", i),
}
}
}
// Release some notes
println!("\nReleasing E4 and G4:");
allocator.note_off(64); // E4
allocator.note_off(67); // G4
println!("\nVoice states after release:");
for i in 0..num_voices {
if let Some(voice) = allocator.voice(i) {
match voice.state {
VoiceState::Active => {
if let Some(note) = voice.note {
println!(" Voice {}: Active, {}", i, note_name(note));
}
}
VoiceState::Free => println!(" Voice {}: Free", i),
VoiceState::Releasing => {
if let Some(note) = voice.note {
println!(" Voice {}: Releasing (was {})", i, note_name(note));
}
}
}
}
}
// Demonstrate different allocation modes
println!("\n--- Allocation Modes ---\n");
for mode in [
AllocationMode::RoundRobin,
AllocationMode::QuietestSteal,
AllocationMode::OldestSteal,
AllocationMode::NoSteal,
AllocationMode::HighestPriority,
AllocationMode::LowestPriority,
] {
let mode_name = match mode {
AllocationMode::RoundRobin => "RoundRobin",
AllocationMode::QuietestSteal => "QuietestSteal",
AllocationMode::OldestSteal => "OldestSteal",
AllocationMode::NoSteal => "NoSteal",
AllocationMode::HighestPriority => "HighestPriority",
AllocationMode::LowestPriority => "LowestPriority",
};
let desc = match mode {
AllocationMode::RoundRobin => "Cycles through voices in order",
AllocationMode::QuietestSteal => "Steals the voice with lowest envelope",
AllocationMode::OldestSteal => "Steals the note held longest",
AllocationMode::NoSteal => "Ignores new notes when full",
AllocationMode::HighestPriority => "Higher notes can steal lower",
AllocationMode::LowestPriority => "Lower notes can steal higher",
};
println!("{}: {}", mode_name, desc);
}
println!("\nPolyphony enables expressive keyboard playing and chord voicings.");
}
Per-Voice Signals
Each voice receives its own:
- V/Oct pitch — from the played note
- Gate — high while key held
- Trigger — pulse at note start
- Velocity — key strike strength
flowchart LR
VA[Voice Allocator]
VA -->|voct| VCO[VCO]
VA -->|gate| ENV[ADSR]
VA -->|velocity| VCA[Velocity VCA]
Unison and Detune
For thicker sounds, stack multiple oscillators per voice:
let config = UnisonConfig::new(3) // 3 oscillators per voice
.with_detune(0.1) // Slight detune between them
.with_spread(0.5); // Stereo spread
The slight detuning creates a chorus-like richness.
MIDI Note to V/Oct
Quiver uses the standard conversion:
$$V_{oct} = \frac{\text{MIDI} - 60}{12}$$
| MIDI Note | Name | V/Oct |
|---|---|---|
| 48 | C3 | -1.0V |
| 60 | C4 | 0.0V |
| 72 | C5 | +1.0V |
| 84 | C6 | +2.0V |
Helper function:
fn midi_note_to_voct(note: u8) -> f64 {
(note as f64 - 60.0) / 12.0
}
Voice Stealing in Action
sequenceDiagram
participant K as Keyboard
participant VA as Allocator
participant V1 as Voice 1
participant V2 as Voice 2
K->>VA: C4 Note On
VA->>V1: Assign C4
Note over V1: Playing C4
K->>VA: E4 Note On
VA->>V2: Assign E4
Note over V1,V2: Playing C4 + E4
K->>VA: G4 Note On (voices full)
VA->>V1: Steal, assign G4
Note over V1: Now playing G4
Note over V2: Still playing E4
Legato Mode
For lead sounds, you might want legato: new notes don’t retrigger the envelope if a previous note is held.
sequenceDiagram
participant K as Keys
participant E as Envelope
K->>E: C4 on
Note over E: Attack→Sustain
K->>E: D4 on (C4 still held)
Note over E: Pitch slides, no retrigger
K->>E: C4 off, D4 still held
Note over E: Sustain continues
K->>E: D4 off
Note over E: Release
Performance Considerations
Polyphony multiplies CPU usage:
- 8 voices × 4 oscillators = 32 oscillators
- Each voice has its own filter, envelope, etc.
Quiver’s block processing helps:
// Process multiple samples at once
let mut block = AudioBlock::new();
for voice in voices.iter_mut() {
voice.process_block(&mut block);
}
That concludes the Tutorials section. Next, explore How-To Guides for task-focused recipes.