Initial commit
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal 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
134
main.py
Normal 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
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
python-rtmidi
|
||||||
272
roland.py
Normal file
272
roland.py
Normal 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
0
src/__init__.py
Normal file
247
src/midi_controller.py
Normal file
247
src/midi_controller.py
Normal 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
0
test/__init__.py
Normal file
30
test/test_midi_controller.py
Normal file
30
test/test_midi_controller.py
Normal 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
61
xplane.py
Normal 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)
|
||||||
|
|
||||||
Reference in New Issue
Block a user