You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

183 lines
8.2 KiB

"""
MIDI Channel Manager Module
Handles MIDI channel management, instrument assignments, and voice allocation.
Manages up to 16 synth channels with individual program changes and polyphonic voice tracking.
"""
import mido
from typing import Dict, List, Optional, Tuple
from PyQt5.QtCore import QObject, pyqtSignal
class MIDIChannelManager(QObject):
"""
Manages MIDI channels, instruments, and voice allocation for multiple synths.
Each synth corresponds to a MIDI channel (1-16).
"""
# Signals for GUI updates
active_synth_count_changed = pyqtSignal(int)
channel_instrument_changed = pyqtSignal(int, int) # channel, program
voice_allocation_changed = pyqtSignal(int, list) # channel, active_notes
# General MIDI Program Names (first 128)
GM_PROGRAMS = [
"Acoustic Grand Piano", "Bright Acoustic Piano", "Electric Grand Piano", "Honky-tonk Piano",
"Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet",
"Celesta", "Glockenspiel", "Music Box", "Vibraphone",
"Marimba", "Xylophone", "Tubular Bells", "Dulcimer",
"Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ",
"Reed Organ", "Accordion", "Harmonica", "Tango Accordion",
"Acoustic Guitar (nylon)", "Acoustic Guitar (steel)", "Electric Guitar (jazz)", "Electric Guitar (clean)",
"Electric Guitar (muted)", "Overdriven Guitar", "Distortion Guitar", "Guitar Harmonics",
"Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass",
"Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2",
"Violin", "Viola", "Cello", "Contrabass",
"Tremolo Strings", "Pizzicato Strings", "Orchestral Harp", "Timpani",
"String Ensemble 1", "String Ensemble 2", "Synth Strings 1", "Synth Strings 2",
"Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit",
"Trumpet", "Trombone", "Tuba", "Muted Trumpet",
"French Horn", "Brass Section", "Synth Brass 1", "Synth Brass 2",
"Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax",
"Oboe", "English Horn", "Bassoon", "Clarinet",
"Piccolo", "Flute", "Recorder", "Pan Flute",
"Blown Bottle", "Shakuhachi", "Whistle", "Ocarina",
"Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", "Lead 4 (chiff)",
"Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8 (bass + lead)",
"Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", "Pad 4 (choir)",
"Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)",
"FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)",
"FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", "FX 8 (sci-fi)",
"Sitar", "Banjo", "Shamisen", "Koto",
"Kalimba", "Bag pipe", "Fiddle", "Shanai",
"Tinkle Bell", "Agogo", "Steel Drums", "Woodblock",
"Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal",
"Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet",
"Telephone Ring", "Helicopter", "Applause", "Gunshot"
]
def __init__(self):
super().__init__()
self.active_synth_count = 8 # Default to 8 synths
self.max_synths = 16
self.max_voices_per_synth = 3
# Channel instruments (1-based channel numbering)
self.channel_instruments: Dict[int, int] = {i: 0 for i in range(1, 17)} # Default to Piano
# Voice allocation tracking
self.active_voices: Dict[int, List[int]] = {i: [] for i in range(1, 17)} # {channel: [note1, note2, note3]}
def set_active_synth_count(self, count: int) -> bool:
"""Set the number of active synths (1-16)"""
if 1 <= count <= self.max_synths:
self.active_synth_count = count
self.active_synth_count_changed.emit(count)
return True
return False
def get_active_channels(self) -> List[int]:
"""Get list of currently active channel numbers"""
return list(range(1, self.active_synth_count + 1))
def set_channel_instrument(self, channel: int, program: int) -> bool:
"""Set instrument program for a specific channel"""
if 1 <= channel <= self.max_synths and 0 <= program <= 127:
self.channel_instruments[channel] = program
self.channel_instrument_changed.emit(channel, program)
return True
return False
def set_all_instruments(self, program: int) -> bool:
"""Set the same instrument for all active channels"""
if 0 <= program <= 127:
for channel in self.get_active_channels():
self.channel_instruments[channel] = program
self.channel_instrument_changed.emit(channel, program)
return True
return False
def get_channel_instrument(self, channel: int) -> Optional[int]:
"""Get current instrument program for a channel"""
if 1 <= channel <= self.max_synths:
return self.channel_instruments.get(channel, 0)
return None
def get_instrument_name(self, program: int) -> str:
"""Get human-readable name for a GM program number"""
if 0 <= program < len(self.GM_PROGRAMS):
return self.GM_PROGRAMS[program]
return f"Program {program}"
def allocate_voice(self, channel: int, note: int) -> bool:
"""
Allocate a voice for a note on a channel.
Returns True if voice was allocated, False if channel is full.
Implements voice stealing if necessary.
"""
if channel not in self.active_voices:
return False
voices = self.active_voices[channel]
# If note is already playing, don't allocate again
if note in voices:
return True
# If we have space, just add it
if len(voices) < self.max_voices_per_synth:
voices.append(note)
self.voice_allocation_changed.emit(channel, voices.copy())
return True
# Voice stealing: remove the oldest note and add the new one
voices.pop(0) # Remove oldest
voices.append(note)
self.voice_allocation_changed.emit(channel, voices.copy())
return True
def release_voice(self, channel: int, note: int) -> bool:
"""Release a voice for a note on a channel"""
if channel not in self.active_voices:
return False
voices = self.active_voices[channel]
if note in voices:
voices.remove(note)
self.voice_allocation_changed.emit(channel, voices.copy())
return True
return False
def get_active_voices(self, channel: int) -> List[int]:
"""Get list of currently active notes for a channel"""
return self.active_voices.get(channel, []).copy()
def is_voice_available(self, channel: int) -> bool:
"""Check if a channel has available voice slots"""
if channel not in self.active_voices:
return False
return len(self.active_voices[channel]) < self.max_voices_per_synth
def get_voice_count(self, channel: int) -> int:
"""Get current number of active voices for a channel"""
return len(self.active_voices.get(channel, []))
def clear_all_voices(self):
"""Clear all active voices on all channels"""
for channel in range(1, self.max_synths + 1):
if self.active_voices[channel]:
self.active_voices[channel].clear()
self.voice_allocation_changed.emit(channel, [])
def get_channel_status(self) -> Dict[int, Dict]:
"""Get comprehensive status for all channels"""
status = {}
for channel in range(1, self.active_synth_count + 1):
status[channel] = {
'instrument': self.channel_instruments[channel],
'instrument_name': self.get_instrument_name(self.channel_instruments[channel]),
'active_voices': self.active_voices[channel].copy(),
'voice_count': len(self.active_voices[channel]),
'voices_available': self.max_voices_per_synth - len(self.active_voices[channel])
}
return status