Tired and Poor and Old - Radio Panel v0.1.1

This commit is contained in:
2026-04-30 00:30:23 +10:00
parent a9249d395f
commit 0c8900ac36
18 changed files with 491 additions and 189 deletions
+15 -53
View File
@@ -2,19 +2,6 @@
"""
MIDI Engine for Radio Panel
Numark DJ2Go controller support.
Confirmed mapping from hardware testing:
------------------------------------------
Play/Pause Deck 1 : Note 0x3B ch=0
Play/Pause Deck 2 : Note 0x42 ch=0
Cue Deck 1 : Note 0x33 ch=0
Cue Deck 2 : Note 0x3C ch=0
Jog Wheel Deck 1 : CC 0x19 ch=0
Jog Wheel Deck 2 : CC 0x18 ch=0
Cart 1 : Note 0x44 ch=0
Cart 2 : Note 0x43 ch=0
Cart 3 : Note 0x46 ch=0
Cart 4 : Note 0x45 ch=0
"""
import logging
@@ -33,7 +20,6 @@ NOTE_ON = 0x90
NOTE_OFF = 0x80
CC = 0xB0
# All controls on ch=0, differentiated by note/CC number only
DECK1_PLAY = 0x3B
DECK1_CUE = 0x33
DECK1_JOG = 0x19
@@ -42,22 +28,24 @@ DECK2_PLAY = 0x42
DECK2_CUE = 0x3C
DECK2_JOG = 0x18
# Cart buttons - confirmed from hardware test
CART_NOTES = {
0x44: 0, # Cart slot 1
0x43: 1, # Cart slot 2
0x46: 2, # Cart slot 3
0x45: 3, # Cart slot 4
0x44: 0,
0x43: 1,
0x46: 2,
0x45: 3,
}
JOG_SCRUB_SECONDS = 5.0
JOG_DEAD_ZONE = 5 # Ignore movements below this threshold
def _decode_relative_jog(value: int) -> int:
"""Returns movement amount, or 0 if within dead zone."""
if 1 <= value <= 63:
return 1
return value if value >= JOG_DEAD_ZONE else 0
elif 65 <= value <= 127:
return -1
backward = 128 - value
return -backward if backward >= JOG_DEAD_ZONE else 0
return 0
@@ -148,14 +136,6 @@ class MidiEngine(QObject):
value = message[2] if len(message) > 2 else 0
status_type = status & 0xF0
channel = status & 0x0F
logger.debug(
f"MIDI IN status=0x{status:02X} "
f"data=0x{data_byte:02X} "
f"val={value} "
f"type=0x{status_type:02X} ch={channel}"
)
if status_type == NOTE_ON and value > 0:
self._handle_note_on(data_byte)
@@ -164,43 +144,25 @@ class MidiEngine(QObject):
def _handle_note_on(self, note: int):
if note == DECK1_PLAY:
logger.debug("DECK 1 PLAY/PAUSE")
self.deck_play_pause.emit('deck1')
elif note == DECK1_CUE:
logger.debug("DECK 1 CUE")
self.deck_cue.emit('deck1')
elif note == DECK2_PLAY:
logger.debug("DECK 2 PLAY/PAUSE")
self.deck_play_pause.emit('deck2')
elif note == DECK2_CUE:
logger.debug("DECK 2 CUE")
self.deck_cue.emit('deck2')
elif note in CART_NOTES:
slot = CART_NOTES[note]
logger.debug(f"CART {slot + 1} TRIGGER")
self.cart_trigger.emit(slot)
else:
logger.debug(f"Unmapped note: 0x{note:02X}")
self.cart_trigger.emit(CART_NOTES[note])
def _handle_cc(self, cc_num: int, value: int):
direction = _decode_relative_jog(value)
if direction == 0:
movement = _decode_relative_jog(value)
if movement == 0:
return
delta = direction * JOG_SCRUB_SECONDS
# Scale movement to seconds
delta = (movement / 10.0) * JOG_SCRUB_SECONDS
if cc_num == DECK1_JOG:
logger.debug(f"DECK 1 JOG delta={delta:+.1f}s")
self.deck_jog.emit('deck1', delta)
elif cc_num == DECK2_JOG:
logger.debug(f"DECK 2 JOG delta={delta:+.1f}s")
self.deck_jog.emit('deck2', delta)
else:
logger.debug(f"Unmapped CC: cc=0x{cc_num:02X} val={value}")
self.deck_jog.emit('deck2', delta)