Source code for jdxi_editor.midi.data.effects.effects

"""
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"""
[docs] THRU = 0x00
[docs] DISTORTION = 0x01
[docs] FUZZ = 0x02
[docs] COMPRESSOR = 0x03
[docs] BITCRUSHER = 0x04
[docs] FLANGER = 0x05
[docs] PHASER = 0x06
[docs] RING_MOD = 0x07
[docs] SLICER = 0x08
# Common parameters
[docs] LEVEL = 0x00
[docs] MIX = 0x01
# Effect-specific parameters
[docs] DRIVE = 0x10
[docs] TONE = 0x11
[docs] ATTACK = 0x12
[docs] RELEASE = 0x13
[docs] THRESHOLD = 0x14
[docs] RATIO = 0x15
[docs] BIT_DEPTH = 0x16
[docs] RATE = 0x17
[docs] DEPTH = 0x18
[docs] FEEDBACK = 0x19
[docs] FREQUENCY = 0x1A
[docs] BALANCE = 0x1B
[docs] PATTERN = 0x1C
# Send levels
[docs] REVERB_SEND = 0x20
[docs] DELAY_SEND = 0x21
[docs] CHORUS_SEND = 0x22
# Reverb parameters
[docs] REVERB_TYPE = 0x30
[docs] REVERB_TIME = 0x31
[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
[docs] level: int = 100
# Effect-specific parameters
[docs] param1: int = 0
[docs] param2: int = 0
# Send levels
[docs] reverb_send: int = 0
[docs] delay_send: int = 0
[docs] chorus_send: int = 0
[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] name: str
[docs] min_value: int = -20000
[docs] max_value: int = 20000
[docs] default: int = 0
[docs] unit: str = ""
[docs] class EfxType(Enum): """Effect types for JD-Xi""" # Reverb Types (0-7)
[docs] ROOM1 = 0
[docs] ROOM2 = 1
[docs] STAGE1 = 2
[docs] STAGE2 = 3
[docs] HALL1 = 4
[docs] HALL2 = 5
[docs] PLATE = 6
[docs] SPRING = 7
# Delay Types (0-4)
[docs] STEREO = 0
[docs] PANNING = 1
[docs] MONO = 2
[docs] TAPE_ECHO = 3
[docs] MOD_DELAY = 4
# FX Types (0-12)
[docs] DISTORTION = 0
[docs] FUZZ = 1
[docs] COMPRESSOR = 2
[docs] BITCRUSHER = 3
[docs] EQUALIZER = 4
[docs] PHASER = 5
[docs] FLANGER = 6
[docs] CHORUS = 7
[docs] TREMOLO = 8
[docs] AUTOPAN = 9
[docs] SLICER = 10
[docs] RING_MOD = 11
[docs] ISOLATOR = 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]