Source code for jdxi_editor.ui.widgets.usb.recording

"""
USB Recording Widget
"""

import re
from datetime import datetime
from pathlib import Path
from typing import Optional

import pyaudio
from decologr import Decologr as log
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QCheckBox, QComboBox, QGroupBox, QLabel, QPushButton

from jdxi_editor.core.jdxi import JDXi
from jdxi_editor.midi.playback.state import MidiPlaybackState
from jdxi_editor.midi.utils.helpers import start_recording
from jdxi_editor.midi.utils.usb_recorder import USBRecorder
from jdxi_editor.ui.editors.helpers.widgets import create_jdxi_button_from_spec
from jdxi_editor.ui.editors.midi_player.helper import (
    create_widget_cell_with_button_spec,
)
from jdxi_editor.ui.widgets.editor.helper import (
    create_group_and_grid_layout,
    create_icon_and_label,
)
from jdxi_editor.ui.widgets.jdxi.midi_group import JDXiMidiGroup
from jdxi_editor.ui.windows.jdxi.utils import show_message_box_from_spec
from picoui.specs.widgets import (
    ButtonSpec,
    FileSelectionMode,
    FileSelectionSpec,
    get_file_save_from_spec,
)


[docs] class USBFileRecordingWidget(JDXiMidiGroup): """USB File Recording Widget""" def __init__(self, midi_state: MidiPlaybackState, parent=None): super().__init__(parent=parent, midi_state=midi_state) """constructor"""
[docs] self.group_title: str = "USB Recorder"
[docs] self.recorder: USBRecorder = USBRecorder(channels=1)
[docs] self.file_select: QPushButton = QPushButton()
[docs] self.file_output_name: str = ""
[docs] self.file_record_checkbox: QCheckBox = QCheckBox()
[docs] self.port_refresh_devices_label: QLabel | None = None
[docs] self.file_auto_generate_checkbox: QCheckBox | None = None
[docs] self.port_select_combo: QComboBox = QComboBox()
[docs] self.port_refresh_devices_button: QPushButton = QPushButton()
self.setup_ui()
[docs] def _build_button_specs(self) -> dict[str, ButtonSpec]: return { "usb_port_refresh": ButtonSpec( label="Refresh", tooltip="Refresh list of USB devices", icon=JDXi.UI.Icon.REFRESH, slot=self.populate_devices, ), }
[docs] def _build_group(self) -> QGroupBox: """build layout""" # --- Row 1: USB Port row = 0 group, grid = create_group_and_grid_layout(self.group_title) usb_port_layout, usb_port_label = create_icon_and_label( label="Port", icon=JDXi.UI.Icon.USB ) grid.addLayout(usb_port_layout, row, 0) self.port_select_combo = QComboBox() self.populate_devices() grid.addWidget(self.port_select_combo, row, 1, 1, 2) spec = self.specs["buttons"]["usb_port_refresh"] self.port_refresh_devices_button = create_jdxi_button_from_spec( spec, checkable=False ) refresh_usb_cell, self.port_refresh_devices_label = ( create_widget_cell_with_button_spec(spec, self.port_refresh_devices_button) ) grid.addWidget(refresh_usb_cell, row, 3) row += 1 # --- Row 2: File to save recording file_layout, file_label = create_icon_and_label( label="File", icon=JDXi.UI.Icon.SAVE ) grid.addLayout(file_layout, row, 0) self.file_select = QPushButton("No File Selected") self.file_select.clicked.connect(self.select_recording_file) grid.addWidget(self.file_select, row, 1, 1, 2) # 2 = colspan I guess # row += 1 # --- Row 2 still: Save USB recording checkbox self.file_record_checkbox = QCheckBox("Save") JDXi.UI.Theme.apply_button_mini_style(self.file_record_checkbox) self.file_record_checkbox.setChecked(self.recorder.file_save_recording) self.file_record_checkbox.stateChanged.connect( self.on_usb_save_recording_toggled ) grid.addWidget(self.file_record_checkbox, row, 3) # row += 1 # --- Row 3: Auto-generate WAV filename checkbox self.file_auto_generate_checkbox = QCheckBox("Auto-filename") JDXi.UI.Theme.apply_button_mini_style(self.file_auto_generate_checkbox) self.file_auto_generate_checkbox.setChecked(False) self.file_auto_generate_checkbox.stateChanged.connect( self.on_usb_file_auto_generate_toggled ) grid.addWidget(self.file_auto_generate_checkbox, row, 4) return group
[docs] def start_recording(self): """start usb recording""" if self.recorder.file_save_recording: recording_rate = "32bit" # Default to 32-bit recording try: rate = self.recorder.usb_recording_rates.get( recording_rate, pyaudio.paInt16 ) self._start_recording(recording_rate=rate) except Exception as ex: log.error(f"Error {ex} occurred starting USB recording")
[docs] def on_usb_save_recording_toggled(self, state: Qt.CheckState): """ on_usb_save_recording_toggled :param state: Qt.CheckState :return: """ self.recorder.file_save_recording = state == JDXi.UI.Constants.CHECKED log.message(f"save USB recording = {self.recorder.file_save_recording}")
[docs] def on_usb_file_auto_generate_toggled(self, state: Qt.CheckState): """ on_usb_file_auto_generate_toggled :param state: Qt.CheckState :return: """ self.file_auto_generate_checkbox.setChecked(state == JDXi.UI.Constants.CHECKED) is_enabled = self.file_auto_generate_checkbox.isChecked() log.message( f"Auto generate filename based on current date and time and Midi file = {is_enabled}" ) self.update_auto_wav_filename()
[docs] def populate_devices(self) -> list: """ usb_populate_devices usb port selection :return: list List of USB devices """ usb_devices = self.recorder.list_devices() self.port_select_combo.clear() self.port_select_combo.addItems(usb_devices) self.port_jdxi_auto_connect(usb_devices) return usb_devices
[docs] def port_jdxi_auto_connect(self, usb_devices: list) -> None: """ usb_port_jdxi_auto_connect :param usb_devices: list :return: None Auto-select the first matching device """ pattern = re.compile(r"jd-?xi", re.IGNORECASE) for i, item in enumerate(usb_devices): if pattern.search(item): self.port_select_combo.setCurrentIndex(i) self.recorder.usb_port_input_device_index = i log.message(f"Auto-selected {item}") break
[docs] def on_usb_file_output_name_changed(self, state: Qt.CheckState): """ on_usb_file_output_name_changed :param state: Qt.CheckState :return: """ self.file_auto_generate_checkbox.setChecked(state == JDXi.UI.Constants.CHECKED) log.message( f"Auto generate filename based on current date and time and Midi file = {self.file_auto_generate_checkbox.isChecked()}" )
[docs] def _start_recording(self, recording_rate: int = pyaudio.paInt16): """ usb_start_recording :param recording_rate: int :return: None Start recording in a separate thread """ try: # If auto-generate is enabled, regenerate filename with fresh timestamp if self.file_auto_generate_checkbox.isChecked(): self.update_auto_wav_filename() if not self.file_output_name: log.warning( "⚠️ No output file selected for WAV recording. Please select a file or enable auto-generate." ) show_message_box_from_spec(self.specs["message_box"]["no_output_file"]) return log.message(f"🎙️ Starting WAV recording to: {self.file_output_name}") log.message( f"🎙️ Recording duration: {self.midi_state.file_duration_seconds} seconds" ) selected_index = self.port_select_combo.currentIndex() log.message(f"🎙️ Using USB input device index: {selected_index}") start_recording( self.recorder, self.midi_state.file_duration_seconds, self.file_output_name, recording_rate, selected_index, ) except Exception as ex: log.error(f"❌ Error {ex} occurred starting recording") import traceback log.error(traceback.format_exc()) show_message_box_from_spec( self.specs["message_box"]["error_saving_file"], message=f"Error {ex} occurred starting recording", )
[docs] def generate_auto_wav_filename(self) -> Optional[str]: """ Generate an automatic WAV filename based on current date/time and MIDI file name. :return: Generated filename path or None if no MIDI file is loaded """ if ( not self.midi_state.file or not hasattr(self.midi_state.file, "filename") or not self.midi_state.file.filename ): return None # Get MIDI file path midi_path = Path(self.midi_state.file.filename) midi_stem = midi_path.stem # filename without extension # Generate timestamp: YYYYMMDD_HHMMSS timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Create WAV filename: YYYYMMDD_HHMMSS_<midi_filename>.wav wav_filename = f"{timestamp}_{midi_stem}.wav" # Use the same directory as the MIDI file, or current directory if no path if midi_path.parent: wav_path = midi_path.parent / wav_filename else: wav_path = Path(wav_filename) return str(wav_path)
[docs] def update_auto_wav_filename(self) -> None: """ Update the WAV filename automatically if auto-generate is enabled. """ if self.file_auto_generate_checkbox.isChecked(): auto_filename = self.generate_auto_wav_filename() if auto_filename: self.file_output_name = auto_filename self.file_select.setText(Path(auto_filename).name) log.message(f"Auto-generated WAV filename: {auto_filename}") else: log.warning("⚠️ Cannot auto-generate filename: No MIDI file loaded")
[docs] def select_recording_file(self): """Open a file picker dialog to select output .wav file.""" file_name_spec = FileSelectionSpec( mode=FileSelectionMode.SAVE, filter="WAV files (*.wav)", caption="Save Recording As", ) file_name = get_file_save_from_spec(file_name_spec, parent=self) if file_name: self.file_select.setText(file_name) self.file_output_name = file_name else: self.file_output_name = ""