Source code for jdxi_editor.midi.message.roland

"""
roland_sysex.py
===============

This module provides a class for constructing and parsing Roland System Exclusive (SysEx) messages. The `RolandSysEx` class allows for easy creation of messages to be sent to Roland devices, as well as the ability to parse incoming SysEx messages.

Usage Example:

```python
from roland_sysex import RolandSysEx

# Creating a SysEx message
message = RolandSysEx(command=0x12, area=0x01, section=0x02, group=0x03, param=0x04, value=0x05)
message_bytes = message.to_bytes()
print("Generated SysEx Message (bytes):", message_bytes)

# Parsing a SysEx message from bytes
received_bytes = b'\xf0\x41\x10\x12\x00\x01\x02\x03\x04\x05\xf7'  # Example received SysEx message
parsed_message = RolandSysEx.from_bytes(received_bytes)
print("Parsed Command:", parsed_message.command)
print("Parsed Address:", parsed_message.address)
print("Parsed Value:", parsed_message.value)

"""

from dataclasses import dataclass, field
from typing import List, Optional, Union

from decologr import Decologr as log
from picomidi.constant import Midi
from picomidi.core.bitmask import BitMask

from jdxi_editor.jdxi.midi.constant import JDXiMidi
from jdxi_editor.jdxi.midi.message.sysex.offset import JDXiSysExMessageLayout
from jdxi_editor.midi.data.address.address import (
    AddressStartMSB,
    CommandID,
    ModelID,
    RolandSysExAddress,
)
from jdxi_editor.midi.data.address.sysex import (
    END_OF_SYSEX,
    START_OF_SYSEX,
    ZERO_BYTE,
    RolandID,
)
from jdxi_editor.midi.message.sysex import SysExMessage
from jdxi_editor.midi.utils.byte import split_16bit_value_to_nibbles


@dataclass
[docs] class RolandSysExMessage(SysExMessage): """Specialized class for Roland JD-Xi SysEx messages."""
[docs] manufacturer_id: int = RolandID.ROLAND_ID
[docs] device_id: int = RolandID.DEVICE_ID
[docs] model_id: list[int] = field( default_factory=lambda: [ ModelID.MODEL_ID_1, ModelID.MODEL_ID_2, ModelID.MODEL_ID_3, ModelID.MODEL_ID_4, ] )
[docs] command: int = CommandID.DT1
[docs] sysex_address: RolandSysExAddress = field(default_factory=RolandSysExAddress)
[docs] value: Union[int, List[int]] = Midi.VALUE.ZERO
[docs] size: int = 1
# These attributes should not be set in `__init__`
[docs] synth_type: int = field(init=False, default=None)
[docs] part: int = field(init=False, default=None)
[docs] dt1_command: int = CommandID.DT1
[docs] rq1_command: int = CommandID.RQ1
[docs] def __post_init__(self): """Initialize data and resolve address bytes.""" self.address = ( self.sysex_address.to_list() ) # Assuming this method returns [msb, umb, lmb, lsb] if isinstance(self.value, int) and self.size == 4: self.data = split_16bit_value_to_nibbles(self.value) else: self.data = [self.value] if isinstance(self.value, int) else self.value
[docs] def to_message_list(self) -> List[int]: """ Convert the SysEx message to a list of integers. :return: list """ msg = ( [Midi.SYSEX.START, self.manufacturer_id, self.device_id] + list(self.model_id) + [self.command] + [self.sysex_address.msb] + [self.sysex_address.umb] + [self.sysex_address.lmb] + [self.sysex_address.lsb] + self.data ) msg.append(self.calculate_checksum()) msg.append(self.end_of_sysex) return msg
@dataclass
[docs] class RolandSysEx(SysExMessage): """Specialized class for Roland JD-Xi SysEx messages."""
[docs] manufacturer_id: int = RolandID.ROLAND_ID
[docs] device_id: int = RolandID.DEVICE_ID
[docs] model_id: List[int] = field( default_factory=lambda: [ ModelID.MODEL_ID_1, ModelID.MODEL_ID_2, ModelID.MODEL_ID_3, ModelID.MODEL_ID_4, ] )
[docs] command: int = CommandID.DT1
[docs] sysex_address: Optional[RolandSysExAddress] = None
[docs] msb: int = Midi.VALUE.ZERO
[docs] umb: int = Midi.VALUE.ZERO
[docs] lmb: int = Midi.VALUE.ZERO
[docs] lsb: int = Midi.VALUE.ZERO
[docs] value: Union[int, List[int]] = Midi.VALUE.ZERO
[docs] size: int = 1
[docs] synth_type: Optional[int] = field(init=False, default=None)
[docs] part: Optional[int] = field(init=False, default=None)
[docs] dt1_command: int = CommandID.DT1
[docs] rq1_command: int = CommandID.RQ1
[docs] def __post_init__(self) -> None: if self.sysex_address: self.msb = self.sysex_address.msb self.umb = self.sysex_address.umb self.lmb = self.sysex_address.lmb self.lsb = self.sysex_address.lsb self.address: List[int] = [self.msb, self.umb, self.lmb, self.lsb] if isinstance(self.value, int) and self.size == 4: self.data: List[int] = split_16bit_value_to_nibbles(self.value) else: self.data = [self.value] if isinstance(self.value, int) else self.value
[docs] def from_sysex_address(self, sysex_address: RolandSysExAddress) -> None: """from_sysex_address :param sysex_address: RolandSysExAddress :return: None """ self.msb = sysex_address.msb self.umb = sysex_address.umb self.lmb = sysex_address.lmb self.lsb = sysex_address.lsb self.address = [self.msb, self.umb, self.lmb, self.lsb]
[docs] def to_message_list(self) -> List[int]: """ to_message_list :return: List[int] """ msg = ( [Midi.SYSEX.START, self.manufacturer_id, self.device_id] + list(self.model_id) + [self.command] + self.address + self.data ) msg.append(self.calculate_checksum()) msg.append(self.end_of_sysex) return msg
[docs] def construct_sysex( self, address: RolandSysExAddress, *data_bytes: Union[List[int], int], request: bool = False, ) -> List[int]: """ Construct a SysEx message based on the provided address and data bytes. :param address: RolandSysExAddress :param data_bytes: list of data bytes :param request: bool is this a request? :return: None """ log.message(f"address: {address} data_bytes: {data_bytes} request: {request}") # Handle address fields directly addr_list = [address.msb, address.umb, address.lmb, address.lsb] # Flatten and normalize data_bytes flat_data: List[int] = [] for db in data_bytes: if isinstance(db, list): flat_data.extend(int(d, 16) if isinstance(d, str) else d for d in db) else: flat_data.append(int(db, 16) if isinstance(db, str) else db) if len(flat_data) == 1: self.parameter = [] self.value = flat_data elif len(flat_data) >= 2 and len(flat_data) != 4: self.parameter = flat_data[:-1] self.value = [flat_data[-1]] elif len(flat_data) == 4: self.parameter = [] self.value = flat_data else: raise ValueError("Invalid data_bytes length. Must be 1, 2+, or 4.") command = self.rq1_command if request else self.dt1_command if not all(isinstance(p, int) for p in self.parameter): raise TypeError("Parameter list must contain only integers.") if not all(isinstance(v, int) for v in self.value): raise TypeError("Value list must contain only integers.") if len(self.parameter) == 0 and len(self.value) == 0: raise ValueError("Both parameter and value cannot be empty.") required_values = { "manufacturer_id": self.manufacturer_id, "device_id": self.device_id, "model_id": self.model_id, "command": self.command, "address": addr_list, "parameter": self.parameter, "value": self.value, } for key, val in required_values.items(): if val is None: raise ValueError(f"Missing required value: {key} cannot be None.") sysex_msg: List[Union[int, List[int]]] = ( [START_OF_SYSEX, self.manufacturer_id, self.device_id] + list(self.model_id) + [command] + addr_list + self.parameter + self.value ) sysex_msg.append(self.calculate_checksum()) sysex_msg.append(END_OF_SYSEX) return sysex_msg
@dataclass
[docs] class JDXiSysEx(RolandSysEx): """JD-Xi specific SysEx message"""
[docs] model_id: List[int] = field( default_factory=lambda: [ ModelID.MODEL_ID_1, ModelID.MODEL_ID_2, ModelID.MODEL_ID_3, ModelID.MODEL_ID_4, ] ) # JD-Xi model ID
[docs] device_id: int = 0x10 # Default device ID
[docs] command: int = CommandID.DT1 # Default to DT1 command
[docs] address: List[int] = field( default_factory=lambda: [0x00, 0x00, 0x00, 0x00] ) # 4-byte address
[docs] data: List[int] = field(default_factory=list) # Data bytes
[docs] def __post_init__(self): """Validate message components""" # Validate device ID if not 0x00 <= self.device_id <= 0x1F and self.device_id != 0x7F: raise ValueError(f"Invalid device ID: {self.device_id:02X}") # Validate model ID if len(self.model_id) != 4: raise ValueError("Model ID must be 4 bytes") if self.model_id != [ ModelID.MODEL_ID_1, ModelID.MODEL_ID_2, ModelID.MODEL_ID_3, ModelID.MODEL_ID_4, ]: raise ValueError(f"Invalid model ID: {[f'{x:02X}' for x in self.model_id]}") # Validate address if len(self.address) != 4: raise ValueError("Address must be 4 bytes") if not all(ZERO_BYTE <= x <= BitMask.FULL_BYTE for x in self.address): raise ValueError( f"Invalid address bytes: {[f'{x:02X}' for x in self.address]}" )
[docs] def to_bytes(self) -> bytes: """Convert message to bytes for sending""" msg = [ START_OF_SYSEX, # Start of SysEx RolandID.ROLAND_ID, # Roland ID self.device_id, # Device ID *self.model_id, # Model ID (4 bytes) self.command, # Command ID *self.address, # Address (4 bytes) *self.data, # Data bytes self.calculate_checksum(), # Checksum END_OF_SYSEX, # End of SysEx ] return bytes(msg)
[docs] def calculate_checksum(self) -> int: """Calculate Roland checksum for the message""" # Checksum = 128 - (sum of address and data bytes % 128) checksum = sum(self.address) + sum(self.data) return (128 - (checksum % 128)) & BitMask.LOW_7_BITS
@classmethod
[docs] def from_bytes(cls, data: bytes): """Create message from received bytes""" if ( len(data) < JDXiMidi.SYSEX.PARAMETER.LENGTH.ONE_BYTE # Minimum length: F0 + ID + dev + model(4) + cmd + addr(4) + sum + F7 or data[JDXiSysExMessageLayout.START] != START_OF_SYSEX or data[JDXiSysExMessageLayout.ROLAND_ID] != ModelID.ROLAND_ID # Roland ID or data[ JDXiSysExMessageLayout.MODEL_ID.POS1 : JDXiSysExMessageLayout.COMMAND_ID ] != bytes( [ ModelID.MODEL_ID_1, ModelID.MODEL_ID_2, ModelID.MODEL_ID_3, ModelID.MODEL_ID_4, ] ) ): # JD-Xi model ID raise ValueError("Invalid JD-Xi SysEx message") device_id = data[JDXiSysExMessageLayout.DEVICE_ID] command = data[JDXiSysExMessageLayout.COMMAND_ID] address = list( data[ JDXiSysExMessageLayout.ADDRESS.MSB : JDXiSysExMessageLayout.TONE_NAME.START ] ) message_data = list( data[ JDXiSysExMessageLayout.TONE_NAME.START : JDXiSysExMessageLayout.CHECKSUM ] ) # Everything between address and checksum received_checksum = data[JDXiSysExMessageLayout.CHECKSUM] # Create message and verify checksum message = cls( device_id=device_id, command=command, address=address, data=message_data ) if message.calculate_checksum() != received_checksum: raise ValueError("Invalid checksum") return message
@dataclass
[docs] class ParameterMessage(JDXiSysEx): """Base class for parameter messages"""
[docs] command: int = CommandID.DT1
[docs] def __post_init__(self): """Handle parameter value conversion""" super().__post_init__() # Convert parameter value if needed if hasattr(self, "convert_value"): self.data = self.convert_value(self.value)
[docs] def convert_value(self, value: int) -> list[int]: """ Convert parameter value to data bytes :param value: int :return: list[int] """ # Default implementation just returns single byte return [value]
@classmethod
[docs] def from_bytes(cls, data: bytes) -> JDXiSysEx: """Create message from received bytes""" msg = super().from_bytes(data) # Convert data back to value if needed if hasattr(cls, "convert_data"): msg.value = cls.convert_data(msg.data) return msg
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """Convert data bytes back to parameter value""" # Default implementation just returns first byte return data[0] if data else 0
@dataclass
[docs] class SystemMessage(ParameterMessage): """System parameter message"""
[docs] msb: int = 0x02 # System area
@dataclass
[docs] class ProgramMessage(ParameterMessage): """Program parameter message"""
[docs] msb: int = 0x18 # Program area
# Update other message classes to inherit from ParameterMessage @dataclass
[docs] class Effect1Message(ParameterMessage): """Effect 1 parameter message"""
[docs] msb: int = 0x18 # Program area
[docs] umb: int = 0x02 # Effect 1 section
@dataclass
[docs] class Effect2Message(ParameterMessage): """Effect 2 parameter message"""
[docs] msb: int = 0x18 # Program area
[docs] umb: int = 0x04 # Effect 2 section
@dataclass
[docs] class DelayMessage(ParameterMessage): """Delay parameter message"""
[docs] msb: int = 0x18 # Program area
[docs] umb: int = 0x06 # Delay section
@dataclass
[docs] class ReverbMessage(ParameterMessage): """Reverb parameter message"""
[docs] msb: int = 0x18 # Program area
[docs] umb: int = 0x08 # Reverb section
@dataclass
[docs] class PartMessage(ParameterMessage): """Program Part parameter message"""
[docs] msb: int = 0x18 # Program area
[docs] umb: int = 0x00 # Part section
[docs] def convert_value(self, value: int) -> List[int]: """Convert parameter value based on parameter preset_type""" # Parameters that need special conversion if self.lsb == 0x0B: # Part Coarse Tune return [value + 64] # Convert -48/+48 to 16-112 elif self.lsb == 0x0C: # Part Fine Tune return [value + 64] # Convert -50/+50 to 14-114 elif self.lsb == 0x13: # Part Cutoff Offset return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x14: # Part Resonance Offset return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x15: # Part Attack Time Offset return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x16: # Part Decay Time Offset return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x17: # Part Release Time Offset return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x18: # Part Vibrato Rate return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x19: # Part Vibrato Depth return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x1A: # Part Vibrato Delay return [value + 64] # Convert -64/+63 to 0-127 elif self.lsb == 0x1B: # Part Octave Shift return [value + 64] # Convert -3/+3 to 61-67 elif self.lsb == 0x1C: # Part Velocity Sens Offset return [value + 64] # Convert -63/+63 to 1-127 elif self.lsb == 0x11: # Part Portamento Time (2 bytes) if value == 128: # TONE setting return [0x00, 0x80] else: return [0x00, value & 0x7F] # Default handling for other parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """Convert data bytes back to parameter value""" param = cls.lsb if hasattr(cls, "param") else 0 # Parameters that need special conversion if param == 0x0B: # Part Coarse Tune return data[0] - 64 # Convert 16-112 to -48/+48 elif param == 0x0C: # Part Fine Tune return data[0] - 64 # Convert 14-114 to -50/+50 elif param in [0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A]: return data[0] - 64 # Convert 0-127 to -64/+63 elif param == 0x1B: # Part Octave Shift return data[0] - 64 # Convert 61-67 to -3/+3 elif param == 0x1C: # Part Velocity Sens Offset return data[0] - 64 # Convert 1-127 to -63/+63 elif param == 0x11: # Part Portamento Time if data[1] & 0x80: # TONE setting return 128 else: return data[1] & 0x7F # Default handling for other parameters return super().convert_data(data)
@dataclass
[docs] class ZoneMessage(ParameterMessage): """Program Zone parameter message"""
[docs] msb: int = 0x18 # Program area
[docs] umb: int = 0x01 # Zone section
[docs] def convert_value(self, value: int) -> List[int]: """Convert parameter value based on parameter preset_type""" # Parameters that need special conversion if self.lsb == 0x19: # Zone Octave Shift return [value + 64] # Convert -3/+3 to 61-67 elif self.lsb == 0x03: # Arpeggio Switch return [value & 0x01] # Ensure boolean value # Default handling for other parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """Convert data bytes back to parameter value""" param = cls.lsb if hasattr(cls, "param") else 0 # Parameters that need special conversion if param == 0x19: # Zone Octave Shift return data[0] - 64 # Convert 61-67 to -3/+3 elif param == 0x03: # Arpeggio Switch return data[0] & 0x01 # Ensure boolean value # Default handling for other parameters return super().convert_data(data)
@dataclass
[docs] class ControllerMessage(ParameterMessage): """Program Controller parameter message"""
[docs] msb: int = AddressStartMSB.TEMPORARY_PROGRAM # Program area
[docs] umb: int = 0x40 # Controller section
[docs] def convert_value(self, value: int) -> List[int]: """ Convert parameter value based on parameter preset_type :param value: :return: List[int] """ # Parameters that need special conversion if self.lsb == 0x07: # Arpeggio Octave Range return [value + 64] # Convert -3/+3 to 61-67 # Default handling for other parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """ Convert data bytes back to parameter value :param data: List :return: int """ param = cls.lsb if hasattr(cls, "param") else 0 # Parameters that need special conversion if param == 0x07: # Arpeggio Octave Range return data[0] - 64 # Convert 61-67 to -3/+3 # Default handling for other parameters return super().convert_data(data)
@dataclass
[docs] class DigitalToneCommonMessage(ParameterMessage): """SuperNATURAL Synth Tone Common parameter message"""
[docs] msb: int = AddressStartMSB.TEMPORARY_TONE # Temporary area
[docs] umb: int = ZERO_BYTE # Common section
[docs] def convert_value(self, value: int) -> List[int]: """ Convert parameter value based on parameter preset_type :param value: :return: List[int] """ # Parameters that need special conversion if self.lsb == 0x15: # Octave Shift return [value + 64] # Convert -3/+3 to 61-67 # Default handling for other parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """ Convert data bytes back to parameter value :param data: List :return: int """ param = cls.lsb if hasattr(cls, "param") else 0 # Parameters that need special conversion if param == 0x15: # Octave Shift return data[0] - 64 # Convert 61-67 to -3/+3 # Default handling for other parameters return super().convert_data(data)
@dataclass
[docs] class DigitalToneModifyMessage(ParameterMessage): """SuperNATURAL Synth Tone Modify parameter message"""
[docs] msb: int = AddressStartMSB.TEMPORARY_TONE # Temporary area
[docs] umb: int = 0x50 # Modify section @@@ looks incorrect - should be lmb
[docs] def convert_value(self, value: int) -> List[int]: """ Convert parameter value based on parameter preset_type :param value: :return: List[int] """ # No special conversion needed for modify parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """ Convert data bytes back to parameter value :param data: List :return: int """ # No special conversion needed for modify parameters return super().convert_data(data)
@dataclass
[docs] class DigitalTonePartialMessage(ParameterMessage): """SuperNATURAL Synth Tone Partial parameter message"""
[docs] msb: int = AddressStartMSB.TEMPORARY_TONE # Temporary area
[docs] umb: int = 0x20 # Partial 1 section (0x20, 0x21, 0x22 for Partials 1-3)
[docs] def convert_value(self, value: int) -> List[int]: """ Convert parameter value based on parameter preset_type :param value: :return: List[int] """ # Parameters that need special conversion if self.lsb == 0x00: # OSC Wave return [value & 0x07] # Ensure 3-bit value (0-7) elif self.lsb == 0x01: # OSC Wave Variation return [value & 0x03] # Ensure 2-bit value (0-2) # Default handling for other parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """ Convert data bytes back to parameter value :param data: List :return: int """ param = cls.lsb if hasattr(cls, "param") else 0 # Parameters that need special conversion if param == 0x00: # OSC Wave return data[0] & 0x07 # Extract 3-bit value elif param == 0x01: # OSC Wave Variation return data[0] & 0x03 # Extract 2-bit value # Default handling for other parameters return super().convert_data(data)
@dataclass
[docs] class AnalogToneMessage(ParameterMessage): """Message for analog tone parameters"""
[docs] msb: int = 0
[docs] umb: int = 0
[docs] lmb: int = 0
[docs] lsb: int = 0
[docs] value: int = 0
[docs] def to_message_list(self) -> List[int]: """Convert to SysEx message bytes""" return [ START_OF_SYSEX, # Start of SysEx RolandID.ROLAND_ID, # Roland ID RolandID.DEVICE_ID, # Device ID ModelID.MODEL_ID_1, ModelID.MODEL_ID_2, ModelID.MODEL_ID_3, ModelID.MODEL_ID_4, # Model ID CommandID.DT1, # DT1 Command self.msb, self.umb, self.lmb, self.lsb, self.value, ZERO_BYTE, # Checksum placeholder END_OF_SYSEX, # End of SysEx ]
@dataclass
[docs] class DrumKitCommonMessage(ParameterMessage): """Drum Kit Common parameter message"""
[docs] msb: int = AddressStartMSB.TEMPORARY_TONE # Temporary area
[docs] umb: int = 0x10 # Drum Kit section
[docs] lmb: int = 0x00 # Common area
[docs] def convert_value(self, value: int) -> List[int]: """ Convert parameter value based on parameter preset_type :param value: :return: List[int] """ # No special conversion needed for drum kit common parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """ Convert data bytes back to parameter value :param data: List :return: int """ # No special conversion needed for drum kit common parameters return super().convert_data(data)
@dataclass
[docs] class DrumKitPartialMessage(ParameterMessage): """Drum Kit Partial parameter message"""
[docs] msb: int = AddressStartMSB.TEMPORARY_TONE # Temporary area
[docs] umb: int = 0x10 # Drum Kit section
[docs] lmb: int = 0x01 # Partial area
[docs] def convert_value(self, value: int) -> List[int]: """ Convert parameter value based on parameter preset_type :param value: :return: List[int] """ # Parameters that need special conversion if self.lsb == 0x10: # Fine Tune return [value + 64] # Convert -50/+50 to 14-114 elif self.lsb == 0x14: # Alternate Pan return [value + 64] # Convert L63-63R to 1-127 # Default handling for other parameters return super().convert_value(value)
@classmethod
[docs] def convert_data(cls, data: List[int]) -> int: """ Convert data bytes back to parameter value :param data: List :return: int """ param = cls.lsb if hasattr(cls, "param") else 0 # Parameters that need special conversion if param == 0x10: # Fine Tune return data[0] - 64 # Convert 14-114 to -50/+50 elif param == 0x14: # Alternate Pan return data[0] - 64 # Convert 1-127 to L63-63R # Default handling for other parameters return super().convert_data(data)
[docs] def create_sysex_message( msb: int, umb: int, lmb: int, lsb: int, value: int ) -> JDXiSysEx: """Create address JD-Xi SysEx message with the given parameters""" return JDXiSysEx( command=CommandID.DT1, msb=msb, umb=umb, lmb=lmb, lsb=lsb, value=value, )
[docs] def create_patch_load_message( bank_msb: int, bank_lsb: int, program: int ) -> List[JDXiSysEx]: """Create messages to load address patch (bank select + program change)""" return [ # Bank Select MSB JDXiSysEx( command=CommandID.DT1, msb=AddressStartMSB.SYSTEM, # Setup area 0x01 umb=0x00, lmb=0x00, lsb=0x04, # Bank MSB parameter value=bank_msb, ), # Bank Select LSB JDXiSysEx( command=CommandID.DT1, msb=AddressStartMSB.SYSTEM, # Setup area umb=0x00, lmb=0x00, lsb=0x05, # Bank LSB parameter value=bank_lsb, ), # Program Change JDXiSysEx( command=CommandID.DT1, msb=AddressStartMSB.SYSTEM, # Setup area umb=0x00, lmb=0x00, lsb=0x06, # Program number parameter value=program, ), ]
[docs] def create_patch_request_message(msb: int, umb: int = 0x00, size: int = 0) -> JDXiSysEx: """Create address message to request patch data""" return JDXiSysEx( command=CommandID.RQ1, # Data request command msb=msb, umb=umb, lmb=0x00, lsb=0x00, data=[size] if size else [], # Some requests need address size parameter )