"""
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.oscillator_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 __str__(self) -> str:
return f"{self.__class__.__name__} {self.preset_type} partial {self.partial_number}"