Source code for jdxi_editor.midi.data.address.address

"""
Parameter Address Map

Example Usage:
--------------

>>> class AddressMemoryAreaMSB(Address):
...    PROGRAM = 0x18
...    TEMPORARY_TONE = 0x19
>>> # Add an offset to a base address
>>> addr_bytes = AddressMemoryAreaMSB.PROGRAM.add_offset((0x00, 0x20, 0x00))
>>> print(addr_bytes)  # (0x18, 0x00, 0x20, 0x00)
(24, 0, 32, 0)
>>> # Get SysEx-ready address
>>> sysex_address = AddressMemoryAreaMSB.PROGRAM.to_sysex_address((0x00, 0x20, 0x00))
>>> print(sysex_address.hex())  # '18002000'
18002000
>>> # Lookup
>>> found = AddressMemoryAreaMSB.get_parameter_by_address(0x19)
>>> print(found)  # ProgramAddress.TEMPORARY_TONE
AddressMemoryAreaMSB.TEMPORARY_TONE: 0x19


SysExByte

Example usage:
--------------
>>> command = CommandID.DT1
>>> print(f"Command: {command}, Value: {command.DT1}, Message Position: {command.message_position}")
Command: 18, Value: 18, Message Position: <bound method CommandID.message_position of <enum 'CommandID'>>
"""

from __future__ import annotations

from enum import IntEnum, unique
from typing import Any, List, Optional, Tuple, Type, TypeVar, Union

from picomidi.core.bitmask import BitMask

from jdxi_editor.midi.data.address.sysex import ZERO_BYTE
from jdxi_editor.midi.data.address.sysex_byte import SysExByte

[docs] T = TypeVar("T", bound="Address")
[docs] DIGITAL_PARTIAL_MAP = {i: 0x1F + i for i in range(1, 4)} # 1: 0x20, 2: 0x21, 3: 0x22
@unique
[docs] class RolandID(IntEnum): """Roland IDs"""
[docs] ROLAND_ID = 0x41
[docs] DEVICE_ID = 0x10
@classmethod
[docs] def to_list(cls) -> list[int]: """ Convert the header to a list of integers """ return [cls.ROLAND_ID, cls.DEVICE_ID]
@unique
[docs] class ResponseID(IntEnum): """Midi responses"""
[docs] ACK = 0x4F # Acknowledge
[docs] ERR = 0x4E # Error
[docs] class Address(SysExByte): """ Base class for Roland-style hierarchical memory address enums (e.g., 0x18, 0x19, etc.) Includes lookup, offset arithmetic, and SysEx-ready address formatting. """
[docs] def add_offset( self, address_offset: Union[int, Tuple[int, ...]] ) -> tuple[int, Any]: """ Returns the full 4-byte address by adding a 3-byte offset to the base address. The base address is assumed to be a single byte (e.g., 0x18). :param address_offset: Union[int, Tuple[int, int, int]] The address offset :return: tuple[int, Any] The full 4-byte address """ base = self.value if isinstance(address_offset, int): offset_bytes = [ (address_offset >> 16) & BitMask.FULL_BYTE, (address_offset >> 8) & BitMask.FULL_BYTE, address_offset & BitMask.FULL_BYTE, ] elif isinstance(address_offset, tuple) and len(address_offset) == 3: offset_bytes = list(address_offset) else: raise ValueError("Offset must be an int or a 3-byte tuple") if any(b > 0x7F for b in offset_bytes): raise ValueError("SysEx address bytes must be 7-bit (0x00 to 0x7F)") return base, *offset_bytes
[docs] def to_sysex_address( self, address_offset: Union[int, Tuple[int, int, int]] = (0, 0, 0) ) -> bytes: """ Returns the full 4-byte address as a `bytes` object, suitable for SysEx messages. :param address_offset: Union[int, Tuple[int, int, int]] The address offset :return: bytes The full 4-byte address """ return bytes(self.add_offset(address_offset))
@classmethod
[docs] def get_parameter_by_address(cls: Type[T], address: int) -> Optional[T]: """ Get parameter by address value. Overrides the base class method to use 'value' instead of 'STATUS'. :param address: int The address value :return: Optional[T] The parameter """ return next( (parameter for parameter in cls if parameter.value == address), None )
@classmethod
[docs] def from_sysex_bytes(cls: Type[T], address: bytes) -> Optional[T]: """ Create an Address object from a 4-byte SysEx address. :param address: bytes The 4-byte SysEx address :return: Optional[T] The Address object """ if len(address) != 4: return None return cls.get_parameter_by_address(address[0])
[docs] def __repr__(self) -> str: return f"<{self.__class__.__name__}.{self.name}: 0x{self.value:02X}>"
[docs] def __str__(self) -> str: return f"{self.__class__.__name__}.{self.name}: 0x{self.value:02X}"
[docs] class RolandSysExAddress: """ Represents a full 4-byte SysEx address (MSB, UMB, LMB, LSB), with support for address arithmetic, formatting, and conversion to/from SysEx message bytes. :param msb: int The MSB :param umb: int The UMB :param lmb: int The LMB :param lsb: int The LSB """ def __init__(self, msb: int, umb: int, lmb: int, lsb: int):
[docs] self.msb = msb
[docs] self.umb = umb
[docs] self.lmb = lmb
[docs] self.lsb = lsb
@classmethod
[docs] def from_bytes(cls, b: bytes) -> Optional[RolandSysExAddress]: """ Create a RolandSysExAddress object from a 4-byte bytes object. :param b: bytes The 4-byte bytes object :return: Optional[RolandSysExAddress] The RolandSysExAddress object """ if len(b) != 4: return None return cls(*b)
[docs] def to_list(self) -> List[int]: """ Convert the RolandSysExAddress object to a list of integers. :return: List[int] The list of integers """ return [self.msb, self.umb, self.lmb, self.lsb]
[docs] def to_bytes(self) -> bytes: """ Convert the RolandSysExAddress object to a 4-byte bytes object. :return: bytes The 4-byte bytes object """ return bytes([self.msb, self.umb, self.lmb, self.lsb])
[docs] def add_offset( self, offset: Union[int, tuple[int, int, int]] ) -> RolandSysExAddress: """ Adds a 3-byte offset to the lower three bytes (UMB, LMB, LSB). MSB remains unchanged. :param offset: Union[int, tuple[int, int, int]] The offset :return: RolandSysExAddress The RolandSysExAddress object """ if isinstance(offset, int): offset_bytes = [ (offset >> 16) & 0x7F, (offset >> 8) & BitMask.LOW_7_BITS, offset & BitMask.LOW_7_BITS, ] elif isinstance(offset, tuple) and len(offset) == 3: offset_bytes = list(offset) else: raise ValueError("Offset must be an int or a 3-byte tuple") new_umb = (self.umb + offset_bytes[0]) & BitMask.LOW_7_BITS new_lmb = (self.lmb + offset_bytes[1]) & BitMask.LOW_7_BITS new_lsb = (self.lsb + offset_bytes[2]) & BitMask.LOW_7_BITS return RolandSysExAddress(self.msb, new_umb, new_lmb, new_lsb)
[docs] def __repr__(self) -> str: """ Return a string representation of the RolandSysExAddress object. :return: str The string representation """ return ( f"<{self.__class__.__name__}(msb=0x{int(self.msb):02X}, umb=0x{int(self.umb):02X}, " f"lmb=0x{int(self.lmb):02X}, lsb=0x{int(self.lsb):02X})>" )
[docs] def __str__(self) -> str: """ Return a string representation of the RolandSysExAddress object. :return: str The string representation """ return f"0x{int(self.msb):02X} 0x{int(self.umb):02X} 0x{int(self.lmb):02X} 0x{int(self.lsb):02X}"
[docs] def __eq__(self, other: object) -> bool: """ Check if the RolandSysExAddress object is equal to another object. :param other: object The other object :return: bool True if the objects are equal, False otherwise """ if not isinstance(other, RolandSysExAddress): return NotImplemented return self.to_bytes() == other.to_bytes()
[docs] def __hash__(self) -> int: """ Return the hash of the RolandSysExAddress object. :return: int The hash of the RolandSysExAddress object """ return hash(self.to_bytes())
[docs] def copy(self) -> RolandSysExAddress: return RolandSysExAddress(self.msb, self.umb, self.lsb, self.lsb)
# ========================== # JD-Xi SysEx Header # ==========================
[docs] class ModelID(Address): """ Model ID """ # Model ID bytes
[docs] MODEL_ID_1 = ZERO_BYTE # Manufacturer ID extension
[docs] MODEL_ID_2 = ZERO_BYTE # Device family code MSB
[docs] MODEL_ID_3 = ZERO_BYTE # Device family code LSB
[docs] MODEL_ID_4 = 0x0E # JD-XI Product code
@classmethod
[docs] def to_list(cls) -> list[int]: """ Convert the header to a list of integers """ return [cls.MODEL_ID_1, cls.MODEL_ID_2, cls.MODEL_ID_3, cls.MODEL_ID_4]
[docs] JD_XI_MODEL_ID = [ ModelID.MODEL_ID_1, ModelID.MODEL_ID_2, ModelID.MODEL_ID_3, ModelID.MODEL_ID_4, ]
# Deprecated: Use JDXiSysexHeader from jdxi_editor.midi.message.jdxi instead # This is kept here for backward compatibility only
[docs] JD_XI_HEADER_LIST = [RolandID.ROLAND_ID, RolandID.DEVICE_ID, *JD_XI_MODEL_ID]
@unique
[docs] class CommandID(SysExByte): """Roland Commands"""
[docs] DT1 = 0x12 # Data Set 1
[docs] RQ1 = 0x11 # Data Request 1
@classmethod
[docs] def message_position(cls) -> int: """Return the fixed message position for command bytes.""" return 7
# ========================== # Memory and Program Areas # ========================== @unique
[docs] class AddressStartMSB(Address): """ Memory and Program Areas """
[docs] SYSTEM = 0x01
[docs] SETUP = 0x02
[docs] TEMPORARY_PROGRAM = 0x18
[docs] TEMPORARY_TONE = 0x19
@classmethod
[docs] def message_position(cls) -> int: """Return the fixed message position for command bytes.""" return 8
@unique
[docs] class AddressOffsetTemporaryToneUMB(Address): """ Address Offset Temporary Tone UMB """
[docs] DIGITAL_SYNTH_1 = 0x01 # Avoiding "Part" because of Partials
[docs] DIGITAL_SYNTH_2 = 0x21
[docs] ANALOG_SYNTH = 0x42
[docs] DRUM_KIT = 0x70
[docs] COMMON = 0x00
@classmethod
[docs] def message_position(cls) -> int: """Return the fixed message position for command bytes.""" return 9
[docs] class AddressOffsetSystemUMB(Address): """ Address Offset System UMB """
[docs] COMMON = 0x00
@classmethod
[docs] def message_position(cls) -> int: """Return the fixed message position for command bytes.""" return 9
@unique
[docs] class AddressOffsetSystemLMB(Address): """ Address Offset System LMB """
[docs] COMMON = 0x00
[docs] CONTROLLER = 0x03
@classmethod
[docs] def message_position(cls) -> int: """Return the fixed message position for command bytes.""" return 10
@unique
[docs] class AddressOffsetSuperNATURALLMB(Address): """ Address Offset SuperNATURAL LMB """
[docs] COMMON = 0x00
[docs] PARTIAL_1 = 0x20
[docs] PARTIAL_2 = 0x21
[docs] PARTIAL_3 = 0x22
[docs] MODIFY = 0x50
@classmethod
[docs] def message_position(cls) -> int: """Return the fixed message position for command bytes.""" return 10
@classmethod
[docs] def digital_partial_offset(cls, partial_number: int) -> int: """Return the LMB offset for the given drum partial (0–37).""" base_address = DIGITAL_PARTIAL_MAP.get(partial_number, 0x00) return base_address
[docs] class AddressOffsetAnalogLMB(Address): """ Analog Synth Tone """
[docs] COMMON = 0x00
[docs] class AddressOffsetProgramLMB(Address): """ Address Offset Program LMB """
[docs] COMMON = 0x00
[docs] VOCAL_EFFECT = 0x01
[docs] EFFECT_1 = 0x02
[docs] EFFECT_2 = 0x04
[docs] DELAY = 0x06
[docs] REVERB = 0x08
[docs] PART_DIGITAL_SYNTH_1 = 0x20
[docs] PART_DIGITAL_SYNTH_2 = 0x21
[docs] PART_ANALOG = 0x22
[docs] PART_DRUM = 0x23
[docs] ZONE_DIGITAL_SYNTH_1 = 0x30
[docs] ZONE_DIGITAL_SYNTH_2 = 0x31
[docs] ZONE_ANALOG = 0x32
[docs] ZONE_DRUM = 0x33
[docs] CONTROLLER = 0x40
[docs] DRUM_DEFAULT_PARTIAL = ( 0x2E # BD1 from DRUM_ADDRESS_MAP (lazy import to avoid circular dependency) )
[docs] DIGITAL_DEFAULT_PARTIAL = DIGITAL_PARTIAL_MAP[1]
[docs] DRUM_KIT_PART_1 = 0x2E
[docs] DRUM_KIT_PART_2 = 0x30
[docs] DRUM_KIT_PART_3 = 0x32
[docs] DRUM_KIT_PART_4 = 0x34
[docs] DRUM_KIT_PART_5 = 0x36
[docs] DRUM_KIT_PART_6 = 0x38
[docs] DRUM_KIT_PART_7 = 0x3A
[docs] DRUM_KIT_PART_8 = 0x3C
[docs] DRUM_KIT_PART_9 = 0x3E
[docs] DRUM_KIT_PART_10 = 0x40
[docs] DRUM_KIT_PART_11 = 0x42
[docs] DRUM_KIT_PART_12 = 0x44
[docs] DRUM_KIT_PART_13 = 0x46
[docs] DRUM_KIT_PART_14 = 0x48
[docs] DRUM_KIT_PART_15 = 0x4A
[docs] DRUM_KIT_PART_16 = 0x4C
[docs] DRUM_KIT_PART_17 = 0x4E
[docs] DRUM_KIT_PART_18 = 0x50
[docs] DRUM_KIT_PART_19 = 0x52
[docs] DRUM_KIT_PART_20 = 0x54
[docs] DRUM_KIT_PART_21 = 0x56
[docs] DRUM_KIT_PART_22 = 0x58
[docs] DRUM_KIT_PART_23 = 0x5A
[docs] DRUM_KIT_PART_24 = 0x5C
[docs] DRUM_KIT_PART_25 = 0x5E
[docs] DRUM_KIT_PART_26 = 0x60
[docs] DRUM_KIT_PART_27 = 0x62
[docs] DRUM_KIT_PART_28 = 0x64
[docs] DRUM_KIT_PART_29 = 0x66
[docs] DRUM_KIT_PART_30 = 0x68
[docs] DRUM_KIT_PART_31 = 0x6A
[docs] DRUM_KIT_PART_32 = 0x6C
[docs] DRUM_KIT_PART_33 = 0x6E
[docs] DRUM_KIT_PART_34 = 0x70
[docs] DRUM_KIT_PART_35 = 0x72
[docs] DRUM_KIT_PART_36 = 0x74
[docs] DRUM_KIT_PART_37 = 0x76
@classmethod
[docs] def message_position(cls) -> int: """ Return the fixed message position for command bytes. :return: int The fixed message position """ return 10
@classmethod
[docs] def drum_partial_offset(cls, partial_number: int) -> int: """ Return the LMB offset for the given drum partial (0–37). :param partial_number: int The partial number :return: int The LMB offset """ base_address = 0x00 step = 0x2E return base_address + (step * partial_number)
[docs] class AddressOffsetDrumKitLMB(Address): """ Address Offset Program LMB """
[docs] COMMON = 0x00
[docs] DRUM_DEFAULT_PARTIAL = ( 0x2E # BD1 from DRUM_ADDRESS_MAP (lazy import to avoid circular dependency) )
[docs] DIGITAL_DEFAULT_PARTIAL = DIGITAL_PARTIAL_MAP[1]
[docs] DRUM_KIT_PART_1 = 0x2E
[docs] DRUM_KIT_PART_2 = 0x30
[docs] DRUM_KIT_PART_3 = 0x32
[docs] DRUM_KIT_PART_4 = 0x34
[docs] DRUM_KIT_PART_5 = 0x36
[docs] DRUM_KIT_PART_6 = 0x38
[docs] DRUM_KIT_PART_7 = 0x3A
[docs] DRUM_KIT_PART_8 = 0x3C
[docs] DRUM_KIT_PART_9 = 0x3E
[docs] DRUM_KIT_PART_10 = 0x40
[docs] DRUM_KIT_PART_11 = 0x42
[docs] DRUM_KIT_PART_12 = 0x44
[docs] DRUM_KIT_PART_13 = 0x46
[docs] DRUM_KIT_PART_14 = 0x48
[docs] DRUM_KIT_PART_15 = 0x4A
[docs] DRUM_KIT_PART_16 = 0x4C
[docs] DRUM_KIT_PART_17 = 0x4E
[docs] DRUM_KIT_PART_18 = 0x50
[docs] DRUM_KIT_PART_19 = 0x52
[docs] DRUM_KIT_PART_20 = 0x54
[docs] DRUM_KIT_PART_21 = 0x56
[docs] DRUM_KIT_PART_22 = 0x58
[docs] DRUM_KIT_PART_23 = 0x5A
[docs] DRUM_KIT_PART_24 = 0x5C
[docs] DRUM_KIT_PART_25 = 0x5E
[docs] DRUM_KIT_PART_26 = 0x60
[docs] DRUM_KIT_PART_27 = 0x62
[docs] DRUM_KIT_PART_28 = 0x64
[docs] DRUM_KIT_PART_29 = 0x66
[docs] DRUM_KIT_PART_30 = 0x68
[docs] DRUM_KIT_PART_31 = 0x6A
[docs] DRUM_KIT_PART_32 = 0x6C
[docs] DRUM_KIT_PART_33 = 0x6E
[docs] DRUM_KIT_PART_34 = 0x70
[docs] DRUM_KIT_PART_35 = 0x72
[docs] DRUM_KIT_PART_36 = 0x74
[docs] DRUM_KIT_PART_37 = 0x76
@classmethod
[docs] def message_position(cls) -> int: """ Return the fixed message position for command bytes. :return: int The fixed message position """ return 10
@classmethod
[docs] def drum_partial_offset(cls, partial_number: int) -> int: """ Return the LMB offset for the given drum partial (0–37). :param partial_number: int The partial number :return: int The LMB offset """ base_address = 0x00 step = 0x2E return base_address + (step * partial_number)