Python bindings for the Dexed DX7 synthesizer, with support for high-level patch editing, ML/JAX workflows, and low-level parameter arrays.
The Yamaha DX7 (1983) is the best-selling hardware synthesizer of all time. Its 6-operator FM synthesis engine produces a huge range of sounds — from electric pianos and basses to bells, pads, and metallic textures. dexed-py wraps the open-source Dexed engine so you can program, render, and manipulate DX7 patches entirely from Python.
Dexed is licensed under the GPL v3. The msfa component (acronym for music synthesizer for android, see src/msfa) stays on the Apache 2.0 license to be able to collaborate between projects.
- Python >= 3.11
- Platforms: macOS, Linux, Windows
- Runtime dependency: NumPy
- Optional: JAX — for PyTree integration and
jax.pure_callbackworkflows
pip install dexed-pyPatch is the human-friendly interface — parameters use native DX7 ranges and string names. Use it for sound design, sysex import/export, and interactive exploration.
from dexed import Patch, DexedSynth
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"
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)from dexed import Patch
# Load a 32-voice bank (4096-byte .syx file)
patches = Patch.load_bank("rom1a.syx")
for i, p in enumerate(patches[:5]):
print(f" {i}: {p.name.strip()} (algorithm {p.algorithm})")
# Save patches back to a bank file
Patch.save_to_bank("my_bank.syx", patches)Preset is the ML-native representation: a single (145,) float32 vector covering all synth state. Continuous parameters are normalized to [0, 1]; discrete parameters (algorithm, curves, etc.) are integer-valued. All fields are JAX PyTree data leaves — changing any value, including algorithm, never triggers JIT recompilation.
import numpy as np
from dexed import Patch, Preset, DexedSynth
# From a sysex bank
preset = Patch.load_bank("rom1a.syx")[0].to_preset()
# Or construct directly (continuous params normalized [0, 1])
preset = Preset(
algorithm=15,
feedback=0.5,
op_output_level=np.full(6, 0.8, dtype=np.float32),
)
synth = DexedSynth()
synth.load_preset(preset)
audio = synth.render(midi_note=60, velocity=100)
# Flat array round-trip
arr = preset.to_array() # (145,) float32
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")]import jax
from jax import numpy as jnp
from dexed import DexedSynth, Preset
SAMPLE_RATE = 44100
NOTE_DURATION = 0.5
RENDER_DURATION = 1.0
NUM_SAMPLES = int(SAMPLE_RATE * RENDER_DURATION)
synth = DexedSynth(sample_rate=SAMPLE_RATE)
def render_fn(preset):
synth.load_preset(preset)
return synth.render(midi_note=60, velocity=100,
note_duration=NOTE_DURATION, render_duration=RENDER_DURATION)
@jax.jit
def jitted_render(preset):
return jax.pure_callback(
render_fn, jax.ShapeDtypeStruct((NUM_SAMPLES,), jnp.float32), preset,
)
audio = jitted_render(Preset(algorithm=0, feedback=0.5))
# Changing algorithm does NOT recompile — all fields are data leaves
audio2 = jitted_render(Preset(algorithm=15, feedback=0.3))For flat-vector policies (Beta distribution over the 145-dim space):
_, treedef = jax.tree.flatten(Preset()) # universal — no meta fields
@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((NUM_SAMPLES,), jnp.float32), preset)from dexed import algorithms, get_carriers, get_modulators, get_mod_matrix
alg = algorithms[15]
print(f"carriers: {alg.carriers}")
print(f"modulators: {alg.modulators}")
print(f"modulation matrix:\n{alg.mod_matrix}") # 6x6 int8
get_carriers(31) # [0, 1, 2, 3, 4, 5] — all parallelsynth.load_patch(patch)
audio = synth.render_all_ops(midi_note=60)
# audio.shape = (7, T): channels 0-5 are operators 0-5, channel 6 is final mixBy default, normalize_feedback is False, which preserves Dexed-authentic behavior: algorithms 3, 5, and 31 (DX7 algorithms 4, 6, 32) have reduced feedback strength, matching the original hardware. Set it to True for consistent feedback scaling across all 32 algorithms, which is useful when feedback should be comparable regardless of algorithm choice (e.g. in ML pipelines).
synth.normalize_feedback = True # consistent across all 32 algorithmsOperatorGraph lets you build arbitrary FM topologies — not limited to the 32 standard DX7 algorithms, and not limited to 6 operators.
from dexed import OperatorGraph
graph = OperatorGraph(num_ops=7)
for i in range(7):
graph.op[i].output_level = 99
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)
# From a modulation matrix
import numpy as np
mod_matrix = np.zeros((4, 4), dtype=np.float32)
mod_matrix[0, 1] = 1.0 # Op 1 modulates Op 0
graph = OperatorGraph.from_matrix(mod_matrix, carriers=[0], feedback={3: 0.5})
# From a standard DX7 algorithm (0-indexed)
graph = OperatorGraph.from_algorithm(15)
# Visualize the graph
print(graph.summary())
print(graph.to_ascii())
print(graph.to_mermaid()) # paste into any Mermaid rendererpatch = Patch(name="My Sound")
patch.algorithm = 15 # 0-31
patch.feedback = 5 # 0-7
patch.osc_key_sync = True
patch.transpose = 24 # 0-48 (24 = C3)
patch.lfo.speed = 35 # 0-99
patch.lfo.delay = 0
patch.lfo.pitch_mod_depth = 0
patch.lfo.amp_mod_depth = 0
patch.lfo.sync = False
patch.lfo.wave = "sine" # triangle, saw_down, saw_up, square, sine, s&h
patch.pitch_envelope.rates = [99, 99, 99, 99]
patch.pitch_envelope.levels = [50, 50, 50, 50]
op = patch.op[0] # 0-indexed: op[0] through op[5]
op.output_level = 99 # 0-99
op.frequency_coarse = 1 # 0-31
op.frequency_fine = 0 # 0-99
op.frequency_mode = 0 # 0=ratio, 1=fixed
op.detune = 7 # 0-14 (7 = center)
op.velocity_sensitivity = 0 # 0-7
op.amp_mod_sensitivity = 0 # 0-3
op.rate_scaling = 0 # 0-7
op.breakpoint = 39 # 0-99
op.left_depth = 0 # 0-99
op.right_depth = 0 # 0-99
op.left_curve = "lin" # lin, exp-, exp+, log
op.right_curve = "lin"
op.envelope.rates = [99, 99, 99, 99]
op.envelope.levels = [99, 99, 99, 0]# Patch <-> Preset
preset = patch.to_preset()
patch = preset.to_patch()
patch = Preset.from_patch(patch) # classmethod alternative
# Sysex (156 bytes unpacked, 128 bytes packed)
sysex = patch.to_sysex()
patch = Patch.from_sysex(sysex_bytes)
packed = patch.to_packed()
patch = Patch.from_packed(packed_bytes)
# Bank (32-voice .syx files)
patches = Patch.load_bank("bank.syx")
Patch.save_to_bank("bank.syx", patches)
# Raw DX7 format (155 integers in native ranges)
raw = patch.to_raw()
patch = Patch.from_raw(raw_params)See docs/parameter-format.md for the full array layout and the per-operator interface.
from dexed import Preset
import numpy as np
preset = Preset(
algorithm=15, # 0-31
feedback=0.71, # 5/7
osc_key_sync=1, # 0 or 1
lfo_sync=0,
lfo_wave=4, # 0-5 (sine)
op_output_level=np.ones(6, dtype=np.float32),
op_frequency_mode=np.zeros(6, dtype=np.int32), # 0=ratio, 1=fixed
op_left_curve=np.zeros(6, dtype=np.int32), # 0-3
op_right_curve=np.zeros(6, dtype=np.int32),
)
arr = preset.to_array() # (145,) float32
preset = Preset.from_array(arr)
# Per-operator decomposition (useful for per-operator ML architectures)
gc = preset.global_continuous() # (15,) float32
gi = preset.global_ints() # (4,) int32
oc = preset.op_continuous() # (6, 18) float32
oi = preset.op_ints() # (6, 3) int32
preset = Preset.from_operator_bundles(gc, gi, oc, oi)synth = DexedSynth(sample_rate=44100)
synth.load_patch(patch) # from Patch
synth.load_preset(preset) # from Preset
synth.algorithm # read-only: currently loaded algorithm (0-31)
synth.normalize_feedback # bool, read-write (default False)
audio = synth.render(midi_note=60, velocity=100,
note_duration=1.0, render_duration=1.5)
audio = synth.render_all_ops(midi_note=60) # (7, T)graph = OperatorGraph(num_ops=6)
# Connection API (all methods return self for chaining)
graph.connect(source, target, amount=1.0)
graph.disconnect(source, target)
graph.disconnect_all()
graph.set_carriers([0, 2])
graph.set_feedback(op, level=7) # 0 disables, 1-7
# Query API
graph.mod_matrix # NxN float32 (read-only copy)
graph.carriers # List[int]
graph.modulators # List[int]
graph.get_connections() # [(source, target, amount), ...]
# Visualization
graph.summary() # human-readable text
graph.to_ascii() # ASCII art
graph.to_mermaid() # Mermaid diagram syntax
# Factory methods
OperatorGraph.from_algorithm(15)
OperatorGraph.from_matrix(mod_matrix, carriers=[0], feedback={5: 7})
# Rendering
audio = graph.render(sample_rate=44100, midi_note=60, velocity=100,
note_duration=1.0, render_duration=1.5)
audio = graph.render_all_ops(midi_note=60) # (num_ops+1, T)Requires a C++17 compiler and CMake >= 3.15.
git clone --recursive https://github.com/DBraun/dexed-py.git
cd dexed-py
pip install -e .
python -m pytest -v tests