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

Serialize and Save Patches

Save patches to JSON and reload them—essential for presets and patch management.

Basic Serialization

Convert a patch to JSON:

// Create your patch
let mut patch = Patch::new(44100.0);
// ... add modules and connections ...

// Serialize to PatchDef
let def = patch.to_def("My Awesome Synth");

// Convert to JSON string
let json = def.to_json()?;
println!("{}", json);

PatchDef Structure

The serialized format:

{
  "version": 1,
  "name": "My Awesome Synth",
  "author": "Your Name",
  "description": "A warm analog-style bass",
  "tags": ["bass", "analog", "subtractive"],
  "modules": [
    {
      "name": "vco",
      "module_type": "vco",
      "position": [100, 200],
      "state": null
    }
  ],
  "cables": [
    {
      "from": "vco.saw",
      "to": "vcf.in",
      "attenuation": 1.0
    }
  ],
  "parameters": {}
}

Loading Patches

Reconstruct a patch from JSON:

// Parse JSON
let def = PatchDef::from_json(&json_string)?;

// Create module registry
let registry = ModuleRegistry::new();

// Rebuild patch
let patch = Patch::from_def(&def, &registry, 44100.0)?;

The Module Registry

The registry maps type names to constructors:

let mut registry = ModuleRegistry::new();

// Built-in modules are registered by default
// For custom modules, register them:
registry.register("my_module", |sr| {
    Box::new(MyCustomModule::new(sr))
});

Default registered modules:

Type IDModule
vcoVco
svfSvf
adsrAdsr
vcaVca
lfoLfo
mixerMixer
stereo_outputStereoOutput
(many more)

File Operations

Save to and load from files:

use std::fs;

// Save
let json = patch.to_def("My Patch").to_json()?;
fs::write("my_patch.json", &json)?;

// Load
let json = fs::read_to_string("my_patch.json")?;
let def = PatchDef::from_json(&json)?;
let patch = Patch::from_def(&def, &registry, 44100.0)?;

Handling External Inputs

ExternalInput modules require Arc<AtomicF64> values that can’t serialize:

// These modules won't round-trip through JSON:
let pitch = patch.add("pitch", ExternalInput::voct(pitch_arc));

// After loading, you'll need to reconnect external inputs manually

Solution: Use Offset for static values, or re-add ExternalInputs after loading.

Patch Metadata

Add descriptive information:

let mut def = patch.to_def("Fat Bass");
def.author = Some("Sound Designer".to_string());
def.description = Some("Classic Moog-style bass with filter sweep".to_string());
def.tags = vec!["bass".into(), "moog".into(), "classic".into()];

Versioning

The version field enables migration:

let def = PatchDef::from_json(&json)?;

match def.version {
    1 => { /* current format */ }
    0 => { /* legacy format - migrate */ }
    _ => { /* unknown version */ }
}

Preset Library

Use the built-in preset system:

let library = PresetLibrary::new();

// List all presets
for preset in library.list() {
    println!("{}: {}", preset.name, preset.description);
}

// Get presets by category
let basses = library.by_category(PresetCategory::Bass);

// Search by tag
let acid = library.search_tags(&["acid", "303"]);

// Load a preset
let preset = library.get("303 Acid")?;
let patch = preset.build(44100.0)?;

Example: Patch Manager

//! How-To: Serialize and Save Patches
//!
//! Demonstrates saving patches to JSON and loading them back.
//! Essential for preset management and patch storage.
//!
//! Run with: cargo run --example howto_serialization

use quiver::prelude::*;

fn main() {
    let sample_rate = 44100.0;

    println!("=== Patch Serialization Demo ===\n");

    // Build a patch
    let mut patch = Patch::new(sample_rate);

    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 lfo = patch.add("lfo", Lfo::new(sample_rate));
    let output = patch.add("output", StereoOutput::new());

    // Audio path
    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
    patch.connect(env.out("env"), vcf.in_("cutoff")).unwrap();
    patch.connect(env.out("env"), vca.in_("cv")).unwrap();
    patch.connect(lfo.out("sin"), vcf.in_("fm")).unwrap();

    patch.set_output(output.id());
    patch.compile().unwrap();

    println!(
        "Original patch: {} modules, {} cables\n",
        patch.node_count(),
        patch.cable_count()
    );

    // Serialize to JSON
    let mut def = patch.to_def("Warm Pad");
    def.author = Some("Quiver Documentation".to_string());
    def.description = Some("A warm pad with LFO filter modulation".to_string());
    def.tags = vec!["pad".into(), "warm".into(), "modulated".into()];

    let json = def.to_json().expect("Serialization failed");

    println!("--- Serialized JSON ---");
    println!("{}\n", json);

    // Deserialize and rebuild
    println!("--- Deserializing ---");
    let loaded_def = PatchDef::from_json(&json).expect("Deserialization failed");

    println!("Loaded patch: {}", loaded_def.name);
    println!("  Author: {:?}", loaded_def.author);
    println!("  Description: {:?}", loaded_def.description);
    println!("  Tags: {:?}", loaded_def.tags);
    println!("  Modules: {}", loaded_def.modules.len());
    println!("  Cables: {}", loaded_def.cables.len());

    // Rebuild the patch using the registry
    let registry = ModuleRegistry::new();
    let mut reloaded_patch =
        Patch::from_def(&loaded_def, &registry, sample_rate).expect("Failed to rebuild patch");

    println!(
        "\nRebuilt patch: {} modules, {} cables",
        reloaded_patch.node_count(),
        reloaded_patch.cable_count()
    );

    // Verify it works by generating audio
    println!("\n--- Testing reloaded patch ---");

    let mut peak = 0.0_f64;
    for _ in 0..(sample_rate * 0.5) as usize {
        let (left, _) = reloaded_patch.tick();
        peak = peak.max(left.abs());
    }

    println!("Generated 0.5s of audio, peak: {:.2}V", peak);
    println!("\nRound-trip serialization successful!");

    // Show available presets using static methods
    println!("\n--- Built-in Presets ---");

    // Get all presets
    let all_presets = PresetLibrary::list();
    println!("\nTotal available presets: {}", all_presets.len());

    // Filter by category using static method
    println!("\nBass presets:");
    for preset in PresetLibrary::by_category(PresetCategory::Bass) {
        let desc = if preset.description.is_empty() {
            "No description"
        } else {
            &preset.description
        };
        println!("  {} - {}", preset.name, desc);
    }

    println!("\nPad presets:");
    for preset in PresetLibrary::by_category(PresetCategory::Pad) {
        let desc = if preset.description.is_empty() {
            "No description"
        } else {
            &preset.description
        };
        println!("  {} - {}", preset.name, desc);
    }

    println!("\nLead presets:");
    for preset in PresetLibrary::by_category(PresetCategory::Lead) {
        let desc = if preset.description.is_empty() {
            "No description"
        } else {
            &preset.description
        };
        println!("  {} - {}", preset.name, desc);
    }
}

Best Practices

  1. Version your patches: Include version numbers for future compatibility
  2. Document parameters: Use description fields liberally
  3. Test round-trips: Verify patches load correctly after saving
  4. Handle missing modules: Gracefully handle unknown module types
  5. Separate external I/O: Document which external connections are needed