"""
DigitalPartialParameter: JD-Xi Digital Synthesizer Parameter Mapping
====================================================================
This class defines digital synthesizer parameters for the Roland JD-Xi, mapping
various synthesis parameters to their corresponding memory addresses and valid
value ranges.
The parameters include:
- Oscillator settings (waveform, pitch, detune, envelope, etc.)
- Filter settings (cutoff, resonance, envelope, key follow, etc.)
- Amplitude settings (level, velocity, envelope, pan, etc.)
- LFO (Low-Frequency Oscillator) settings (waveform, rate, depth, sync, etc.)
- Modulation LFO settings (waveform, rate, depth, sync, etc.)
- Additional synthesis controls (aftertouch, wave gain, super saw detune, etc.)
- PCM wave settings (wave number, gain, high-pass filter cutoff, etc.)
Each parameter is stored as address tuple containing:
(memory_address, min_value, max_value)
Attributes:
- OSC_WAVE: Defines the oscillator waveform preset_type.
- FILTER_CUTOFF: Controls the filter cutoff frequency.
- AMP_LEVEL: Sets the overall amplitude level.
- LFO_RATE: Adjusts the rate of the low-frequency oscillator.
- MOD_LFO_PITCH_DEPTH: Modulates pitch using the secondary LFO.
- (Other parameters follow address similar structure.)
Methods:
__init__(self, address: int, min_val: int, max_val: int):
Initializes address DigitalParameter instance with an address and value range.
Usage Example:
filter_cutoff = DigitalParameter(0x0C, 0, 127) # Filter Cutoff Frequency
print(filter_cutoff.address) # Output: 0x0C
This class helps structure and manage parameter mappings for JD-Xi SysEx processing.
"""
from typing import Optional, Tuple
from picomidi.sysex.parameter.address import AddressParameter
from picomidi.sysex.parameter.map import map_range
from jdxi_editor.midi.data.parameter.digital.mapping import ENVELOPE_MAPPING
[docs]
class DigitalPartialParam(AddressParameter):
"""Digital synth parameters with their addresses and value ranges"""
def __init__(
self,
address: int,
min_val: int,
max_val: int,
display_min: Optional[int] = None,
display_max: Optional[int] = None,
tooltip: Optional[str] = None,
):
super().__init__(address, min_val, max_val)
[docs]
self.display_min = display_min if display_min is not None else min_val
[docs]
self.display_max = display_max if display_max is not None else max_val
[docs]
self.bipolar_parameters = [
# Oscillator parameters
"OSC_PITCH",
"OSC_DETUNE",
"OSC_PITCH_ENV_DEPTH",
# Filter parameters
"FILTER_CUTOFF_KEYFOLLOW",
"FILTER_ENV_VELOCITY_SENSITIVITY",
"FILTER_ENV_DEPTH",
# Amplitude parameters
"AMP_VELOCITY",
"AMP_PAN",
"AMP_LEVEL_KEYFOLLOW",
# LFO parameters
"LFO_PITCH_DEPTH",
"LFO_FILTER_DEPTH",
"LFO_AMP_DEPTH",
"LFO_PAN_DEPTH",
# Mod LFO parameters
"MOD_LFO_PITCH_DEPTH",
"MOD_LFO_FILTER_DEPTH",
"MOD_LFO_AMP_DEPTH",
"MOD_LFO_PAN",
"MOD_LFO_RATE_CTRL",
"CUTOFF_AFTERTOUCH",
"LEVEL_AFTERTOUCH",
]
# Centralized conversion offsets
[docs]
self.CONVERSION_OFFSETS = {
"OSC_DETUNE": 64,
"OSC_PITCH_ENV_DEPTH": 64,
"AMP_PAN": 64,
"FILTER_CUTOFF_KEYFOLLOW": "map_range",
"AMP_LEVEL_KEYFOLLOW": "map_range",
"OSC_PITCH": 64,
"FILTER_ENV_VELOCITY_SENSITIVITY": 64,
"FILTER_ENV_DEPTH": 64,
"LFO_PITCH_DEPTH": 64,
"LFO_FILTER_DEPTH": 64,
"LFO_AMP_DEPTH": 64,
"LFO_PAN_DEPTH": 64,
"MOD_LFO_PITCH_DEPTH": 64,
"MOD_LFO_FILTER_DEPTH": 64,
"MOD_LFO_AMP_DEPTH": 64,
"MOD_LFO_PAN": 64,
"MOD_LFO_RATE_CTRL": 64,
"CUTOFF_AFTERTOUCH": 64,
"LEVEL_AFTERTOUCH": 64,
}
[docs]
def get_display_value(self) -> Tuple[int, int]:
"""Get the display range for the parameter"""
return self.display_min, self.display_max
# Oscillator parameters
[docs]
OSC_WAVE = (
0x00,
0,
7,
0,
7,
"Waveform of the Oscillator; Select from classic waveforms: SAW, SQR, TRI, SINE, NOISE, SUPER SAW or PCM. \nEach offers unique harmonic content for shaping tone and texture",
) # Waveform preset_type
[docs]
OSC_WAVE_VARIATION = (
0x01,
0,
2,
0,
2,
"You can select variations of the currently selected WAVE",
) # Wave variation
[docs]
OSC_PITCH = (
0x03,
-24,
24,
-24,
24,
"Adjusts the pitch in semitone steps",
) # Coarse tune
[docs]
OSC_DETUNE = (
0x04,
-50,
50,
-50,
50,
"Adjusts the pitch in steps of one cent",
) # Fine tune (-50 to +50)
[docs]
OSC_PULSE_WIDTH_MOD_DEPTH = (
0x05,
0,
127,
0,
127,
"Specifies the amount (depth) of LFO that is applied to PW (Pulse Width). \nIf the OSC Wave has selected (PW-SQR), you can use this slider to specify the amount of LFO modulation applied to PW (pulse width).",
) # PWM Depth
[docs]
OSC_PULSE_WIDTH = (
0x06,
0,
127,
0,
127,
"Sets the pulse width when PW-SQR is selected. \nSmaller values narrow the waveform; higher values widen it, shaping the tone",
) # Pulse Width
[docs]
OSC_PITCH_ENV_ATTACK_TIME = (
0x07,
0,
127,
0,
127,
"Specifies the attack time of the pitch envelope. \nThis specifies the time from the moment you press the key until the pitch reaches its highest (or lowest) point",
) # Pitch Envelope Attack
[docs]
OSC_PITCH_ENV_DECAY_TIME = (
0x08,
0,
127,
0,
127,
"Specifies the decay time of the pitch envelope. \nThis specifies the time from the moment the pitch reaches its highest \n(or lowest) point until it returns to the pitch of the key you pressed",
) # Pitch Envelope Decay
[docs]
OSC_PITCH_ENV_DEPTH = (
0x09,
-63,
63,
-63,
63,
"This specifies how much the pitch envelope will affect the pitch\nNegative values will invert the shape of the envelope",
) # Pitch Envelope Depth (-63 to +63)
[docs]
FILTER_MODE_SWITCH = (
0x0A,
0,
7,
0,
7,
"Selects the type of filter; \nBYPASS, LPF1, LPF2, LPF3, LPF4, HPF, BPF, PKG",
) # Filter mode
[docs]
FILTER_SLOPE = (
0x0B,
0,
1,
0,
1,
"Selects the slope (steepness) of the filter. -12, -24 [dB]",
) # Filter slope
[docs]
FILTER_CUTOFF = (
0x0C,
0,
127,
0,
127,
"Specifies the cutoff frequency",
) # Cutoff frequency
[docs]
FILTER_CUTOFF_KEYFOLLOW = (
0x0D,
54,
74,
-100,
100,
"Specifies how you can make the filter cutoff frequency, \nto vary according to the key you play",
) # Key follow
[docs]
FILTER_ENV_VELOCITY_SENSITIVITY = (
0x0E,
1,
127,
-63,
63,
"Specifies how you can make the filter envelope depth vary, \naccording to the strength with which you play the key",
) # Velocity sensitivity
[docs]
FILTER_RESONANCE = (
0x0F,
0,
127,
0,
127,
"Emphasizes the sound in the region of the filter cutoff frequency",
) # Resonance
[docs]
FILTER_ENV_ATTACK_TIME = (
0x10,
0,
127,
0,
127,
"Specifies the time from the moment you press the key until\n the cutoff frequency reaches its highest (or lowest) point",
) # Filter envelope attack
[docs]
FILTER_ENV_DECAY_TIME = (
0x11,
0,
127,
0,
127,
"Specifies the time from when the cutoff frequency reaches its\n highest (or lowest) point, until it decays to the sustain level",
) # Filter envelope decay
[docs]
FILTER_ENV_SUSTAIN_LEVEL = (
0x12,
0,
127,
0,
127,
"Specifies the cutoff frequency that will be maintained\n from when the decay time has elapsed until you release the key",
) # Filter envelope sustain
[docs]
FILTER_ENV_RELEASE_TIME = (
0x13,
0,
127,
0,
127,
"Specifies the time from when you release the key until\n the cutoff frequency reaches its minimum value",
) # Filter envelope release
[docs]
FILTER_ENV_DEPTH = (
0x14,
1,
127,
-63,
63,
"Specifies the direction and depth to which the cutoff frequency will change",
) # Filter envelope depth
# Amplitude parameters
[docs]
AMP_LEVEL = (0x15, 0, 127, 0, 127, "Partial volume") # Amplitude level
[docs]
AMP_VELOCITY = (
0x16,
-63,
63,
-63,
63,
"Specifies how the volume will vary according to the strength with which you play the keyboard.",
) # Velocity sensitivity
[docs]
AMP_ENV_ATTACK_TIME = (
0x17,
0,
127,
0,
127,
"Specifies the time from the \nmoment you press the key until \n the maximum volume is reached.",
) # Amplitude envelope attack
[docs]
AMP_ENV_DECAY_TIME = (
0x18,
0,
127,
0,
127,
"Specifies the time from when the\nmaximum volume is reached, until\nit decays to the sustain level.",
) # Amplitude envelope decay
[docs]
AMP_ENV_SUSTAIN_LEVEL = (
0x19,
0,
127,
0,
127,
"Specifies the volume level that\nwill be maintained from when\nthe attack and decay times have\nelapsed until you release the key",
) # Amplitude envelope sustain
[docs]
AMP_ENV_RELEASE_TIME = (
0x1A,
0,
127,
0,
127,
"Specifies the time from when you\nrelease the key until the volume\nreaches its minimum value.",
) # Amplitude envelope release
[docs]
AMP_PAN = (
0x1B,
0,
127,
-64,
63,
"Specifies the stereo position of the partial; Left-Right",
) # Pan position
[docs]
AMP_LEVEL_KEYFOLLOW = (
0x1C,
54,
74,
-100,
100,
"Specify this if you want to vary the volume according to the position of the key that you play.\nWith positive (“+”) settings the volume increases as you play upward from the C4 key (middle C);\n with negative (“-”) settings the volume decreases.\nHigher values will produce greater change.",
) # Key follow (-100 to +100)
# LFO parameters
[docs]
LFO_SHAPE = (
0x1C,
0,
5,
0,
5,
"Selects the LFO waveform; Trangle, Sine, Sawtooth, Square, \nSample and Hold (The LFO value will change once each cycle.), Random wave",
) # LFO waveform
[docs]
LFO_RATE = (
0x1D,
0,
127,
0,
127,
"Specifies the LFO rate when LFO Tempo Sync Sw is OFF",
) # LFO rate
[docs]
LFO_TEMPO_SYNC_SWITCH = (
0x1E,
0,
1,
0,
1,
"If this is ON, the LFO rate can be specified as a note value relative to the tempo",
) # Tempo sync switch
[docs]
LFO_TEMPO_SYNC_NOTE = (
0x1F,
0,
19,
0,
19,
"Specifies the LFO rate when LFO Tempo Sync Sw is ON. \n16, 12, 8, 4, 2, 1, 3/4,\n2/3, 1/2, 3/8, 1/3, 1/4,\n3/16, 1/6, 1/8, 3/32,\n1/12, 1/16, 1/24, 1/32",
) # Tempo sync note
[docs]
LFO_FADE_TIME = (
0x20,
0,
127,
0,
127,
"Specifies the time from when the partial sounds until the LFO reaches its maximum amplitude",
) # Fade time
[docs]
LFO_KEY_TRIGGER = (
0x21,
0,
1,
0,
1,
"If this is on, the LFO cycle will be restarted when you press a key",
) # Key trigger
[docs]
LFO_PITCH_DEPTH = (
0x22,
1,
127,
-63,
63,
"Allows the LFO to modulate the pitch, producing a vibrato effect",
) # Pitch mod depth
[docs]
LFO_FILTER_DEPTH = (
0x23,
1,
127,
-63,
63,
"Allows the LFO to modulate the FILTER CUTOFF (cutoff frequency), producing a wah effect",
) # Filter mod depth
[docs]
LFO_AMP_DEPTH = (
0x24,
1,
127,
-63,
63,
"Allows the LFO to modulate the AMP LEVEL (volume), producing a tremolo effect",
) # Amp mod depth
[docs]
LFO_PAN_DEPTH = (
0x25,
1,
127,
-63,
63,
"Allows the LFO to modulate the PAN (stereo position), producing an auto panning effect",
) # Pan mod depth
# Modulation LFO parameters
[docs]
MOD_LFO_SHAPE = (
0x26,
0,
5,
0,
5,
"Selects the MODULATION LFO waveform.\n Trangle, Sine, Sawtooth, Square, \nSample and Hold (The LFO value will change once each cycle.), Random wave. \nThere is an LFO that is always applied to the partial, \nand a MODULATION LFO for applying modulation with the modulation\ncontroller (CC01).",
) # Mod LFO waveform
[docs]
MOD_LFO_RATE = (
0x27,
0,
127,
0,
127,
"Specifies the LFO rate when ModLFO TempoSyncSw is OFF.",
) # Mod LFO rate
[docs]
MOD_LFO_TEMPO_SYNC_SWITCH = (
0x28,
0,
1,
0,
1,
"If this is ON, the LFO rate can be specified as a note value relative to the tempo",
) # Tempo sync switch
[docs]
MOD_LFO_TEMPO_SYNC_NOTE = (
0x29,
0,
19,
0,
19,
"Specifies the LFO rate when ModLFO TempoSyncSw is ON",
) # Tempo sync note
[docs]
OSC_PULSE_WIDTH_SHIFT = (
0x2A,
0,
127,
0,
127,
"Shifts the range of change. Normally, you can leave this at 127.\n * If the Ring Switch is on, this has no effect on partials 1 and 2.",
) # OSC Pulse Width Shift
# 2B is reserved
[docs]
MOD_LFO_PITCH_DEPTH = (
0x2C,
1,
127,
-63,
63,
"Allows the LFO to modulate the pitch, producing a vibrato effect.",
) # Pitch mod depth
[docs]
MOD_LFO_FILTER_DEPTH = (
0x2D,
1,
127,
-63,
63,
"Allows the LFO to modulate the FILTER CUTOFF (cutoff frequency), producing a wah effect.",
) # Filter mod depth
[docs]
MOD_LFO_AMP_DEPTH = (
0x2E,
1,
127,
-63,
63,
"Allows the LFO to modulate the AMP LEVEL (volume), producing a tremolo effect.",
) # Amp mod depth
[docs]
MOD_LFO_PAN = (
0x2F,
1,
127,
-63,
63,
"Allows the LFO to modulate the pan (stereo position), producing an auto panning effect.",
) # Pan mod depth
[docs]
MOD_LFO_RATE_CTRL = (
0x3B,
1,
127,
-63,
63,
"Make these settings if you want to change the Modulation LFO Rate when the modulation lever\nis operated.\n Specify a positive (“+”) setting if you want ModLFO Rate to become faster when you increase\nthe modulation controller (CC01) value; \nspecify a negative (“-”) setting if you want it to become slower.",
) # Rate control
# Additional parameters
[docs]
CUTOFF_AFTERTOUCH = (
0x30,
1,
127,
-63,
63,
"Specifies how aftertouch pressure will affect the cutoff frequency",
) # Cutoff aftertouch
[docs]
LEVEL_AFTERTOUCH = (
0x31,
1,
127,
-63,
63,
"Specifies how aftertouch pressure affects the volume",
) # Level aftertouch
[docs]
HPF_CUTOFF = (
0x39,
0,
127,
0,
127,
"Specifies the cutoff frequency of an independent -6 dB high-pass filter",
) # HPF cutoff
[docs]
SUPER_SAW_DETUNE = (
0x3A,
0,
127,
0,
127,
"Specifies the amount of pitch difference between the seven sawtooth waves layered within a single oscillator.\n * Lower values will produce a more subtle detune effect, similar to a single sawtooth wave.\n* Higher values will increase the pitch difference",
) # Super saw detune
[docs]
PCM_WAVE_GAIN = (
0x34,
0,
3,
0,
3,
"Sets the gain for PCM waveforms; 0dB, -6dB, +6dB, +12dB",
) # PCM Wave Gain
[docs]
PCM_WAVE_NUMBER = (
0x35,
0,
16384,
0,
16384,
"Selects the PCM waveform; 0-16383 * This is valid only if PCM is selected for OSC Wave.",
) # PCM Wave Number
@property
[docs]
def display_name(self) -> str:
"""Get display name for the parameter"""
return {self.OSC_WAVE_VARIATION: "Variation"}.get(
self, self.name.replace("_", " ").title()
)
[docs]
def get_switch_text(self, value: int) -> str:
"""Get display text for switch values"""
if self == self.OSC_WAVE_VARIATION:
return ["A", "B", "C"][value]
elif self == self.FILTER_MODE_SWITCH:
return ["BYPASS", "LPF", "HPF", "BPF", "PKG", "LPF2", "LPF3", "LPF4"][value]
elif self == self.FILTER_SLOPE:
return ["-12dB", "-24dB"][value]
elif self == self.MOD_LFO_SHAPE:
return ["TRI", "SIN", "SAW", "SQR", "S&H", "RND"][value]
elif self == self.MOD_LFO_TEMPO_SYNC_SWITCH:
return "ON" if value else "OFF"
elif self == self.LFO_SHAPE:
return ["TRI", "SIN", "SAW", "SQR", "S&H", "RND"][value]
elif self in [self.LFO_TEMPO_SYNC_SWITCH, self.LFO_KEY_TRIGGER]:
return "ON" if value else "OFF"
elif self == self.PCM_WAVE_GAIN:
return f"{[-6, 0, 6, 12][value]:+d}dB"
return str(value)
[docs]
def validate_value(self, value: int) -> int:
"""Validate and convert parameter value to MIDI range (0-127)."""
if not isinstance(value, int):
raise ValueError(f"Value must be an integer, got {type(value)}")
conversion = self.CONVERSION_OFFSETS.get(self.name)
if conversion == "map_range":
value = map_range(
value, -100, 100, 0, 127
) # Normalize -100 to 100 into 0 to 127
elif isinstance(conversion, int):
value += conversion # Apply offset (e.g., +64 or -64)
# Ensure value is within MIDI range
value = max(0, min(127, value))
return value
[docs]
def get_address_for_partial(self, partial_number: int) -> Tuple[int, int]:
"""
Get parameter area and address adjusted for partial number.
:param partial_number: int The partial number
:return: Tuple[int, int] The (group, address) tuple
"""
group_map = {1: 0x20, 2: 0x21, 3: 0x22}
group = group_map.get(
partial_number, 0x20
) # Default to 0x20 if partial_name is not 1, 2, or 3
return group, self.address
@staticmethod
[docs]
def get_by_name(param_name: str) -> Optional[object]:
"""
Get the DigitalParameter by name.
:param param_name: str The parameter name
:return: Optional[AddressParameterDigitalPartial] The parameter
Return the parameter member by name, or None if not found
"""
return DigitalPartialParam.__members__.get(param_name, None)
[docs]
def convert_value(self, value: int, reverse: bool = False) -> int:
"""
Converts value in both directions based on CONVERSION_OFFSETS
:param value: int The value
:param reverse: bool The reverse flag
:return: int The converted value
"""
if value is None:
return
conversion = self.CONVERSION_OFFSETS.get(self.name)
if conversion == "map_range":
return (
map_range(value, 54, 74, -100, 100)
if reverse
else map_range(value, -100, 100, 54, 74)
)
if isinstance(conversion, int):
return value - conversion if reverse else value + conversion
return value # Default case: return as is
[docs]
def convert_to_midi(self, slider_value: int) -> int:
"""
Convert from display value to MIDI value
:param slider_value: int The display value
:return: int The MIDI value
"""
return self.convert_value(slider_value)
[docs]
def convert_from_midi(self, midi_value: int) -> int:
"""
Convert from MIDI value to display value
:param midi_value: int The MIDI value
:return: int The display value
"""
return self.convert_value(midi_value, reverse=True)
[docs]
def get_envelope_param_type(self):
"""
Returns a envelope_param_type, if the parameter is part of an envelope,
otherwise returns None.
:return: Optional[str] The envelope parameter type
"""
return ENVELOPE_MAPPING.get(self.name)