Initial commit

This commit is contained in:
2026-01-14 19:18:40 +02:00
commit b19f454bf9
9 changed files with 761 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Ignore JetBrains project files
.idea/
# Ignore Python cache directories
__pycache__/
**/__pycache__/
# Ignore compiled Python files
*.pyc
*.pyo
*.pyd
# Ignore macOS system files
.DS_Store
**/.DS_Store

134
main.py Normal file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3.11
import itertools
import sys
import time
from typing import Optional, Tuple
import rtmidi
from xplane import XPlaneInput
G_STATUS_NAMES = {
0x80: "NOTE_OFF",
0x90: "NOTE_ON",
0xa0: "KEY_PRESSR",
0xb0: "CTRL_CHANGE",
0xc0: "PROG_CHANGE",
0xd0: "CHAN_PRESSR",
0xe0: "PITCH_BEND",
0xf0: "XXX",
}
def note_on(chan: int, note: int, velocity: int) -> Tuple[int, int, int]:
assert 0 < chan <= 16, chan
assert 0 <= note <= 0xFF, note
assert 0 <= velocity <= 0xFF, velocity
msg_byte = 0x90 + (chan - 1)
return msg_byte, note, velocity
def note_off(chan: int, note: int) -> Tuple[int, int, int]:
assert 0 < chan <= 16, chan
assert 0 <= note <= 0xFF, note
msg_byte = 0x80 + (chan - 1)
return msg_byte, note, 0
def get_status_name(status: int) -> str:
return G_STATUS_NAMES.get(status & 0xF0) or f'<? {status:02x} ?>'
def test_midi_out() -> int:
midi_out = rtmidi.MidiOut()
ports: list[str] = midi_out.get_ports()
print('Ports:')
for i, port_name in enumerate(ports):
print(f'{i:2d} {port_name}')
# nanokey_index: int = ([i for i, port_name in enumerate(ports) if 'nanoKEY' in port_name] or [-1])[0]
# if nanokey_index < 0:
# print(f'nanoKEY2 port -> NOT FOUND [{ports}]', file=sys.stderr)
# return 1
#
# print(f'nanoKEY2 port -> {nanokey_index} ({ports[nanokey_index]})')
# midi_out.open_port(nanokey_index)
midi_out.open_port(0)
with midi_out:
midi_out.send_message(note_on(1, 60, 112))
time.sleep(0.5)
midi_out.send_message(note_off(1, 60))
time.sleep(0.1)
return 0
def handle_midi_event(msg, midi_out):
midi_msg, delay = msg
status, b1, b2 = midi_msg
b_hi = b1 + (b2 << 8)
status_name = get_status_name(status)
# if status_name == "PITCH_BEND":
print(f'{status_name:11s} [ch {1 + (status & 0x0f)}] -> {b1:02x}[{b1:3d}] {b2:02x}[{b2:3d}] hi: {b_hi}')
# print(f'[d:{delay:2.2f}] {status_name:11s} [ch {1 + (status & 0x0f)}] -> {b1:02x}[{b1:3d}] {b2:02x}[{b2:3d}] hi: {b_hi}')
midi_out.send_message([status, b1, 127])
def set_all(midi_out, value):
for ch, btn_id in itertools.product([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 16], range(128)):
midi_out.send_message(note_on(ch, btn_id, value))
def test_midi_in() -> int:
midi_in = rtmidi.MidiIn()
ports: list[str] = midi_in.get_ports()
print('Ports:')
for i, port_name in enumerate(ports):
print(f'{i:2d} {port_name}')
midi_out = rtmidi.MidiOut()
port = 1
midi_in.set_callback(handle_midi_event, midi_out)
midi_in.open_port(port)
midi_out.open_port(port)
try:
set_all(midi_out, 127)
time.sleep(0.5)
set_all(midi_out, 0)
i = 0
levels = list(range(3))
while True:
if i >= len(levels):
i = -len(levels) + 1
midi_out.send_message(note_on(3, 2, levels[abs(i)]))
i += 1
time.sleep(0.5)
except KeyboardInterrupt:
print('CTRL+C TRIGGERED')
finally:
set_all(midi_out, 0)
midi_in.cancel_callback()
midi_in.close_port()
return 0
def main() -> int:
return test_midi_in()
# xp = XPlaneInput('127.0.0.1', 49009)
# xp.run()
# return 0
if __name__ == '__main__':
raise SystemExit(main())

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
python-rtmidi

272
roland.py Normal file
View File

@@ -0,0 +1,272 @@
from midi_controller import BasicControl, Button, ModeButton, Knob, Led, MultiLed, MidiMsg
class StepKnobButton(BasicControl):
STEP_CLOCKWISE = 1
STEP_COUNTERCLOCKWISE = 127
EVENT_TYPE_STEP = MidiMsg.EVENT_TYPE_CTRL_CHANGE
EVENT_TYPE_BUTTON = Button.EVENT_TYPE
def __init__(self, name: str, chan: int, ctrl_id: int, button: Button):
super().__init__(name, chan, ctrl_id)
self.button = button
class RolandDj202:
def __init__(self):
self.shiftButton = Button('SHIFT', chan=16, ctrl_id=0, led_class=Led) # both sides; LED might not work
# middle section
self.songSelectorKnob = StepKnobButton(
'SONG SELECTOR',
chan=16,
ctrl_id=0,
button=Button('SONG SELECTOR, BUTTON', chan=16, ctrl_id=6))
self.songSelectorKnobShift = StepKnobButton(
'SONG SELECTOR (SHIFT)',
chan=16,
ctrl_id=1,
button=Button('SONG SELECTOR, BUTTON (SHIFT)', chan=16, ctrl_id=7))
self.mixingKnob = Knob('MIXING', chan=16, ctrl_id=13, mid_=64)
self.phonesLevelKnob = Knob('PHONES LEVEL', chan=16, ctrl_id=12)
self.samplerLevelKnob = Knob('SAMPLER LEVEL', chan=16, ctrl_id=26)
self.cueMiddleButton = Button('CUE (MIDDLE)', chan=16, ctrl_id=29, led_class=Led)
self.sequencerSyncButton = Button('SEQUENCER: SYNC', chan=16, ctrl_id=83, led_class=Led)
self.sequencerSyncButtonShift = Button('SEQUENCER: SYNC', chan=16, ctrl_id=85, led_class=Led)
# SEQUENCER: START/STOP / TAP -> not assigned yet
# SEQUENCER: START/STOP / TAP -> sequencer event not assigned yet
self.crossfaderKnob = Knob('CROSSFADER', chan=16, ctrl_id=8, mid_=64)
# middle section (left and right), deck 1 and 2
self.loadBpmButtonLeft = Button('LOAD/BPM, LEFT', chan=16, ctrl_id=2, led_class=Led)
self.loadBpmButtonLeftShift = Button('LOAD/BPM, LEFT', chan=16, ctrl_id=19, led_class=Led)
self.trimKnobLeft = Knob('TRIM, LEFT', chan=1, ctrl_id=22, ctrl_id2=52)
self.hiKnobLeft = Knob('HI, LEFT', chan=1, ctrl_id=23, ctrl_id2=53)
self.midKnobLeft = Knob('MID, LEFT', chan=1, ctrl_id=24, ctrl_id2=54)
self.lowKnobLeft = Knob('LOW, LEFT', chan=1, ctrl_id=25, ctrl_id2=55)
self.filterKnobLeft = Knob('FILTER, LEFT', chan=1, ctrl_id=26, ctrl_id2=None)
self.cueTapButtonLeft = Button('CUE/TAP, LEFT', chan=1, ctrl_id=27, led_class=Led)
self.cueTapButtonLeftShift = Button('CUE/TAP, LEFT, SHIFT', chan=1, ctrl_id=18, led_class=Led)
self.faderKnobLeft = Knob('FADER, LEFT', chan=1, ctrl_id=28, ctrl_id2=58)
self.loadBpmButtonRight = Button('LOAD/BPM, RIGHT', chan=16, ctrl_id=3, led_class=Led)
self.loadBpmButtonRightShift = Button('LOAD/BPM, RIGHT', chan=16, ctrl_id=18, led_class=Led)
self.trimKnobRight = Knob('TRIM, RIGHT', chan=2, ctrl_id=22, ctrl_id2=52)
self.hiKnobRight = Knob('HI, RIGHT', chan=2, ctrl_id=23, ctrl_id2=53)
self.midKnobRight = Knob('MID, RIGHT', chan=2, ctrl_id=24, ctrl_id2=54)
self.lowKnobRight = Knob('LOW, RIGHT', chan=2, ctrl_id=25, ctrl_id2=55)
self.filterKnobRight = Knob('FILTER, RIGHT', chan=2, ctrl_id=26, ctrl_id2=None)
self.cueTapButtonRight = Button('CUE/TAP, RIGHT', chan=2, ctrl_id=27, led_class=Led)
self.cueTapButtonRightShift = Button('CUE/TAP, RIGHT, SHIFT', chan=2, ctrl_id=18, led_class=Led)
self.faderKnobRight = Knob('FADER, RIGHT', chan=2, ctrl_id=28, ctrl_id2=58)
# fx section (left and right), deck 1 and 2
self.fx1LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 1), LEFT', chan=9, ctrl_id=0, ctrl_id2=None)
self.fx2LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 2), LEFT', chan=9, ctrl_id=1, ctrl_id2=None)
self.fx3LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 3), LEFT', chan=9, ctrl_id=2, ctrl_id2=None)
self.fx4LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 4), LEFT, SHIFT', chan=9, ctrl_id=3, ctrl_id2=None)
self.fx1ButtonLeft = Button('FX 1, LEFT', chan=9, ctrl_id=0, led_class=Led)
self.fx2ButtonLeft = Button('FX 2, LEFT', chan=9, ctrl_id=1, led_class=Led)
self.fx3ButtonLeft = Button('FX 3, LEFT', chan=9, ctrl_id=2, led_class=Led)
self.tapFxModeButtonLeft = Button('TAP/FX MODE, LEFT', chan=9, ctrl_id=4, led_class=Led)
self.fx1ButtonLeftShift = Button('FX 1, LEFT, SHIFT', chan=9, ctrl_id=11, led_class=Led)
self.fx2ButtonLeftShift = Button('FX 2, LEFT, SHIFT', chan=9, ctrl_id=12, led_class=Led)
self.fx3ButtonLeftShift = Button('FX 3, LEFT, SHIFT', chan=9, ctrl_id=13, led_class=Led)
self.tapFxModeButtonLeftShift = Button('TAP/FX MODE, LEFT, SHIFT', chan=9, ctrl_id=10, led_class=Led)
self.fx1LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 1), RIGHT', chan=10, ctrl_id=0, ctrl_id2=None)
self.fx2LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 2), RIGHT', chan=10, ctrl_id=1, ctrl_id2=None)
self.fx3LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 3), RIGHT', chan=10, ctrl_id=2, ctrl_id2=None)
self.fx4LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 4), RIGHT, SHIFT', chan=10, ctrl_id=3, ctrl_id2=None)
self.fx1ButtonRight = Button('FX 1, RIGHT', chan=10, ctrl_id=0, led_class=Led)
self.fx2ButtonRight = Button('FX 2, RIGHT', chan=10, ctrl_id=1, led_class=Led)
self.fx3ButtonRight = Button('FX 3, RIGHT', chan=10, ctrl_id=2, led_class=Led)
self.tapFxModeButtonRight = Button('TAP/FX MODE, RIGHT', chan=10, ctrl_id=4, led_class=Led)
self.fx1ButtonRightShift = Button('FX 1, RIGHT, SHIFT', chan=10, ctrl_id=11, led_class=Led)
self.fx2ButtonRightShift = Button('FX 2, RIGHT, SHIFT', chan=10, ctrl_id=12, led_class=Led)
self.fx3ButtonRightShift = Button('FX 3, RIGHT, SHIFT', chan=10, ctrl_id=13, led_class=Led)
self.tapFxModeButtonRightShift = Button('TAP/FX MODE, RIGHT, SHIFT', chan=10, ctrl_id=10, led_class=Led)
# general deck section (left and right), deck 1 and 2
self.tempoKnobLeft = Knob('TEMPO, LEFT', chan=1, ctrl_id=9, ctrl_id2=59, mid_=64)
self.vinylSlipButtonLeft = Button('VYNIL/SLIP, LEFT', chan=1, ctrl_id=7, led_class=Led)
# JOG WHEELS not yet assigned
self.syncButtonLeft = Button('SYNC, LEFT', chan=1, ctrl_id=2, led_class=Led)
self.syncButtonLeftShift = Button('SYNC, LEFT, SHIFT', chan=1, ctrl_id=3, led_class=Led)
self.cueButtonLeft = Button('CUE, LEFT', chan=1, ctrl_id=1, led_class=Led)
self.cueButtonLeftShift = Button('CUE, LEFT, SHIFT', chan=1, ctrl_id=5, led_class=Led)
self.playButtonLeft = Button('PLAY/PAUSE, LEFT', chan=1, ctrl_id=0, led_class=Led)
self.playButtonLeftShift = Button('PLAY/PAUSE, LEFT, SHIFT', chan=1, ctrl_id=4, led_class=Led)
self.keyLockRangeLeft = Button('KEY LOCK/RANGE, LEFT', chan=1, ctrl_id=13, led_class=Led)
self.keyLockRangeLeftShift = Button('KEY LOCK/RANGE, LEFT, SHIFT', chan=1, ctrl_id=14, led_class=Led)
self.tempoKnobRight = Knob('TEMPO, RIGHT', chan=2, ctrl_id=9, ctrl_id2=59, mid_=64)
self.vinylSlipButtonRight = Button('VYNIL/SLIP, RIGHT', chan=2, ctrl_id=7, led_class=Led)
# JOG WHEELS not yet assigned
self.syncButtonRight = Button('SYNC, RIGHT', chan=2, ctrl_id=2, led_class=Led)
self.syncButtonRightShift = Button('SYNC, RIGHT, SHIFT', chan=2, ctrl_id=3, led_class=Led)
self.cueButtonRight = Button('CUE, RIGHT', chan=2, ctrl_id=1, led_class=Led)
self.cueButtonRightShift = Button('CUE, RIGHT, SHIFT', chan=2, ctrl_id=5, led_class=Led)
self.playButtonRight = Button('PLAY/PAUSE, RIGHT', chan=2, ctrl_id=0, led_class=Led)
self.playButtonRightShift = Button('PLAY/PAUSE, RIGHT, SHIFT', chan=2, ctrl_id=4, led_class=Led)
self.keyLockRangeRight = Button('KEY LOCK/RANGE, RIGHT', chan=2, ctrl_id=13, led_class=Led)
self.keyLockRangeRightShift = Button('KEY LOCK/RANGE, RIGHT, SHIFT', chan=2, ctrl_id=14, led_class=Led)
# pads mode section (left and right), deck 1 and 2
self.padModeHotCueLeft = ModeButton('PAD MODE: HOT CUE / CUE LOOP, LEFT', chan=5, ctrl_id=0, value=3)
self.padModeLoopRollLeft = ModeButton('PAD MODE: LOOP / ROLL, LEFT', chan=5, ctrl_id=0, value=19)
self.padModeSequencerPattern1Left = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), LEFT', chan=5, ctrl_id=0, value=32)
self.padModeSequencerPattern2Left = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), LEFT', chan=5, ctrl_id=0, value=34)
self.padModeSequencerPattern1LeftShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), LEFT, SHIFT', chan=5, ctrl_id=0, value=33)
self.padModeSequencerPattern2LeftShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), LEFT, SHIFT', chan=5, ctrl_id=0, value=35)
self.padModeSamplerSlicerLeft = ModeButton('PAD MODE: SAMPLER / SLICER, LEFT', chan=5, ctrl_id=0, value=51)
self.padModeHotCueRight = ModeButton('PAD MODE: HOT CUE / CUE LOOP, RIGHT', chan=6, ctrl_id=0, value=3)
self.padModeLoopRollRight = ModeButton('PAD MODE: LOOP / ROLL, RIGHT', chan=6, ctrl_id=0, value=19)
self.padModeSequencerPattern1Right = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), RIGHT', chan=6, ctrl_id=0, value=32)
self.padModeSequencerPattern2Right = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), RIGHT', chan=6, ctrl_id=0, value=34)
self.padModeSequencerPattern1RightShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), RIGHT, SHIFT', chan=6, ctrl_id=0, value=33)
self.padModeSequencerPattern2RightShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), RIGHT, SHIFT', chan=6, ctrl_id=0, value=35)
self.padModeSamplerSlicerRight = ModeButton('PAD MODE: SAMPLER / SLICER, RIGHT', chan=6, ctrl_id=0, value=51)
# pads mode 3 (HOT CUE / CUE LOOP) -> deck 1 and 2
self.mode3ParamMinusButtonLeft = Button('MODE 3: PARAM MINUS, LEFT', chan=5, ctrl_id=67, led_class=Led)
self.mode3ParamPlusButtonLeft = Button('MODE 3: PARAM PLUS, LEFT', chan=5, ctrl_id=68, led_class=Led)
self.mode3Pad1ButtonLeft = Button('MODE 3: PAD 1, LEFT', chan=5, ctrl_id=1, led_class=MultiLed)
self.mode3Pad2ButtonLeft = Button('MODE 3: PAD 2, LEFT', chan=5, ctrl_id=2, led_class=MultiLed)
self.mode3Pad3ButtonLeft = Button('MODE 3: PAD 3, LEFT', chan=5, ctrl_id=3, led_class=MultiLed)
self.mode3Pad4ButtonLeft = Button('MODE 3: PAD 4, LEFT', chan=5, ctrl_id=4, led_class=MultiLed)
self.mode3Pad5ButtonLeft = Button('MODE 3: PAD 5, LEFT', chan=5, ctrl_id=5, led_class=MultiLed)
self.mode3Pad6ButtonLeft = Button('MODE 3: PAD 6, LEFT', chan=5, ctrl_id=6, led_class=MultiLed)
self.mode3Pad7ButtonLeft = Button('MODE 3: PAD 7, LEFT', chan=5, ctrl_id=7, led_class=MultiLed)
self.mode3Pad8ButtonLeft = Button('MODE 3: PAD 8, LEFT', chan=5, ctrl_id=8, led_class=MultiLed)
self.mode3ParamMinusButtonLeftShift = Button('MODE 3: PARAM MINUS, LEFT, SHIFT', chan=5, ctrl_id=75, led_class=Led)
self.mode3ParamPlusButtonLeftShift = Button('MODE 3: PARAM PLUS, LEFT, SHIFT', chan=5, ctrl_id=76, led_class=Led)
self.mode3Pad1ButtonLeftShift = Button('MODE 3: PAD 1, LEFT, SHIFT', chan=5, ctrl_id=9, led_class=MultiLed)
self.mode3Pad2ButtonLeftShift = Button('MODE 3: PAD 2, LEFT, SHIFT', chan=5, ctrl_id=10, led_class=MultiLed)
self.mode3Pad3ButtonLeftShift = Button('MODE 3: PAD 3, LEFT, SHIFT', chan=5, ctrl_id=11, led_class=MultiLed)
self.mode3Pad4ButtonLeftShift = Button('MODE 3: PAD 4, LEFT, SHIFT', chan=5, ctrl_id=12, led_class=MultiLed)
self.mode3Pad5ButtonLeftShift = Button('MODE 3: PAD 5, LEFT, SHIFT', chan=5, ctrl_id=13, led_class=MultiLed)
self.mode3Pad6ButtonLeftShift = Button('MODE 3: PAD 6, LEFT, SHIFT', chan=5, ctrl_id=14, led_class=MultiLed)
self.mode3Pad7ButtonLeftShift = Button('MODE 3: PAD 7, LEFT, SHIFT', chan=5, ctrl_id=15, led_class=MultiLed)
self.mode3Pad8ButtonLeftShift = Button('MODE 3: PAD 8, LEFT, SHIFT', chan=5, ctrl_id=16, led_class=MultiLed)
self.mode3ParamMinusButtonRight = Button('MODE 3: PARAM MINUS, RIGHT', chan=6, ctrl_id=67, led_class=Led)
self.mode3ParamPlusButtonRight = Button('MODE 3: PARAM PLUS, RIGHT', chan=6, ctrl_id=68, led_class=Led)
self.mode3Pad1ButtonRight = Button('MODE 3: PAD 1, RIGHT', chan=6, ctrl_id=1, led_class=MultiLed)
self.mode3Pad2ButtonRight = Button('MODE 3: PAD 2, RIGHT', chan=6, ctrl_id=2, led_class=MultiLed)
self.mode3Pad3ButtonRight = Button('MODE 3: PAD 3, RIGHT', chan=6, ctrl_id=3, led_class=MultiLed)
self.mode3Pad4ButtonRight = Button('MODE 3: PAD 4, RIGHT', chan=6, ctrl_id=4, led_class=MultiLed)
self.mode3Pad5ButtonRight = Button('MODE 3: PAD 5, RIGHT', chan=6, ctrl_id=5, led_class=MultiLed)
self.mode3Pad6ButtonRight = Button('MODE 3: PAD 6, RIGHT', chan=6, ctrl_id=6, led_class=MultiLed)
self.mode3Pad7ButtonRight = Button('MODE 3: PAD 7, RIGHT', chan=6, ctrl_id=7, led_class=MultiLed)
self.mode3Pad8ButtonRight = Button('MODE 3: PAD 8, RIGHT', chan=6, ctrl_id=8, led_class=MultiLed)
self.mode3ParamMinusButtonRightShift = Button('MODE 3: PARAM MINUS, RIGHT, SHIFT', chan=6, ctrl_id=75, led_class=Led)
self.mode3ParamPlusButtonRightShift = Button('MODE 3: PARAM PLUS, RIGHT, SHIFT', chan=6, ctrl_id=76, led_class=Led)
self.mode3Pad1ButtonRightShift = Button('MODE 3: PAD 1, RIGHT, SHIFT', chan=6, ctrl_id=9, led_class=MultiLed)
self.mode3Pad2ButtonRightShift = Button('MODE 3: PAD 2, RIGHT, SHIFT', chan=6, ctrl_id=10, led_class=MultiLed)
self.mode3Pad3ButtonRightShift = Button('MODE 3: PAD 3, RIGHT, SHIFT', chan=6, ctrl_id=11, led_class=MultiLed)
self.mode3Pad4ButtonRightShift = Button('MODE 3: PAD 4, RIGHT, SHIFT', chan=6, ctrl_id=12, led_class=MultiLed)
self.mode3Pad5ButtonRightShift = Button('MODE 3: PAD 5, RIGHT, SHIFT', chan=6, ctrl_id=13, led_class=MultiLed)
self.mode3Pad6ButtonRightShift = Button('MODE 3: PAD 6, RIGHT, SHIFT', chan=6, ctrl_id=14, led_class=MultiLed)
self.mode3Pad7ButtonRightShift = Button('MODE 3: PAD 7, RIGHT, SHIFT', chan=6, ctrl_id=15, led_class=MultiLed)
self.mode3Pad8ButtonRightShift = Button('MODE 3: PAD 8, RIGHT, SHIFT', chan=6, ctrl_id=16, led_class=MultiLed)
# pads mode 19 (LOOP / ROLL) -> deck 1 and 2
self.mode19ParamMinusButtonLeft = Button('MODE 19: PARAM MINUS, LEFT', chan=5, ctrl_id=65, led_class=Led)
self.mode19ParamPlusButtonLeft = Button('MODE 19: PARAM PLUS, LEFT', chan=5, ctrl_id=66, led_class=Led)
self.mode19Pad1ButtonLeft = Button('MODE 19: PAD 1, LEFT', chan=5, ctrl_id=17, led_class=MultiLed)
self.mode19Pad2ButtonLeft = Button('MODE 19: PAD 2, LEFT', chan=5, ctrl_id=18, led_class=MultiLed)
self.mode19Pad3ButtonLeft = Button('MODE 19: PAD 3, LEFT', chan=5, ctrl_id=19, led_class=MultiLed)
self.mode19Pad4ButtonLeft = Button('MODE 19: PAD 4, LEFT', chan=5, ctrl_id=20, led_class=MultiLed)
self.mode19Pad5ButtonLeft = Button('MODE 19: PAD 5, LEFT', chan=5, ctrl_id=21, led_class=MultiLed)
self.mode19Pad6ButtonLeft = Button('MODE 19: PAD 6, LEFT', chan=5, ctrl_id=22, led_class=MultiLed)
self.mode19Pad7ButtonLeft = Button('MODE 19: PAD 7, LEFT', chan=5, ctrl_id=23, led_class=MultiLed)
self.mode19Pad8ButtonLeft = Button('MODE 19: PAD 8, LEFT', chan=5, ctrl_id=24, led_class=MultiLed)
self.mode19ParamMinusButtonLeftShift = Button('MODE 19: PARAM MINUS, LEFT, SHIFT', chan=5, ctrl_id=73, led_class=Led)
self.mode19ParamPlusButtonLeftShift = Button('MODE 19: PARAM PLUS, LEFT, SHIFT', chan=5, ctrl_id=74, led_class=Led)
self.mode19Pad1ButtonLeftShift = Button('MODE 19: PAD 1, LEFT, SHIFT', chan=5, ctrl_id=25, led_class=MultiLed)
self.mode19Pad2ButtonLeftShift = Button('MODE 19: PAD 2, LEFT, SHIFT', chan=5, ctrl_id=26, led_class=MultiLed)
self.mode19Pad3ButtonLeftShift = Button('MODE 19: PAD 3, LEFT, SHIFT', chan=5, ctrl_id=27, led_class=MultiLed)
self.mode19Pad4ButtonLeftShift = Button('MODE 19: PAD 4, LEFT, SHIFT', chan=5, ctrl_id=28, led_class=MultiLed)
self.mode19Pad5ButtonLeftShift = Button('MODE 19: PAD 5, LEFT, SHIFT', chan=5, ctrl_id=29, led_class=MultiLed)
self.mode19Pad6ButtonLeftShift = Button('MODE 19: PAD 6, LEFT, SHIFT', chan=5, ctrl_id=30, led_class=MultiLed)
self.mode19Pad7ButtonLeftShift = Button('MODE 19: PAD 7, LEFT, SHIFT', chan=5, ctrl_id=31, led_class=MultiLed)
self.mode19Pad8ButtonLeftShift = Button('MODE 19: PAD 8, LEFT, SHIFT', chan=5, ctrl_id=32, led_class=MultiLed)
self.mode19ParamMinusButtonRight = Button('MODE 19: PARAM MINUS, RIGHT', chan=6, ctrl_id=65, led_class=Led)
self.mode19ParamPlusButtonRight = Button('MODE 19: PARAM PLUS, RIGHT', chan=6, ctrl_id=66, led_class=Led)
self.mode19Pad1ButtonRight = Button('MODE 19: PAD 1, RIGHT', chan=6, ctrl_id=17, led_class=MultiLed)
self.mode19Pad2ButtonRight = Button('MODE 19: PAD 2, RIGHT', chan=6, ctrl_id=18, led_class=MultiLed)
self.mode19Pad3ButtonRight = Button('MODE 19: PAD 3, RIGHT', chan=6, ctrl_id=19, led_class=MultiLed)
self.mode19Pad4ButtonRight = Button('MODE 19: PAD 4, RIGHT', chan=6, ctrl_id=20, led_class=MultiLed)
self.mode19Pad5ButtonRight = Button('MODE 19: PAD 5, RIGHT', chan=6, ctrl_id=21, led_class=MultiLed)
self.mode19Pad6ButtonRight = Button('MODE 19: PAD 6, RIGHT', chan=6, ctrl_id=22, led_class=MultiLed)
self.mode19Pad7ButtonRight = Button('MODE 19: PAD 7, RIGHT', chan=6, ctrl_id=23, led_class=MultiLed)
self.mode19Pad8ButtonRight = Button('MODE 19: PAD 8, RIGHT', chan=6, ctrl_id=24, led_class=MultiLed)
self.mode19ParamMinusButtonRightShift = Button('MODE 19: PARAM MINUS, RIGHT, SHIFT', chan=6, ctrl_id=73, led_class=Led)
self.mode19ParamPlusButtonRightShift = Button('MODE 19: PARAM PLUS, RIGHT, SHIFT', chan=6, ctrl_id=74, led_class=Led)
self.mode19Pad1ButtonRightShift = Button('MODE 19: PAD 1, RIGHT, SHIFT', chan=6, ctrl_id=25, led_class=MultiLed)
self.mode19Pad2ButtonRightShift = Button('MODE 19: PAD 2, RIGHT, SHIFT', chan=6, ctrl_id=26, led_class=MultiLed)
self.mode19Pad3ButtonRightShift = Button('MODE 19: PAD 3, RIGHT, SHIFT', chan=6, ctrl_id=27, led_class=MultiLed)
self.mode19Pad4ButtonRightShift = Button('MODE 19: PAD 4, RIGHT, SHIFT', chan=6, ctrl_id=28, led_class=MultiLed)
self.mode19Pad5ButtonRightShift = Button('MODE 19: PAD 5, RIGHT, SHIFT', chan=6, ctrl_id=29, led_class=MultiLed)
self.mode19Pad6ButtonRightShift = Button('MODE 19: PAD 6, RIGHT, SHIFT', chan=6, ctrl_id=30, led_class=MultiLed)
self.mode19Pad7ButtonRightShift = Button('MODE 19: PAD 7, RIGHT, SHIFT', chan=6, ctrl_id=31, led_class=MultiLed)
self.mode19Pad8ButtonRightShift = Button('MODE 19: PAD 8, RIGHT, SHIFT', chan=6, ctrl_id=32, led_class=MultiLed)
# pads mode 51 (SAMPLER / SLICER) -> deck 1 and 2
self.mode51ParamMinusButtonLeft = Button('MODE 51: PARAM MINUS, LEFT', chan=5, ctrl_id=69, led_class=Led)
self.mode51ParamPlusButtonLeft = Button('MODE 51: PARAM PLUS, LEFT', chan=5, ctrl_id=70, led_class=Led)
self.mode51Pad1ButtonLeft = Button('MODE 51: PAD 1, LEFT', chan=5, ctrl_id=33, led_class=MultiLed)
self.mode51Pad2ButtonLeft = Button('MODE 51: PAD 2, LEFT', chan=5, ctrl_id=34, led_class=MultiLed)
self.mode51Pad3ButtonLeft = Button('MODE 51: PAD 3, LEFT', chan=5, ctrl_id=35, led_class=MultiLed)
self.mode51Pad4ButtonLeft = Button('MODE 51: PAD 4, LEFT', chan=5, ctrl_id=36, led_class=MultiLed)
self.mode51Pad5ButtonLeft = Button('MODE 51: PAD 5, LEFT', chan=5, ctrl_id=37, led_class=MultiLed)
self.mode51Pad6ButtonLeft = Button('MODE 51: PAD 6, LEFT', chan=5, ctrl_id=38, led_class=MultiLed)
self.mode51Pad7ButtonLeft = Button('MODE 51: PAD 7, LEFT', chan=5, ctrl_id=39, led_class=MultiLed)
self.mode51Pad8ButtonLeft = Button('MODE 51: PAD 8, LEFT', chan=5, ctrl_id=40, led_class=MultiLed)
self.mode51ParamMinusButtonLeftShift = Button('MODE 51: PARAM MINUS, LEFT, SHIFT', chan=5, ctrl_id=77, led_class=Led)
self.mode51ParamPlusButtonLeftShift = Button('MODE 51: PARAM PLUS, LEFT, SHIFT', chan=5, ctrl_id=78, led_class=Led)
self.mode51Pad1ButtonLeftShift = Button('MODE 51: PAD 1, LEFT, SHIFT', chan=5, ctrl_id=41, led_class=MultiLed)
self.mode51Pad2ButtonLeftShift = Button('MODE 51: PAD 2, LEFT, SHIFT', chan=5, ctrl_id=42, led_class=MultiLed)
self.mode51Pad3ButtonLeftShift = Button('MODE 51: PAD 3, LEFT, SHIFT', chan=5, ctrl_id=43, led_class=MultiLed)
self.mode51Pad4ButtonLeftShift = Button('MODE 51: PAD 4, LEFT, SHIFT', chan=5, ctrl_id=44, led_class=MultiLed)
self.mode51Pad5ButtonLeftShift = Button('MODE 51: PAD 5, LEFT, SHIFT', chan=5, ctrl_id=45, led_class=MultiLed)
self.mode51Pad6ButtonLeftShift = Button('MODE 51: PAD 6, LEFT, SHIFT', chan=5, ctrl_id=46, led_class=MultiLed)
self.mode51Pad7ButtonLeftShift = Button('MODE 51: PAD 7, LEFT, SHIFT', chan=5, ctrl_id=47, led_class=MultiLed)
self.mode51Pad8ButtonLeftShift = Button('MODE 51: PAD 8, LEFT, SHIFT', chan=5, ctrl_id=48, led_class=MultiLed)
self.mode51ParamMinusButtonRight = Button('MODE 51: PARAM MINUS, RIGHT', chan=6, ctrl_id=69, led_class=Led)
self.mode51ParamPlusButtonRight = Button('MODE 51: PARAM PLUS, RIGHT', chan=6, ctrl_id=70, led_class=Led)
self.mode51Pad1ButtonRight = Button('MODE 51: PAD 1, RIGHT', chan=6, ctrl_id=33, led_class=MultiLed)
self.mode51Pad2ButtonRight = Button('MODE 51: PAD 2, RIGHT', chan=6, ctrl_id=34, led_class=MultiLed)
self.mode51Pad3ButtonRight = Button('MODE 51: PAD 3, RIGHT', chan=6, ctrl_id=35, led_class=MultiLed)
self.mode51Pad4ButtonRight = Button('MODE 51: PAD 4, RIGHT', chan=6, ctrl_id=36, led_class=MultiLed)
self.mode51Pad5ButtonRight = Button('MODE 51: PAD 5, RIGHT', chan=6, ctrl_id=37, led_class=MultiLed)
self.mode51Pad6ButtonRight = Button('MODE 51: PAD 6, RIGHT', chan=6, ctrl_id=38, led_class=MultiLed)
self.mode51Pad7ButtonRight = Button('MODE 51: PAD 7, RIGHT', chan=6, ctrl_id=39, led_class=MultiLed)
self.mode51Pad8ButtonRight = Button('MODE 51: PAD 8, RIGHT', chan=6, ctrl_id=40, led_class=MultiLed)
self.mode51ParamMinusButtonRightShift = Button('MODE 51: PARAM MINUS, RIGHT, SHIFT', chan=6, ctrl_id=77, led_class=Led)
self.mode51ParamPlusButtonRightShift = Button('MODE 51: PARAM PLUS, RIGHT, SHIFT', chan=6, ctrl_id=78, led_class=Led)
self.mode51Pad1ButtonRightShift = Button('MODE 51: PAD 1, RIGHT, SHIFT', chan=6, ctrl_id=41, led_class=MultiLed)
self.mode51Pad2ButtonRightShift = Button('MODE 51: PAD 2, RIGHT, SHIFT', chan=6, ctrl_id=42, led_class=MultiLed)
self.mode51Pad3ButtonRightShift = Button('MODE 51: PAD 3, RIGHT, SHIFT', chan=6, ctrl_id=43, led_class=MultiLed)
self.mode51Pad4ButtonRightShift = Button('MODE 51: PAD 4, RIGHT, SHIFT', chan=6, ctrl_id=44, led_class=MultiLed)
self.mode51Pad5ButtonRightShift = Button('MODE 51: PAD 5, RIGHT, SHIFT', chan=6, ctrl_id=45, led_class=MultiLed)
self.mode51Pad6ButtonRightShift = Button('MODE 51: PAD 6, RIGHT, SHIFT', chan=6, ctrl_id=46, led_class=MultiLed)
self.mode51Pad7ButtonRightShift = Button('MODE 51: PAD 7, RIGHT, SHIFT', chan=6, ctrl_id=47, led_class=MultiLed)
self.mode51Pad8ButtonRightShift = Button('MODE 51: PAD 8, RIGHT, SHIFT', chan=6, ctrl_id=48, led_class=MultiLed)
# deck switch buttons
self.deck3Button = Button('DECK 3', chan=1, ctrl_id=8, led_class=Led)
self.deck4Button = Button('DECK 4', chan=2, ctrl_id=8, led_class=Led)

0
src/__init__.py Normal file
View File

247
src/midi_controller.py Normal file
View File

@@ -0,0 +1,247 @@
from array import array
from dataclasses import dataclass
from typing import Optional, List
MIDI_MSG_TYPES = {
0x80: "NOTE_OFF",
0x90: "NOTE_ON",
0xa0: "KEY_PRESSURE",
0xb0: "CTRL_CHANGE",
0xc0: "PROG_CHANGE",
0xd0: "CHAN_PRESSURE",
0xe0: "PITCH_BEND",
0xf0: "SYSTEM",
}
class MidiMsg:
"""
Represents a single MIDI message
"""
EVENT_TYPE_NOTE_OFF = 0x80
EVENT_TYPE_NOTE_ON = 0x90
EVENT_TYPE_KEY_PRESSURE = 0xa0
EVENT_TYPE_CTRL_CHANGE = 0xb0
EVENT_TYPE_PROG_CHANGE = 0xc0
EVENT_TYPE_CHAN_PRESSURE = 0xd0
EVENT_TYPE_PITCH_BEND = 0xe0
EVENT_TYPE_SYSTEM = 0xf0
@classmethod
def create(cls, status_byte: int, data0: int, data1: int = 0) -> 'MidiMsg':
return cls(array("B", (status_byte, data0, data1)))
@classmethod
def create_from_bytes(cls, byte_array: array) -> 'MidiMsg':
return cls(byte_array)
def __init__(self, b: array):
self.__bytes = b
def __str__(self) -> str:
return f'{self.msg_type_str}(chan={self.channel}, b0={self.data0}, b1={self.data1})'
@property
def msg_type(self) -> int:
return self.__bytes[0] & 0xf0
@property
def msg_type_str(self) -> str:
return MIDI_MSG_TYPES.get(self.msg_type, "<ERROR>")
@property
def channel(self) -> int:
return (self.__bytes[0] & 0x0f) + 1
@property
def data0(self) -> int:
return self.__bytes[1]
@property
def data1(self) -> int:
return self.__bytes[2]
def as_tuple(self) -> (int, int, int):
return tuple(self.__bytes)
class BasicControl:
MIN_STATE = 0x00
MAX_STATE = 0x7f
DEFAULT_STATE = MIN_STATE
def __init__(self, name: str, midi_channel: int, control_id: int, state: int = DEFAULT_STATE):
"""
:param name: name of this control
:param midi_channel: MIDI channel this control operates on
:param control_id: MIDI control (button/knob/led) id (first MIDI data byte)
:param state: initial state of this control
"""
assert 1 <= midi_channel <= 16, f'invalid MIDI channel {midi_channel} (expected 1..16)'
assert 0 <= control_id <= 127, f'invalid control id {control_id} (expected 0..127)'
assert 0 <= state <= 127, f'invalid state {state} (expected 0..127)'
self.name: str = name
self._channel: int = midi_channel
self._id: int = control_id
self._state: int = state
def __str__(self) -> str:
"""
:return: name of this control
"""
return self.name
###############################################################################################
# PROPERTIES ##################################################################################
###############################################################################################
@property
def id(self) -> int:
"""
:return: control id
"""
return self._id
@property
def channel(self) -> int:
"""
:return: MIDI channel this device operates on
"""
return self.channel
@property
def state(self) -> int:
"""
:return: current state of this control
"""
return self._state
###############################################################################################
# MUTATION ####################################################################################
###############################################################################################
def change_state(self, state: int):
"""Change/set current state of this control
:param state: new state of this control
"""
if not (self.MIN_STATE <= state <= self.MAX_STATE):
raise ValueError(f'invalid state: {state} (expected {self.MIN_STATE}..{self.MAX_STATE})')
self._state = state
# TODO: notify state changed
class Led(BasicControl):
LIGHT_OFF = 0x00
LIGHT_ON = 0x7f
def __init__(self, name: str, chan: int, ctrl_id: int, states: List[int] = (LIGHT_OFF, LIGHT_ON)):
"""
:param name: name of this LED control
:param chan: MIDI channel used for this LED
:param ctrl_id: MIDI control ID
:param states: all available states, from the dimmest to the brightest
"""
assert states, 'states cannot be empty'
super().__init__(name, chan, ctrl_id, state=states[0])
class MultiLed(Led):
LIGHT_OFF = 0x00
LIGHT_DIM = 0x01
LIGHT_ON = 0x02
def __init__(self, name: str, chan: int, ctrl_id: int, states: List[int] = (LIGHT_OFF, LIGHT_DIM, LIGHT_ON)):
"""
:param name: name of this LED control
:param chan: MIDI channel used for this LED
:param ctrl_id: MIDI control ID
:param states: all available states, from the dimmest to the brightest
"""
assert states, 'states cannot be empty'
super().__init__(name, chan, ctrl_id, states)
class Button(BasicControl):
STATE_RELEASED = BasicControl.MIN_STATE
STATE_PRESSED = BasicControl.MAX_STATE
EVENT_TYPE = MidiMsg.EVENT_TYPE_NOTE_ON
def __init__(self, name: str, chan: int, ctrl_id: int, led_class: Optional[type] = None):
"""
:param name: name of this button
:param chan: MIDI channel used for this button
:param ctrl_id: button ID (i.e., MIDI data byte 1)
:param led_class: (optional) LED control associated with this button
"""
super().__init__(name, chan, ctrl_id, state=self.STATE_RELEASED)
self._led: Optional[Led] = led_class and led_class(f'LED: {name}', chan, ctrl_id)
###############################################################################################
# PROPERTIES ##################################################################################
###############################################################################################
@property
def led(self) -> Optional[Led]:
return self._led
@property
def has_led(self) -> bool:
"""
:return: True if this button has an LED
"""
return self._led is not None
@property
def is_pressed(self):
"""
:return: True if this button is currently pressed
"""
return self.state == self.STATE_PRESSED
class ModeButton(BasicControl):
EVENT_TYPE = MidiMsg.EVENT_TYPE_NOTE_ON
def __init__(self, name: str, chan: int, ctrl_id: int, value: int):
"""
:param name: name of this button
:param chan: MIDI channel used for this button
:param ctrl_id: button ID (i.e., MIDI data byte 1)
:param value: button mode (i.e., MIDI data byte 2)
"""
super().__init__(name, chan, ctrl_id)
self.value = value
class Knob(BasicControl):
STATE_MAX = 0x7f
STATE_MIN = 0x00
EVENT_TYPE = MidiMsg.EVENT_TYPE_CTRL_CHANGE
def __init__(self, name: str, chan: int, ctrl_id: int, ctrl_id2: int = None, min_: int = STATE_MIN, max_: int = STATE_MAX, mid_: Optional[int] = None):
"""
:param name: name of the knob
:param chan: MIDI channel
:param ctrl_id: control id
:param ctrl_id2: secondary control id for precision marks (0, 64, 112)
:param min_: minimal value of the knob (usually 0)
:param max_: maximal value of the knob (usually 127, or 0x7f)
:param mid_: midpoint val of the knob, or None if no specific midpoint
"""
super().__init__(name, chan, ctrl_id, state=min_)
self.precision_ctrl_id = ctrl_id2 # TODO: actually do something with this control
self.min = min_
self.max = max_
self.mid = mid_
@property
def has_midpoint(self) -> bool:
"""
:return: True if this knob has a concrete midpoint; False if midpoint is somewhere between min and max
"""
return self.mid is not None

0
test/__init__.py Normal file
View File

View File

@@ -0,0 +1,30 @@
from array import array
from unittest import TestCase
from midi_controller import MidiMsg
class TestMidiMsg(TestCase):
def test_create(self):
msg = MidiMsg.create(0x92, 127, 127)
self.assertEqual(0x90, msg.msg_type)
self.assertEqual('NOTE_ON', msg.msg_type_str)
self.assertEqual(3, msg.channel)
self.assertEqual(127, msg.data0)
self.assertEqual(127, msg.data1)
self.assertEqual((0x92, 0x7f, 0x7f), msg.as_tuple())
def test_create_from_bytes(self):
msg = MidiMsg.create_from_bytes(array('B', (0x8f, 0x00, 0x7f)))
self.assertEqual(0x80, msg.msg_type)
self.assertEqual('NOTE_OFF', msg.msg_type_str)
self.assertEqual(16, msg.channel)
self.assertEqual(0, msg.data0)
self.assertEqual(127, msg.data1)
self.assertEqual((0x8f, 0x00, 0x7f), msg.as_tuple())
def test_equality(self):
m1 = MidiMsg.create_from_bytes(array('B', (0x92, 0x00, 0x7f)))
m2 = MidiMsg.create(0x92, 0x00, 0x7f)
self.assertEqual(m1, m2)
self.assertIsNot(m1, m2)

61
xplane.py Normal file
View File

@@ -0,0 +1,61 @@
import array
import socket
import sys
from collections import namedtuple
from struct import unpack
from typing import Iterator
DataItem = namedtuple('DataItem', ('index', 'values'))
def parse_raw(b: bytes, offset: int = 4) -> str:
return b.decode('latin')[:offset] + ' ' + ' '.join(f'{c:02x}' for c in b[offset:])
def str_dataitem(di: DataItem) -> str:
values = ' '.join(f'{v:10.4f}' for v in di.values)
return f'[{di.index:3d}] {values}'
def parse_data_packet(b: bytes) -> Iterator[DataItem]:
data_header_offset = 5
bytes_per_item = 4 * (8+1)
if not b.startswith(b'DATA*'):
raw_str = parse_raw(b, offset=5)
raise ValueError(f'invalid DATA* packet: {raw_str}')
len_b = len(b)
if (len_b - data_header_offset) % bytes_per_item != 0:
raw_str = parse_raw(b, data_header_offset)
raise ValueError(f'improperly aligned DATA* packet: [len: {len_b}] {raw_str}')
i = 5
while i < len_b:
data = unpack('iffffffff', b[i:i+bytes_per_item])
yield DataItem(data[0], data[1:])
i += bytes_per_item
class XPlaneInput:
def __init__(self, addr: str, port: int):
self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.s.bind((addr, port))
def run(self):
try:
old_data = None
while True:
data = self.s.recv(4096)
assert data.startswith(b"DATA"), data
data = list(parse_data_packet(data))
if data != old_data:
print('---')
for di in data:
print(str_dataitem(di))
old_data = data
except KeyboardInterrupt:
print("XPlaneInput -> CTRL+C", file=sys.stderr)