Quick Start
Using the Patch API
Patch is the human-friendly interface — parameters use native DX7 ranges (e.g. 0-99 for levels) and string names (e.g. "sine" for LFO wave). Use it for sound design, sysex import/export, and interactive exploration.
from dexed import Patch, DexedSynth
# Create a patch with named parameter access (algorithm is 0-indexed)
patch = Patch(name="My Sound")
patch.algorithm = 15 # 0-31
patch.feedback = 5
patch.op[0].output_level = 99
patch.op[0].envelope.rates = [99, 85, 35, 50]
patch.op[0].envelope.levels = [99, 75, 0, 0]
patch.lfo.wave = "sine"
# Load and render
synth = DexedSynth(sample_rate=44100)
synth.load_patch(patch)
audio = synth.render(midi_note=60, velocity=100, note_duration=1.0, render_duration=1.5)
audio2 = synth.render(midi_note=64, velocity=80)
audio3 = synth.render(midi_note=67, velocity=120)
Loading and Saving DX7 Sysex Files
DX7 sysex banks are 4096-byte .syx files containing 32 patches. Individual voice dumps are 163 bytes (with a 6-byte sysex header + 155 data bytes + 2 trailing bytes).
from dexed import Patch
# Load a 32-voice bank
patches = Patch.load_bank("rom1a.syx")
print(f"Loaded {len(patches)} patches")
for i, p in enumerate(patches[:5]):
print(f" {i}: {p.name.strip()} (algorithm {p.algorithm})")
# Save patches to a bank file
Patch.save_to_bank("my_bank.syx", patches)
# Load from an individual voice dump (skip 6-byte sysex header)
with open("voice.syx", "rb") as f:
data = f.read()
patch = Patch.from_sysex(data[6:162]) # 156 bytes of unpacked voice data
ML / JAX Workflow with Preset
Preset is the ML-native representation: all parameters in a single flat (145,) float32 array. Continuous params are normalized to [0, 1]; discrete params (algorithm, curves, etc.) are int arrays. All fields are JAX PyTree data leaves — changing any field, including algorithm, never triggers JIT recompilation.
import numpy as np
from dexed import Patch, Preset, DexedSynth
# Convert a sysex patch to a Preset
preset = Patch.load_bank("rom1a.syx")[0].to_preset()
# Or construct directly
preset = Preset(
algorithm=15, # 0-31
feedback=0.5, # normalized [0, 1]
op_output_level=np.full(6, 0.8, dtype=np.float32),
)
# Load and render
synth = DexedSynth()
synth.load_preset(preset)
audio = synth.render(midi_note=60, velocity=100)
# Flat array round-trip — everything in one (145,) float32 vector
arr = preset.to_array()
preset2 = Preset.from_array(arr)
# Bulk storage: 100k presets ~ 55 MB
bank = np.stack([p.to_array() for p in presets]) # (N, 145)
np.save("bank.npy", bank)
presets = [Preset.from_array(row) for row in np.load("bank.npy")]
Converting Between Patch and Preset
The two representations convert losslessly in both directions:
from dexed import Patch, Preset
# Patch -> Preset
patch = Patch(name="My Sound")
patch.algorithm = 15
preset = patch.to_preset()
# Preset -> Patch
patch2 = preset.to_patch()
# Classmethod alternatives
preset = Preset.from_patch(patch)
JAX pure_callback
import jax
import jax.numpy as jnp
from dexed import DexedSynth, Preset
synth = DexedSynth()
def render_fn(preset):
synth.load_preset(preset)
return synth.render(midi_note=60, velocity=100,
note_duration=0.5, render_duration=1.0)
@jax.jit
def jitted_render(preset):
return jax.pure_callback(
render_fn,
jax.ShapeDtypeStruct((44100,), jnp.float32),
preset,
)
audio = jitted_render(Preset(algorithm=0, feedback=0.5))
# Changing any field — including algorithm — does NOT recompile
audio2 = jitted_render(Preset(algorithm=15, feedback=0.3))
For flat-vector policies (e.g. sampling all 145 dims from a Beta distribution):
# One treedef works for every Preset — no meta fields
_, treedef = jax.tree.flatten(Preset())
@jax.jit
def render_from_flat(flat_params): # (145,) float32
preset = jax.tree.unflatten(treedef, Preset.array_to_leaves(flat_params))
return jax.pure_callback(render_fn, jax.ShapeDtypeStruct((44100,), jnp.float32), preset)
Algorithm Metadata
The 32 standard DX7 algorithms define which operators are carriers (output to audio) and which are modulators (modulate other operators). All indices are 0-based.
from dexed import algorithms, get_carriers, get_modulators, get_mod_matrix
alg = algorithms[15]
print(f"Algorithm 15 carriers: {alg.carriers}")
print(f"Algorithm 15 modulators: {alg.modulators}")
print(f"Feedback operator: {alg.feedback_op}")
print(f"Modulation matrix:\n{alg.mod_matrix}") # 6x6 int8
carriers = get_carriers(31) # [0, 1, 2, 3, 4, 5] — all parallel
Individual Operator Outputs
Render each operator’s contribution separately — useful for analysis and visualization:
from dexed import Patch, DexedSynth
synth = DexedSynth(sample_rate=44100)
patch = Patch(name="Test")
patch.algorithm = 0
patch.op[0].output_level = 99
synth.load_patch(patch)
# 7-channel output: operators 0-5 + final mix
audio = synth.render_all_ops(midi_note=60)
# audio.shape = (7, num_samples)
# audio[0..5] = Operators 0-5, audio[6] = Final mix
Feedback Normalization
By default, algorithms 3, 5, and 31 (DX7 algorithms 4, 6, 32) have reduced feedback, matching original Dexed/DX7 behavior. For consistent feedback across all algorithms (useful for ML pipelines where feedback should be comparable across algorithms):
synth = DexedSynth()
synth.normalize_feedback = True
synth.load_patch(patch)
audio = synth.render(midi_note=60)
Custom Operator Graphs
OperatorGraph lets you build arbitrary FM topologies — not limited to the 32 standard DX7 algorithms, and not limited to 6 operators.
from dexed import OperatorGraph
# Create a 7-operator graph (beyond DX7's 6)
graph = OperatorGraph(num_ops=7)
for i in range(7):
graph.op[i].output_level = 99
graph.op[i].frequency_coarse = i + 1
# Chain: op6 -> op5 -> ... -> op1 -> op0 (carrier)
for i in range(6, 0, -1):
graph.connect(i, i - 1)
graph.set_carriers([0])
graph.set_feedback(6, level=7)
audio = graph.render(sample_rate=44100, midi_note=60, velocity=100,
note_duration=1.0, render_duration=1.5)
# Render each operator individually
audio_all = graph.render_all_ops(midi_note=60) # (8, T) for 7 ops + final
From a Modulation Matrix
import numpy as np
from dexed import OperatorGraph
mod_matrix = np.zeros((4, 4), dtype=np.float32)
mod_matrix[0, 1] = 1.0 # Op 1 modulates Op 0
mod_matrix[1, 2] = 1.0 # Op 2 modulates Op 1
graph = OperatorGraph.from_matrix(mod_matrix, carriers=[0], feedback={3: 0.5})
From a Standard DX7 Algorithm
from dexed import OperatorGraph
# Creates a 6-op graph matching the DX7 algorithm (0-indexed)
graph = OperatorGraph.from_algorithm(15)
Visualization
graph = OperatorGraph.from_algorithm(0)
print(graph.summary()) # human-readable text description
print(graph.to_ascii()) # ASCII art of the signal flow
print(graph.to_mermaid()) # paste into any Mermaid renderer