]> sigrok.org Git - libsigrokdecode.git/blob - decoders/midi/pd.py
midi: Substantially improve decoding of MIDI messages.
[libsigrokdecode.git] / decoders / midi / pd.py
1 ##
2 ## This file is part of the libsigrokdecode project.
3 ##
4 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
5 ##
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
10 ##
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 ## GNU General Public License for more details.
15 ##
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
19 ##
20
21 import sigrokdecode as srd
22 from .lists import *
23
24 RX = 0
25 TX = 1
26
27 class Decoder(srd.Decoder):
28     api_version = 2
29     id = 'midi'
30     name = 'MIDI'
31     longname = 'Musical Instrument Digital Interface'
32     desc = 'Musical Instrument Digital Interface (MIDI) protocol.'
33     license = 'gplv2+'
34     inputs = ['uart']
35     outputs = ['midi']
36     annotations = (
37         ('text-verbose', 'Human-readable text (verbose)'),
38     )
39
40     def __init__(self):
41         self.cmd = []
42         self.state = 'IDLE'
43         self.ss = None
44         self.es = None
45         self.ss_block = None
46         self.es_block = None
47
48     def start(self):
49         self.out_ann = self.register(srd.OUTPUT_ANN)
50
51     def putx(self, data):
52         self.put(self.ss_block, self.es_block, self.out_ann, data)
53
54     def get_note_name(self, channel, note):
55         if channel != 10:
56             return chromatic_notes[note]
57         else:
58             return 'assuming ' + percussion_notes.get(note, 'undefined')
59
60     def handle_channel_msg_0x80(self):
61         # Note off: 8n kk vv
62         # n = channel, kk = note, vv = velocity
63         c = self.cmd
64         if len(c) < 3:
65             return
66         self.es_block = self.es
67         msg, chan, note, velocity = c[0] & 0xf0, (c[0] & 0x0f) + 1, c[1], c[2]
68         note_name = self.get_note_name(chan, note)
69         self.putx([0, ['Channel %d: %s (note = %d \'%s\', velocity = %d)' % \
70                   (chan, status_bytes[msg], note, note_name, velocity)]])
71         self.cmd, self.state = [], 'IDLE'
72
73     def handle_channel_msg_0x90(self):
74         # Note on: 9n kk vv
75         # n = channel, kk = note, vv = velocity
76         # If velocity == 0 that actually means 'note off', though.
77         c = self.cmd
78         if len(c) < 3:
79             return
80         self.es_block = self.es
81         msg, chan, note, velocity = c[0] & 0xf0, (c[0] & 0x0f) + 1, c[1], c[2]
82         s = 'note off' if (velocity == 0) else status_bytes[msg]
83         note_name = self.get_note_name(chan, note)
84         self.putx([0, ['Channel %d: %s (note = %d \'%s\', velocity = %d)' % \
85                   (chan, s, note, note_name, velocity)]])
86         self.cmd, self.state = [], 'IDLE'
87
88     def handle_channel_msg_0xa0(self):
89         # Polyphonic key pressure / aftertouch: An kk vv
90         # n = channel, kk = polyphonic key pressure, vv = pressure value
91         c = self.cmd
92         if len(c) < 3:
93             return
94         self.es_block = self.es
95         msg, chan, note, pressure = c[0] & 0xf0, (c[0] & 0x0f) + 1, c[1], c[2]
96         note_name = self.get_note_name(chan, note)
97         self.putx([0, ['Channel %d: %s (note = %d \'%s\', pressure = %d)' % \
98                   (chan, status_bytes[msg], note, note_name, pressure)]])
99         self.cmd, self.state = [], 'IDLE'
100
101     def handle_controller_0x44(self):
102         # Legato footswitch: Bn 44 vv
103         # n = channel, vv = value (<= 0x3f: normal, > 0x3f: legato)
104         chan, vv = (self.cmd[0] & 0x0f) + 1, self.cmd[2]
105         t = 'normal' if vv <= 0x3f else 'legato'
106         self.putx([0, ['Channel %d: control function \'%s\' = %s' % \
107                   (chan, control_functions[0x44], t)]])
108
109     def handle_controller_0x54(self):
110         # Portamento control (PTC): Bn 54 kk
111         # n = channel, kk = source note for pitch reference
112         chan, kk = (self.cmd[0] & 0x0f) + 1, self.cmd[2]
113         kk_name = self.get_note_name(chan, kk)
114         self.putx([0, ['Channel %d: control function \'%s\' (source note ' \
115                   '= %d / %s)' % \
116                   (chan, control_functions[0x54], kk, kk_name)]])
117
118     def handle_controller_generic(self):
119         c = self.cmd
120         chan, fn, param = (c[0] & 0x0f) + 1, c[1], c[2]
121         default_name = 'undefined'
122         ctrl_fn = control_functions.get(fn, default_name)
123         if ctrl_fn == default_name:
124             ctrl_fn = '%s 0x%02x' % (default_name, fn)
125         self.putx([0, ['Channel %d: control change to function \'%s\' ' \
126                   '(param = 0x%02x)' % (chan, ctrl_fn, param)]])
127
128     def handle_channel_mode(self):
129         # Channel Mode: Bn mm vv
130         # n = channel, mm = mode number (120 - 127), vv = value
131         c = self.cmd
132         chan, mm, vv = (c[0] & 0x0f) + 1, c[1], c[2]
133         mode_fn = control_functions.get(mm, 'undefined')
134         # Decode the value based on the mode number.
135         vv_string = ''
136         if mm == 122:           # mode = local control?
137             if vv == 0:
138                 vv_string = 'off'
139             elif vv == 127:     # mode = poly mode on?
140                 vv_string = 'on'
141             else:
142                 vv_string = '(non-standard param value of 0x%02x)' % vv
143         elif mm == 126:         # mode = mono mode on?
144             if vv != 0:
145                 vv_string = '(%d channels)' % vv
146             else:
147                 vv_string = '(channels \'basic\' through 16)'
148         elif vv != 0: # All other channel mode messages expect vv == 0.
149             vv_string = '(non-standard param value of 0x%02x)' % vv
150         self.putx([0, ['Channel %d: mode message \'%s\' %s' % \
151             (chan, mode_fn, vv_string)]])
152         self.cmd, self.state = [], 'IDLE'
153
154     def handle_channel_msg_0xb0(self):
155         # Control change (or channel mode messages): Bn cc vv
156         # n = channel, cc = control number (0 - 119), vv = control value
157         c = self.cmd
158         if len(c) < 3:
159             return
160         self.es_block = self.es
161         if c[1] in range(0x78, 0x7f + 1):
162             self.handle_channel_mode()
163             return
164         handle_ctrl = getattr(self, 'handle_controller_0x%02x' % c[1],
165                               self.handle_controller_generic)
166         handle_ctrl()
167         self.cmd, self.state = [], 'IDLE'
168
169     def handle_channel_msg_0xc0(self):
170         # Program change: Cn pp
171         # n = channel, pp = program number (0 - 127)
172         c = self.cmd
173         if len(c) < 2:
174             return
175         self.es_block = self.es
176         msg, chan, pp = self.cmd[0] & 0xf0, (self.cmd[0] & 0x0f) + 1, \
177                         self.cmd[1] + 1
178         change_type = 'instrument'
179         name = ''
180         if chan != 10:  # channel != percussion
181             name = gm_instruments.get(pp, 'undefined')
182         else:
183             change_type = 'drum kit'
184             name = drum_kit.get(pp, 'undefined')
185         self.putx([0, ['Channel %d: %s to %s %d (assuming %s)' % \
186             (chan, status_bytes[msg], change_type, pp, name)]])
187         self.cmd, self.state = [], 'IDLE'
188
189     def handle_channel_msg_0xd0(self):
190         # Channel pressure / aftertouch: Dn vv
191         # n = channel, vv = pressure value
192         c = self.cmd
193         if len(c) < 2:
194             return
195         self.es_block = self.es
196         msg, chan, vv = self.cmd[0] & 0xf0, (self.cmd[0] & 0x0f) + 1, self.cmd[1]
197         self.putx([0, ['Channel %d: %s %d' % (chan, status_bytes[msg], vv)]])
198         self.cmd, self.state = [], 'IDLE'
199
200     def handle_channel_msg_0xe0(self):
201         # Pitch bend change: En ll mm
202         # n = channel, ll = pitch bend change LSB, mm = pitch bend change MSB
203         c = self.cmd
204         if len(c) < 3:
205             return
206         self.es_block = self.es
207         msg, chan, ll, mm = self.cmd[0] & 0xf0, (self.cmd[0] & 0x0f) + 1, \
208                             self.cmd[1], self.cmd[2]
209         decimal = (mm << 7) + ll
210         self.putx([0, ['Channel %d: %s 0x%02x 0x%02x (%d)' % \
211             (chan, status_bytes[msg], ll, mm, decimal)]])
212         self.cmd, self.state = [], 'IDLE'
213
214     def handle_channel_msg_generic(self):
215         # TODO: It should not be possible to hit this code.
216         # It currently can not be unit tested.
217         msg_type = self.cmd[0] & 0xf0
218         self.es_block = self.es
219         self.putx([0, ['Unknown channel message type: 0x%02x' % msg_type]])
220         self.cmd, self.state = [], 'IDLE'
221
222     def handle_channel_msg(self, newbyte):
223         self.cmd.append(newbyte)
224         msg_type = self.cmd[0] & 0xf0
225         handle_msg = getattr(self, 'handle_channel_msg_0x%02x' % msg_type,
226                              self.handle_channel_msg_generic)
227         handle_msg()
228
229     def handle_sysex_msg(self, newbyte):
230         # SysEx message: 1 status byte, 1-3 manuf. bytes, x data bytes, EOX byte
231         self.cmd.append(newbyte)
232         if newbyte != 0xf7: # EOX
233             return
234         self.es_block = self.es
235         # Note: Unlike other methods, this code pops bytes out of self.cmd
236         # to isolate the data.
237         msg, eox = self.cmd.pop(0), self.cmd.pop()
238         if len(self.cmd) < 1:
239             self.putx([0, ['SysEx: truncated manufacturer code (<1 bytes)']])
240             self.cmd, self.state = [], 'IDLE'
241             return
242         # Extract the manufacturer name (or SysEx realtime or non-realtime).
243         m1 = self.cmd.pop(0)
244         manu = (m1,)
245         if m1 == 0x00:  # If byte == 0, then 2 more manufacturer bytes follow.
246             if len(self.cmd) < 2:
247                 self.putx([0, ['SysEx: truncated manufacturer code (<3 bytes)']])
248                 self.cmd, self.state = [], 'IDLE'
249                 return
250             manu = (m1, self.cmd.pop(0), self.cmd.pop(0))
251         default_name = 'undefined'
252         manu_name = sysex_manufacturer_ids.get(manu, default_name)
253         if manu_name == default_name:
254             if len(manu) == 3:
255                 manu_name = '%s (0x%02x 0x%02x 0x%02x)' % \
256                             (default_name, manu[0], manu[1], manu[2])
257             else:
258                 manu_name = '%s (0x%02x)' % (default_name, manu[0])
259         # Extract the payload.
260         # TODO: Write methods to decode SysEx realtime & non-realtime payloads.
261         payload = ''
262         while len(self.cmd) > 0:
263             payload += '0x%02x ' % (self.cmd.pop(0))
264         if payload == '':
265             payload = '<empty>'
266         self.putx([0, ['SysEx: for \'%s\' with payload %s' % \
267                        (manu_name, payload)]])
268         self.cmd, self.state = [], 'IDLE'
269
270     def handle_syscommon_midi_time_code_quarter_frame_msg(self, newbyte):
271         # MIDI time code quarter frame: F1 nd
272         # n = message type
273         # d = values
274         c = self.cmd
275         if len(c) < 2:
276             return
277         msg = self.cmd[0]
278         nn, dd = (self.cmd[1] & 0x70) >> 4, self.cmd[1] & 0x0f
279         group = 'System Common'
280         self.es_block = self.es
281         if nn != 7: # If message type does not contain SMPTE type.
282             self.putx([0, ['%s: %s of %s, value 0x%01x' % \
283                 (group, status_bytes[msg], quarter_frame_type[nn], dd)]])
284             self.cmd, self.state = [], 'IDLE'
285             return
286         tt = (dd & 0x6) >> 1
287         self.putx([0, ['%s: %s of %s, value 0x%01x for %s' % \
288                        (group, status_bytes[msg], quarter_frame_type[nn], \
289                        dd, smpte_type[tt])]])
290         self.cmd, self.state = [], 'IDLE'
291
292     def handle_syscommon_msg(self, newbyte):
293         # System common messages
294         #
295         # There are 5 simple formats (which are directly handled here) and
296         # 1 complex one called MIDI time code quarter frame.
297         #
298         # Note: While the MIDI lists 0xf7 as a "system common" message, it
299         # is actually only used with SysEx messages so it is processed there.
300         self.cmd.append(newbyte)
301         msg = self.cmd[0]
302         c = self.cmd
303         group = 'System Common'
304         if msg == 0xf1:
305             # MIDI time code quarter frame
306             self.handle_syscommon_midi_time_code_quarter_frame_msg(newbyte)
307             return
308         elif msg == 0xf2:
309             # Song position pointer: F2 ll mm
310             # ll = LSB position, mm = MSB position
311             if len(c) < 3:
312                 return
313             ll, mm = self.cmd[1], self.cmd[2]
314             decimal = (mm << 7) + ll
315             self.es_block = self.es
316             self.putx([0, ['%s: %s 0x%02x 0x%02x (%d)' % \
317                 (group, status_bytes[msg], ll, mm, decimal)]])
318         elif msg == 0xf3:
319             # Song select: F3 ss
320             # ss = song selection number
321             if len(c) < 2:
322                 return
323             ss = self.cmd[1]
324             self.es_block = self.es
325             self.putx([0, ['%s: %s number %d' % (group, status_bytes[msg], ss)]])
326         elif msg == 0xf4 or msg == 0xf5 or msg == 0xf6:
327             # Undefined 0xf4, Undefined 0xf5, and Tune Request (respectively).
328             # All are only 1 byte long with no data bytes.
329             self.es_block = self.es
330             self.putx([0, ['%s: %s' % (group, status_bytes[msg])]])
331         self.cmd, self.state = [], 'IDLE'
332
333     def handle_sysrealtime_msg(self, newbyte):
334         # System realtime message: 0b11111ttt (t = message type)
335         self.es_block = self.es
336         self.putx([0, ['System realtime message: %s' % status_bytes[newbyte]]])
337         self.cmd, self.state = [], 'IDLE'
338
339     def decode(self, ss, es, data):
340         ptype, rxtx, pdata = data
341
342         # For now, ignore all UART packets except the actual data packets.
343         if ptype != 'DATA':
344             return
345
346         self.ss, self.es = ss, es
347
348         # We're only interested in the byte value (not individual bits).
349         pdata = pdata[0]
350
351         # Short MIDI overview:
352         #  - Status bytes are 0x80-0xff, data bytes are 0x00-0x7f.
353         #  - Most messages: 1 status byte, 1-2 data bytes.
354         #  - Real-time system messages: always 1 byte.
355         #  - SysEx messages: 1 status byte, n data bytes, EOX byte.
356
357         # State machine.
358         if self.state == 'IDLE':
359             # Wait until we see a status byte (bit 7 must be set).
360             if pdata < 0x80:
361                 return # TODO: How to handle? Ignore?
362             # This is a status byte, remember the start sample.
363             self.ss_block = ss
364             if pdata in range(0x80, 0xef + 1):
365                 self.state = 'HANDLE CHANNEL MSG'
366             elif pdata == 0xf0 or pdata == 0xf7:
367                 self.state = 'HANDLE SYSEX MSG'
368             elif pdata in range(0xf1, 0xf7):
369                 self.state = 'HANDLE SYSCOMMON MSG'
370             elif pdata in range(0xf8, 0xff + 1):
371                 self.state = 'HANDLE SYSREALTIME MSG'
372
373         # Yes, this is intentionally _not_ an 'elif' here.
374         if self.state == 'HANDLE CHANNEL MSG':
375             self.handle_channel_msg(pdata)
376         elif self.state == 'HANDLE SYSEX MSG':
377             self.handle_sysex_msg(pdata)
378         elif self.state == 'HANDLE SYSCOMMON MSG':
379             self.handle_syscommon_msg(pdata)
380         elif self.state == 'HANDLE SYSREALTIME MSG':
381             self.handle_sysrealtime_msg(pdata)