Skip to content

Commit 581a662

Browse files
committed
usbd: Add midi interface definition from @paulhamsh.
Based on https://github.com/paulhamsh/Micropython-Midi-Device as of commit 2678d13. With additions/edits by me.
1 parent 24f7422 commit 581a662

File tree

1 file changed

+306
-0
lines changed

1 file changed

+306
-0
lines changed

micropython/usbd/midi.py

+306
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# MicroPython USB MIDI module
2+
# MIT license; Copyright (c) 2023 Angus Gratton, Paul Hamshere
3+
from micropython import const
4+
import ustruct
5+
6+
from .device import USBInterface
7+
from .utils import endpoint_descriptor, EP_IN_FLAG
8+
9+
_INTERFACE_CLASS_AUDIO = const(0x01)
10+
_INTERFACE_SUBCLASS_AUDIO_CONTROL = const(0x01)
11+
_INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING = const(0x03)
12+
_PROTOCOL_NONE = const(0x00)
13+
14+
_JACK_TYPE_EMBEDDED = const(0x01)
15+
_JACK_TYPE_EXTERNAL = const(0x02)
16+
17+
18+
class RingBuf:
19+
def __init__(self, size):
20+
self.data = bytearray(size)
21+
self.size = size
22+
self.index_put = 0
23+
self.index_get = 0
24+
25+
def put(self, value):
26+
next_index = (self.index_put + 1) % self.size
27+
# check for overflow
28+
if self.index_get != next_index:
29+
self.data[self.index_put] = value
30+
self.index_put = next_index
31+
return value
32+
else:
33+
return None
34+
35+
def get(self):
36+
if self.index_get == self.index_put:
37+
return None # buffer empty
38+
else:
39+
value = self.data[self.index_get]
40+
self.index_get = (self.index_get + 1) % self.size
41+
return value
42+
43+
def is_empty(self):
44+
return self.index_get == self.index_put
45+
46+
47+
class DummyAudioInterface(USBInterface):
48+
# An Audio Class interface is mandatory for MIDI Interfaces as well, this
49+
# class implements the minimum necessary for this.
50+
def __init__(self):
51+
super().__init__(_INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_CONTROL, _PROTOCOL_NONE)
52+
53+
def get_itf_descriptor(self, num_eps, itf_idx, str_idx):
54+
# Return the MIDI USB interface descriptors.
55+
56+
# Get the parent interface class
57+
desc, strs = super().get_itf_descriptor(num_eps, itf_idx, str_idx)
58+
59+
# Append the class-specific AudioControl interface descriptor
60+
desc += ustruct.pack(
61+
"<BBBHHBB",
62+
9, # bLength
63+
0x24, # bDescriptorType CS_INTERFACE
64+
0x01, # bDescriptorSubtype MS_HEADER
65+
0x0100, # BcdADC
66+
0x0009, # wTotalLength
67+
0x01, # bInCollection,
68+
# baInterfaceNr value assumes the next interface will be MIDIInterface
69+
itf_idx + 1, # baInterfaceNr
70+
)
71+
72+
return (desc, strs)
73+
74+
75+
class MIDIInterface(USBInterface):
76+
# Base class to implement a USB MIDI device in Python.
77+
78+
# To be compliant two USB interfaces should be registered in series, first a
79+
# _DummyAudioInterface() and then this one immediately after.
80+
def __init__(self, num_rx=1, num_tx=1):
81+
# Arguments are number of MIDI IN and OUT connections (default 1 each way).
82+
83+
# 'rx' and 'tx' are from the point of view of this device, i.e. a 'tx'
84+
# connection is device to host. RX and TX are used here to avoid the
85+
# even more confusing "MIDI IN" and "MIDI OUT", which varies depending
86+
# on whether you look from the perspective of the device or the USB
87+
# interface.
88+
super().__init__(
89+
_INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING, _PROTOCOL_NONE
90+
)
91+
self._num_rx = num_rx
92+
self._num_tx = num_tx
93+
self.ep_out = None # Set during enumeration
94+
self.ep_in = None
95+
self._rx_buf = bytearray(64)
96+
97+
def send_data(self, tx_data):
98+
"""Helper function to send data."""
99+
self.submit_xfer(self.ep_out, tx_data)
100+
101+
def midi_received(self):
102+
return not self.rb.is_empty()
103+
104+
def get_rb(self):
105+
return self.rb.get()
106+
107+
def receive_data_callback(self, ep_addr, result, xferred_bytes):
108+
for i in range(0, xferred_bytes):
109+
self.rb.put(self.rx_data[i])
110+
self.submit_xfer(0x03, self.rx_data, self.receive_data_callback)
111+
112+
def start_receive_data(self):
113+
self.submit_xfer(self.ep_in, self.rx_data, self.receive_data_callback)
114+
115+
def get_itf_descriptor(self, num_eps, itf_idx, str_idx):
116+
# Return the MIDI USB interface descriptors.
117+
118+
# Get the parent interface class
119+
desc, strs = super().get_itf_descriptor(num_eps, itf_idx, str_idx)
120+
121+
# Append the class-specific interface descriptors
122+
123+
_JACK_IN_DESC_LEN = const(6)
124+
_JACK_OUT_DESC_LEN = const(9)
125+
126+
# Midi Streaming interface descriptor
127+
cs_ms_interface = ustruct.pack(
128+
"<BBBHH",
129+
7, # bLength
130+
0x24, # bDescriptorType CS_INTERFACE
131+
0x01, # bDescriptorSubtype MS_HEADER
132+
0x0100, # BcdADC
133+
# wTotalLength: this descriptor, plus length of all Jack descriptors
134+
(7 + (2 * (_JACK_IN_DESC_LEN + _JACK_OUT_DESC_LEN) * (self._num_rx + self._num_tx))),
135+
)
136+
137+
def jack_in_desc(bJackType, bJackID):
138+
return ustruct.pack(
139+
"<BBBBBB",
140+
_JACK_IN_DESC_LEN, # bLength
141+
0x24, # bDescriptorType CS_INTERFACE
142+
0x02, # bDescriptorSubtype MIDI_IN_JACK
143+
bJackType,
144+
bJackID,
145+
0x00, # iJack, no string descriptor support yet
146+
)
147+
148+
def jack_out_desc(bJackType, bJackID, bSourceId, bSourcePin):
149+
return ustruct.pack(
150+
"<BBBBBBBBB",
151+
_JACK_OUT_DESC_LEN, # bLength
152+
0x24, # bDescriptorType CS_INTERFACE
153+
0x03, # bDescriptorSubtype MIDI_OUT_JACK
154+
bJackType,
155+
bJackID,
156+
0x01, # bNrInputPins
157+
bSourceId, # baSourceID(1)
158+
bSourcePin, # baSourcePin(1)
159+
0x00, # iJack, no string descriptor support yet
160+
)
161+
162+
jacks = bytearray() # TODO: pre-allocate this whole descriptor and pack into it
163+
164+
# The USB MIDI standard 1.0 allows modelling a baffling range of MIDI
165+
# devices with different permutations of Jack descriptors, with a lot of
166+
# scope for indicating internal connections in the device (as
167+
# "virtualised" by the USB MIDI standard). Much of the options don't
168+
# really change the USB behaviour but provide metadata to the host.
169+
#
170+
# As observed elsewhere online, the standard ends up being pretty
171+
# complex and unclear in parts, but there is a clear simple example in
172+
# an Appendix. So nearly everyone implements the device from the
173+
# Appendix as-is, even when it's not a good fit for their application,
174+
# and ignores the rest of the standard.
175+
#
176+
# We'll try to implement a slightly more flexible subset that's still
177+
# very simple, without getting caught in the weeds:
178+
#
179+
# - For each rx (total _num_rx), we have data flowing from the USB host
180+
# to the USB MIDI device:
181+
# * Data comes from a MIDI OUT Endpoint (Host->Device)
182+
# * Data goes via an Embedded MIDI IN Jack ("into" the USB-MIDI device)
183+
# * Data goes out via a virtual External MIDI OUT Jack ("out" of the
184+
# USB-MIDI device and into the world). This "out" jack may be
185+
# theoretical, and only exists in the USB descriptor.
186+
#
187+
# - For each tx (total _num_tx), we have data flowing from the USB MIDI
188+
# device to the USB host:
189+
# * Data comes in via a virtual External MIDI IN Jack (from the
190+
# outside world, theoretically)
191+
# * Data goes via an Embedded MIDI OUT Jack ("out" of the USB-MIDI
192+
# device).
193+
# * Data goes into the host via MIDI IN Endpoint (Device->Host)
194+
195+
# rx side
196+
for idx in range(self._num_rx):
197+
emb_id = self._emb_id(False, idx)
198+
ext_id = emb_id + 1
199+
pin = idx + 1
200+
jacks += jack_in_desc(_JACK_TYPE_EMBEDDED, emb_id) # bJackID)
201+
jacks += jack_out_desc(
202+
_JACK_TYPE_EXTERNAL,
203+
ext_id, # bJackID
204+
emb_id, # baSourceID(1)
205+
pin, # baSourcePin(1)
206+
)
207+
208+
# tx side
209+
for idx in range(self._num_tx):
210+
emb_id = self._emb_id(True, idx)
211+
ext_id = emb_id + 1
212+
pin = idx + 1
213+
214+
jacks += jack_in_desc(
215+
_JACK_TYPE_EXTERNAL,
216+
ext_id, # bJackID
217+
)
218+
jacks += jack_out_desc(
219+
_JACK_TYPE_EMBEDDED,
220+
emb_id,
221+
ext_id, # baSourceID(1)
222+
pin, # baSourcePin(1)
223+
)
224+
225+
iface = desc + cs_ms_interface + jacks
226+
return (iface, strs)
227+
228+
def _emb_id(self, is_tx, idx):
229+
# Given a direction (False==rx, True==tx) and a 0-index
230+
# of the MIDI connection, return the embedded JackID value.
231+
#
232+
# Embedded JackIDs take odd numbers 1,3,5,etc with all
233+
# 'RX' jack numbers first and then all 'TX' jack numbers
234+
# (see long comment above for explanation of RX, TX in
235+
# this context.)
236+
#
237+
# This is used to keep jack IDs in sync between
238+
# get_itf_descriptor() and get_endpoint_descriptors()
239+
return 1 + 2 * (idx + (is_tx * self._num_rx))
240+
241+
def get_endpoint_descriptors(self, ep_addr, str_idx):
242+
# One MIDI endpoint in each direction, plus the
243+
# associated CS descriptors
244+
245+
# The following implementation is *very* memory inefficient
246+
# and needs optimising
247+
248+
self.ep_out = ep_addr + 1
249+
self.ep_in = ep_addr + 2 | EP_IN_FLAG
250+
251+
# rx side, USB "in" endpoint and embedded MIDI IN Jacks
252+
e_out = endpoint_descriptor(self.ep_in, "bulk", 64, 0)
253+
cs_out = ustruct.pack(
254+
"<BBBB" + "B" * self._num_rx,
255+
4 + self._num_rx, # bLength
256+
0x25, # bDescriptorType CS_ENDPOINT
257+
0x01, # bDescriptorSubtype MS_GENERAL
258+
self._num_rx, # bNumEmbMIDIJack
259+
*(self._emb_id(False, idx) for idx in range(self._num_rx)) # baSourcePin(1..._num_rx)
260+
)
261+
262+
# tx side, USB "out" endpoint and embedded MIDI OUT jacks
263+
e_in = endpoint_descriptor(self.ep_out, "bulk", 64, 0)
264+
cs_in = ustruct.pack(
265+
"<BBBB" + "B" * self._num_tx,
266+
4 + self._num_tx, # bLength
267+
0x25, # bDescriptorType CS_ENDPOINT
268+
0x01, # bDescriptorSubtype MS_GENERAL
269+
self._num_tx, # bNumEmbMIDIJack
270+
*(self._emb_id(True, idx) for idx in range(self._num_tx)) # baSourcePin(1..._num_rx)
271+
)
272+
273+
desc = e_out + cs_out + e_in + cs_in
274+
275+
return (desc, [], (self.ep_out, self.ep_in))
276+
277+
278+
class MidiUSB(MIDIInterface):
279+
# Very basic synchronous USB MIDI interface
280+
281+
def __init__(self):
282+
super().__init__()
283+
284+
def note_on(self, channel, pitch, vel):
285+
obuf = ustruct.pack("<BBBB", 0x09, 0x90 | channel, pitch, vel)
286+
super().send_data(obuf)
287+
288+
def note_off(self, channel, pitch, vel):
289+
obuf = ustruct.pack("<BBBB", 0x08, 0x80 | channel, pitch, vel)
290+
super().send_data(obuf)
291+
292+
def start(self):
293+
super().start_receive_data()
294+
295+
def midi_received(self):
296+
return super().midi_received()
297+
298+
def get_midi(self):
299+
if super().midi_received():
300+
cin = super().get_rb()
301+
cmd = super().get_rb()
302+
val1 = super().get_rb()
303+
val2 = super().get_rb()
304+
return (cin, cmd, val1, val2)
305+
else:
306+
return (None, None, None, None)

0 commit comments

Comments
 (0)