"""
Digital Oscillator Section for the JDXI Editor
"""
from typing import Callable
from decologr import Decologr as log
from picomidi.sysex.parameter.address import AddressParameter
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QComboBox,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QTabWidget,
QVBoxLayout,
QWidget,
)
from jdxi_editor.jdxi.style import JDXiStyle, JDXiThemeManager
from jdxi_editor.midi.data.address.address import RolandSysExAddress
from jdxi_editor.midi.data.digital.oscillator import DigitalOscWave
from jdxi_editor.midi.data.parameter.digital.partial import DigitalPartialParam
from jdxi_editor.midi.data.pcm.waves import PCM_WAVES_CATEGORIZED
from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.ui.editors.digital.partial.pwm import PWMWidget
from jdxi_editor.ui.image.utils import base64_to_pixmap
from jdxi_editor.ui.image.waveform import generate_waveform_icon
from jdxi_editor.ui.widgets.button.waveform.waveform import WaveformButton
from jdxi_editor.ui.widgets.pitch.envelope import PitchEnvelopeWidget
from jdxi_editor.ui.windows.jdxi.dimensions import JDXiDimensions
[docs]
class DigitalOscillatorSection(QWidget):
"""Digital Oscillator Section for the JDXI Editor"""
def __init__(
self,
create_parameter_slider: Callable,
create_parameter_switch: Callable,
create_parameter_combo_box: Callable,
send_midi_parameter: Callable,
partial_number: int,
midi_helper: MidiIOHelper,
controls: dict[AddressParameter, QWidget],
address: RolandSysExAddress,
):
super().__init__()
[docs]
self.partial_number = partial_number
[docs]
self.midi_helper = midi_helper
[docs]
self.controls = controls
[docs]
self._create_parameter_slider = create_parameter_slider
[docs]
self._create_parameter_switch = create_parameter_switch
[docs]
self._create_parameter_combo_box = create_parameter_combo_box
[docs]
self.send_midi_parameter = send_midi_parameter
self.setup_ui()
log.parameter(f"initialization complete for", self)
[docs]
def setup_ui(self):
"""Setup the oscillator section UI."""
layout = QVBoxLayout()
layout.setContentsMargins(1, 1, 1, 1)
self.setLayout(layout)
from jdxi_editor.jdxi.style.theme_manager import JDXiThemeManager
JDXiThemeManager.apply_adsr_style(self)
# --- Top row: Waveform buttons and variation switch ---
layout.addLayout(self.create_waveform_buttons())
# --- Create tab widget ---
self.oscillator_tab_widget = QTabWidget()
layout.addWidget(self.oscillator_tab_widget)
# --- Tuning and Pitch tab (combines Tuning and Pitch Envelope like Analog) ---
tuning_pitch_widget = self._create_tuning_pitch_widget()
self.oscillator_tab_widget.addTab(tuning_pitch_widget, "Tuning and Pitch")
# --- Pulse Width tab ---
pw_group = self._create_pw_group()
self.oscillator_tab_widget.addTab(pw_group, "Pulse Width")
# --- PCM Wave tab (unique to Digital) ---
pcm_group = self._create_pcm_group()
self.oscillator_tab_widget.addTab(pcm_group, "PCM Wave")
layout.addStretch()
# --- Initialize states ---
self._update_pw_controls_enabled_state(DigitalOscWave.SAW)
self._update_pcm_controls_enabled_state(DigitalOscWave.PCM)
self._update_supersaw_controls_enabled_state(DigitalOscWave.PCM)
[docs]
def _create_tuning_group(self) -> QGroupBox:
"""Create tuning group"""
tuning_group = QGroupBox("Tuning")
tuning_layout = QHBoxLayout()
tuning_layout.addStretch()
tuning_group.setLayout(tuning_layout)
tuning_layout.addWidget(
self._create_parameter_slider(
DigitalPartialParam.OSC_PITCH, "Pitch (1/2 tones)", vertical=True
)
)
tuning_layout.addWidget(
self._create_parameter_slider(
DigitalPartialParam.OSC_DETUNE, "Detune (cents)", vertical=True
)
)
self.super_saw_detune = self._create_parameter_slider(
DigitalPartialParam.SUPER_SAW_DETUNE, "Super-Saw Detune", vertical=True
)
tuning_layout.addWidget(self.super_saw_detune)
tuning_layout.addStretch()
JDXiThemeManager.apply_adsr_style(tuning_group)
return tuning_group
[docs]
def _create_pitch_env_group(self) -> QGroupBox:
"""Create pitch envelope group"""
pitch_env_group = QGroupBox("Pitch Envelope")
pitch_env_layout = QVBoxLayout()
pitch_env_group.setLayout(pitch_env_layout)
# --- Pitch Env Widget ---
self.pitch_env_widget = PitchEnvelopeWidget(
attack_param=DigitalPartialParam.OSC_PITCH_ENV_ATTACK_TIME,
decay_param=DigitalPartialParam.OSC_PITCH_ENV_DECAY_TIME,
depth_param=DigitalPartialParam.OSC_PITCH_ENV_DEPTH,
midi_helper=self.midi_helper,
create_parameter_slider=self._create_parameter_slider,
controls=self.controls,
address=self.address,
)
JDXiThemeManager.apply_adsr_style(self.pitch_env_widget)
pitch_env_layout.addWidget(self.pitch_env_widget)
return pitch_env_group
[docs]
def _create_pw_group(self) -> QGroupBox:
"""Create pulse width group"""
pw_group = QGroupBox("Pulse Width")
pw_layout = QVBoxLayout()
pw_layout.addStretch()
pw_group.setLayout(pw_layout)
self.pw_shift_slider = self._create_parameter_slider(
DigitalPartialParam.OSC_PULSE_WIDTH_SHIFT,
"Shift (range of change)",
vertical=True,
)
JDXiThemeManager.apply_adsr_style(self.pw_shift_slider)
pwm_widget_layout = QHBoxLayout()
pwm_widget_layout.addStretch()
self.pwm_widget = PWMWidget(
pulse_width_param=DigitalPartialParam.OSC_PULSE_WIDTH,
mod_depth_param=DigitalPartialParam.OSC_PULSE_WIDTH_MOD_DEPTH,
midi_helper=self.midi_helper,
address=self.address,
create_parameter_slider=self._create_parameter_slider,
controls=self.controls,
)
JDXiThemeManager.apply_adsr_style(self.pwm_widget)
self.pwm_widget.setMaximumHeight(JDXiStyle.PWM_WIDGET_HEIGHT)
pwm_widget_layout.addWidget(self.pwm_widget)
pwm_widget_layout.addWidget(self.pw_shift_slider)
pwm_widget_layout.addStretch()
pw_layout.addLayout(pwm_widget_layout)
pw_layout.addStretch()
return pw_group
[docs]
def _create_pcm_group(self) -> QGroupBox:
"""Create PCM wave group"""
pcm_group = QGroupBox("PCM Wave")
pcm_layout = QGridLayout()
pcm_group.setLayout(pcm_layout)
self.pcm_wave_gain = self._create_parameter_combo_box(
DigitalPartialParam.PCM_WAVE_GAIN,
"Gain [dB]",
["-6", "0", "+6", "+12"],
)
self.pcm_wave_number = self._create_parameter_combo_box(
DigitalPartialParam.PCM_WAVE_NUMBER,
"Number",
[f"{w['Wave Number']}: {w['Wave Name']}" for w in PCM_WAVES_CATEGORIZED],
)
self.pcm_category_combo = QComboBox()
self.pcm_categories = ["No selection"] + sorted(
set(w["Category"] for w in PCM_WAVES_CATEGORIZED)
)
self.pcm_category_combo.addItems(self.pcm_categories)
self.pcm_category_combo.currentIndexChanged.connect(self.update_waves)
pcm_layout.setColumnStretch(0, 1) # left side stretches
pcm_layout.addWidget(self.pcm_wave_gain, 0, 1)
pcm_layout.addWidget(QLabel("Category"), 0, 2)
pcm_layout.addWidget(self.pcm_category_combo, 0, 3)
pcm_layout.addWidget(self.pcm_wave_number, 0, 4)
pcm_layout.setColumnStretch(5, 1) # right side stretches
return pcm_group
[docs]
def update_waves(self):
"""Update PCM waves based on selected category"""
selected_category = self.pcm_category_combo.currentText()
# --- Filter waves or show all if "No selection" ---
if selected_category == "No selection":
filtered_waves = PCM_WAVES_CATEGORIZED # Show all waves
else:
filtered_waves = [
w for w in PCM_WAVES_CATEGORIZED if w["Category"] == selected_category
]
# --- Update wave combo box ---
self.pcm_wave_number.combo_box.clear()
self.pcm_wave_number.combo_box.addItems(
[f"{w['Wave Number']}: {w['Wave Name']}" for w in filtered_waves]
)
self.pcm_wave_number.values = [w["Wave Number"] for w in filtered_waves]
[docs]
def _update_pw_controls_enabled_state(self, waveform: DigitalOscWave):
"""Update pulse width controls enabled state based on waveform"""
pw_enabled = waveform == DigitalOscWave.PW_SQUARE
self.pwm_widget.setEnabled(pw_enabled)
self.pw_shift_slider.setEnabled(pw_enabled)
[docs]
def _update_pcm_controls_enabled_state(self, waveform: DigitalOscWave):
"""Update PCM wave controls visibility based on waveform"""
pcm_enabled = waveform == DigitalOscWave.PCM
self.pcm_wave_gain.setEnabled(pcm_enabled)
self.pcm_category_combo.setEnabled(pcm_enabled)
self.pcm_wave_number.setEnabled(pcm_enabled)
[docs]
def _update_supersaw_controls_enabled_state(self, waveform: DigitalOscWave):
"""Update supersaw controls visibility based on waveform"""
supersaw_enabled = waveform == DigitalOscWave.SUPER_SAW
self.super_saw_detune.setEnabled(supersaw_enabled)