Visualize Your Patch
Quiver provides tools to visualize patch topology and analyze signals.
DOT/GraphViz Export
Generate visual diagrams of your patch:
use quiver::prelude::*;
let patch = /* your patch */;
// Create exporter with default style
let exporter = DotExporter::new(&patch);
let dot = exporter.to_dot();
println!("{}", dot);
Save to file and render:
# Save DOT output
cargo run > patch.dot
# Render with GraphViz
dot -Tpng patch.dot -o patch.png
dot -Tsvg patch.dot -o patch.svg
Styling Options
Customize the visualization:
let style = DotStyle::new()
.with_theme("dark") // dark, light, minimal
.with_rankdir("LR") // LR (left-right) or TB (top-bottom)
.with_show_port_names(true)
.with_signal_colors(true); // Color-code by signal type
let exporter = DotExporter::with_style(&patch, style);
Signal type colors:
- Audio: Blue
- CV: Orange
- Gate/Trigger: Green
- V/Oct: Red
Example Output
flowchart LR
subgraph Oscillators
VCO[VCO]
LFO[LFO]
end
subgraph Processing
VCF[VCF]
VCA[VCA]
end
subgraph Envelope
ADSR[ADSR]
end
VCO -->|saw| VCF
LFO -->|sin| VCF
VCF -->|lp| VCA
ADSR -->|env| VCF
ADSR -->|env| VCA
VCA --> Output
style VCO fill:#4a9eff
style LFO fill:#f9a826
style ADSR fill:#50c878
Oscilloscope
Monitor signals in real-time:
let scope = Scope::new(44100.0)
.with_buffer_size(1024)
.with_trigger_mode(TriggerMode::RisingEdge);
// In your audio loop
let sample = patch.tick().0;
scope.write(sample);
// Get waveform for display
let waveform = scope.buffer();
Trigger modes:
Free: Continuous displayRisingEdge: Trigger on positive zero-crossingFallingEdge: Trigger on negative zero-crossingSingle: One-shot capture
Spectrum Analyzer
View frequency content:
let analyzer = SpectrumAnalyzer::new(44100.0);
// Feed samples
for sample in samples.iter() {
analyzer.write(*sample);
}
// Get spectrum data
let bins = analyzer.bins(); // Frequency bins
let magnitudes = analyzer.magnitudes(); // dB values
// Find dominant frequency
let peak_freq = analyzer.peak_frequency();
println!("Fundamental: {:.1} Hz", peak_freq);
Level Meter
Monitor audio levels:
let mut meter = LevelMeter::new(44100.0)
.with_peak_hold(500.0); // 500ms peak hold
// Process samples
for sample in samples.iter() {
meter.write(*sample);
}
println!("RMS: {:.1} dB", meter.rms_db());
println!("Peak: {:.1} dB", meter.peak_db());
Automation Recording
Record parameter changes:
let mut recorder = AutomationRecorder::new();
// Create a track for filter cutoff
let track_id = recorder.create_track("filter_cutoff");
// Record automation points
recorder.record(track_id, 0.0, 0.5); // Time 0s: 0.5
recorder.record(track_id, 1.0, 0.8); // Time 1s: 0.8
recorder.record(track_id, 2.0, 0.2); // Time 2s: 0.2
// Get automation data
let data = recorder.data();
let json = serde_json::to_string(&data)?;
Example: Complete Visualization
//! How-To: Visualize Your Patch
//!
//! Demonstrates patch visualization including DOT export,
//! signal analysis, and metering.
//!
//! Run with: cargo run --example howto_visualization
use quiver::prelude::*;
fn main() {
let sample_rate = 44100.0;
println!("=== Patch Visualization Demo ===\n");
// Build a patch to visualize
let mut patch = Patch::new(sample_rate);
let vco = patch.add("vco", Vco::new(sample_rate));
let lfo = patch.add("lfo", Lfo::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());
// Connections
patch.connect(vco.out("saw"), vcf.in_("in")).unwrap();
patch.connect(lfo.out("sin"), vcf.in_("fm")).unwrap();
patch.connect(vcf.out("lp"), vca.in_("in")).unwrap();
patch.connect(env.out("env"), vcf.in_("cutoff")).unwrap();
patch.connect(env.out("env"), vca.in_("cv")).unwrap();
patch.connect(vca.out("out"), output.in_("left")).unwrap();
patch.connect(vca.out("out"), output.in_("right")).unwrap();
patch.set_output(output.id());
patch.compile().unwrap();
// Generate DOT visualization
println!("--- DOT Graph Output ---");
println!("(Save this to a .dot file and render with GraphViz)\n");
let style = DotStyle::default();
let dot = DotExporter::export(&patch, &style);
println!("{}", dot);
// Generate audio for analysis
println!("\n--- Signal Analysis ---\n");
// Collect samples
let num_samples = (sample_rate * 0.5) as usize;
let mut samples = Vec::with_capacity(num_samples);
for _ in 0..num_samples {
let (left, _) = patch.tick();
samples.push(left);
}
// Basic statistics
let peak = samples.iter().map(|s| s.abs()).fold(0.0_f64, f64::max);
let rms = (samples.iter().map(|s| s * s).sum::<f64>() / num_samples as f64).sqrt();
let dc_offset = samples.iter().sum::<f64>() / num_samples as f64;
println!("Sample Statistics:");
println!(" Samples: {}", num_samples);
println!(
" Peak: {:.3}V ({:.1} dB)",
peak,
20.0 * (peak / 5.0).log10()
);
println!(" RMS: {:.3}V ({:.1} dB)", rms, 20.0 * (rms / 5.0).log10());
println!(" DC Offset: {:.6}V", dc_offset);
// Estimate frequency via zero crossings
let mut zero_crossings = 0;
for i in 1..samples.len() {
if (samples[i] >= 0.0) != (samples[i - 1] >= 0.0) {
zero_crossings += 1;
}
}
let estimated_freq = zero_crossings as f64 / 2.0 / (num_samples as f64 / sample_rate);
println!(" Estimated Frequency: {:.1} Hz", estimated_freq);
// ASCII waveform visualization
println!("\n--- Waveform (ASCII) ---\n");
let display_samples = 80; // Characters wide
let step = samples.len() / display_samples;
for row in (0..11).rev() {
let threshold = (row as f64 - 5.0) / 5.0 * peak;
let mut line = String::new();
for col in 0..display_samples {
let sample = samples[col * step];
if (sample >= threshold && row > 5) || (sample <= threshold && row < 5) {
line.push('█');
} else if row == 5 {
line.push('─');
} else {
line.push(' ');
}
}
let label = match row {
10 => "+peak",
5 => " 0V ",
0 => "-peak",
_ => " ",
};
println!("{} |{}", label, line);
}
// Using the Scope module
println!("\n--- Scope Analysis ---\n");
let mut scope = Scope::new(1024); // Buffer size in samples
// Recreate patch for fresh samples
patch.compile().unwrap();
// Fill scope buffer
for _ in 0..1024 {
let (left, _) = patch.tick();
scope.tick(left);
}
let buffer = scope.buffer_vec();
println!("Scope buffer size: {} samples", buffer.len());
// Using LevelMeter
println!("\n--- Level Meter ---\n");
let mut meter = LevelMeter::new(sample_rate);
for _ in 0..(sample_rate * 0.1) as usize {
let (left, _) = patch.tick();
meter.tick(left);
}
println!("Level Meter:");
println!(" RMS Level: {:.2} dB", meter.rms());
println!(" Peak Level: {:.2} dB", meter.peak());
// Module graph summary
println!("\n--- Patch Summary ---\n");
println!("Modules: {}", patch.node_count());
println!("Cables: {}", patch.cable_count());
println!("\nTo visualize graphically:");
println!(" 1. Save the DOT output above to 'patch.dot'");
println!(" 2. Run: dot -Tpng patch.dot -o patch.png");
println!(" 3. Open patch.png in an image viewer");
}
Integration with GUIs
The visualization data is designed for easy GUI integration:
// For immediate-mode GUIs (egui, imgui)
for (i, magnitude) in analyzer.magnitudes().enumerate() {
let freq = i as f64 * sample_rate / fft_size;
draw_bar(freq, magnitude);
}
// For retained-mode GUIs
let path: Vec<(f64, f64)> = scope.buffer()
.enumerate()
.map(|(i, sample)| (i as f64, *sample))
.collect();
draw_path(&path);