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