v0.4 - cockybastard

This commit is contained in:
2026-05-13 20:53:15 +10:00
parent 7de0cc3de0
commit 6067f442b0
5 changed files with 174 additions and 87 deletions
+61 -23
View File
@@ -18,6 +18,8 @@ logger = logging.getLogger(__name__)
NOTE_ON = 0x90
NOTE_OFF = 0x80
CC = 0xB0
PITCH_BEND = 0xE0
PITCH_BEND = 0xE0
CONFIG_DIR = os.path.expanduser("~/.gti_radiostudio")
CONFIG_FILE = os.path.join(CONFIG_DIR, "midi_map.json")
@@ -40,12 +42,13 @@ class MidiMapping:
self.bindings = {}
def add(self, action: str, target_id, midi_type: int, midi_channel: int,
midi_note: int, inverse: bool = False):
midi_note: int, inverse: bool = False, is_relative: bool = False):
key = (midi_type, midi_channel, midi_note)
self.bindings[key] = {
'action': action,
'target_id': target_id,
'inverse': inverse,
'is_relative': is_relative,
}
def lookup(self, midi_type: int, midi_channel: int,
@@ -63,6 +66,7 @@ class MidiMapping:
'action': val['action'],
'target_id': val['target_id'],
'inverse': val['inverse'],
'is_relative': val.get('is_relative', False),
})
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
@@ -78,6 +82,7 @@ class MidiMapping:
'action': entry['action'],
'target_id': entry['target_id'],
'inverse': entry.get('inverse', False),
'is_relative': entry.get('is_relative', False),
}
@@ -93,6 +98,7 @@ class MidiEngine(QObject):
master_control = Signal(str, float)
controller_connected = Signal(bool, str)
mapping_learned = Signal(str, object, int, int, int, bool)
raw_midi_event = Signal(str, int, int, int)
def __init__(self, parent=None):
super().__init__(parent)
@@ -107,6 +113,9 @@ class MidiEngine(QObject):
self._learn_mode = False
self._last_cc_values = {}
self._channel_volumes = {}
self._master_knob_values = {}
self._master_knob_values = {}
@property
def learn_mode(self) -> bool:
@@ -200,13 +209,18 @@ class MidiEngine(QObject):
status_type = status & 0xF0
channel = status & 0x0F
# Learn mode: capture any MIDI event
type_names = {0x90: "note_on", 0x80: "note_off", 0xB0: "cc", 0xE0: "pitch", 0xD0: "aftertouch"}
type_name = type_names.get(status_type, f"0x{status_type:02X}")
self.raw_midi_event.emit(type_name, channel, data_byte, value)
if self._learn_mode and self._learn_target:
action, target_id = self._learn_target
mapping_type = status_type
self.mapping.add(action, target_id, mapping_type, channel, data_byte)
is_rel = (status_type == 0xB0 and (1 <= value <= 63 or 65 <= value <= 127))
note = 0 if status_type == PITCH_BEND else data_byte
self.mapping.add(action, target_id, mapping_type, channel, note, is_relative=is_rel)
self.mapping.save()
self.mapping_learned.emit(action, target_id, mapping_type, channel, data_byte, value > 0)
self.mapping_learned.emit(action, target_id, mapping_type, channel, note, value > 0)
self.stop_learn()
return
@@ -218,6 +232,11 @@ class MidiEngine(QObject):
elif status_type == CC:
self._handle_cc(data_byte, value)
self._handle_learned_event(status_type, channel, data_byte, value)
elif status_type == PITCH_BEND:
lsb = data_byte
msb = message[2] if len(message) > 2 else 0
pb_value = (msb << 7) | lsb
self._handle_learned_event(status_type, channel, 0, pb_value)
def _handle_learned_event(self, midi_type: int, channel: int,
note: int, value: int):
@@ -228,6 +247,7 @@ class MidiEngine(QObject):
action = binding['action']
target_id = binding['target_id']
inv = binding.get('inverse', False)
is_rel = binding.get('is_relative', False)
if action == 'deck_play_pause' and isinstance(target_id, str):
self.deck_play_pause.emit(target_id)
@@ -235,15 +255,25 @@ class MidiEngine(QObject):
self.deck_cue.emit(target_id)
elif action == 'cart_trigger':
self.cart_trigger.emit(target_id)
elif action == 'deck_play_pause' and isinstance(target_id, str):
self.deck_play_pause.emit(target_id)
elif action == 'deck_cue' and isinstance(target_id, str):
self.deck_cue.emit(target_id)
elif action == 'cart_trigger':
self.cart_trigger.emit(target_id)
elif action == 'mixer_volume':
scaled = (127 - value if inv else value) / 127.0
self.mixer_volume.emit(target_id, scaled)
if is_rel and 1 <= value <= 63:
prev = self._channel_volumes.get(target_id, 1.0)
new_vol = min(1.0, prev + value * 0.02)
self._channel_volumes[target_id] = new_vol
self.mixer_volume.emit(target_id, new_vol)
elif is_rel and 65 <= value <= 127:
prev = self._channel_volumes.get(target_id, 1.0)
steps = value & 0x3F
new_vol = max(0.0, prev - steps * 0.02)
self._channel_volumes[target_id] = new_vol
self.mixer_volume.emit(target_id, new_vol)
else:
if midi_type == PITCH_BEND:
scaled = (16383 - value if inv else value) / 16383.0
else:
scaled = (127 - value if inv else value) / 127.0
self._channel_volumes[target_id] = scaled
self.mixer_volume.emit(target_id, scaled)
elif action == 'mixer_mute':
muted = (value < 64) if inv else (value > 63)
self.mixer_mute.emit(target_id, muted)
@@ -254,17 +284,25 @@ class MidiEngine(QObject):
on = (value > 63) if not inv else (value < 64)
self.mixer_pfl.emit(target_id, on)
elif action == 'master_control':
scaled = (127 - value if inv else value) / 127.0
self.master_control.emit(str(target_id), scaled)
elif action == 'mixer_solo':
on = (value > 63) if not inv else (value < 64)
self.mixer_solo.emit(target_id, on)
elif action == 'mixer_pfl':
on = (value > 63) if not inv else (value < 64)
self.mixer_pfl.emit(target_id, on)
elif action == 'master_control':
scaled = (127 - value if inv else value) / 127.0
self.master_control.emit(str(target_id), scaled)
knob = str(target_id)
if is_rel and 1 <= value <= 63:
prev = self._master_knob_values.get(knob, 0.5)
new_val = min(1.0, prev + value * 0.02)
self._master_knob_values[knob] = new_val
self.master_control.emit(knob, new_val)
elif is_rel and 65 <= value <= 127:
prev = self._master_knob_values.get(knob, 0.5)
steps = value & 0x3F
new_val = max(0.0, prev - steps * 0.02)
self._master_knob_values[knob] = new_val
self.master_control.emit(knob, new_val)
else:
if midi_type == PITCH_BEND:
scaled = (16383 - value if inv else value) / 16383.0
else:
scaled = (127 - value if inv else value) / 127.0
self._master_knob_values[knob] = scaled
self.master_control.emit(knob, scaled)
def _handle_note_on(self, note: int):
# Hardcoded Numark DJ2Go mapping (legacy)