v0.4 - cockybastard
This commit is contained in:
+61
-23
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user