Source code for jdxi_editor.ui.editors.effects.vocal

"""
VocalFXEditor Module

This module defines the `VocalFXEditor` class, a PySide6-based editor for controlling
the Vocal FX section of the Roland JD-Xi synthesizer. It provides a graphical interface
for adjusting various vocal effects such as vocoder settings, auto-pitch parameters,
and mixer controls.

Features:
- Scrollable UI with multiple tabs for organizing vocal effect settings.
- Support for vocoder controls, including envelope, mic sensitivity, and synthesis levels.
- Auto-pitch settings with selectable pitch type, scale, key, and gender adjustment.
- Mixer section for controlling levels, panning, reverb, and delay send levels.
- MIDI integration for real-time parameter control using `MIDIHelper`.
- Dynamic instrument image loading to visually represent the effect in use.

Dependencies:
- PySide6 for UI components.
- `MIDIHelper` for sending MIDI messages to the JD-Xi.
- `VocalFXParameter` for managing effect-specific MIDI parameters.

"""

from typing import Dict, Optional

from picomidi.sysex.parameter.address import AddressParameter
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QScrollArea,
    QTabWidget,
    QVBoxLayout,
    QWidget,
)

from jdxi_editor.jdxi.preset.helper import JDXiPresetHelper
from jdxi_editor.jdxi.style import JDXiStyle, JDXiThemeManager
from jdxi_editor.midi.data.address.address import (
    ZERO_BYTE,
    AddressOffsetProgramLMB,
    AddressOffsetTemporaryToneUMB,
    AddressStartMSB,
    RolandSysExAddress,
)
from jdxi_editor.midi.data.parameter.program.common import ProgramCommonParam
from jdxi_editor.midi.data.parameter.vocal_fx import VocalFXParam
from jdxi_editor.midi.data.vocal_effects.vocal import (
    VocalAutoPitchKey,
    VocalAutoPitchNote,
    VocalAutoPitchType,
    VocalFxSwitch,
    VocalOctaveRange,
    VocalOutputAssign,
    VocoderEnvelope,
    VocoderHPF,
)
from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.ui.editors.synth.simple import BasicEditor
from jdxi_editor.ui.widgets.display.digital import DigitalTitle


[docs] class VocalFXEditor(BasicEditor): """Vocal Effects Window Class""" def __init__( self, midi_helper: Optional[MidiIOHelper] = None, preset_helper: JDXiPresetHelper = None, parent: Optional[QWidget] = None, ): super().__init__(midi_helper=midi_helper, parent=parent) self.setWindowTitle("Vocal FX")
[docs] self.preset_helper = preset_helper
[docs] self.address = RolandSysExAddress( AddressStartMSB.TEMPORARY_PROGRAM, AddressOffsetTemporaryToneUMB.COMMON, AddressOffsetProgramLMB.VOCAL_EFFECT, ZERO_BYTE, )
from jdxi_editor.jdxi.style.theme_manager import JDXiThemeManager JDXiThemeManager.apply_editor_style(self) JDXiThemeManager.apply_tabs_style(self) # Main layout main_layout = QVBoxLayout() self.setLayout(main_layout)
[docs] self.controls: Dict[AddressParameter, QWidget] = {}
# Create scroll area scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) # Create container widget for scroll area container = QWidget() container_hlayout = QHBoxLayout() container.setLayout(container_hlayout) container_layout = QVBoxLayout() container_hlayout.addStretch() container_hlayout.addLayout(container_layout) container_hlayout.addStretch() # self.title_label = QLabel("Vocal Effects")
[docs] self.title_label = DigitalTitle()
self.title_label.setText("Vocal Effects") from jdxi_editor.jdxi.style.theme_manager import JDXiThemeManager JDXiThemeManager.apply_instrument_title_label(self.title_label) title_layout = QHBoxLayout() title_layout.addWidget(self.title_label) main_rows_hlayout = QHBoxLayout() main_layout.addLayout(main_rows_hlayout) container_layout.addLayout(main_rows_hlayout) # Image display
[docs] self.image_label = QLabel()
self.image_label.setAlignment( Qt.AlignmentFlag.AlignCenter ) # Center align the image
[docs] self.default_image = "vocal_fx.png"
[docs] self.instrument_icon_folder = "vocal_fx"
container_layout.addWidget(self.image_label) self.update_instrument_image() ### title_group_box = QGroupBox() title_group_layout = QHBoxLayout() title_group_box.setLayout(title_group_layout) title_group_layout.addWidget(self.title_label) title_group_layout.addWidget(self.image_label) main_row_hlayout = QHBoxLayout() main_rows_hlayout.addLayout(main_row_hlayout) main_row_hlayout.addStretch() rows_layout = QVBoxLayout() main_row_hlayout.addLayout(rows_layout) rows_layout.addWidget(title_group_box) main_rows_hlayout.addLayout(rows_layout) main_row_hlayout.addStretch() ###
[docs] self.tab_widget = QTabWidget()
self.tab_widget.addTab(self._create_common_section(), "Common") self.tab_widget.addTab(self._create_vocal_effect_section(), "Vocal FX") self.tab_widget.addTab(self._create_mixer_section(), "Mixer") self.tab_widget.addTab(self._create_auto_pitch_section(), "Auto Pitch") # Add sections to container container_layout.addWidget(self.tab_widget) # Add container to scroll area scroll.setWidget(container) main_layout.addWidget(scroll)
[docs] def _create_common_section(self) -> QWidget: """ _create_common_section :return: QWidget """ common_section = QWidget() layout = QVBoxLayout() common_section.setLayout(layout) self.program_tempo = self._create_parameter_slider( ProgramCommonParam.PROGRAM_TEMPO, "Tempo" ) layout.addWidget(self.program_tempo) vocal_effect_switch_row = QHBoxLayout() self.vocal_effect_type = self._create_parameter_combo_box( ProgramCommonParam.VOCAL_EFFECT, "Vocal Effect", ["OFF", "VOCODER", "AUTO - PITCH"], [0, 1, 2], ) vocal_effect_switch_row.addWidget(self.vocal_effect_type) layout.addLayout(vocal_effect_switch_row) self.vocal_effect_number = self._create_parameter_slider( ProgramCommonParam.VOCAL_EFFECT_NUMBER, "Effect Number" ) layout.addWidget(self.vocal_effect_number) self.program_level = self._create_parameter_slider( ProgramCommonParam.PROGRAM_LEVEL, "Level" ) layout.addWidget(self.program_level) # Add Effect Part switch effect_part_switch_row = QHBoxLayout() self.effect_part_switch = self._create_parameter_switch( VocalFXParam.VOCODER_SWITCH, "Effect Part:", ["OFF", "ON"] ) effect_part_switch_row.addWidget(self.effect_part_switch) layout.addLayout(effect_part_switch_row) # Add at bottom # Add Auto Note switch auto_note_switch_row = QHBoxLayout() self.auto_note_switch = self._create_parameter_switch( ProgramCommonParam.AUTO_NOTE_SWITCH, "Auto Note:", ["OFF", "ON"] ) auto_note_switch_row.addWidget(self.auto_note_switch) layout.addLayout(auto_note_switch_row) # Add at bottom return common_section
[docs] def _create_vocal_effect_section(self) -> QWidget: """Create general vocal effect controls section""" vocal_effect_section = QWidget() layout = QVBoxLayout() vocal_effect_section.setLayout(layout) # Add vocoder switch switch_row = QHBoxLayout() self.vocoder_switch = self._create_parameter_switch( VocalFXParam.VOCODER_SWITCH, "Vocoder:", ["OFF", "ON"] ) switch_row.addWidget(self.vocoder_switch) layout.addLayout(switch_row) # Add at top # Add Vocoder controls vocoder_group = QGroupBox("Vocoder Settings") vocoder_layout = QVBoxLayout() vocoder_group.setLayout(vocoder_layout) # Envelope Type env_row = QHBoxLayout() env_row.addWidget(QLabel("Envelope")) self.vocoder_env = self._create_parameter_combo_box( VocalFXParam.VOCODER_ENVELOPE, "Envelope", [env.display_name for env in VocoderEnvelope], [env.value for env in VocoderEnvelope], ) env_row.addWidget(self.vocoder_env) vocoder_layout.addLayout(env_row) # Level controls levels_row_layout = QHBoxLayout() self.vocoder_level = self._create_parameter_slider( VocalFXParam.VOCODER_LEVEL, "Level", 1 ) self.vocoder_mic_sens = self._create_parameter_slider( VocalFXParam.VOCODER_MIC_SENS, "Mic Sensitivity", 1 ) self.vocoder_synth_level = self._create_parameter_slider( VocalFXParam.VOCODER_SYNTH_LEVEL, "Synth Level", 1 ) self.vocoder_mic_mix = self._create_parameter_slider( VocalFXParam.VOCODER_MIC_MIX, "Mic Mix", 1 ) self.vocoder_hpf = self._create_parameter_combo_box( VocalFXParam.VOCODER_MIC_HPF, "HPF", [freq.display_name for freq in VocoderHPF], [freq.value for freq in VocoderHPF], ) # HPF Frequency hpf_row = QHBoxLayout() hpf_row.addWidget(self.vocoder_hpf) # Add all controls levels_row_layout.addWidget(self.vocoder_level) levels_row_layout.addWidget(self.vocoder_mic_sens) levels_row_layout.addWidget(self.vocoder_synth_level) levels_row_layout.addWidget(self.vocoder_mic_mix) vocoder_layout.addLayout(levels_row_layout) vocoder_layout.addLayout(hpf_row) layout.addWidget(vocoder_group) JDXiThemeManager.apply_adsr_style(vocoder_group) return vocal_effect_section
[docs] def _create_mixer_section(self) -> QWidget: """ _create_mixer_section :return: QWidget """ mixer_section = QWidget() layout = QVBoxLayout() mixer_section.setLayout(layout) # Level and Pan self.level = self._create_parameter_slider( VocalFXParam.LEVEL, "Level", ) self.pan = self._create_parameter_slider(VocalFXParam.PAN, "Pan") # Center at 0 # Send Levels self.delay_send_level_slider = self._create_parameter_slider( VocalFXParam.DELAY_SEND_LEVEL, "Delay Send" ) self.reverb_send_level_slider = self._create_parameter_slider( VocalFXParam.REVERB_SEND_LEVEL, "Reverb Send" ) # Output Assign output_row = QHBoxLayout() output_row.addWidget(QLabel("Output")) self.output_assign = self._create_parameter_combo_box( VocalFXParam.OUTPUT_ASSIGN, "Output", [output.display_name for output in VocalOutputAssign], [output.value for output in VocalOutputAssign], ) output_row.addWidget(self.output_assign) layout.addLayout(output_row) layout.addWidget(self.level) layout.addWidget(self.pan) layout.addWidget(self.delay_send_level_slider) layout.addWidget(self.reverb_send_level_slider) return mixer_section
[docs] def _create_auto_pitch_section(self): """ _create_auto_pitch_section :return: QWidget """ auto_pitch_section = QWidget() self.auto_pitch_group = auto_pitch_section # Store reference layout = QVBoxLayout() auto_pitch_section.setLayout(layout) self.pitch_switch = self._create_parameter_switch( VocalFXParam.AUTO_PITCH_SWITCH, "Auto Pitch", [switch.display_name for switch in VocalFxSwitch], ) # Type selector type_row = QHBoxLayout() self.auto_pitch_type = self._create_parameter_combo_box( VocalFXParam.AUTO_PITCH_TYPE, "Pitch Type", [pitch_type.display_name for pitch_type in VocalAutoPitchType], [pitch_type.value for pitch_type in VocalAutoPitchType], ) type_row.addWidget(self.auto_pitch_type) # Scale selector scale_row = QHBoxLayout() self.pitch_scale = self._create_parameter_combo_box( VocalFXParam.AUTO_PITCH_SCALE, "Scale", ["CHROMATIC", "Maj(Min)"], [0, 1], ) scale_row.addWidget(self.pitch_scale) # Key selector key_row = QHBoxLayout() self.pitch_key = self._create_parameter_combo_box( VocalFXParam.AUTO_PITCH_KEY, "Key", [key.display_name for key in VocalAutoPitchKey], [key.value for key in VocalAutoPitchKey], ) key_row.addWidget(self.pitch_key) # Note selector note_row = QHBoxLayout() self.pitch_note = self._create_parameter_combo_box( VocalFXParam.AUTO_PITCH_NOTE, "Note", [note.display_name for note in VocalAutoPitchNote], [note.value for note in VocalAutoPitchNote], ) note_row.addWidget(self.pitch_note) # Gender and Octave controls self.gender = self._create_parameter_slider( VocalFXParam.AUTO_PITCH_GENDER, "Gender" ) self.octave = self._create_parameter_switch( VocalFXParam.AUTO_PITCH_OCTAVE, "Octave", [range.name for range in VocalOctaveRange], ) # Dry/Wet Balance self.auto_pitch_balance = self._create_parameter_slider( VocalFXParam.AUTO_PITCH_BALANCE, "D/W Balance" ) # Add all controls to layout layout.addWidget(self.pitch_switch) layout.addLayout(type_row) layout.addLayout(scale_row) layout.addLayout(key_row) layout.addLayout(note_row) layout.addWidget(self.gender) layout.addWidget(self.octave) layout.addWidget(self.auto_pitch_balance) return auto_pitch_section