Initial commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user