@ -13,16 +13,19 @@ from PyQt5.QtCore import Qt, pyqtSlot, QTimer
import json
import os
import random
import mido
import time
class PresetControls ( QWidget ) :
""" Control panel for preset management """
def __init__ ( self , arpeggiator , channel_manager , volume_engine , arpeggiator_controls = None ) :
def __init__ ( self , arpeggiator , channel_manager , volume_engine , arpeggiator_controls = None , volume_controls = None ) :
super ( ) . __init__ ( )
self . arpeggiator = arpeggiator
self . channel_manager = channel_manager
self . volume_engine = volume_engine
self . arpeggiator_controls = arpeggiator_controls
self . volume_controls = volume_controls
# Preset storage
self . presets = { }
@ -308,6 +311,11 @@ class PresetControls(QWidget):
self . load_master_button . setStyleSheet ( " background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px; " )
master_layout . addWidget ( self . load_master_button , 1 , 1 )
self . export_master_button = QPushButton ( " Export Master " )
self . export_master_button . clicked . connect ( self . export_master_midi )
self . export_master_button . setStyleSheet ( " background: #5a2d5a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #8a4a8a; padding: 5px 10px; " )
master_layout . addWidget ( self . export_master_button , 1 , 2 )
layout . addWidget ( master_frame )
# Connect group list selection
@ -446,18 +454,28 @@ class PresetControls(QWidget):
channel = int ( channel_str )
self . channel_manager . set_channel_instrument ( channel , program )
# Apply volume pattern settings
# Apply volume pattern settings (check for overrides first)
volume_settings = preset . get ( " volume_patterns " , { } )
self . volume_engine . set_pattern ( volume_settings . get ( " current_pattern " , " static " ) )
self . volume_engine . set_pattern_speed ( volume_settings . get ( " pattern_speed " , 1.0 ) )
self . volume_engine . set_pattern_intensity ( volume_settings . get ( " pattern_intensity " , 1.0 ) )
if not self . _is_volume_parameter_overridden ( ' volume_pattern ' ) :
self . volume_engine . set_pattern ( volume_settings . get ( " current_pattern " , " static " ) )
self . volume_engine . set_pattern_speed ( volume_settings . get ( " pattern_speed " , 1.0 ) )
self . volume_engine . set_pattern_intensity ( volume_settings . get ( " pattern_intensity " , 1.0 ) )
# Apply global ranges
global_vol = volume_settings . get ( " global_volume_range " , ( 0.2 , 1.0 ) )
global_vel = volume_settings . get ( " global_velocity_range " , ( 40 , 127 ) )
self . volume_engine . set_global_ranges (
global_vol [ 0 ] , global_vol [ 1 ] , global_vel [ 0 ] , global_vel [ 1 ]
)
if not self . _is_volume_parameter_overridden ( ' volume_range ' ) :
global_vol = volume_settings . get ( " global_volume_range " , ( 0.2 , 1.0 ) )
self . volume_engine . set_global_ranges (
global_vol [ 0 ] , global_vol [ 1 ] ,
self . volume_engine . global_velocity_range [ 0 ] , self . volume_engine . global_velocity_range [ 1 ]
)
if not self . _is_volume_parameter_overridden ( ' velocity_range ' ) :
global_vel = volume_settings . get ( " global_velocity_range " , ( 40 , 127 ) )
self . volume_engine . set_global_ranges (
self . volume_engine . global_volume_range [ 0 ] , self . volume_engine . global_volume_range [ 1 ] ,
global_vel [ 0 ] , global_vel [ 1 ]
)
# Apply individual channel ranges
ch_vol_ranges = volume_settings . get ( " channel_volume_ranges " , { } )
@ -741,6 +759,15 @@ class PresetControls(QWidget):
return is_overridden
return False
def _is_volume_parameter_overridden ( self , param_name ) :
""" Check if a volume parameter is overridden (checkbox checked) """
if self . volume_controls and hasattr ( self . volume_controls , ' is_parameter_overridden ' ) :
is_overridden = self . volume_controls . is_parameter_overridden ( param_name )
if is_overridden :
print ( f " DEBUG: Skipping volume {param_name} - parameter is overridden " )
return is_overridden
return False
def load_presets_from_directory ( self ) :
""" Load all presets from the presets directory """
if not os . path . exists ( self . presets_directory ) :
@ -1266,6 +1293,214 @@ class PresetControls(QWidget):
except Exception as e :
QMessageBox . critical ( self , " Error " , f " Failed to load master file: \n {str(e)} " )
def export_master_midi ( self ) :
""" Export the current master preset sequence as a single MIDI file """
try :
# Check if we have a loaded master preset group
if not hasattr ( self , ' preset_group ' ) or not self . preset_group :
QMessageBox . warning ( self , " No Master Preset " ,
" No master preset loaded. Please load a master file first. " )
return
# Open file dialog for saving MIDI file
filename , _ = QFileDialog . getSaveFileName (
self ,
" Export Master Preset as MIDI " ,
" master_export.mid " ,
" MIDI Files (*.mid);;All Files (*) "
)
if not filename :
return
print ( f " DEBUG: Exporting {len(self.preset_group)} presets: {self.preset_group} " )
# Create new MIDI file with simpler approach
mid = mido . MidiFile ( ticks_per_beat = 480 )
track = mido . MidiTrack ( )
mid . tracks . append ( track )
# Set tempo (120 BPM default)
track . append ( mido . MetaMessage ( ' set_tempo ' , tempo = mido . bpm2tempo ( 120 ) , time = 0 ) )
# Process each preset in the master sequence with simplified timing
absolute_time = 0
last_time = 0
preset_duration_ticks = 1920 # 1 bar at 480 ticks per beat in 4/4 time
for preset_name in self . preset_group :
print ( f " DEBUG: Processing preset: {preset_name} " )
if preset_name in self . presets :
preset = self . presets [ preset_name ]
print ( f " DEBUG: Found preset data for {preset_name} " )
self . _add_preset_to_midi_track_simple ( track , preset , absolute_time , last_time )
absolute_time + = preset_duration_ticks
last_time = absolute_time
else :
print ( f " DEBUG: Preset {preset_name} not found in self.presets " )
# Save the MIDI file
mid . save ( filename )
QMessageBox . information ( self , " MIDI Export Complete " ,
f " Master preset exported as MIDI file: \n {filename} \n \n "
f " Contains {len(self.preset_group)} presets \n "
f " Duration: {len(self.preset_group)} bars " )
except Exception as e :
import traceback
error_details = traceback . format_exc ( )
QMessageBox . critical ( self , " Export Error " , f " Failed to export MIDI file: \n {str(e)} \n \n Details: \n {error_details} " )
def _add_preset_to_midi_track ( self , track , preset , preset_start_time , duration_ticks ) :
""" Add a single preset ' s MIDI data to the track """
try :
arp_settings = preset . get ( " arpeggiator " , { } )
# Get preset parameters
root_note = arp_settings . get ( " root_note " , 60 ) # Middle C default
scale = arp_settings . get ( " scale " , " major " )
scale_note_start = arp_settings . get ( " scale_note_start " , 0 )
pattern_type = arp_settings . get ( " pattern_type " , " up " )
note_speed = arp_settings . get ( " note_speed " , " 1/4 " )
gate = arp_settings . get ( " gate " , 0.8 )
velocity = arp_settings . get ( " velocity " , 100 )
user_pattern_length = arp_settings . get ( " user_pattern_length " , 8 )
# Scale definitions
scales = {
" major " : [ 0 , 2 , 4 , 5 , 7 , 9 , 11 ] ,
" minor " : [ 0 , 2 , 3 , 5 , 7 , 8 , 10 ] ,
" dorian " : [ 0 , 2 , 3 , 5 , 7 , 9 , 10 ] ,
" phrygian " : [ 0 , 1 , 3 , 5 , 7 , 8 , 10 ] ,
" lydian " : [ 0 , 2 , 4 , 6 , 7 , 9 , 11 ] ,
" mixolydian " : [ 0 , 2 , 4 , 5 , 7 , 9 , 10 ] ,
" locrian " : [ 0 , 1 , 3 , 5 , 6 , 8 , 10 ] ,
" harmonic_minor " : [ 0 , 2 , 3 , 5 , 7 , 8 , 11 ] ,
" melodic_minor " : [ 0 , 2 , 3 , 5 , 7 , 9 , 11 ] ,
" pentatonic " : [ 0 , 2 , 4 , 7 , 9 ] ,
" blues " : [ 0 , 3 , 5 , 6 , 7 , 10 ]
}
scale_intervals = scales . get ( scale , scales [ " major " ] )
# Calculate note timing based on note_speed (ticks per note)
note_speed_ticks = {
" 1/1 " : 1920 , # Whole note
" 1/2 " : 960 , # Half note
" 1/4 " : 480 , # Quarter note
" 1/8 " : 240 , # Eighth note
" 1/16 " : 120 , # Sixteenth note
" 1/2T " : 640 , # Half note triplet
" 1/4T " : 320 , # Quarter note triplet
" 1/8T " : 160 # Eighth note triplet
}
note_duration = note_speed_ticks . get ( note_speed , 480 )
gate_duration = int ( note_duration * gate )
rest_duration = note_duration - gate_duration
# Generate scale notes
notes = [ ]
# Handle "random" scale_note_start by converting to integer
if scale_note_start == " random " :
import random
start_degree = random . randint ( 0 , len ( scale_intervals ) - 1 )
else :
start_degree = int ( scale_note_start ) % len ( scale_intervals )
# Create simple ascending pattern for now
for i in range ( user_pattern_length ) :
degree = ( start_degree + i ) % len ( scale_intervals )
note = root_note + scale_intervals [ degree ]
if 0 < = note < = 127 :
notes . append ( note )
if not notes :
return
# Calculate how many notes fit in the duration
notes_in_duration = duration_ticks / / note_duration
# Add gap at start of preset (except for first preset)
current_time = preset_start_time
# Add MIDI notes for this preset
for i in range ( int ( notes_in_duration ) ) :
note = notes [ i % len ( notes ) ]
# Note on
track . append ( mido . Message ( ' note_on ' ,
channel = 0 ,
note = note ,
velocity = int ( velocity ) ,
time = current_time ) )
# Note off
track . append ( mido . Message ( ' note_off ' ,
channel = 0 ,
note = note ,
velocity = 0 ,
time = gate_duration ) )
# Time until next note (only rest_duration since we already used gate_duration)
current_time = rest_duration
except Exception as e :
print ( f " Error adding preset to MIDI track: {e} " )
import traceback
traceback . print_exc ( )
def _add_preset_to_midi_track_simple ( self , track , preset , absolute_start_time , last_time ) :
""" Add a single preset ' s MIDI data to the track with simplified timing """
try :
arp_settings = preset . get ( " arpeggiator " , { } )
# Get basic preset parameters
root_note = arp_settings . get ( " root_note " , 60 )
velocity = arp_settings . get ( " velocity " , 100 )
# Simple scale - just use major scale starting from root
notes = [ root_note , root_note + 2 , root_note + 4 , root_note + 5 ,
root_note + 7 , root_note + 9 , root_note + 11 , root_note + 12 ]
# Filter to valid MIDI range
notes = [ n for n in notes if 0 < = n < = 127 ]
if not notes :
return
# Add a few notes for this preset (quarter notes)
note_duration = 480 # Quarter note in ticks
gate_duration = 400 # Slightly shorter than full duration
# Time since last event
delta_time = absolute_start_time - last_time
for i in range ( 4 ) : # 4 quarter notes per preset
note = notes [ i % len ( notes ) ]
# Note on
track . append ( mido . Message ( ' note_on ' ,
channel = 0 ,
note = note ,
velocity = int ( velocity ) ,
time = delta_time if i == 0 else ( note_duration - gate_duration ) ) )
# Note off
track . append ( mido . Message ( ' note_off ' ,
channel = 0 ,
note = note ,
velocity = 0 ,
time = gate_duration ) )
delta_time = 0 # Only first note has the preset gap
except Exception as e :
print ( f " Error in simple MIDI track: {e} " )
import traceback
traceback . print_exc ( )
def update_preset_list ( self ) :
""" Update the main preset list display """
self . preset_list . clear ( )