Source code for jdxi_editor.ui.editors.digital.partial.editor

"""
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 typing import Dict, Optional, Union

import qtawesome as qta
from decologr import Decologr as log
from PySide6.QtWidgets import (
    QTabWidget,
    QVBoxLayout,
    QWidget,
)

from jdxi_editor.jdxi.style import JDXiStyle
from jdxi_editor.jdxi.synth.type import JDXiSynth
from jdxi_editor.midi.data.address.address import AddressOffsetSuperNATURALLMB
from jdxi_editor.midi.data.digital.oscillator import DigitalOscWave
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.io.helper import MidiIOHelper
from jdxi_editor.ui.editors.digital.partial.amp import DigitalAmpSection
from jdxi_editor.ui.editors.digital.partial.filter import DigitalFilterSection
from jdxi_editor.ui.editors.digital.partial.lfo import DigitalLFOSection
from jdxi_editor.ui.editors.digital.partial.mod_lfo import DigitalModLFOSection
from jdxi_editor.ui.editors.digital.partial.oscillator import DigitalOscillatorSection
from jdxi_editor.ui.editors.synth.partial import PartialEditor


[docs] class DigitalPartialEditor(PartialEditor): """Editor for address single partial""" def __init__( self, midi_helper: Optional[MidiIOHelper] = None, synth_number: int = 1, partial_number: int = 1, preset_type: JDXiSynth = None, parent: Optional[QWidget] = None, ): super().__init__(parent)
[docs] self.filter_mode_switch = None
""" Initialize the DigitalPartialEditor :param midi_helper: MidiIOHelper :param synth_number: int :param partial_number: int :param preset_type: JDXiSynth :param parent: QWidget """
[docs] self.partial_address_default = AddressOffsetSuperNATURALLMB.PARTIAL_1
[docs] self.partial_address_map = { 1: AddressOffsetSuperNATURALLMB.PARTIAL_1, 2: AddressOffsetSuperNATURALLMB.PARTIAL_2, 3: AddressOffsetSuperNATURALLMB.PARTIAL_3, }
[docs] self.bipolar_parameters = [ DigitalPartialParam.OSC_DETUNE, DigitalPartialParam.OSC_PITCH, DigitalPartialParam.OSC_PITCH_ENV_DEPTH, DigitalPartialParam.AMP_PAN, ]
[docs] self.midi_helper = midi_helper
[docs] self.partial_number = partial_number
[docs] self.preset_type = preset_type
if synth_number == 1: self._init_synth_data( synth_type=JDXiSynth.DIGITAL_SYNTH_1, partial_number=self.partial_number ) elif synth_number == 2: self._init_synth_data( synth_type=JDXiSynth.DIGITAL_SYNTH_2, partial_number=self.partial_number ) """elif synth_number == 3: self._init_synth_data(synth_type=JDXiSynth.DIGITAL_SYNTH_3, partial_number=self.partial_number)""" else: raise ValueError(f"Invalid synth_number: {synth_number}. Must be 1 or 2.") log.parameter("Initializing partial:", self.synth_data.address) if 0 <= partial_number < len(DIGITAL_PARTIAL_NAMES): self.part_name = DIGITAL_PARTIAL_NAMES[partial_number] log.parameter("Partial name:", self.part_name) else: log.error(f"Invalid partial_num: {partial_number}. Using default value.") self.part_name = "Unknown" # Provide a fallback value # Store parameter controls for easy access
[docs] self.controls: Dict[ Union[DigitalPartialParam, DigitalCommonParam], QWidget, ] = {}
# Main layout main_layout = QVBoxLayout() self.setLayout(main_layout) # Create container widget for the tabs container = QWidget() container_layout = QVBoxLayout() container.setLayout(container_layout)
[docs] self.tab_widget = QTabWidget()
container_layout.addWidget(self.tab_widget) # Add sections in address vertical layout
[docs] self.oscillator_tab = DigitalOscillatorSection( self._create_parameter_slider, self._create_parameter_switch, self._create_parameter_combo_box, self.send_midi_parameter, self.partial_number, self.midi_helper, self.controls, self.address, )
self.tab_widget.addTab( self.oscillator_tab, qta.icon("mdi.triangle-wave", color="#666666"), "Oscillator", )
[docs] self.filter_tab = DigitalFilterSection( self._create_parameter_slider, self._create_parameter_switch, self.partial_number, self.midi_helper, self.controls, self.synth_data.address, )
self.tab_widget.addTab( self.filter_tab, qta.icon("ri.filter-3-fill", color="#666666"), "Filter" )
[docs] self.amp_tab = DigitalAmpSection( self._create_parameter_slider, self.partial_number, self.midi_helper, self.controls, self.synth_data.address, )
self.tab_widget.addTab( self.amp_tab, qta.icon("mdi.amplifier", color="#666666"), "Amp" )
[docs] self.lfo_tab = DigitalLFOSection( self._create_parameter_slider, self._create_parameter_switch, self._create_parameter_combo_box, self.controls, )
self.tab_widget.addTab( self.lfo_tab, qta.icon("mdi.sine-wave", color="#666666"), "LFO" )
[docs] self.mod_lfo_tab = DigitalModLFOSection( self._create_parameter_slider, self._create_parameter_switch, self._on_parameter_changed, self.controls, )
self.tab_widget.addTab( self.mod_lfo_tab, qta.icon("mdi.waveform", color="#666666"), "Mod LFO" ) # Add container to scroll area main_layout.addWidget(container)
[docs] self.updating_from_spinbox = False
log.parameter(f"DigitalPartialEditor initialized for", self)
[docs] def __str__(self): return f"{self.__class__.__name__} {self.preset_type} partial: {self.partial_number}"
[docs] def __repr__(self): return f"{self.__class__.__name__} {self.preset_type} partial: {self.partial_number}"
[docs] def update_filter_controls_state(self, mode: int): """ Update filter controls enabled state based on mode :param mode: int """ enabled = mode != 0 # Enable if not BYPASS for param in [ DigitalPartialParam.FILTER_CUTOFF, DigitalPartialParam.FILTER_RESONANCE, DigitalPartialParam.FILTER_CUTOFF_KEYFOLLOW, DigitalPartialParam.FILTER_ENV_VELOCITY_SENSITIVITY, DigitalPartialParam.FILTER_ENV_DEPTH, DigitalPartialParam.FILTER_SLOPE, ]: if param in self.controls: self.filter_tab.controls[param].setEnabled(enabled) self.filter_tab.filter_adsr_widget.setEnabled(enabled)
[docs] def _on_waveform_selected(self, waveform: DigitalOscWave): """ Handle waveform button clicks :param waveform: DigitalOscWave """ # Reset all buttons to default style for btn in self.oscillator_tab.wave_buttons.values(): btn.setChecked(False) btn.setStyleSheet(JDXiStyle.BUTTON_RECT) # Apply active style to the selected waveform button selected_btn = self.oscillator_tab.wave_buttons.get(waveform) if selected_btn: selected_btn.setChecked(True) selected_btn.setStyleSheet(JDXiStyle.BUTTON_RECT_ACTIVE) # Send MIDI message if not self.send_midi_parameter(DigitalPartialParam.OSC_WAVE, waveform.value): log.warning(f"Failed to set waveform to {waveform.name}")