-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmidi_device_in.py
More file actions
140 lines (126 loc) · 5.54 KB
/
Copy pathmidi_device_in.py
File metadata and controls
140 lines (126 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# Device MIDI IN: prove the board receives MIDI from a host.
#
# This is the last open corner of M3's four-way MIDI matrix. The other
# three are closed: the board plays a DAW and a hardware GM box (device
# MIDI OUT), reads a keyboard (host MIDI IN), and drives an instrument
# (host MIDI OUT). What has never been exercised is the direction a
# software instrument needs most -- a host sequencer playing *into* the
# board, so `audioif` can render what a DAW sends it.
#
# The board wears a pure-MIDI costume, so on the host it is a MIDI
# device and nothing else. Anything that can send MIDI drives it: a DAW
# track whose output is this board, or a keyboard routed through one.
#
# Run it with the REPL on the UART bridge, not on native USB -- the
# costume change drops CDC, and a REPL living on the port under test
# would be cut off mid-run.
#
# mpftp run usbif/examples/midi_device_in.py -d COM49 --follow --timeout 90
#
# The tally at the end is the evidence: which message types arrived,
# how many of each, and whether the parser ever fell out of sync. A
# clean run shows note-ons with a spread of velocities (not a column of
# 64s, which would mean the source is sending a fixed velocity and the
# velocity byte is never really being tested), note-offs matching them,
# and zero desync bytes.
import time
import _usbif
import usbif
DURATION_MS = 20000 # how long to listen once the host has mounted us
MOUNT_TIMEOUT_MS = 10000 # how long to wait for the host to configure us
BUF = bytearray(256)
NAMES = {
0x8: "note-off", 0x9: "note-on", 0xA: "aftertouch",
0xB: "cc", 0xC: "program", 0xD: "pressure", 0xE: "pitch-bend",
}
def wait_for_mount():
deadline = time.ticks_add(time.ticks_ms(), MOUNT_TIMEOUT_MS)
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
connected, mounted, _ = _usbif.dev_state()
if mounted:
return True
time.sleep_ms(50)
connected, mounted, suspended = _usbif.dev_state()
print("not mounted after {} ms: connected={} mounted={} suspended={}".format(
MOUNT_TIMEOUT_MS, connected, mounted, suspended))
if not connected:
# No bus reset ever seen. Nothing above this layer can help.
print(" connected=False means no host is on the native USB port at all --")
print(" check the cable is a data cable and is in the native USB jack,")
print(" not only the UART bridge.")
return False
def main():
restore = _usbif.dev_functions()
built = _usbif.dev_functions_built()
if not (built & _usbif.FN_MIDI):
print("this firmware has no MIDI device function built in")
return False
counts = {}
velocities = set()
channels = set()
notes_on = 0
notes_off = 0
bend_lo = 8192
bend_hi = 8192
parser = usbif.MidiParser()
total_bytes = 0
try:
# Only re-enumerate if we are not already wearing it: changing the
# function mask drops the host's connection and costs a fresh
# enumeration, which is pure disruption when it is already correct.
if restore != _usbif.FN_MIDI:
_usbif.dev_functions(_usbif.FN_MIDI)
print("costume: midi only -- look for the board as a MIDI device on the host")
else:
print("costume: already midi only, left alone")
if not wait_for_mount():
return False
print("mounted. play into it for {} s ...".format(DURATION_MS // 1000))
deadline = time.ticks_add(time.ticks_ms(), DURATION_MS)
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
n = _usbif.midi_read(BUF)
if not n:
time.sleep_ms(2)
continue
total_bytes += n
parser.feed(BUF, n)
for status, data in parser.drain():
high = status >> 4
name = NAMES.get(high, "system")
counts[name] = counts.get(name, 0) + 1
if high < 0xF:
channels.add(status & 0x0F)
if high == 0x9 and len(data) == 2 and data[1]:
notes_on += 1
velocities.add(data[1])
print(" note-on ch{:<2} note {:<3} vel {}".format(
(status & 0x0F) + 1, data[0], data[1]))
elif high == 0x8 or (high == 0x9 and len(data) == 2 and not data[1]):
# A note-on with velocity 0 is a note-off; count it as one.
notes_off += 1
elif high == 0xE and len(data) == 2:
value = data[0] | (data[1] << 7)
bend_lo = min(bend_lo, value)
bend_hi = max(bend_hi, value)
finally:
if _usbif.dev_functions() != restore:
_usbif.dev_functions(restore)
print()
print("--- device MIDI IN ---")
print("bytes read {}".format(total_bytes))
for name in sorted(counts):
print("{:<15} {}".format(name, counts[name]))
print("notes on/off {} / {}".format(notes_on, notes_off))
print("channels seen {}".format(
sorted(c + 1 for c in channels) if channels else "none"))
print("velocities {} distinct{}".format(
len(velocities),
" (min {} max {})".format(min(velocities), max(velocities))
if velocities else ""))
if bend_hi != bend_lo:
print("pitch bend {} .. {}".format(bend_lo, bend_hi))
print("desync bytes {}".format(parser.desync))
ok = total_bytes > 0 and parser.desync == 0
print("result {}".format("PASS" if ok else "no traffic" if not total_bytes else "FAIL"))
return ok
main()