# main.py import json import logging import os import sys __version__ = "0.6" from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QFileDialog, QVBoxLayout, QHBoxLayout, QSplitter ) from PySide6.QtCore import Qt from PySide6.QtGui import QFont, QColor, QPalette from audio_engine import AudioEngine from deck_widget import DeckWidget from cart_widget import CartPlayerWidget from playlist_widget import PlaylistWidget from midi_engine import MidiEngine from top_bar_widget import TopBarWidget, THEMES from history_widget import ShowHistoryWidget from mixer_widget import MixerWidget, MicTimerWidget, LUFSExpandedSection from event_logger import SessionLogger logger = logging.getLogger(__name__) class RadioPanelWindow(QMainWindow): def __init__(self): super().__init__() self.engine = AudioEngine() self.session_logger = SessionLogger() self.engine.logger = self.session_logger self.setWindowTitle(f"Getting To It v{__version__}") self.setMinimumSize(1400, 860) self.resize(1920, 1080) self._apply_global_style(THEMES[0]) central = QWidget() self.setCentralWidget(central) outer = QVBoxLayout(central) outer.setContentsMargins(6, 6, 6, 6) outer.setSpacing(6) # Top bar self.top_bar = TopBarWidget() self.top_bar.theme_changed.connect(self._on_theme_changed) self.top_bar.ratio_changed.connect(self._apply_ratio) self.top_bar.save_session.connect(self._save_session) self.top_bar.load_session.connect(self._load_session) self.top_bar.recording_toggled.connect(self._on_recording_toggled) outer.addWidget(self.top_bar) # ── Build widgets ───────────────────────────────────────────────── self.deck1 = DeckWidget("deck1", "DECK_001", "#00b894", self.engine, mode='deck') self.deck2 = DeckWidget("deck2", "DECK_002", "#6c5ce7", self.engine, mode='deck') self.cart_player = CartPlayerWidget(self.engine) self.cart_player.setFixedHeight(160) self.mic_timer = MicTimerWidget(self.engine) self.lufs_section = LUFSExpandedSection(self.engine) self.playlist_widget = PlaylistWidget( engine=self.engine, deck_widgets={ "deck1": self.deck1, "deck2": self.deck2, }, cart_player=self.cart_player ) self.history_widget = ShowHistoryWidget() self.history_widget.set_engine(self.engine) self.mixer = MixerWidget(self.engine) # ── Centre column with vertical splitter ───────────────────────── centre_col = QVBoxLayout() centre_col.setSpacing(6) # Top: 2 decks + cart deck_area = QWidget() deck_area.setStyleSheet("background: transparent; border: none;") deck_layout = QVBoxLayout(deck_area) deck_layout.setContentsMargins(0, 0, 0, 0) deck_layout.setSpacing(4) deck_row1 = QHBoxLayout() deck_row1.setSpacing(4) deck_row1.addWidget(self.deck1) deck_row1.addWidget(self.deck2) deck_layout.addLayout(deck_row1) cart_row = QHBoxLayout() cart_row.setSpacing(4) cart_row.addWidget(self.cart_player) mic_lufs_row = QHBoxLayout() mic_lufs_row.setSpacing(4) mic_lufs_row.addWidget(self.mic_timer, stretch=1) mic_lufs_row.addWidget(self.lufs_section, stretch=1) cart_row.addLayout(mic_lufs_row) deck_layout.addLayout(cart_row) # Mixer fills remaining space below decks centre_col.addWidget(deck_area) centre_col.addWidget(self.mixer, stretch=1) centre_widget = QWidget() centre_widget.setStyleSheet("background: transparent; border: none;") centre_widget.setLayout(centre_col) # ── Horizontal splitter ────────────────────────────────────────── self.splitter = QSplitter(Qt.Horizontal) self.splitter.setHandleWidth(4) self._style_splitter(THEMES[0]) self.splitter.addWidget(self.playlist_widget) self.splitter.addWidget(centre_widget) self.splitter.addWidget(self.history_widget) self.splitter.setSizes([360, 1160, 360]) outer.addWidget(self.splitter) # ── Wire signals ───────────────────────────────────────────────── self.deck1.track_started.connect(self.history_widget.add_song) self.deck2.track_started.connect(self.history_widget.add_song) self.cart_player.cart_triggered.connect(self.history_widget.add_cart) # ── MIDI ────────────────────────────────────────────────────────── self._setup_midi() def _style_splitter(self, theme: dict): self.splitter.setStyleSheet(f""" QSplitter::handle {{ background-color: #22242a; border-radius: 2px; }} QSplitter::handle:hover {{ background-color: {theme['accent']}; }} QSplitter::handle:pressed {{ background-color: {theme['accent_dim']}; }} """) def _on_theme_changed(self, theme: dict): self._apply_global_style(theme) self._style_splitter(theme) self.deck1.apply_theme(theme) self.deck2.apply_theme(theme) self.cart_player.apply_theme(theme) self.history_widget.apply_theme(theme) self.mixer.apply_theme(theme) self.mic_timer.apply_theme(theme) self.lufs_section.apply_theme(theme) def _apply_ratio(self, w, h): self.resize(w, h) def _apply_global_style(self, theme: dict): self.setStyleSheet(f""" QMainWindow {{ background-color: {theme['bg']}; }} QWidget {{ background-color: {theme['bg']}; color: {theme['text']}; font-family: "Segoe UI", Arial, sans-serif; }} QToolTip {{ background-color: {theme['bg2']}; color: {theme['text']}; border: 1px solid #444; padding: 4px; }} """) def _save_session(self): path, _ = QFileDialog.getSaveFileName( self.top_bar, "Save Session", os.path.expanduser("~/.gti_radiostudio/session.json"), "JSON Files (*.json)" ) if not path: return state = { 'window_size': (self.width(), self.height()), 'pfl_hp_blend': self.engine.mixer['master'].pfl_hp_blend, 'channels': [], } for ch in self.engine.mixer['channels']: state['channels'].append({ 'name': ch.name, 'volume': ch.volume, 'gain': ch.gain, 'muted': ch.muted, 'pfl': ch.pfl, 'mic_on': ch.mic_on, }) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'w') as f: json.dump(state, f, indent=2) def _load_session(self): path, _ = QFileDialog.getOpenFileName( self.top_bar, "Load Session", os.path.expanduser("~/.gti_radiostudio/session.json"), "JSON Files (*.json)" ) if not path: return with open(path) as f: state = json.load(f) ws = state.get('window_size') if ws: self.resize(ws[0], ws[1]) pfl_hp_blend = state.get('pfl_hp_blend') if pfl_hp_blend is not None: self.engine.mixer['master'].pfl_hp_blend = pfl_hp_blend for i, ch_state in enumerate(state.get('channels', [])): if i < len(self.engine.mixer['channels']): ch = self.engine.mixer['channels'][i] ch.name = ch_state.get('name', ch.name) ch.volume = ch_state.get('volume', ch.volume) ch.gain = ch_state.get('gain', ch.gain) ch.muted = ch_state.get('muted', ch.muted) ch.solo = ch_state.get('solo', ch.solo) ch.pfl = ch_state.get('pfl', ch.pfl) ch.mic_on = ch_state.get('mic_on', ch.mic_on) self.mixer.refresh_strips() def _setup_midi(self): self.midi = MidiEngine() self.midi.deck_play_pause.connect(self._on_midi_play_pause) self.midi.deck_cue.connect(self._on_midi_cue) self.midi.deck_jog.connect(self._on_midi_jog) self.midi.cart_trigger.connect(self._on_midi_cart) self.midi.mixer_volume.connect(self._on_midi_mixer_volume) self.midi.mixer_mute.connect(self._on_midi_mixer_mute) self.midi.mixer_pfl.connect(self._on_midi_mixer_pfl) self.midi.mixer_mic_on.connect(self._on_midi_mixer_mic_on) self.midi.mixer_gain.connect(self._on_midi_mixer_gain) self.midi.master_control.connect(self._on_midi_master_control) self.midi.master_volume.connect(self._on_midi_master_volume) self.midi.controller_connected.connect(self._on_midi_status) self.midi.start() self.top_bar.set_midi_engine(self.midi) ports = self.midi.list_ports() if ports: logger.info("Available ports: %s", ports) def _on_midi_play_pause(self, deck: str): if deck == 'deck1': self.deck1.toggle_play() elif deck == 'deck2': self.deck2.toggle_play() def _on_midi_cue(self, deck: str): if deck == 'deck1': self.deck1.cue() elif deck == 'deck2': self.deck2.cue() def _on_midi_jog(self, deck: str, delta: float): if deck == 'deck1': self.deck1.scrub(delta) elif deck == 'deck2': self.deck2.scrub(delta) def _on_midi_cart(self, slot: int): self.cart_player.toggle() def _on_midi_mixer_volume(self, channel: int, value: float): self.engine.set_channel_volume(channel, value) def _on_midi_mixer_mute(self, channel: int, muted: bool): self.engine.set_channel_mute(channel, muted) def _on_midi_mixer_pfl(self, channel: int, on: bool): self.engine.set_channel_pfl(channel, on) def _on_midi_mixer_mic_on(self, channel: int, on: bool): if channel < 2: current = self.engine.get_channel_mic_on(channel) self.engine.set_channel_mic_on(channel, not current) def _on_midi_mixer_gain(self, channel: int, gain: float): self.engine.set_channel_gain(channel, gain) def _on_midi_master_control(self, knob: str, val: float): if knob == "pfl_hp_blend": self.engine.mixer['master'].pfl_hp_blend = max(0.0, min(1.0, val)) def _on_midi_master_volume(self, val: float): self.engine.mixer['master'].volume = max(0.0, min(1.0, val)) def _on_midi_status(self, connected: bool, name: str): if connected: logger.info("Connected: %s", name) else: logger.info("Not connected: %s", name) def closeEvent(self, event): self.midi.stop() self.engine.shutdown() event.accept() def _on_recording_toggled(self, checked): if checked: self.engine.start_recording() else: self.engine.stop_recording() def main(): app = QApplication(sys.argv) app.setApplicationName("Radio Panel") app.setOrganizationName("Getting To It") palette = QPalette() palette.setColor(QPalette.Window, QColor("#121418")) palette.setColor(QPalette.WindowText, QColor("#d0d4dc")) palette.setColor(QPalette.Base, QColor("#1a1c23")) palette.setColor(QPalette.AlternateBase, QColor("#22242c")) palette.setColor(QPalette.Text, QColor("#d0d4dc")) palette.setColor(QPalette.Button, QColor("#2a2c35")) palette.setColor(QPalette.ButtonText, QColor("#d0d4dc")) palette.setColor(QPalette.Highlight, QColor("#00b894")) palette.setColor(QPalette.HighlightedText, QColor("#ffffff")) app.setPalette(palette) window = RadioPanelWindow() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()