"""
Effects
"""
from dataclasses import dataclass
from enum import Enum
from picomidi.constant import Midi
from picomidi.core.bitmask import BitMask
from jdxi_editor.midi.data.address.address import (
AddressOffsetProgramLMB,
AddressStartMSB,
CommandID,
)
from jdxi_editor.midi.message.roland import RolandSysEx
[docs]
class EffectType(Enum):
"""Effect types available on JD-Xi"""
# Common parameters
# Effect-specific parameters
# Send levels
# Reverb parameters
[docs]
REVERB_PRE_DELAY = 0x32
[docs]
class FX:
"""Effect parameter ranges and defaults"""
[docs]
RANGES = {
# Common parameters
"level": (0, 127),
# Distortion parameters
"dist_drive": (0, 127),
"dist_tone": (0, 127),
# Compressor parameters
"comp_attack": (0, 127),
"comp_release": (0, 127),
"comp_threshold": (0, 127),
"comp_ratio": (0, 127),
# Bitcrusher parameters
"bit_depth": (0, 127),
"sample_rate": (0, 127),
# Flanger parameters
"flanger_rate": (0, 127),
"flanger_depth": (0, 127),
"flanger_feedback": (0, 127),
# Phaser parameters
"phaser_rate": (0, 127),
"phaser_depth": (0, 127),
"phaser_resonance": (0, 127),
# Ring Modulator parameters
"ring_freq": (0, 127),
"ring_balance": (0, 127),
# Slicer parameters
"slicer_rate": (0, 127),
"slicer_pattern": (0, 15),
}
[docs]
DEFAULTS = {
"level": 100,
"dist_drive": 64,
"dist_tone": 64,
"comp_attack": 0,
"comp_release": 50,
"comp_threshold": 0,
"comp_ratio": 0,
"bit_depth": 127,
"sample_rate": 127,
"flanger_rate": 64,
"flanger_depth": 64,
"flanger_feedback": 64,
"phaser_rate": 64,
"phaser_depth": 64,
"phaser_resonance": 64,
"ring_freq": 64,
"ring_balance": 64,
"slicer_rate": 64,
"slicer_pattern": 0,
}
@dataclass
[docs]
class EffectPatch:
"""Effect patch data"""
# Effect preset_type and common parameters
[docs]
type: EffectType = EffectType.THRU
# Effect-specific parameters
# Send levels
[docs]
def validate_param(self, param: str, value: int) -> bool:
"""Validate parameter value is in range"""
if param in FX.RANGES:
min_val, max_val = FX.RANGES[param]
return min_val <= value <= max_val
return False
@dataclass
[docs]
class EffectParam:
"""Effect parameter definition"""
[docs]
min_value: int = -20000
[docs]
class EfxType(Enum):
"""Effect types for JD-Xi"""
# Reverb Types (0-7)
# Delay Types (0-4)
# FX Types (0-12)
@staticmethod
[docs]
def get_display_name(value: int, effect_type: str) -> str:
"""Get display name for effect preset_type"""
names = {
"reverb": {
0: "Room 1",
1: "Room 2",
2: "Stage 1",
3: "Stage 2",
4: "Hall 1",
5: "Hall 2",
6: "Plate",
7: "Spring",
},
"delay": {
0: "Stereo",
1: "Panning",
2: "Mono",
3: "Tape Echo",
4: "Mod Delay",
},
"fx": {
0: "Distortion",
1: "Fuzz",
2: "Compressor",
3: "Bitcrusher",
4: "Equalizer",
5: "Phaser",
6: "Flanger",
7: "Chorus",
8: "Tremolo",
9: "Auto Pan",
10: "Slicer",
11: "Ring Mod",
12: "Isolator",
},
}
return names.get(effect_type, {}).get(value, "???")
[docs]
class EffectGroup(Enum):
"""Effect parameter groups"""
[docs]
COMMON = 0x00 # Common parameters
[docs]
INSERT = 0x10 # Insert effect parameters
[docs]
REVERB = 0x20 # Reverb parameters
[docs]
DELAY = 0x30 # Delay parameters
[docs]
class Effect1(Enum):
"""Program Effect 1 parameters"""
[docs]
TYPE = 0x00 # Effect preset_type (0-4)
[docs]
LEVEL = 0x01 # Effect level (0-127)
[docs]
DELAY_SEND = 0x02 # Delay send level (0-127)
[docs]
REVERB_SEND = 0x03 # Reverb send level (0-127)
[docs]
OUTPUT_ASSIGN = 0x04 # Output assign (0: DIR, 1: EFX2)
# Parameters start at 0x11 and go up to 0x10D
# Each parameter is 4 bytes (12768-52768: -20000 to +20000)
[docs]
PARAM_1 = 0x11 # Parameter 1
[docs]
PARAM_2 = 0x15 # Parameter 2
# ... continue for all 32 parameters
[docs]
PARAM_32 = 0x10D # Parameter 32
@staticmethod
[docs]
def get_param_offset(param_num: int) -> int:
"""Get parameter offset from parameter number (1-32)"""
if 1 <= param_num <= 32:
return 0x11 + ((param_num - 1) * 4)
return 0x00
@staticmethod
[docs]
def get_display_value(param: int, value: int) -> str:
"""Convert raw value to display value"""
if param == 0x00: # Effect preset_type
return ["OFF", "DISTORTION", "FUZZ", "COMPRESSOR", "BITCRUSHER"][value]
elif param == 0x04: # Output assign
return ["DIR", "EFX2"][value]
elif 0x11 <= param <= 0x10D: # Effect parameters
return f"{value - 32768:+d}" # Convert 12768-52768 to -20000/+20000
return str(value)
@dataclass
[docs]
class Effect1Message(RolandSysEx):
"""Program Effect 1 parameter message"""
[docs]
command: int = CommandID.DT1
[docs]
area: int = AddressStartMSB.TEMPORARY_PROGRAM # 0x18: Program area
[docs]
section: int = 0x02 # 0x02: Effect 1 section
[docs]
group: int = AddressOffsetProgramLMB.COMMON # Always 0x00
[docs]
lsb: int = 0x00 # Parameter number
[docs]
value: int = 0x00 # Parameter value
[docs]
def __post_init__(self):
"""Set up address and data"""
self.address = [
self.msb, # Program area (0x18)
self.section, # Effect 1 section (0x02)
self.group, # Always 0x00
self.param, # Parameter number
]
# Handle 4-byte parameters
if 0x11 <= self.param <= 0x10D:
# Convert -20000/+20000 to 12768-52768
value = self.value + 32768
self.data = [
(value >> 24) & BitMask.LOW_4_BITS, # High nibble
(value >> 16) & BitMask.LOW_4_BITS,
(value >> 8) & BitMask.LOW_4_BITS,
value & BitMask.LOW_4_BITS, # Low nibble
]
else:
self.data = [self.value]
[docs]
class Effect2(Enum):
"""Program Effect 2 parameters"""
[docs]
TYPE = 0x00 # Effect preset_type (0, 5-8: OFF, PHASER, FLANGER, DELAY, CHORUS)
[docs]
LEVEL = 0x01 # Effect level (0-127)
[docs]
DELAY_SEND = 0x02 # Delay send level (0-127)
[docs]
REVERB_SEND = 0x03 # Reverb send level (0-127)
# Reserved (0x04-0x10)
# Parameters start at 0x11 and go up to 0x10D
# Each parameter is 4 bytes (12768-52768: -20000 to +20000)
[docs]
PARAM_1 = 0x11 # Parameter 1
[docs]
PARAM_2 = 0x15 # Parameter 2
# ... continue for all 32 parameters
[docs]
PARAM_32 = 0x10D # Parameter 32
@staticmethod
[docs]
def get_param_offset(param_num: int) -> int:
"""Get parameter offset from parameter number (1-32)"""
if 1 <= param_num <= 32:
return 0x11 + ((param_num - 1) * 4)
return 0x00
@staticmethod
[docs]
def get_display_value(param: int, value: int) -> str:
"""Convert raw value to display value"""
if param == Midi.VALUE.ZERO: # Effect preset_type
if value == 0:
return "OFF"
types = ["OFF", "PHASER", "FLANGER", "DELAY", "CHORUS"]
return types[value - 4] if 5 <= value <= 8 else str(value)
elif 0x11 <= param <= 0x10D: # Effect parameters
return f"{value - 32768:+d}" # Convert 12768-52768 to -20000/+20000
return str(value)
@dataclass
[docs]
class Effect2Message(RolandSysEx):
"""Program Effect 2 parameter message"""
[docs]
command: int = CommandID.DT1
[docs]
msb: int = AddressStartMSB.TEMPORARY_PROGRAM # 0x18: Program area
[docs]
umb: int = 0x04 # 0x04: Effect 2 section
[docs]
lmb: int = 0x00 # Always 0x00
[docs]
lsb: int = 0x00 # Parameter number
[docs]
value: int = 0x00 # Parameter value
[docs]
def __post_init__(self, param):
"""Set up address and data"""
self.param = param
self.address = [
self.msb, # Program area (0x18)
self.umb, # Effect 2 section (0x04)
self.lmb, # Always 0x00
self.param, # Parameter number
]
# Handle 4-byte parameters
if 0x11 <= self.param <= 0x10D:
# Convert -20000/+20000 to 12768-52768
value = self.value + 32768
self.data = [
(value >> 24) & BitMask.LOW_4_BITS, # High nibble
(value >> 16) & BitMask.LOW_4_BITS,
(value >> 8) & BitMask.LOW_4_BITS,
value & BitMask.LOW_4_BITS, # Low nibble
]
else:
self.data = [self.value]