from typing import Callable
from decologr import Decologr as log
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QDoubleSpinBox, QSpinBox, QVBoxLayout, QWidget
from jdxi_editor.ui.widgets.envelope.parameter import EnvelopeParameter
from picomidi.constant import Midi
from picomidi.sysex.parameter.address import AddressParameter
from picomidi.utils.conversion import midi_value_to_ms, ms_to_midi_value
[docs]
def create_spinbox(min_value: int, max_value: int, suffix: str, value: int) -> QSpinBox:
"""
Create a spinbox with specified range and suffix
:param min_value: int
:param max_value: int
:param suffix: str
:param value: int
:return: QSpinBox
"""
sb = QSpinBox()
sb.setRange(min_value, max_value)
sb.setSuffix(suffix)
sb.setValue(value)
return sb
[docs]
def create_double_spinbox(
min_value: float, max_value: float, step: float, value: int
) -> QDoubleSpinBox:
"""
Create a double spinbox with specified range, step, and initial value.
:param min_value: int
:param max_value: int
:param step: float
:param value: int
:return: QDoubleSpinBox
"""
sb = QDoubleSpinBox()
sb.setRange(min_value, max_value)
sb.setSingleStep(step)
sb.setValue(value)
return sb
[docs]
class PitchEnvSliderSpinbox(QWidget):
"""
Pitch Env Slider and Spinbox widget for Roland JD-Xi
"""
[docs]
envelope_changed = Signal(dict)
[docs]
valueChanged = Signal(
object
) # Emitted when value changes (avoids AttributeError in shared controls)
def __init__(
self,
param: AddressParameter,
min_value: float = 0.0,
max_value: float = 1.0,
units: str = "",
label: str = "",
value: int = None,
create_parameter_slider: Callable = None,
parent: QWidget = None,
show_spinbox: bool = False
):
"""
Initialize the ADSR slider and spinbox widget.
:param param: AddressParameter
:param min_value: int
:param max_value: int
:param units: str
:param label: str
:param value: int
:param create_parameter_slider: Callable
:param parent: QWidget
"""
super().__init__(parent)
[docs]
self.factor = Midi.value.max.SEVEN_BIT
if max_value > 1:
self.factor = max_value
[docs]
self.create_parameter_slider = create_parameter_slider
[docs]
self.slider = self.create_parameter_slider(
param=param,
label=label,
vertical=True,
initial_value=value,
)
param_type = param.get_envelope_param_type()
if param_type in [
EnvelopeParameter.SUSTAIN_LEVEL,
EnvelopeParameter.PEAK_LEVEL,
]:
self.spinbox = create_double_spinbox(
min_value=min_value, max_value=max_value, step=0.01, value=value
)
else:
self.spinbox = create_spinbox(
min_value=int(min_value),
max_value=int(max_value),
suffix=units,
value=value,
)
self.spinbox.setRange(min_value, max_value)
layout = QVBoxLayout()
layout.addWidget(self.slider)
if show_spinbox:
layout.addWidget(self.spinbox)
self.setLayout(layout)
# Connect both ways
self.slider.valueChanged.connect(self._slider_changed)
self.spinbox.valueChanged.connect(self._spinbox_changed)
[docs]
def convert_to_envelope(self, value: float) -> float:
"""
Convert MIDI value to envelope value
:param value: float
:return: float
"""
param_type = self.param.get_envelope_param_type()
if param_type in [
EnvelopeParameter.SUSTAIN_LEVEL,
EnvelopeParameter.PEAK_LEVEL,
EnvelopeParameter.DEPTH,
]:
converted_value = value / Midi.value.max.SEVEN_BIT
elif param_type in [
EnvelopeParameter.ATTACK_TIME,
EnvelopeParameter.DECAY_TIME,
EnvelopeParameter.RELEASE_TIME,
EnvelopeParameter.FADE_LOWER,
EnvelopeParameter.FADE_UPPER,
EnvelopeParameter.RANGE_LOWER,
EnvelopeParameter.DEPTH,
EnvelopeParameter.RANGE_UPPER,
]:
converted_value = midi_value_to_ms(int(value), min_time=10, max_time=5000)
else:
log.error(f"Unknown envelope parameter type {param_type} for {self.param}")
converted_value = 0.0 # or raise an error, depending on design
return converted_value
[docs]
def convert_from_envelope(self, value: float) -> int:
"""
Convert envelope value to MIDI value
:param value: int
:return: int
"""
param_type = self.param.get_envelope_param_type()
if param_type in [
EnvelopeParameter.PEAK_LEVEL,
EnvelopeParameter.SUSTAIN_LEVEL,
EnvelopeParameter.MOD_DEPTH,
EnvelopeParameter.DEPTH,
]:
converted_value = int(value * Midi.value.max.SEVEN_BIT)
elif param_type in [
EnvelopeParameter.ATTACK_TIME,
EnvelopeParameter.DECAY_TIME,
EnvelopeParameter.RELEASE_TIME,
EnvelopeParameter.FADE_LOWER,
EnvelopeParameter.FADE_UPPER,
EnvelopeParameter.RANGE_LOWER,
EnvelopeParameter.RANGE_UPPER,
]:
# Ensure value is a number, not a string
value_num = float(value) if not isinstance(value, (int, float)) else value
converted_value = int(
ms_to_midi_value(value_num, min_time=10, max_time=5000)
)
else:
converted_value = 64
log.message(f"convert_from_envelope: {value} -> {converted_value}")
return converted_value
[docs]
def _slider_changed(self, value: int) -> None:
"""
slider changed
:param value: int slider value
:return: None
"""
self.spinbox.blockSignals(True)
self.spinbox.setValue(int(self.convert_to_envelope(value)))
self.spinbox.blockSignals(False)
self.envelope_changed.emit(
{self.param.get_envelope_param_type(): self.convert_to_envelope(value)}
)
self.valueChanged.emit(self.value())
[docs]
def _spinbox_changed(self, value: float):
"""
Spinbox changed
:param value: float double spinbox value
:return: None
"""
if value is None:
return
# Defensive: make sure we can work with the value
if isinstance(value, float):
try:
value = int(value)
except Exception as ex:
log.error(f"Error {ex} occurred casting float {value} to int")
return
if not isinstance(value, int):
log.error(f"{value} is neither int nor castable float")
return
converted_value = self.convert_from_envelope(value)
if converted_value is None:
log.error(f"convert_from_envelope({value}) returned None")
return
self.slider.blockSignals(True)
self.slider.setValue(int(converted_value))
self.slider.blockSignals(False)
self.envelope_changed.emit({self.param.get_envelope_param_type(): value})
self.valueChanged.emit(self.value())
[docs]
def setValue(self, value: float):
"""
Set the value of the double spinbox and slider
:param value: float
:return: None
"""
self.slider.setValue(value)
self.spinbox.setValue(int(value))
[docs]
def value(self) -> float:
"""
Get the value of the spinbox
:return: int
"""
return self.spinbox.value()
[docs]
def update(self):
"""Update the envelope values and plot"""
super().update()
self.slider.update()
self.spinbox.update()
self.parent.update()