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"],
"output": "vca",
"modules": [
{
"name": "vco",
"module_type": "vco",
"position": [100, 200],
"state": null
}
],
"cables": [
{
"from": "vco.saw",
"to": "vcf.in",
"attenuation": 1.0
}
],
"parameters": {
"vcf.cutoff": 0.6
}
}
The output field records which module drives the patch’s stereo out. It is optional
and additive — patches written before it existed simply omit it, and Patch::from_def
falls back to an output-node heuristic. parameters maps "module_name.param_id" to a
value, where param_id is either a control-input port name (its unpatched base value) or
an internal parameter id exposed through introspection.
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 a factory with metadata:
registry.register_factory(
"my_module", // type_id
"My Module", // display name
"effect", // category
"A custom effect", // description
|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
Describe a patch either on the live Patch via PatchMeta (which survives a
to_def/from_def round-trip) or directly on the PatchDef:
// On the live patch — carried through serialization:
patch.set_meta(PatchMeta {
name: Some("Fat Bass".into()),
author: Some("Sound Designer".into()),
description: Some("Classic Moog-style bass with filter sweep".into()),
tags: vec!["bass".into(), "moog".into(), "classic".into()],
});
// Or on the serialized def:
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()];
Parameters Round-Trip
Module parameters are captured and restored via introspection. to_def records each
module’s parameters into PatchDef.parameters, and from_def re-applies them with
set_param_by_id. You can inspect and drive parameters directly on a Patch:
// Discover a node's parameters:
for info in patch.param_infos(node_id) {
println!("{} = {:?}", info.id, patch.get_param_by_id(node_id, &info.id));
}
// Set one by id (returns false if the id is unknown):
patch.set_param_by_id(node_id, "cutoff", 0.6);
Modules opt in to this by implementing GraphModule::introspect, which exposes their
parameters to the GUI/serialization layer.
Versioning
PatchDef.version is checked against CURRENT_PATCH_VERSION. The format only grows
additively, so the policy is: accept any patch with version <= CURRENT_PATCH_VERSION,
and reject anything newer. Patch::from_def enforces this and returns an error for a
patch written by a future version:
use quiver::serialize::CURRENT_PATCH_VERSION;
let def = PatchDef::from_json(&json)?;
assert!(def.version <= CURRENT_PATCH_VERSION); // from_def rejects newer patches
Preset Library
Use the built-in preset system:
let library = PresetLibrary::new();
// List all presets (associated functions — no receiver needed)
for preset in PresetLibrary::list() {
println!("{}: {}", preset.name, preset.description);
}
// Get presets by category
let basses = PresetLibrary::by_category(PresetCategory::Bass);
// Search by tag
let acid = library.search_tags(&["acid", "303"]);
// Load and build a preset (get returns Option, build returns Result)
if let Some(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