Source code for jdxi_editor.midi.data.parameter.vocal_fx

"""
Module: VocalFXParameter
========================

This module defines the VocalFXParameter class, which represents various vocal effects parameters
in a synthesizer. These parameters control different aspects of vocal processing, including
level, pan, delay/reverb send levels, auto pitch settings, vocoder effects, and more.

The class provides methods to:

- Initialize vocal FX parameters with a given address, range, and optional digital range.
- Validate and convert parameter values to the MIDI range (0-127).
- Define a variety of vocal effects parameters with specific ranges, including:
  - Level, pan, delay/reverb send levels, and output assignment
  - Auto pitch settings such as switch, type, scale, key, note, gender, octave, and balance
  - Vocoder parameters such as switch, envelope type, level, mic sensitivity, and mix level

The class also offers conversion utilities:
- Convert between MIDI values and digital values.
- Handle special bipolar cases (e.g., pan, auto pitch gender).
- Retrieve the digital value range or MIDI value range for parameters.

Parameters include:
- Level, pan, delay and reverb send levels, output assignment, and auto pitch settings
- Vocoder settings for on/off, envelope, level, mic sensitivity, synth level, and mic mix
- Auto pitch gender, octave, balance, and key/note configurations

The class also includes utility functions to get a parameter's address, range, digital range,
and to convert between MIDI values and digital values.

Usage example:
==============
    >>> # Initialize a VocalFXParam object for the LEVEL parameter
    >>> param = VocalFXParam.LEVEL

    >>> # Access digital range values
    >>> print(param.display_min)  # Output: 0
    0
    >>> print(param.display_max)  # Output: 127
    127

    >>> # Validate a MIDI value
    >>> midi_value = param.convert_to_midi(64) # Simple linear scaling, not bipolar
    >>> print(midi_value)
    64
"""

from typing import Optional, Tuple

from jdxi_editor.midi.parameter.spec import ParameterSpec
from picomidi.sysex.parameter.address import AddressParameter


[docs] class VocalFXParam(AddressParameter): """Vocal FX parameters""" 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, display_name: Optional[str] = None, options: Optional[list] = None, values: Optional[list] = 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.tooltip = tooltip
[docs] self._display_name = display_name
[docs] self.options = options
[docs] self.values = values
[docs] LEVEL = ParameterSpec( 0x00, 0, 127, 0, 127, "Sets the level of the vocal FX." ) # Level (0-127)
[docs] PAN = ParameterSpec( 0x01, -64, 63, -64, 63, "Sets the pan of the vocal FX.", ) # Pan (-64 to +63, centered at 64)
[docs] DELAY_SEND_LEVEL = ParameterSpec( 0x02, 0, 127, description="Sets the level of the delay send.", ) # Delay send level (0-127)
[docs] REVERB_SEND_LEVEL = ParameterSpec( 0x03, 0, 127, description="Sets the level of the reverb send.", ) # Reverb send level (0-127)
[docs] OUTPUT_ASSIGN = ParameterSpec( 0x04, 0, 4, 0, 4, "Sets the output assignment.", ) # Output assignment (0-4)
[docs] AUTO_PITCH_SWITCH = ParameterSpec( 0x05, 0, 1, 0, 1, "Sets the auto note on/off.", ) # Auto Note on/off (0-1)
[docs] AUTO_PITCH_TYPE = ParameterSpec( 0x06, 0, 3, 0, 3, "Sets the auto pitch preset_type.", ) # Auto Pitch preset_type (0-3)
[docs] AUTO_PITCH_SCALE = ParameterSpec( 0x07, 0, 1, 0, 1, "Sets the auto pitch scale.", ) # Scale CHROMATIC, Maj(Min)
[docs] AUTO_PITCH_KEY = ParameterSpec( 0x08, 0, 23, 0, 23, "Sets the auto pitch key.", ) # Auto Pitch key (0-23) C, Db, D, Eb, E, F, F#, G, Ab, A, Bb, B, Cm, C#m, Dm, D#m, Em, Fm, F#m, Gm, G#m, Am, Bbm, Bm
[docs] AUTO_PITCH_NOTE = ParameterSpec( 0x09, 0, 11, 0, 11, "Sets the auto pitch note.", ) # Auto Pitch note (0-11)
[docs] AUTO_PITCH_GENDER = ParameterSpec( 0x0A, -10, 10, -10, 10, "Sets the auto pitch gender.", ) # Gender (-10 to +10, centered at 0)
[docs] AUTO_PITCH_OCTAVE = ParameterSpec( 0x0B, -1, 1, -1, 1, "Sets the auto pitch octave.", ) # Octave (-1 to +1: 0-2)
[docs] AUTO_PITCH_BALANCE = ParameterSpec( 0x0C, 0, 100, 0, 100, "Sets the auto pitch balance.", ) # Dry/Wet Balance (0-100)
[docs] VOCODER_SWITCH = ParameterSpec( 0x0D, 0, 1, 0, 1, "Sets the vocoder on/off.", ) # Vocoder on/off (0-1)
[docs] VOCODER_ENVELOPE = ParameterSpec( 0x0E, 0, 2, 0, 2, "Sets the vocoder envelope preset_type.", ) # Vocoder envelope preset_type (0-2)
[docs] VOCODER_LEVEL = ParameterSpec( 0x0F, 0, 127, description="Sets the vocoder level.", ) # Vocoder level (0-127)
[docs] VOCODER_MIC_SENS = ParameterSpec( 0x10, 0, 127, description="Sets the vocoder mic sensitivity.", ) # Vocoder mic sensitivity (0-127)
[docs] VOCODER_SYNTH_LEVEL = ParameterSpec( 0x11, 0, 127, description="Sets the vocoder synth level.", ) # Vocoder synth level (0-127)
[docs] VOCODER_MIC_MIX = ParameterSpec( 0x12, 0, 127, description="Sets the vocoder mic mix level.", ) # Vocoder mic mix level (0-127)
[docs] VOCODER_MIC_HPF = ParameterSpec( 0x13, 0, 13, 0, 13, "Sets the vocoder mic HPF freq.", ) # Vocoder mic HPF freq (0-13)
[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 integer, got {type(value)}") if value < self.min_val or value > self.max_val: raise ValueError( f"Value {value} out of range for {self.name} " f"(valid range: {self.min_val}-{self.max_val})" ) return value
@staticmethod
[docs] def get_by_name(param_name: str) -> Optional["VocalFXParam"]: """Get the parameter by name.""" return VocalFXParam.__members__.get(param_name, None)
@staticmethod
[docs] def get_name_by_address(address: int) -> Optional[str]: """Return the parameter name for address given address. :param address: int The address :return: Optional[str] The parameter name """ for param in VocalFXParam: if param.address == address: return param.name return None # Return None if the address is not found
@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()
@property
[docs] def is_switch(self) -> bool: """Returns True if parameter is address binary/enum switch""" return self in [ self.AUTO_PITCH_SWITCH, self.VOCODER_SWITCH, ]
@staticmethod
[docs] def get_address(param_name: str) -> Optional[int]: """ Get the address of address parameter by name. :param param_name: str The parameter name :return: Optional[int] The address """ param = VocalFXParam.get_by_name(param_name) if param: return param.STATUS[0] return None
@staticmethod
[docs] def get_range(param_name: str) -> Tuple[int, int]: """ Get the value range (min, max) of address parameter by name. :param param_name: str The parameter name :return: Tuple[int, int] The value range """ param = VocalFXParam.get_by_name(param_name) if param: return param.STATUS[1], param.STATUS[2] return None, None
@staticmethod
[docs] def get_display_range(param_name: str) -> Tuple[int, int]: """ Get the digital value range (min, max) of address parameter by name. :param param_name: str The parameter name :return: Tuple[int, int] The digital value range """ param = VocalFXParam.get_by_name(param_name) if param: return param.display_min, param.display_max return None, None
[docs] def get_display_value(self) -> Tuple[int, int]: """ Get the digital value range (min, max) for the parameter :return: Tuple[int, int] The digital value range """ if hasattr(self, "display_min") and hasattr(self, "display_max"): return self.display_min, self.display_max return self.min_val, self.max_val
[docs] def convert_from_display(self, display_value: int) -> int: """ Convert from digital value to MIDI value (0-127) :param display_value: int The digital value :return: int The MIDI value """ # Delegate to convert_to_midi for consistency return self.convert_to_midi(display_value)
@staticmethod
[docs] def convert_to_display( value: int, min_val: int, max_val: int, display_min: int, display_max: int ) -> int: """ Convert address value to address digital value within address range. :param value: int The address value :param min_val: int The address minimum value :param max_val: int The address maximum value :param display_min: int The digital minimum value :param display_max: int The digital maximum value :return: int The digital value """ return int( (value - min_val) * (display_max - display_min) / (max_val - min_val) + display_min )
[docs] def convert_to_midi(self, display_value: int) -> int: """ Convert from digital value to MIDI value :param display_value: int The digital value :return: int The MIDI value """ # Handle special bipolar cases first if self == VocalFXParam.PAN: return display_value + 64 # -64..+63 -> 0..127 (center 64) elif self == VocalFXParam.AUTO_PITCH_GENDER: return display_value + 10 # -10..+10 -> 0..20 (center 10) elif self == VocalFXParam.AUTO_PITCH_OCTAVE: return display_value + 1 # -1..+1 -> 0..2 (center 1) # For parameters with simple linear scaling if hasattr(self, "display_min") and hasattr(self, "display_max"): return int( self.min_val + (display_value - self.display_min) * (self.max_val - self.min_val) / (self.display_max - self.display_min) ) return display_value
[docs] def convert_from_midi(self, midi_value: int) -> int: """ Convert from MIDI value to digital value :param midi_value: int The MIDI value :return: int The digital value """ # Handle special bipolar cases first if self == VocalFXParam.PAN: return midi_value - 64 # 0..127 -> -64..+63 elif self == VocalFXParam.AUTO_PITCH_GENDER: return midi_value - 10 # 0..20 -> -10..+10 elif self == VocalFXParam.AUTO_PITCH_OCTAVE: return midi_value - 1 # 0..2 -> -1..+1 # For parameters with simple linear scaling if hasattr(self, "display_min") and hasattr(self, "display_max"): return int( self.display_min + (midi_value - self.min_val) * (self.display_max - self.display_min) / (self.max_val - self.min_val) ) return midi_value
@staticmethod
[docs] def get_display_value_by_name(param_name: str, value: int) -> int: """ Get the digital value for address parameter by name and value. :param param_name: str The parameter name :param value: int The value :return: int The digital value """ param = VocalFXParam.get_by_name(param_name) if param: return param.convert_from_midi(value) return value
@staticmethod
[docs] def get_midi_range(param_name: str) -> Tuple[int, int]: """ Get the MIDI value range (min, max) of address parameter by name. :param param_name: str The parameter name :return: Tuple[int, int] The MIDI value range """ param = VocalFXParam.get_by_name(param_name) if param: return param.min_value, param.max_value
@staticmethod
[docs] def get_midi_value(param_name: str, value: int) -> Optional[int]: """ Get the MIDI value for address parameter by name and value. :param param_name: str The parameter name :param value: int The value :return: Optional[int] The MIDI value """ param = VocalFXParam.get_by_name(param_name) if param: return param.convert_to_midi(value) return None