Source code for jdxi_editor.midi.data.control_change.analog

"""Analog Control Change"""

from dataclasses import dataclass
from enum import Enum
from typing import Any, Tuple

from picomidi.constant import Midi
from shiboken6.Shiboken import Object

from jdxi_editor.midi.data.control_change.base import ControlChange


[docs] class AnalogControlChange(ControlChange): """Analog synth CC parameters""" # Direct CC parameters
[docs] CUTOFF = 102 # Cutoff (0-127)
[docs] RESONANCE = 105 # Resonance (0-127)
[docs] LEVEL = 117 # Level (0-127)
[docs] LFO_RATE = 16 # LFO Rate (0-127)
@staticmethod
[docs] def get_display_value(param: int, value: int) -> str: """Convert raw value to display value""" if param == 3: # LFO Shape shapes = ["TRI", "SIN", "SAW", "SQR", "S&H", "RND"] return shapes[value] return str(value)
@dataclass(frozen=True)
[docs] class RPNValue: """Represents a MIDI RPN value with its MSB, LSB, and value range."""
[docs] msb_lsb: Tuple[int, int]
[docs] value_range: Tuple[int, int]
[docs] def midi_bytes(self, value: int) -> list[int]: """Generate CC messages for this RPN and a given value.""" msb, lsb = self.msb_lsb value = max(min(value, self.value_range[1]), self.value_range[0]) return [ (Midi.CC.STATUS, 101, msb), # RPN MSB (Midi.CC.STATUS, 100, lsb), # RPN LSB (Midi.CC.STATUS, 6, value >> 7), # Data Entry MSB (Midi.CC.STATUS, 38, value & 0x7F), # Data Entry LSB ]
[docs] class AnalogRPN(Enum): """Analog synth RPN parameters with their MSB, LSB, and value range."""
[docs] ENVELOPE = RPNValue((0, 124), (0, 127))
[docs] LFO_SHAPE = RPNValue((0, 3), (0, 5))
[docs] LFO_PITCH_DEPTH = RPNValue((0, 15), (0, 127))
[docs] LFO_FILTER_DEPTH = RPNValue((0, 18), (0, 127))
[docs] LFO_AMP_DEPTH = RPNValue((0, 21), (0, 127))
[docs] PULSE_WIDTH = RPNValue((0, 37), (0, 127))
@dataclass(frozen=True) class PartialRPNValue: """Represents a MIDI RPN value with base MSB/LSB, value range, and partial.""" base_msb_lsb: Tuple[int, int] value_range: Tuple[int, int] partial: int @property def msb_lsb(self) -> Tuple[int, int]: """Return the dynamically adjusted MSB/LSB based on the partial number.""" msb, base_lsb = self.base_msb_lsb return msb, base_lsb + (self.partial - 1)
[docs] def __post_init__(self) -> None: if not (1 <= self.partial <= 3): raise ValueError("Partial must be between 1 and 3.")
[docs] def midi_bytes(self, value: int) -> list[tuple[Any, int, int]]: """Generate CC messages for this RPN and a given value.""" msb, lsb = self.msb_lsb value = max(min(value, self.value_range[1]), self.value_range[0]) return [ (Midi.CC.STATUS, 101, msb), # RPN MSB (Midi.CC.STATUS, 100, lsb), # RPN LSB (Midi.CC.STATUS, 6, value >> 7), # Data Entry MSB (Midi.CC.STATUS, 38, value & 0x7F), # Data Entry LSB ]
@dataclass(frozen=True)
[docs] class PartialRPNValue:
[docs] base_msb_lsb: Tuple[int, int]
[docs] value_range: Tuple[int, int]
[docs] partial: int
@property
[docs] def msb_lsb(self) -> Tuple[int, int]: msb, base_lsb = self.base_msb_lsb return (msb, base_lsb + (self.partial - 1))
[docs] def make_digital_rpn(partial: int) -> Object: """ make_digital_rpn :param partial: int :return: Object """ class DigitalPartialRPN(Enum): ENVELOPE = PartialRPNValue((0, 124), (0, 127), partial) LFO_SHAPE = PartialRPNValue((0, 3), (0, 5), partial) LFO_PITCH_DEPTH = PartialRPNValue((0, 15), (0, 127), partial) LFO_FILTER_DEPTH = PartialRPNValue((0, 18), (0, 127), partial) LFO_AMP_DEPTH = PartialRPNValue((0, 21), (0, 127), partial) return DigitalPartialRPN
[docs] DigitalRPN_Partial1 = make_digital_rpn(1)
[docs] DigitalRPN_Partial2 = make_digital_rpn(2)
[docs] DigitalRPN_Partial3 = make_digital_rpn(3)
# Not using drums (26 Partials) thankfully :-) if __name__ == "__main__": # Example usage print(AnalogRPN.ENVELOPE.value.msb_lsb) # (0, 124) print(DigitalRPN_Partial1.ENVELOPE.STATUS.msb_lsb) # (0, 124) print(DigitalRPN_Partial2.ENVELOPE.STATUS.msb_lsb) # (0, 125)