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

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 display
  • RisingEdge: Trigger on positive zero-crossing
  • FallingEdge: Trigger on negative zero-crossing
  • Single: 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);