Source code for jdxi_editor.ui.editors.digital.partial.pwm
"""
PWM Widget
==========
This widget provides a user interface for controlling Pulse Width Modulation (PWM) parameters,
with a graphical plot to visualize the modulation envelope.
It includes controls for pulse width and modulation depth,
and can communicate with MIDI devices.
"""
from typing import Callable, Optional
from decologr import Decologr as log
from picomidi.constant import Midi
from picomidi.sysex.parameter.address import AddressParameter
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QGridLayout, QSlider, QWidget
from jdxi_editor.midi.data.address.address import RolandSysExAddress
from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.midi.utils.conversions import midi_value_to_ms, ms_to_midi_value
from jdxi_editor.ui.widgets.envelope.base import TOOLTIPS, EnvelopeWidgetBase
from jdxi_editor.ui.widgets.pitch.pwm_plot import PWMPlot
from jdxi_editor.ui.widgets.pulse_width.slider_spinbox import PWMSliderSpinbox
from jdxi_editor.ui.windows.jdxi.dimensions import JDXiDimensions
[docs]
class PWMWidget(EnvelopeWidgetBase):
def __init__(
self,
pulse_width_param: AddressParameter,
mod_depth_param: AddressParameter,
midi_helper: Optional[MidiIOHelper] = None,
controls: dict[AddressParameter, QWidget] = None,
address: Optional[RolandSysExAddress] = None,
create_parameter_slider: Callable = None,
parent: Optional[QWidget] = None,
):
super().__init__(
envelope_keys=["pulse_width", "mod_depth"],
create_parameter_slider=create_parameter_slider,
parameters=[pulse_width_param, mod_depth_param],
midi_helper=midi_helper,
address=address,
controls=controls,
parent=parent,
)
self.setWindowTitle("PWM Widget")
if controls:
self.controls = controls
else:
self.controls = {}
[docs]
self.pulse_width_control = PWMSliderSpinbox(
pulse_width_param,
min_value=0,
max_value=Midi.VALUE.MAX.SEVEN_BIT,
units=" %",
label="Width",
value=self.envelope["pulse_width"]
* Midi.VALUE.MAX.SEVEN_BIT, # Convert from 0.0–1.0 to 0–100
create_parameter_slider=self._create_parameter_slider,
parent=self,
)
[docs]
self.mod_depth_control = PWMSliderSpinbox(
mod_depth_param,
min_value=0,
max_value=Midi.VALUE.MAX.SEVEN_BIT,
units=" %",
label="Mod Depth",
value=self.envelope["mod_depth"]
* Midi.VALUE.MAX.SEVEN_BIT, # Convert from 0.0–1.0 to 0–100
create_parameter_slider=self._create_parameter_slider,
parent=self,
)
self.controls[pulse_width_param] = self.pulse_width_control
self.controls[mod_depth_param] = self.mod_depth_control
for key, widget in [
("pulse_width", self.pulse_width_control),
("mod_depth", self.mod_depth_control),
]:
if tooltip := TOOLTIPS.get(key):
widget.setToolTip(tooltip)
self.layout.setColumnStretch(0, 1) # left side stretches
self.layout.addWidget(self.mod_depth_control, 0, 1)
self.layout.addWidget(self.pulse_width_control, 0, 2)
self.setLayout(self.layout)
self.plot = PWMPlot(
width=JDXiDimensions.PWM_WIDGET_WIDTH - 20,
height=JDXiDimensions.PWM_WIDGET_HEIGHT - 20,
parent=self,
envelope=self.envelope,
)
self.layout.addWidget(self.plot, 0, 3)
self.layout.setColumnStretch(4, 1) # right side stretches
self.pulse_width_control.slider.valueChanged.connect(
self.on_pulse_width_changed
)
self.mod_depth_control.slider.valueChanged.connect(self.on_mod_depth_changed)
self.pulse_width_control.setValue(
self.envelope["pulse_width"] * Midi.VALUE.MAX.SEVEN_BIT
)
self.mod_depth_control.setValue(
self.envelope["mod_depth"] * Midi.VALUE.MAX.SEVEN_BIT
)
[docs]
def on_envelope_changed(self, envelope: dict) -> None:
"""
Handle envelope changes from controls
:param envelope: dict
:return: None
"""
self.envelope = envelope
print(f"Envelope changed: {self.envelope}")
self.update() # Trigger repaint if needed
[docs]
def on_pulse_width_changed(self, val: int) -> None:
"""
Handle pulse width changes from slider
:param val: int
:return: None
"""
self.envelope["pulse_width"] = (
val / Midi.VALUE.MAX.SEVEN_BIT
) # Convert from 0–100 to 0.0–1.0
self.update() # Trigger repaint if needed
[docs]
def on_mod_depth_changed(self, val: int) -> None:
"""
Handle modulation depth changes from slider
:param val: int
:return: None
"""
self.envelope["mod_depth"] = (
val / Midi.VALUE.MAX.SEVEN_BIT
) # Convert from 0–100 to 0.0–1.0
self.update() # Trigger repaint if needed
[docs]
def update_envelope_from_slider(self, slider: QSlider) -> None:
"""Update envelope with value from a single slider"""
for param, ctrl in self.controls.items():
if ctrl is slider:
envelope_param_type = param.get_envelope_param_type()
if envelope_param_type == "mod_depth":
self.envelope["mod_depth"] = (
slider.value() / Midi.VALUE.MAX.SEVEN_BIT
)
elif envelope_param_type == "pulse_width":
self.envelope["pulse_width"] = (
slider.value() / Midi.VALUE.MAX.SEVEN_BIT
)
else:
pass
break
[docs]
def update_envelope_from_controls(self) -> None:
"""Update envelope values from slider controls"""
try:
for param, slider in self.controls.items():
envelope_param_type = param.get_envelope_param_type()
log.message(f"envelope_param_type = {envelope_param_type}")
if envelope_param_type == "mod_depth":
self.envelope["mod_depth"] = (
slider.STATUS() / Midi.VALUE.MAX.SEVEN_BIT
)
if envelope_param_type == "pulse_width":
self.envelope["pulse_width"] = (
slider.STATUS() / Midi.VALUE.MAX.SEVEN_BIT
)
else:
self.envelope[envelope_param_type] = midi_value_to_ms(
slider.STATUS()
)
log.message(f"{self.envelope}")
except Exception as ex:
log.error(f"Error updating envelope from controls: {ex}")
self.plot.set_values(self.envelope)
[docs]
def update_controls_from_envelope(self) -> None:
"""Update slider controls from envelope values."""
try:
for param, slider in self.controls.items():
envelope_param_type = param.get_envelope_param_type()
if envelope_param_type == "mod_depth":
slider.setValue(
int(self.envelope["mod_depth"] * Midi.VALUE.MAX.SEVEN_BIT)
)
if envelope_param_type == "pulse_width":
slider.setValue(
int(self.envelope["pulse_width"] * Midi.VALUE.MAX.SEVEN_BIT)
)
else:
slider.setValue(
int(ms_to_midi_value(self.envelope[envelope_param_type]))
)
except Exception as ex:
log.error(f"Error updating controls from envelope: {ex}")
self.plot.set_values(self.envelope)