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, ®istry, 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 ID | Module |
|---|---|
vco | Vco |
svf | Svf |
adsr | Adsr |
vca | Vca |
lfo | Lfo |
mixer | Mixer |
stereo_output | StereoOutput |
| … | (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, ®istry, 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, ®istry, 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
- Version your patches: Include version numbers for future compatibility
- Document parameters: Use description fields liberally
- Test round-trips: Verify patches load correctly after saving
- Handle missing modules: Gracefully handle unknown module types
- Separate external I/O: Document which external connections are needed