quiver-dsp 0.3.3

A modular audio synthesis library using Arrow-style combinators and graph-based patching
Documentation
# Visualize Your Patch

Quiver provides tools to visualize patch topology and analyze signals. These
are standalone helpers, not graph modules—you feed them samples from
`patch.tick()` rather than patching them into the graph.

## DOT/GraphViz Export

Generate visual diagrams of your patch:

```rust,ignore
use quiver::prelude::*;

let patch = /* your patch */;

// Export with the default (dark) style
let dot = DotExporter::export_default(&patch);
println!("{}", dot);

// Or pass an explicit style
let style = DotStyle::default();
let dot = DotExporter::export(&patch, &style);
```

Save to file and render:

```bash
# 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

`DotStyle` has preset constructors and a couple of builder methods:

```rust,ignore
// Presets: default() is a dark theme
let style = DotStyle::light();      // Light theme
let style = DotStyle::minimal();    // No port names, no signal colors

// Builders
let style = DotStyle::default()
    .with_rankdir("LR")             // LR, TB, BT, RL
    .with_node_shape("box");

// All fields are public for full control
let style = DotStyle {
    show_port_names: true,
    color_by_signal: true,          // Color-code edges by signal type
    ..DotStyle::default()
};
```

Signal type colors:
- **Audio**: Blue
- **CV**: Orange
- **Gate/Trigger**: Green
- **V/Oct**: Red

## Example Output

```mermaid
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. `Scope::new` takes the buffer size in samples;
trigger settings are configured with setters:

```rust,ignore
let mut scope = Scope::new(1024);   // Buffer size in samples
scope.set_trigger_mode(TriggerMode::RisingEdge);
scope.set_trigger_level(0.0);

// In your audio loop
let (left, _right) = patch.tick();
scope.tick(left);

// Get waveform for display
let waveform = scope.buffer_vec();          // Vec<f64>
let points = scope.get_display_data();      // Vec<(x 0.0-1.0, voltage)>
```

Trigger modes:
- `Free`: Continuous display
- `RisingEdge`: Trigger on positive crossing of the trigger level
- `FallingEdge`: Trigger on negative crossing
- `AnyEdge`: Trigger on either crossing
- `Single`: One-shot capture (buffer freezes after trigger)

## Spectrum Analyzer

View frequency content. The constructor takes the FFT size (rounded up to a
power of two) and the sample rate:

```rust,ignore
let mut analyzer = SpectrumAnalyzer::new(2048, 44100.0);
analyzer.set_smoothing(0.8);        // 0.0 = none, up to 0.99

// Feed samples; the spectrum recomputes each time the buffer fills
for sample in samples.iter() {
    analyzer.tick(*sample);
}

// Get (frequency_hz, magnitude_db) pairs
let spectrum = analyzer.get_spectrum();

// Query a specific frequency
let db_at_440 = analyzer.magnitude_at(440.0);

// Find dominant frequency
let peak_freq = analyzer.peak_frequency();
println!("Fundamental: {:.1} Hz", peak_freq);
```

## Level Meter

Monitor audio levels. All readings are in dB; peak hold defaults to 1.5
seconds and is adjusted with a setter:

```rust,ignore
let mut meter = LevelMeter::new(44100.0);
meter.set_peak_hold_time(0.5, 44100.0);  // 500ms peak hold

// Process samples
for sample in samples.iter() {
    meter.tick(*sample);
}

println!("RMS: {:.1} dB", meter.rms());
println!("Peak: {:.1} dB", meter.peak());
println!("Peak hold: {:.1} dB", meter.peak_hold());
if meter.is_clipping() {
    println!("Clipping!");
}
```

## Automation Recording

Record parameter changes over time. The recorder samples parameter values at a
configurable interval via a closure; times are in samples:

```rust,ignore
let mut recorder = AutomationRecorder::new(44100.0);
recorder.set_interval(441);              // Sample every 441 ticks (10ms)
recorder.add_track("filter_cutoff");
recorder.start();

// In your audio loop: the closure supplies the current value per track
for _ in 0..44100 {
    patch.tick();
    recorder.tick(|param_id| match param_id {
        "filter_cutoff" => Some(current_cutoff),
        _ => None,
    });
}

recorder.stop();

// Inspect or export
if let Some(track) = recorder.get_track("filter_cutoff") {
    println!("Duration: {:.2}s", track.duration_seconds());
    let value = track.value_at(22050);   // Interpolated value at sample 22050
}

let data = recorder.export();            // AutomationData (serde-serializable)
let json = serde_json::to_string(&data)?;
```

You can also build tracks by hand with `AutomationTrack::new(param_id,
sample_rate)` and `track.record(time_in_samples, value)`, then thin dense data
with `simplify(tolerance)`.

## Example: Complete Visualization

```rust,ignore
{{#include ../../../examples/howto_visualization.rs}}
```

## Integration with GUIs

The visualization data is designed for easy GUI integration:

```rust,ignore
// For immediate-mode GUIs (egui, imgui)
for (freq, magnitude_db) in analyzer.get_spectrum() {
    draw_bar(freq, magnitude_db);
}

// For retained-mode GUIs
let path: Vec<(f64, f64)> = scope.get_display_data();
draw_path(&path);
```