Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,24 @@ def program_change_cli(args):
outport.send(msg)


def apply_state_to_synth(state, outport_name):
with mido.open_output(outport_name) as outport:
LOGGER.debug("Ready to use MIDI port %s", outport_name)
for msg in state.to_cc_messages():
outport.send(msg)
# Sleeping a bit to avoid flooding the MIDI connection
time.sleep(0.001)


def send_patch_cli(args):
outport_name = args.midi_out
path = args.path
LOGGER.info("Applying PRM file %s", path)

state = JU06AState.from_path(path)
apply_state_to_synth(state, outport_name)


def main():
logging.basicConfig(level=logging.DEBUG,
format="%(levelname)s:%(module)s.%(funcName)s: %(message)s")
Expand All @@ -134,6 +152,11 @@ def main():
help="MIDI channel (default: 1)")
pc_parser.set_defaults(func=program_change_cli)

send_patch_parser = subparsers.add_parser("send-patch",
help="Apply the given patch file to the synth through MIDI")
send_patch_parser.add_argument("path", type=str, help="Path to the PRM file")
send_patch_parser.set_defaults(func=send_patch_cli)

cc_parser = subparsers.add_parser("control-change", aliases=["cc"],
help="Send a MIDI control change message")
cc_parser.add_argument("cc_number", type=int, help="Control change number")
Expand Down
52 changes: 52 additions & 0 deletions src/text2synth/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,35 @@
from enum import IntEnum, verify, UNIQUE
from typing import Self

import mido

from pydantic import BaseModel, ConfigDict, Field

from .synths import JU_A6_A


# List of fields that support a range of 0..255 at the synth level. This list
# was created automatically from the CLI by analyzing a bunch of real patches
DOUBLE_ATTRIBUTES = {
"amp_level",
"attack",
"cutoff",
"decay",
"env_mod",
"flt_key_follow",
"flt_lfo_mod",
"hpf",
"lfo_delay_time",
"lfo_rate",
"noise_level",
"osc_lfo_mod",
"pwm",
"release",
"resonance",
"sub_level",
"sustain",
}


@verify(UNIQUE)
class OscRange(IntEnum):
Expand Down Expand Up @@ -319,3 +346,28 @@ def attribute_to_patch_key(self, attribute):
""" Convert the given attribute name into the key used in .PRN files.
"""
return attribute.replace("_", " ").upper()

def to_cc_messages(self):
""" Create a list of MIDI messages that when applied to the synth, will
update the synth to the current state.
"""
messages = []
for data in JU_A6_A.values():
cc = data["cc"]

patch_attribute = data["patch_attribute"]
value = getattr(self, patch_attribute)
if patch_attribute in DOUBLE_ATTRIBUTES:

Copilot AI Oct 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The division by 2 for double attributes lacks explanation. Consider adding a comment explaining why these values need to be halved for CC transmission.

Suggested change
if patch_attribute in DOUBLE_ATTRIBUTES:
if patch_attribute in DOUBLE_ATTRIBUTES:
# Double attributes have a range of 0..255 at the synth level, but MIDI CC messages only support 0..127.
# Therefore, we divide by 2 to fit the value into the MIDI CC range for transmission.

Copilot uses AI. Check for mistakes.
cc_value = value // 2
elif isinstance(value, IntEnum):
if isinstance(value, PortamentoSwitch):
# cc value [0, 63[ -> off, [63-128[ -> on
cc_value = 63 * value
else:
cc_value = value
else:
cc_value = value

messages.append(mido.Message("control_change", control=cc, value=cc_value))

return messages