"""
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._display_name = display_name
[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