#!/usr/bin/env python3 """ MIDI Arpeggiator - Main Application Entry Point A modular MIDI arpeggiator with lighting control and Native Instruments Maschine integration """ import sys import os from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QTimer from gui.main_window import MainWindow from core.output_manager import OutputManager from core.arpeggiator_engine import ArpeggiatorEngine from core.midi_channel_manager import MIDIChannelManager from core.synth_router import SynthRouter from core.volume_pattern_engine import VolumePatternEngine from simulator.simulator_engine import SimulatorEngine from config.configuration import Configuration from maschine.maschine_controller import MaschineController class ArpeggiatorApp: def __init__(self): self.app = QApplication(sys.argv) self.config = Configuration() # Initialize core modules self.channel_manager = MIDIChannelManager() self.volume_engine = VolumePatternEngine() self.synth_router = SynthRouter(self.channel_manager) self.simulator = SimulatorEngine() self.output_manager = OutputManager(self.simulator) self.arpeggiator = ArpeggiatorEngine( self.channel_manager, self.synth_router, self.volume_engine, self.output_manager ) # Initialize Maschine controller self.maschine_controller = MaschineController( self.arpeggiator, self.channel_manager, self.volume_engine, self.synth_router, self.output_manager ) # Initialize GUI self.main_window = MainWindow( self.arpeggiator, self.channel_manager, self.volume_engine, self.output_manager, self.simulator, self.maschine_controller ) # Volume changes are now handled directly in update_systems for active channels only self.previous_active_channels = set() # Setup update timer for real-time updates self.update_timer = QTimer() self.update_timer.timeout.connect(self.update_systems) self.update_timer.start(16) # ~60 FPS # Volume updates are now handled directly in update_systems for active channels only def update_systems(self): """Update all systems that need regular refresh""" self.arpeggiator.update() self.simulator.update_lighting_display() # Update volume patterns if arpeggiator is playing if self.arpeggiator.is_playing: # Advance pattern position (16ms delta at 60fps) self.volume_engine.update_pattern(0.016) # Only update volumes for channels that have active notes active_channels = set([ch for ch, voices in self.channel_manager.active_voices.items() if voices]) if active_channels: # Update volume patterns for active channels only for channel in active_channels: volume = self.volume_engine.get_channel_volume(channel, len(active_channels)) midi_volume = int(volume * 127) self.output_manager.send_volume_change(channel, midi_volume) # Handle channels that just became inactive newly_inactive = self.previous_active_channels - active_channels for channel in newly_inactive: # Send one CC7 message to reset to default volume self.output_manager.send_volume_change(channel, 100) # Dim the visual display if hasattr(self.main_window.arp_controls, 'simulator_display'): self.main_window.arp_controls.simulator_display.on_midi_volume_changed(channel, 20) # Update previous active channels self.previous_active_channels = active_channels.copy() else: # No active channels - reset all previously active ones for channel in self.previous_active_channels: self.output_manager.send_volume_change(channel, 100) if hasattr(self.main_window.arp_controls, 'simulator_display'): self.main_window.arp_controls.simulator_display.on_midi_volume_changed(channel, 20) self.previous_active_channels = set() def run(self): self.main_window.show() return self.app.exec_() if __name__ == "__main__": app = ArpeggiatorApp() sys.exit(app.run())