Skip to content
Merged
55 changes: 47 additions & 8 deletions software/desktop/osc_8mu.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import re
import socket
import struct
import threading
import time


Expand All @@ -49,6 +50,7 @@ def __init__(
controls: dict[int, int],
scale: float = 1.0,
debug: bool = False,
use_bundle: bool = False,
):
"""
Create the interface between 8mu and EuroPi
Expand All @@ -59,6 +61,7 @@ def __init__(
@param europi_namespace The OSC namespace that the destination EuroPi is using
@param controls A dict that maps MIDI controls to CV1-6
@param debug Enable additional debugging output
@param use_bundle Enable sending data as an OSC Bundle instead of individual packets

@exception ValueError if port is out of range, or IP address is invalid
@exception FileNotFoundError if the 8mu was not found in the MIDI inputs
Expand All @@ -77,6 +80,10 @@ def __init__(

self.scale = scale
self.controls = controls
self.use_bundle = use_bundle

self.midi_readings_lock = threading.Lock()
self.midi_readings = {}

if not self.europi_namespace.startswith("/"):
self.europi_namespace = f"/{self.europi_namespace}"
Expand Down Expand Up @@ -105,14 +112,10 @@ def on_8mu_slider(self, msg: mido.Message):
cv_out = self.controls[msg.control]
osc_value = msg.value / 127.0 * self.scale # convert to 0-1 float
address = f"{self.europi_namespace}/cv{cv_out}"
packet = self.encode_packet(address, osc_value)

try:
if self.debug:
print(f"{self.europi_namespace}/cv{cv_out} -> {osc_value}")
self.osc_socket.sendto(packet, (self.europi_ip, self.osc_port))
except Exception as err:
print(err)
self.midi_readings_lock.acquire()
self.midi_readings[address] = osc_value
self.midi_readings_lock.release()

def encode_packet(self, address: str, value: float) -> bytearray:
"""
Expand Down Expand Up @@ -150,7 +153,35 @@ def pad_length(arr):

def spin(self):
while True:
time.sleep(0.001)
time.sleep(0.01)

packets = []
self.midi_readings_lock.acquire()
for address in self.midi_readings.keys():
packets.append(self.encode_packet(address, self.midi_readings[address]))
self.midi_readings_lock.release()

if self.use_bundle:
# bundle header + all-zero timestamp
bundle = "#bundle\0\0\0\0\0\0\0\0\0".encode("utf-8")
for p in packets:
bundle += len(p).to_bytes(length=4, byteorder="big")
bundle += p

try:
if self.debug:
print(f"Sending bundle {bundle}")
self.osc_socket.sendto(bundle, (self.europi_ip, self.osc_port))
except Exception as err:
print(err)
else:
for p in packets:
try:
if self.debug:
print(f"Sending packet {p}")
self.osc_socket.sendto(p, (self.europi_ip, self.osc_port))
except Exception as err:
print(err)


def main():
Expand Down Expand Up @@ -186,6 +217,13 @@ def main():
default="192.168.4.1",
help="EuroPi's IP address. Default: 192.168.4.1",
)
parser.add_argument(
"-b",
"--bundle",
dest="bundle",
action="store_true",
help="Send data as a single OSC bundle instead of individual packets",
)
parser.add_argument(
"-s",
"--scale",
Expand Down Expand Up @@ -277,6 +315,7 @@ def main():
},
scale=args.scale,
debug=args.debug,
use_bundle=args.bundle,
)

print("Press CTRL+C to terminate")
Expand Down
183 changes: 108 additions & 75 deletions software/firmware/experimental/osc.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,80 +88,76 @@ def __init__(self, data: bytes):
data_start = align_next_word(data_start)
i = type_start + 1
d = data_start
while data[i] != 0x00:
t = chr(data[i])
if t == "i":
types.append(int)
n = (data[d] << 24) | (data[d + 1] << 16) | (data[d + 2] << 8) | data[d + 1]
self.values.append(n)
d += 4
elif t == "f":
types.append(float)
n = struct.unpack(">f", data[d : d + 4])[0]
self.values.append(n)
d += 4
elif t == "s" or t == "S": # include the alternate "S" here
types.append(str)
s = ""
string_end = data.index(b"\0", d)
for c in range(d, string_end):
s += chr(data[c])
self.values.append(s)
d = string_end
d = align_next_word(d)
elif t == "b":
# blob; int32 -> n, followed by n bytes
types.append(bytearray)
n = (data[d] << 24) | (data[d + 1] << 16) | (data[d + 2] << 8) | data[d + 1]
d += 4
b = []
for i in range(n):
b.append(data[d + i])
d += n
d = align_next_word(d)
self.values.append(bytearray(b))
elif t == "T" or t == "F":
# zero-byte boolean; skip
pass
elif t == "t":
# 8-byte timestamp; skip
d += 8
elif t == "h":
# 64-bit signed integer
# treat as a normal int
types.append(int)
n = (
(data[d] << 56)
| (data[d + 1] << 48)
| (data[d + 2] << 40)
| (data[d + 3] << 32)
| (data[d + 4] << 24)
| (data[d + 5] << 16)
| (data[d + 6] << 8)
| data[d + 7]
)
self.values.append(n)
d += 8
elif t == "c":
# a single character; treat as a string
types.append(str)
self.values.append(
data[d + 3].decode() # data is in the 4th byte; padded with leading zeros
)
d += 4
elif t == "m":
# 4-byte midi; skip
d += 4
elif t == "N":
# nil; skip
pass
elif t == "I":
# infinity; skip
pass
else:
log_warning(f"Unsupported type {t}", "osc")
try:
while i < len(data) and data[i] != 0x00:
t = chr(data[i])
if t == "i":
types.append(int)
n = int.from_bytes(data[d : d + 4], "big")
self.values.append(n)
d += 4
elif t == "f":
types.append(float)
n = struct.unpack(">f", data[d : d + 4])[0]
self.values.append(n)
d += 4
elif t == "s" or t == "S": # include the alternate "S" here
types.append(str)
s = ""
string_end = data.index(b"\0", d)
for c in range(d, string_end):
s += chr(data[c])
self.values.append(s)
d = string_end
d = align_next_word(d)
elif t == "b":
# blob; int32 -> n, followed by n bytes
types.append(bytearray)
n = int.from_bytes(data[d : d + 4], "big")
d += 4
b = []
for j in range(n):
b.append(data[d + j])
d += n
d = align_next_word(d)
self.values.append(bytearray(b))
elif t == "T" or t == "F":
# zero-byte boolean; skip
pass
elif t == "t":
# 8-byte timestamp; skip
d += 8
elif t == "h":
# 64-bit signed integer
# treat as a normal int
types.append(int)
n = int.from_bytes(data[d : d + 8], "big")
self.values.append(n)
d += 8
elif t == "c":
# a single character; treat as a string
types.append(str)
self.values.append(
data[d + 3].decode() # data is in the 4th byte; padded with leading zeros
)
d += 4
elif t == "m":
# 4-byte midi; skip
d += 4
elif t == "N":
# nil; skip
pass
elif t == "I":
# infinity; skip
pass
else:
log_warning(f"Unsupported type {t}", "osc")

i += 1
i += 1
except IndexError:
# If we fall off of the array for any reason, just keep what we have so far.
# This could happen if the data got corrupted in transport.
pass

@property
def values(self) -> list[int | float | str | bytearray]:
Expand All @@ -177,6 +173,9 @@ def address(self) -> str:
"""This packet's address"""
return self._address

def __str__(self):
return f"{self.address}: {self.values}"


class OpenSoundServer:
"""
Expand Down Expand Up @@ -250,13 +249,47 @@ def wrapper(*args, **kwargs):
self.recv_callback = wrapper
return wrapper

def parse_packets(self, data, result):
"""
Recursively process the raw data, decomposing bundles into an array of packets.

:param[in] data: The raw byte data received over the socket
:param[out] result: An array we can recusively append bundle data to
"""
if len(data) > 8 and data[0:7].decode("utf-8") == "#bundle":
# We're processing a bundle
# The first 8 bytes after the header are the timestamp, which we don't support
# so skip that and go straight to the payload
# ['#', 'b', 'u', 'n', 'd', l', 'e', '\0', t0, t1, t2, t3, t4, t5, t6, t7]
try:
data = data[16:]
while len(data) > 0:
element_length = int.from_bytes(data[0:4], "big")
data = data[4:]
self.parse_packets(data, result)
data = data[element_length:]
except IndexError as err:
# either the length got corrupted, or the packet was partially dropped
# either way, just process what we can and move on
pass
else:
# we're processing a normal packet; add it to the result
packet = OpenSoundPacket(data)
result.append(packet)

@property
def elements(self):
return self._elements

def receive_data(self):
"""Check if we have any new data to process, invoke data_handler as needed"""
while True:
try:
(data, connection) = self.recv_socket.recvfrom(1024)
packet = OpenSoundPacket(data)
self.recv_callback(connection=connection, data=packet)
packets = []
self.parse_packets(data, packets)
for packet in packets:
self.recv_callback(connection=connection, data=packet)
except ValueError as err:
log_warning(f"Failed to process packet: {err}", "osc")
break
Expand Down
Loading