Skip to content

Commit 83dc8d5

Browse files
committed
Add 'm' 4-byte MIDI type tag
1 parent 66acb41 commit 83dc8d5

5 files changed

Lines changed: 94 additions & 3 deletions

File tree

pythonosc/osc_message.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def _parse_datagram(self):
4747
val, index = osc_types.get_blob(self._dgram, index)
4848
elif param == "r": # RGBA.
4949
val, index = osc_types.get_rgba(self._dgram, index)
50+
elif param == "m": # MIDI.
51+
val, index = osc_types.get_midi(self._dgram, index)
5052
elif param == "t": # osc time tag:
5153
val, index = osc_types.get_ttag(self._dgram, index)
5254
elif param == "T": # True.

pythonosc/osc_message_builder.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ class OscMessageBuilder(object):
1616
ARG_TYPE_STRING = "s"
1717
ARG_TYPE_BLOB = "b"
1818
ARG_TYPE_RGBA = "r"
19+
ARG_TYPE_MIDI = "m"
1920
ARG_TYPE_TRUE = "T"
2021
ARG_TYPE_FALSE = "F"
2122

2223
ARG_TYPE_ARRAY_START = "["
2324
ARG_TYPE_ARRAY_STOP = "]"
2425

2526
_SUPPORTED_ARG_TYPES = (
26-
ARG_TYPE_FLOAT, ARG_TYPE_INT, ARG_TYPE_BLOB, ARG_TYPE_STRING, ARG_TYPE_RGBA, ARG_TYPE_TRUE, ARG_TYPE_FALSE)
27+
ARG_TYPE_FLOAT, ARG_TYPE_INT, ARG_TYPE_BLOB, ARG_TYPE_STRING, ARG_TYPE_RGBA,
28+
ARG_TYPE_MIDI, ARG_TYPE_TRUE, ARG_TYPE_FALSE)
2729

2830
def __init__(self, address=None):
2931
"""Initialize a new builder for a message.
@@ -103,6 +105,8 @@ def _get_arg_type(self, arg_value):
103105
arg_type = self.ARG_TYPE_INT
104106
elif isinstance(arg_value, float):
105107
arg_type = self.ARG_TYPE_FLOAT
108+
elif isinstance(arg_value, tuple) and len(arg_value) == 4:
109+
arg_type = self.ARG_TYPE_MIDI
106110
elif isinstance(arg_value, list):
107111
arg_type = [self._get_arg_type(v) for v in arg_value]
108112
else:
@@ -143,6 +147,8 @@ def build(self):
143147
dgram += osc_types.write_blob(value)
144148
elif arg_type == self.ARG_TYPE_RGBA:
145149
dgram += osc_types.write_rgba(value)
150+
elif arg_type == self.ARG_TYPE_MIDI:
151+
dgram += osc_types.write_midi(value)
146152
elif arg_type in (self.ARG_TYPE_TRUE,
147153
self.ARG_TYPE_FALSE,
148154
self.ARG_TYPE_ARRAY_START,

pythonosc/parsing/osc_types.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,44 @@ def get_rgba(dgram, start_index):
319319
start_index + _INT_DGRAM_LEN)
320320
except (struct.error, TypeError) as e:
321321
raise ParseError('Could not parse datagram %s' % e)
322+
323+
324+
def write_midi(val):
325+
"""Returns the datagram for the given MIDI message parameter value
326+
327+
A valid MIDI message: (port id, status byte, data1, data2).
328+
329+
Raises:
330+
- BuildError if the MIDI message could not be converted.
331+
332+
"""
333+
try:
334+
assert 4 == len(val)
335+
value = sum((value & 0xFF) << 8 * (3-pos) for pos, value in enumerate(val))
336+
return struct.pack('>I', value)
337+
except (struct.error, AssertionError) as e:
338+
raise BuildError('Wrong argument value passed: {}'.format(e))
339+
340+
341+
def get_midi(dgram, start_index):
342+
"""Get a MIDI message (port id, status byte, data1, data2) from the datagram.
343+
344+
Args:
345+
dgram: A datagram packet.
346+
start_index: An index where the MIDI message starts in the datagram.
347+
348+
Returns:
349+
A tuple containing the MIDI message and the new end index.
350+
351+
Raises:
352+
ParseError if the datagram could not be parsed.
353+
"""
354+
try:
355+
if len(dgram[start_index:]) < _INT_DGRAM_LEN:
356+
raise ParseError('Datagram is too short')
357+
val = struct.unpack('>I',
358+
dgram[start_index:start_index + _INT_DGRAM_LEN])[0]
359+
midi_msg = tuple((val & 0xFF << 8 * i) >> 8 * i for i in range(3,-1, -1))
360+
return (midi_msg, start_index + _INT_DGRAM_LEN)
361+
except (struct.error, TypeError) as e:
362+
raise ParseError('Could not parse datagram %s' % e)

pythonosc/test/parsing/test_osc_types.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,45 @@ def test_datagram_too_short(self):
123123
self.assertRaises(osc_types.ParseError, osc_types.get_rgba, dgram, 2)
124124

125125

126+
class TestMidi(unittest.TestCase): # TODO
127+
128+
def test_get_midi(self):
129+
cases = {
130+
b"\x00\x00\x00\x00": ((0,0,0,0), 4),
131+
b"\x00\x00\x00\x02": ((0,0,0,1), 4),
132+
b"\x00\x00\x00\x02": ((0,0,0,2), 4),
133+
b"\x00\x00\x00\x03": ((0,0,0,3), 4),
134+
135+
b"\x00\x00\x01\x00": ((0,0,1,0), 4),
136+
b"\x00\x01\x00\x00": ((0,1,0,0), 4),
137+
b"\x01\x00\x00\x00": ((1,0,0,0), 4),
138+
139+
b"\x00\x00\x00\x01GARBAGE": ((0,0,0,1), 4),
140+
}
141+
142+
for dgram, expected in cases.items():
143+
self.assertEqual(
144+
expected, osc_types.get_midi(dgram, 0))
145+
146+
def test_get_midi_raises_on_type_error(self):
147+
cases = [b'', True]
148+
149+
for case in cases:
150+
self.assertRaises(osc_types.ParseError, osc_types.get_midi, case, 0)
151+
152+
def test_get_midi_raises_on_wrong_start_index(self):
153+
self.assertRaises(
154+
osc_types.ParseError, osc_types.get_midi, b'\x00\x00\x00\x11', 1)
155+
156+
def test_get_midi_raises_on_wrong_start_index_negative(self):
157+
self.assertRaises(
158+
osc_types.ParseError, osc_types.get_midi, b'\x00\x00\x00\x00', -1)
159+
160+
def test_datagram_too_short(self):
161+
dgram = b'\x00' * 3
162+
self.assertRaises(osc_types.ParseError, osc_types.get_midi, dgram, 2)
163+
164+
126165
class TestDate(unittest.TestCase):
127166
def test_get_ttag(self):
128167
cases = {

pythonosc/test/test_osc_message_builder.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ def test_all_param_types(self):
4242
builder.add_arg(b"\x01\x02\x03", builder.ARG_TYPE_BLOB)
4343
builder.add_arg([1, ["abc"]], [builder.ARG_TYPE_INT, [builder.ARG_TYPE_STRING]])
4444
builder.add_arg(4278255360, builder.ARG_TYPE_RGBA)
45-
self.assertEqual(len("fisTFb[i[s]]")*2+1, len(builder.args))
45+
builder.add_arg((1,145, 36, 125), builder.ARG_TYPE_MIDI)
46+
self.assertEqual(len("fisTFb[i[s]]")*2+2, len(builder.args))
4647
self.assertEqual("/SYNC", builder.address)
4748
builder.address = '/SEEK'
4849
msg = builder.build()
4950
self.assertEqual("/SEEK", msg.address)
5051
self.assertSequenceEqual(
51-
[4.0, 2, "value", True, False, b"\x01\x02\x03", [1, ["abc"]]] * 2 + [4278255360], msg.params)
52+
[4.0, 2, "value", True, False, b"\x01\x02\x03", [1, ["abc"]]] * 2 +
53+
[4278255360, (1,145, 36, 125)],
54+
msg.params)
5255

5356
def test_long_list(self):
5457
huge_list = list(range(512))

0 commit comments

Comments
 (0)