Source code for jdxi_editor.ui.editors.base.panel

"""
Digital Partial Editor Module

This module defines the `DigitalPartialEditor` class, a specialized editor for managing a single
digital partial in a synthesizer. It extends the `PartialEditor` class, providing a structured UI
to control and modify parameters related to oscillators, filters, amplifiers, and modulation sources.

Classes:
    - DigitalPartialEditor: A `QWidget` subclass that allows users to modify digital synthesis
      parameters using a tabbed interface with various control sections.

Features:
    - Supports editing a single partial within a digital synth part.
    - Provides categorized parameter sections: Oscillator, Filter, Amp, LFO, and Mod LFO.
    - Integrates with `MIDIHelper` for real-time MIDI parameter updates.
    - Uses icons for waveform selection, filter controls, and modulation settings.
    - Stores UI controls for easy access and interaction.

Usage:
    ```python
    from PySide6.QtWidgets import QApplication
    from midi_helper import MIDIHelper

    app = QApplication([])
    midi_helper = MIDIHelper()
    editor = DigitalPartialEditor(midi_helper=midi_helper)
    editor.show()
    app.exec()
    ```

Dependencies:
    - PySide6 (for UI components)
    - MIDIHelper (for MIDI communication)
    - DigitalParameter, DigitalCommonParameter (for parameter management)
    - WaveformButton (for waveform selection UI)
    - QIcons generated from waveform base64 data
"""

from decologr import Decologr as log

from jdxi_editor.core.synth.type import JDXiSynth
from jdxi_editor.midi.data.address.address import JDXiSysExOffsetSuperNATURALLMB
from jdxi_editor.midi.data.digital.oscillator import DigitalWaveOsc
from jdxi_editor.midi.data.digital.partial import DIGITAL_PARTIAL_NAMES
from jdxi_editor.midi.data.parameter.digital import DigitalCommonParam
from jdxi_editor.midi.data.parameter.digital.partial import DigitalPartialParam
from jdxi_editor.midi.data.parameter.digital.spec import JDXiMidiDigital as Digital
from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.ui.common import JDXi, QWidget
from jdxi_editor.ui.editors.synth.partial import PartialPanel


[docs] class BasePartialPanel(PartialPanel): """Editor for a single Digital Synth partial"""
[docs] SYNTH_MAP = { 1: JDXi.Synth.DIGITAL_SYNTH_1, 2: JDXi.Synth.DIGITAL_SYNTH_2, }
[docs] PARTIAL_ADDRESS_MAP = { 1: JDXiSysExOffsetSuperNATURALLMB.PARTIAL_1, 2: JDXiSysExOffsetSuperNATURALLMB.PARTIAL_2, 3: JDXiSysExOffsetSuperNATURALLMB.PARTIAL_3, }
[docs] BIPOLAR_PARAMETERS = { Digital.Param.OSC_PITCH_FINE, Digital.Param.OSC_PITCH_COARSE, Digital.Param.OSC_PITCH_ENV_DEPTH, Digital.Param.AMP_PAN, }
def __init__( self, midi_helper: MidiIOHelper | None = None, synth_number: int = 1, partial_number: int = 1, preset_type: JDXiSynth | None = None, parent: QWidget | None = None, ): super().__init__(parent)
[docs] self.lfo_shape_buttons = {}
[docs] self.mod_lfo_shape_buttons = {}
[docs] self.oscillator_tab = None
[docs] self.filter_tab = None
[docs] self.midi_helper = midi_helper
[docs] self.partial_number = partial_number
[docs] self.preset_type = preset_type
[docs] self.controls: dict[DigitalPartialParam | DigitalCommonParam, QWidget] = {}
self._resolve_synth_data(synth_number) self._resolve_partial_name() self._init_state() self._build_ui() log.parameter("[BasePartialPanel] initialized:", self) # ------------------------------------------------------------------ # Initialization helpers # ------------------------------------------------------------------
[docs] def _resolve_synth_data(self, synth_number: int) -> None: """resolve synth data""" try: synth_type = self.SYNTH_MAP[synth_number] except KeyError: raise ValueError( f"Invalid synth_number: {synth_number}. Must be {list(self.SYNTH_MAP)}" ) self._init_synth_data(synth_type=synth_type, partial_number=self.partial_number) log.parameter("Synth address:", self.synth_data.address)
[docs] def _resolve_partial_name(self) -> None: try: self.part_name = DIGITAL_PARTIAL_NAMES[self.partial_number] except IndexError: log.error(f"Invalid partial_number: {self.partial_number}") self.part_name = "Unknown" log.parameter("Partial name:", self.part_name)
[docs] def _init_state(self) -> None: self.updating_from_spinbox = False
@property
[docs] def lfo_depth_controls(self) -> dict: """ Get a dictionary of LFO depth controls filtered from the main controls dictionary. This provides compatibility with the base class's _update_partial_lfo_depth method. :return: dict mapping LFO depth parameters to their control widgets """ lfo_depth_params = { Digital.Param.LFO_PITCH_DEPTH, Digital.Param.LFO_FILTER_DEPTH, Digital.Param.LFO_AMP_DEPTH, Digital.Param.LFO_PAN_DEPTH, Digital.Param.MOD_LFO_PITCH_DEPTH, Digital.Param.MOD_LFO_FILTER_DEPTH, Digital.Param.MOD_LFO_AMP_DEPTH, } return { param: self.controls[param] for param in lfo_depth_params if param in self.controls }
# ------------------------------------------------------------------ # Behavior # ------------------------------------------------------------------
[docs] def update_filter_controls_state(self, mode: int) -> None: """update filter controls state""" enabled = mode != 0 # BYPASS == 0 params = ( Digital.Param.FILTER_CUTOFF, Digital.Param.FILTER_RESONANCE, Digital.Param.FILTER_CUTOFF_KEYFOLLOW, Digital.Param.FILTER_ENV_VELOCITY_SENSITIVITY, Digital.Param.FILTER_ENV_DEPTH, Digital.Param.FILTER_SLOPE, ) for param in params: widget = self.controls.get(param) if widget: widget.setEnabled(enabled) if self.filter_tab.adsr_widget: self.filter_tab.adsr_widget.setEnabled(enabled)
[docs] def _on_waveform_selected(self, waveform: DigitalWaveOsc) -> None: """on waveform selected (harmonised Theme API)""" for btn in self.oscillator_tab.widgets.waveform_buttons.values(): btn.setChecked(False) JDXi.UI.Theme.apply_button_rect(btn, analog=False) selected = self.oscillator_tab.widgets.waveform_buttons.get(waveform) if selected: selected.setChecked(True) JDXi.UI.Theme.apply_button_active(selected, analog=False) if not self.send_midi_parameter(Digital.Param.OSC_WAVEFORM, waveform.value): log.warning(f"Failed to set waveform: {waveform.name}")
# ------------------------------------------------------------------
[docs] def __str__(self) -> str: return f"{self.__class__.__name__} {self.preset_type} partial {self.partial_number}"
[docs] __repr__ = __str__