micropython-lib/micropython/usb/usb-device-midi/usb/device/midi.py

307 wiersze
11 KiB
Python

# MicroPython USB MIDI module
# MIT license; Copyright (c) 2023 Paul Hamshere, 2023-2024 Angus Gratton
from micropython import const, schedule
import struct
from .core import Interface, Buffer
_EP_IN_FLAG = const(1 << 7)
_INTERFACE_CLASS_AUDIO = const(0x01)
_INTERFACE_SUBCLASS_AUDIO_CONTROL = const(0x01)
_INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING = const(0x03)
# Audio subclass extends the standard endpoint descriptor
# with two extra bytes
_STD_DESC_AUDIO_ENDPOINT_LEN = const(9)
_CLASS_DESC_ENDPOINT_LEN = const(5)
_STD_DESC_ENDPOINT_TYPE = const(0x5)
_JACK_TYPE_EMBEDDED = const(0x01)
_JACK_TYPE_EXTERNAL = const(0x02)
_JACK_IN_DESC_LEN = const(6)
_JACK_OUT_DESC_LEN = const(9)
# MIDI Status bytes. For Channel messages these are only the upper 4 bits, ORed with the channel number.
# As per https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message
_MIDI_NOTE_OFF = const(0x80)
_MIDI_NOTE_ON = const(0x90)
_MIDI_POLY_KEYPRESS = const(0xA0)
_MIDI_CONTROL_CHANGE = const(0xB0)
# USB-MIDI CINs (Code Index Numbers), as per USB MIDI Table 4-1
_CIN_SYS_COMMON_2BYTE = const(0x2)
_CIN_SYS_COMMON_3BYTE = const(0x3)
_CIN_SYSEX_START = const(0x4)
_CIN_SYSEX_END_1BYTE = const(0x5)
_CIN_SYSEX_END_2BYTE = const(0x6)
_CIN_SYSEX_END_3BYTE = const(0x7)
_CIN_NOTE_OFF = const(0x8)
_CIN_NOTE_ON = const(0x9)
_CIN_POLY_KEYPRESS = const(0xA)
_CIN_CONTROL_CHANGE = const(0xB)
_CIN_PROGRAM_CHANGE = const(0xC)
_CIN_CHANNEL_PRESSURE = const(0xD)
_CIN_PITCH_BEND = const(0xE)
_CIN_SINGLE_BYTE = const(0xF) # Not currently supported
# Jack IDs for a simple bidrectional MIDI device(!)
_EMB_IN_JACK_ID = const(1)
_EXT_IN_JACK_ID = const(2)
_EMB_OUT_JACK_ID = const(3)
_EXT_OUT_JACK_ID = const(4)
# Data flows, as modelled by USB-MIDI and this hypothetical interface, are as follows:
# Device RX = USB OUT EP => _EMB_IN_JACK => _EMB_OUT_JACK
# Device TX = _EXT_IN_JACK => _EMB_OUT_JACK => USB IN EP
class MIDIInterface(Interface):
# Base class to implement a USB MIDI device in Python.
#
# To be compliant this also regisers a dummy USB Audio interface, but that
# interface isn't otherwise used.
def __init__(self, rxlen=16, txlen=16):
# Arguments are size of transmit and receive buffers in bytes.
super().__init__()
self.ep_out = None # Set during enumeration. RX direction (host to device)
self.ep_in = None # TX direction (device to host)
self._rx = Buffer(rxlen)
self._tx = Buffer(txlen)
# Callbacks for handling received MIDI messages.
#
# Subclasses can choose between overriding on_midi_event
# and handling all MIDI events manually, or overriding the
# functions for note on/off and control change, only.
def on_midi_event(self, cin, midi0, midi1, midi2):
ch = midi0 & 0x0F
if cin == _CIN_NOTE_ON:
self.on_note_on(ch, midi1, midi2)
elif cin == _CIN_NOTE_OFF:
self.on_note_off(ch, midi1, midi2)
elif cin == _CIN_CONTROL_CHANGE:
self.on_control_change(ch, midi1, midi2)
def on_note_on(self, channel, pitch, vel):
pass # Override to handle Note On messages
def on_note_off(self, channel, pitch, vel):
pass # Override to handle Note On messages
def on_control_change(self, channel, controller, value):
pass # Override to handle Control Change messages
# Helper functions for sending common MIDI messages
def note_on(self, channel, pitch, vel=0x40):
self.send_event(_CIN_NOTE_ON, _MIDI_NOTE_ON | channel, pitch, vel)
def note_off(self, channel, pitch, vel=0x40):
self.send_event(_CIN_NOTE_OFF, _MIDI_NOTE_OFF | channel, pitch, vel)
def control_change(self, channel, controller, value):
self.send_event(_CIN_CONTROL_CHANGE, _MIDI_CONTROL_CHANGE | channel, controller, value)
def send_event(self, cin, midi0, midi1=0, midi2=0):
# Queue a MIDI Event Packet to send to the host.
#
# CIN = USB-MIDI Code Index Number, see USB MIDI 1.0 section 4 "USB-MIDI Event Packets"
#
# Remaining arguments are 0-3 MIDI data bytes.
#
# Note this function returns when the MIDI Event Packet has been queued,
# not when it's been received by the host.
#
# Returns False if the TX buffer is full and the MIDI Event could not be queued.
w = self._tx.pend_write()
if len(w) < 4:
return False # TX buffer is full. TODO: block here?
w[0] = cin # leave cable number as 0?
w[1] = midi0
w[2] = midi1
w[3] = midi2
self._tx.finish_write(4)
self._tx_xfer()
return True
def _tx_xfer(self):
# Keep an active IN transfer to send data to the host, whenever
# there is data to send.
if self.is_open() and not self.xfer_pending(self.ep_in) and self._tx.readable():
self.submit_xfer(self.ep_in, self._tx.pend_read(), self._tx_cb)
def _tx_cb(self, ep, res, num_bytes):
if res == 0:
self._tx.finish_read(num_bytes)
self._tx_xfer()
def _rx_xfer(self):
# Keep an active OUT transfer to receive MIDI events from the host
if self.is_open() and not self.xfer_pending(self.ep_out) and self._rx.writable():
self.submit_xfer(self.ep_out, self._rx.pend_write(), self._rx_cb)
def _rx_cb(self, ep, res, num_bytes):
if res == 0:
self._rx.finish_write(num_bytes)
schedule(self._on_rx, None)
self._rx_xfer()
def on_open(self):
super().on_open()
# kick off any transfers that may have queued while the device was not open
self._tx_xfer()
self._rx_xfer()
def _on_rx(self, _):
# Receive MIDI events. Called via micropython.schedule, outside of the USB callback function.
m = self._rx.pend_read()
i = 0
while i <= len(m) - 4:
cin = m[i] & 0x0F
self.on_midi_event(cin, m[i + 1], m[i + 2], m[i + 3])
i += 4
self._rx.finish_read(i)
def desc_cfg(self, desc, itf_num, ep_num, strs):
# Start by registering a USB Audio Control interface, that is required to point to the
# actual MIDI interface
desc.interface(itf_num, 0, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_CONTROL)
# Append the class-specific AudioControl interface descriptor
desc.pack(
"<BBBHHBB",
9, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype MS_HEADER
0x0100, # BcdADC
0x0009, # wTotalLength
0x01, # bInCollection,
itf_num + 1, # baInterfaceNr - points to the MIDI Streaming interface
)
# Next add the MIDI Streaming interface descriptor
desc.interface(
itf_num + 1, 2, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING
)
# Append the class-specific interface descriptors
# Midi Streaming interface descriptor
desc.pack(
"<BBBHH",
7, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype MS_HEADER
0x0100, # BcdADC
# wTotalLength: of all class-specific descriptors
7
+ 2
* (
_JACK_IN_DESC_LEN
+ _JACK_OUT_DESC_LEN
+ _STD_DESC_AUDIO_ENDPOINT_LEN
+ _CLASS_DESC_ENDPOINT_LEN
),
)
# The USB MIDI standard 1.0 allows modelling a baffling range of MIDI
# devices with different permutations of Jack descriptors, with a lot of
# scope for indicating internal connections in the device (as
# "virtualised" by the USB MIDI standard). Much of the options don't
# really change the USB behaviour but provide metadata to the host.
#
# As observed elsewhere online, the standard ends up being pretty
# complex and unclear in parts, but there is a clear simple example in
# an Appendix. So nearly everyone implements the device from the
# Appendix as-is, even when it's not a good fit for their application,
# and ignores the rest of the standard.
#
# For now, this is what this class does as well.
_jack_in_desc(desc, _JACK_TYPE_EMBEDDED, _EMB_IN_JACK_ID)
_jack_in_desc(desc, _JACK_TYPE_EXTERNAL, _EXT_IN_JACK_ID)
_jack_out_desc(desc, _JACK_TYPE_EMBEDDED, _EMB_OUT_JACK_ID, _EXT_IN_JACK_ID, 1)
_jack_out_desc(desc, _JACK_TYPE_EXTERNAL, _EXT_OUT_JACK_ID, _EMB_IN_JACK_ID, 1)
# One MIDI endpoint in each direction, plus the
# associated CS descriptors
self.ep_out = ep_num
self.ep_in = ep_num | _EP_IN_FLAG
# rx side, USB "in" endpoint and embedded MIDI IN Jacks
_audio_endpoint(desc, self.ep_in, _EMB_OUT_JACK_ID)
# tx side, USB "out" endpoint and embedded MIDI OUT jacks
_audio_endpoint(desc, self.ep_out, _EMB_IN_JACK_ID)
def num_itfs(self):
return 2
def num_eps(self):
return 1
def _jack_in_desc(desc, bJackType, bJackID):
# Utility function appends a "JACK IN" descriptor with
# specified bJackType and bJackID
desc.pack(
"<BBBBBB",
_JACK_IN_DESC_LEN, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x02, # bDescriptorSubtype MIDI_IN_JACK
bJackType,
bJackID,
0x00, # iJack, no string descriptor support yet
)
def _jack_out_desc(desc, bJackType, bJackID, bSourceId, bSourcePin):
# Utility function appends a "JACK IN" descriptor with
# specified bJackType and bJackID
desc.pack(
"<BBBBBBBBB",
_JACK_OUT_DESC_LEN, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x03, # bDescriptorSubtype MIDI_OUT_JACK
bJackType,
bJackID,
0x01, # bNrInputPins
bSourceId, # baSourceID(1)
bSourcePin, # baSourcePin(1)
0x00, # iJack, no string descriptor support yet
)
def _audio_endpoint(desc, bEndpointAddress, emb_jack_id):
# Append a standard USB endpoint descriptor and the USB class endpoint descriptor
# for this endpoint.
#
# Audio Class devices extend the standard endpoint descriptor with two extra bytes,
# so we can't easily call desc.endpoint() for the first part.
desc.pack(
# Standard USB endpoint descriptor (plus audio tweaks)
"<BBBBHBBB"
# Class endpoint descriptor
"BBBBB",
_STD_DESC_AUDIO_ENDPOINT_LEN, # wLength
_STD_DESC_ENDPOINT_TYPE, # bDescriptorType
bEndpointAddress,
2, # bmAttributes, bulk
64, # wMaxPacketSize
0, # bInterval
0, # bRefresh (unused)
0, # bSynchInterval (unused)
_CLASS_DESC_ENDPOINT_LEN, # bLength
0x25, # bDescriptorType CS_ENDPOINT
0x01, # bDescriptorSubtype MS_GENERAL
1, # bNumEmbMIDIJack
emb_jack_id, # BaAssocJackID(1)
)