"""
AddressParameterDigitalModify: JD-Xi Digital Synthesizer Parameter Mapping
====================================================================
Defines the AddressParameterDigitalModify class for modifying parameters of
Digital/SuperNATURAL synth tones in the JD-Xi.
This class provides attributes and methods to manage various modulation
parameters shared across all partials of a digital synth tone. It also
includes methods for retrieving digital text representations of switch
values, parameter lookup by name, and value validation.
Example usage:
# Create a AddressParameterDigitalModify instance for Attack Time Interval Sensitivity
attack_time_param = AddressParameterDigitalModify(*AddressParameterDigitalModify.ATTACK_TIME_INTERVAL_SENS)
# Validate a value
validated_value = attack_time_param.validate_value(100)
# Get digital text for a switch value
text = attack_time_param.get_switch_text(1) # For ENVELOPE_LOOP_MODE, returns "FREE-RUN"
# Retrieve parameter by name
param = AddressParameterDigitalModify.get_by_name("ENVELOPE_LOOP_MODE")
if param:
print(param.name, param.min_val, param.max_val)
"""
from typing import Optional
from jdxi_editor.midi.data.address.address import JDXiSysExOffsetSuperNATURALLMB
from jdxi_editor.midi.parameter.spec import ParameterSpec
from picomidi.sysex.parameter.address import AddressParameter
[docs]
class DigitalModifyParam(AddressParameter):
"""Modify parameters for Digital/SuperNATURAL synth tones.
These parameters are shared across all partials.
"""
def __init__(
self,
address: int,
min_val: int,
max_val: int,
display_min: int = None,
display_max: int = None,
description: str = None,
display_name: str = None,
options: Optional[list] = None,
values: Optional[list] = None,
):
"""
Initialize the digital modify parameter with address and value range.
Accepts 7 arguments when unpacked from ParameterSpec tuple:
(address, min_val, max_val, min_display, max_display, description, display_name)
"""
super().__init__(address, min_val, max_val)
# Use description as tooltip if provided
[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._display_name = display_name
[docs]
ATTACK_TIME_INTERVAL_SENS = ParameterSpec(
0x01,
0,
127,
description="Shortens the FILTER and AMP Attack Time according to the spacing between note-on events.\nHigher values produce a greater effect. With a setting of 0, there will be no effect.\nThis is effective when you want to play rapid notes using a sound that has a slow attack\n(Attack Time).",
display_name="Attack Time Interval Sens",
)
[docs]
RELEASE_TIME_INTERVAL_SENS = ParameterSpec(
0x02,
0,
127,
description="Shortens the FILTER and AMP Release Time if the interval between one note-on and the next\nnote-off is brief. Higher values produce a greater effect. With a setting of 0, there will be no effect.\nThis is effective when you want to play staccato notes using a sound that has a slow release",
display_name="Release Time Interval Sens",
)
[docs]
PORTAMENTO_TIME_INTERVAL_SENS = ParameterSpec(
0x03,
0,
127,
description="Shortens the Portamento Time according to the spacing between note-on events. Higher values\nproduce a greater effect. With a setting of 0, there will be no effect.",
display_name="Portamento Time Interval Sens",
)
[docs]
ENVELOPE_LOOP_MODE = ParameterSpec(
0x04,
0,
2,
0,
2,
"Use this to loop the envelope between certain regions during a note-on.\nOFF The envelope will operate normally.\nFREE-RUN When the Decay segment has ended, the envelope will return to the Attack. The Attack through\nDecay segments will repeat until note-off occurs.\nTEMPO-SYNC Specifies the loop rate as a note value (Sync Note parameter).",
"Envelope Loop Mode",
)
[docs]
ENVELOPE_LOOP_SYNC_NOTE = ParameterSpec(
0x05,
0,
19,
0,
19,
"Returns to the Attack at the specified rate. If the Attack+Decay time is shorter than the specified\nloop, the sound is maintained at the Sustain Level. If the Attack+Decay time is longer than the\nspecified loop, the sound returns to the Attack even if the Decay has not completed. This will\n\ncontinue repeating until note-off occurs.",
"Envelope Loop Sync Note",
)
[docs]
CHROMATIC_PORTAMENTO = ParameterSpec(
0x06,
0,
1,
0,
1,
"If this is turned ON, portamento will operate in semitone steps. If this is turned OFF, the pitch will\nchange smoothly from one note to the next.\n This is effective when you want to play chromatic portamento\n using a sound that has a\nslow portamento time.",
"Chromatic Portamento",
)
@property
[docs]
def display_name(self) -> str:
"""Get digital name for the parameter (from ParameterSpec or fallback)."""
if getattr(self, "_display_name", None) is not None:
return self._display_name
return self.name.replace("_", " ").title()
[docs]
def get_switch_text(self, value: int) -> str:
"""Get digital text for switch values"""
if self == self.ENVELOPE_LOOP_MODE:
return ["OFF", "FREE-RUN", "TEMPO-SYNC"][value]
elif self == self.CHROMATIC_PORTAMENTO:
return ["OFF", "ON"][value]
elif self == self.ENVELOPE_LOOP_SYNC_NOTE:
return [
"16",
"12",
"8",
"4",
"2",
"1",
"3/4",
"2/3",
"1/2",
"3/8",
"1/3",
"1/4",
"3/16",
"1/6",
"1/8",
"3/32",
"1/12",
"1/16",
"1/24",
"1/32",
][value]
return str(value)
@staticmethod
[docs]
def get_by_name(param_name):
"""Get the Parameter by name."""
# Return the parameter member by name, or None if not found
return DigitalModifyParam.__members__.get(param_name, None)
[docs]
def validate_value(self, value: int) -> int:
"""Validate and convert parameter value"""
if not isinstance(value, int):
raise ValueError(f"Value must be an integer, got {type(value)}")
# Validate range for specific parameters
if self == self.ENVELOPE_LOOP_SYNC_NOTE and not (0 <= value <= 19):
raise ValueError(
f"Value {value} out of range for {self.name} (valid range: 0-19)"
)
return value
[docs]
def get_address_for_partial(self, partial_number: int = 0):
return JDXiSysExOffsetSuperNATURALLMB.MODIFY, 0x00