Source code for jdxi_editor.ui.editors.program.mixer.section

"""
Program Mixer Widget Module

This module defines the `ProgramMixerWidget` class, a widget for managing
mixer level controls for all synthesizer parts (Master, Digital 1/2, Drums, Analog).

Classes:
    ProgramMixerWidget(SynthBase)
        A widget for displaying and controlling mixer levels.
"""

from dataclasses import dataclass
from typing import Dict, Optional

from decologr import Decologr as log
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QGridLayout, QGroupBox, QLabel, QSlider, QWidget

from jdxi_editor.core.jdxi import JDXi
from jdxi_editor.core.synth.factory import create_synth_data
from jdxi_editor.core.synth.type import JDXiSynth
from jdxi_editor.midi.data.address.address import JDXiSysExAddress
from jdxi_editor.midi.data.address.program import ProgramCommonAddress
from jdxi_editor.midi.data.parameter.analog.address import AnalogParam
from jdxi_editor.midi.data.parameter.digital import DigitalCommonParam
from jdxi_editor.midi.data.parameter.drum.common import DrumCommonParam
from jdxi_editor.midi.data.parameter.program.common import ProgramCommonParam
from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.ui.editors.program.mixer.widgets import LabelWidgetRegistry
from jdxi_editor.ui.editors.program.track import MixerTrack, MixerTrackEntity
from jdxi_editor.ui.editors.synth.base import SynthBase
from jdxi_editor.ui.widgets.editor.helper import create_group_with_layout
from picomidi.sysex.parameter.address import AddressParameter


@dataclass
[docs] class TrackSpec: """Track Spec"""
[docs] label: str = ""
[docs] synth: str = ""
[docs] icon: QIcon = ""
[docs] param: AddressParameter = None
@dataclass
[docs] class MixerTrackSpec: """Track specs for the Mixer"""
[docs] master: TrackSpec = None
[docs] digital1: TrackSpec = None
[docs] digital2: TrackSpec = None
[docs] drums: TrackSpec = None
[docs] analog: TrackSpec = None
[docs] class MixerAttrs: """Mixer Attributes"""
[docs] MASTER = "master_level_slider"
[docs] ANALOG = "analog_level_slider"
[docs] DRUMS = "drums_level_slider"
[docs] DIGITAL1 = "digital1_level_slider"
[docs] DIGITAL2 = "digital2_level_slider"
[docs] class ProgramMixer(SynthBase): """Widget for managing mixer level controls.""" def __init__( self, midi_helper: Optional[MidiIOHelper] = None, parent: Optional[QWidget] = None, ): """ Initialize the ProgramMixerWidget. :param midi_helper: Optional[MidiIOHelper] for MIDI communication :param parent: Optional[QWidget] parent widget """ super().__init__(midi_helper=midi_helper, parent=parent)
[docs] self.mixer_group: Optional[QGroupBox] = None
[docs] self.mixer_layout: Optional[QGridLayout] = None
# Labels for displaying current program/synth names
[docs] self.label_widgets = LabelWidgetRegistry()
# Sliders
[docs] self.controls: dict[AddressParameter, QWidget] = {}
[docs] self.master_level_slider: QWidget | None = None
[docs] self.digital1_level_slider: QWidget | None = None
[docs] self.digital2_level_slider: QWidget | None = None
[docs] self.drums_level_slider: QWidget | None = None
[docs] self.analog_level_slider: QWidget | None = None
# Track addresses (for mute functionality)
[docs] self.master_level_address: Optional[ProgramCommonAddress] = None
[docs] self.digital1_level_address: Optional[JDXiSysExAddress] = None
[docs] self.digital2_level_address: Optional[JDXiSysExAddress] = None
[docs] self.drums_level_address: Optional[JDXiSysExAddress] = None
[docs] self.analog_level_address: Optional[JDXiSysExAddress] = None
[docs] self.mixer_spec = self.build_mixer_spec()
[docs] def _make_track( self, entity: MixerTrackEntity, param: AddressParameter, synth_type: str | None, label_text: str, icon: QIcon, address: JDXiSysExAddress | ProgramCommonAddress, ) -> MixerTrack: slider = self._create_parameter_slider( param=param, label=label_text, vertical=True, address=address, ) if synth_type == JDXiSynth.ANALOG_SYNTH: analog = True else: analog = False # Track name (static) name_label = QLabel(label_text) JDXi.UI.Theme.apply_mixer_label(name_label, analog=analog) # Value label (dynamic) value_label = QLabel("---") JDXi.UI.Theme.apply_mixer_label(value_label, analog=analog) if synth_type: self.label_widgets.register(synth_type, value_label) # Icon icon_label = QLabel() icon_label.setAlignment(Qt.AlignHCenter) icon_label.setPixmap(icon.pixmap(32, 32)) return MixerTrack( entity=entity, slider=slider, value_label=value_label, icon=icon_label, label=name_label, param=param, address=address, send_midi_callback=self.send_midi_parameter, analog=analog, )
[docs] def _build_tracks(self): """build tracks""" pc_addr = ProgramCommonAddress() self.tracks = [ self._make_track( MixerTrackEntity.MASTER, self.mixer_spec.master.param, self.mixer_spec.master.synth, self.mixer_spec.master.label, self.mixer_spec.master.icon, pc_addr, ), self._track_from_synth_from_spec(self.mixer_spec.digital1), self._track_from_synth_from_spec(self.mixer_spec.digital2), self._track_from_synth_from_spec(self.mixer_spec.drums), self._track_from_synth_from_spec(self.mixer_spec.analog), ] # Assign level sliders so the program editor can pass them to _update_slider for # incoming MIDI (self.controls has only one TONE_LEVEL key, so Digital 1 would # otherwise get Digital 2’s slider when using controls.get(param)) self.controls[self.mixer_spec.master.param] = self.master_level_slider = ( self.tracks[0].slider ) self.controls[self.mixer_spec.digital1.param] = self.digital1_level_slider = ( self.tracks[1].slider ) self.controls[self.mixer_spec.digital2.param] = self.digital2_level_slider = ( self.tracks[2].slider ) self.controls[self.mixer_spec.drums.param] = self.drums_level_slider = ( self.tracks[3].slider ) self.controls[self.mixer_spec.analog.param] = self.analog_level_slider = ( self.tracks[4].slider ) # index 4 = Analog; same pattern as others
[docs] def build_mixer_spec(self) -> MixerTrackSpec: """build mixer track specs""" master_track_spec = TrackSpec( label="Master", synth=JDXiSynth.MASTER, icon=JDXi.UI.Icon.get_icon(JDXi.UI.Icon.KEYBOARD), param=ProgramCommonParam.PROGRAM_LEVEL, ) digital1_track_spec = TrackSpec( label="Digital 1", synth=JDXiSynth.DIGITAL_SYNTH_1, icon=JDXi.UI.Icon.get_icon(JDXi.UI.Icon.KEYBOARD), param=DigitalCommonParam.TONE_LEVEL, ) digital2_track_spec = TrackSpec( label="Digital 2", synth=JDXiSynth.DIGITAL_SYNTH_2, icon=JDXi.UI.Icon.get_icon(JDXi.UI.Icon.KEYBOARD), param=DigitalCommonParam.TONE_LEVEL, ) drums_track_spec = TrackSpec( label="Drums", synth=JDXiSynth.DRUM_KIT, icon=JDXi.UI.Icon.get_icon(JDXi.UI.Icon.DRUM), param=DrumCommonParam.KIT_LEVEL, ) analog_track_spec = TrackSpec( label="Analog", synth=JDXiSynth.ANALOG_SYNTH, icon=JDXi.UI.Icon.get_icon(JDXi.UI.Icon.KEYBOARD), param=AnalogParam.AMP_LEVEL, ) mixer = MixerTrackSpec( master=master_track_spec, digital1=digital1_track_spec, digital2=digital2_track_spec, drums=drums_track_spec, analog=analog_track_spec, ) return mixer
[docs] def _track_from_synth_from_spec(self, spec: TrackSpec) -> MixerTrack: """Make a track from a tack spec""" return self._track_for_synth(spec.synth, spec.label, spec.param)
[docs] def _track_for_synth(self, synth: str, name: str, param: AddressParameter): """track for synth""" synth_data = create_synth_data(synth) return self._make_track( MixerTrackEntity.from_synth(synth), param, synth, name, JDXi.UI.Icon.icon_for_synth(synth), synth_data.address, )
[docs] def _populate_layout(self) -> None: if not self.mixer_layout: return self.mixer_layout.setColumnStretch(0, 1) self.mixer_layout.setColumnStretch(len(self.tracks) + 1, 1) for col, track in enumerate(self.tracks, start=1): strip = track.build_strip() self.mixer_layout.addWidget( strip, 0, col, 1, 1, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, ) JDXi.UI.Theme.apply_adsr_style(track.slider, track.analog) self.mixer_layout.setRowStretch(0, 1)
[docs] def create_mixer_widget(self) -> QGroupBox: """ Create and return the mixer group widget with all controls. :return: QGroupBox containing the mixer controls """ # Create mixer layout and group self.mixer_layout = QGridLayout() self.mixer_group, _ = create_group_with_layout( label="Mixer Level Settings", layout=self.mixer_layout ) # Build Tracks self._build_tracks() # Populate layout self._populate_layout() # Apply styling JDXi.UI.Theme.apply_adsr_style(widget=self.mixer_group) if self.analog_level_slider: JDXi.UI.Theme.apply_adsr_style(widget=self.analog_level_slider, analog=True) return self.mixer_group
[docs] def _init_synth_data( self, synth_type: str = JDXiSynth.DIGITAL_SYNTH_1, partial_number: Optional[int] = 0, ) -> None: """ Initialize synth-specific data for slider creation. :param synth_type: JDXiSynth synth type :param partial_number: int partial number (default 0) """ synth_data = create_synth_data(synth_type, partial_number=partial_number) self.address = synth_data.address
[docs] def update_tone_name_for_synth(self, tone_name: str, synth_type: str) -> None: """Update tone name for synth""" log.message(f"Update tone name triggered: {tone_name} {synth_type}") label = self.label_widgets.get(synth_type) if label: label.setText(tone_name) else: log.warning( f"synth type: {synth_type} not registered in mixer", scope=self.__class__.__name__, )
[docs] def update_program_name(self, program_name: str) -> None: """ Update the master level label with the current program name. :param program_name: str program name to digital """ master_level = self.label_widgets.get(JDXiSynth.MASTER) if master_level: master_level.setText(program_name or "Current Program")
[docs] def get_controls(self) -> Dict[AddressParameter, QWidget]: """ Get the controls dictionary for access by parent. :return: Dict[AddressParameter, QWidget] controls dictionary """ return self.controls