OSC Communication
Build a serial-to-OSC bridge in Python or Node.js and route Touch Board touch events directly into Max/MSP, Ableton Live, or any OSC-aware application.
Overview
Open Sound Control (OSC) is a network-friendly message protocol that is far more flexible than MIDI. It supports floating-point values, arbitrary address hierarchies, and bundle messages that timestamp-synchronise multiple parameters. The Touch Board speaks over USB serial, not UDP, so you need a small bridge application running on your computer that reads the serial port and re-sends the data as OSC. This tutorial walks through that bridge in both Python and Node.js, and then explains how to receive those messages in Max/MSP and Ableton Live.
How the Bridge Works
Touch Board (USB Serial) → Bridge script (Python / Node.js) → OSC (UDP) → Max/MSP / Ableton
The Touch Board firmware prints one line per event:
TOUCH,5
RELEASE,5
PROXIMITY,3,427
The bridge parses each line and maps it to an OSC address like /touch/5, /release/5, or /proximity/3 with a float payload.
Part 1 — Touch Board Firmware
Flash this sketch to the Touch Board. It prints structured CSV over Serial (USB):
#include <MPR121.h>
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin();
if (!MPR121.begin(0x5C)) {
Serial.println("ERROR: MPR121 not found");
while (true);
}
MPR121.setTouchThreshold(40);
MPR121.setReleaseThreshold(20);
}
void loop() {
MPR121.updateAll();
for (uint8_t i = 0; i < 12; i++) {
if (MPR121.isNewTouch(i)) {
Serial.print("TOUCH,");
Serial.println(i);
}
if (MPR121.isNewRelease(i)) {
Serial.print("RELEASE,");
Serial.println(i);
}
}
// Send proximity data for every electrode every loop
for (uint8_t i = 0; i < 12; i++) {
int raw = MPR121.getFilteredData(i);
int baseline = MPR121.getBaselineData(i);
int delta = baseline - raw;
if (delta > 5) { // only send when above noise floor
Serial.print("PROXIMITY,");
Serial.print(i);
Serial.print(",");
Serial.println(delta);
}
}
delay(20);
}
Part 2a — Python Bridge
Install the dependencies:
pip install pyserial python-osc
#!/usr/bin/env python3
"""
touch_board_osc_bridge.py
Reads Touch Board serial output and forwards it as OSC messages.
"""
import serial
import argparse
from pythonosc import udp_client
SERIAL_PORT = "/dev/ttyACM0" # Windows: "COM3", macOS: "/dev/cu.usbmodem..."
BAUD_RATE = 115200
OSC_HOST = "127.0.0.1"
OSC_PORT = 9000
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--port", default=SERIAL_PORT)
parser.add_argument("--osc-ip", default=OSC_HOST)
parser.add_argument("--osc-port",default=OSC_PORT, type=int)
args = parser.parse_args()
client = udp_client.SimpleUDPClient(args.osc_ip, args.osc_port)
ser = serial.Serial(args.port, BAUD_RATE, timeout=1)
print(f"Bridging {args.port} → OSC {args.osc_ip}:{args.osc_port}")
try:
while True:
raw = ser.readline()
if not raw:
continue
line = raw.decode("utf-8", errors="ignore").strip()
parts = line.split(",")
if parts[0] == "TOUCH" and len(parts) == 2:
electrode = int(parts[1])
client.send_message(f"/touch/{electrode}", 1)
print(f" /touch/{electrode} 1")
elif parts[0] == "RELEASE" and len(parts) == 2:
electrode = int(parts[1])
client.send_message(f"/release/{electrode}", 0)
print(f" /release/{electrode} 0")
elif parts[0] == "PROXIMITY" and len(parts) == 3:
electrode = int(parts[1])
delta = int(parts[2])
norm = min(delta / 500.0, 1.0) # normalise 0.0–1.0
client.send_message(f"/proximity/{electrode}", norm)
except KeyboardInterrupt:
print("\nStopped.")
finally:
ser.close()
if __name__ == "__main__":
main()
Run with:
python touch_board_osc_bridge.py --port /dev/ttyACM0 --osc-ip 127.0.0.1 --osc-port 9000
Part 2b — Node.js Bridge
npm init -y
npm install serialport @serialport/parser-readline osc
// bridge.js
const { SerialPort } = require("serialport");
const { ReadlineParser } = require("@serialport/parser-readline");
const OSC = require("osc");
const SERIAL_PATH = "/dev/ttyACM0"; // adjust for your OS
const OSC_HOST = "127.0.0.1";
const OSC_PORT = 9000;
const port = new SerialPort({ path: SERIAL_PATH, baudRate: 115200 });
const parser = port.pipe(new ReadlineParser({ delimiter: "\r\n" }));
const udpPort = new OSC.UDPPort({
localAddress: "0.0.0.0",
localPort: 57121,
remoteAddress: OSC_HOST,
remotePort: OSC_PORT,
});
udpPort.open();
parser.on("data", (line) => {
const parts = line.split(",");
if (parts[0] === "TOUCH") {
const el = parseInt(parts[1]);
udpPort.send({ address: `/touch/${el}`, args: [{ type: "i", value: 1 }] });
} else if (parts[0] === "RELEASE") {
const el = parseInt(parts[1]);
udpPort.send({ address: `/release/${el}`, args: [{ type: "i", value: 0 }] });
} else if (parts[0] === "PROXIMITY") {
const el = parseInt(parts[1]);
const delta = parseInt(parts[2]);
const norm = Math.min(delta / 500.0, 1.0);
udpPort.send({ address: `/proximity/${el}`, args: [{ type: "f", value: norm }] });
}
});
console.log(`Bridging ${SERIAL_PATH} → OSC ${OSC_HOST}:${OSC_PORT}`);
Part 3 — OSC Bundle Messages
OSC bundles let you send multiple values with a shared timestamp, which is essential when you want several parameters to update atomically — for example, sending all 12 proximity values in one network packet:
from pythonosc import osc_bundle_builder, osc_message_builder
import time
def send_proximity_bundle(client, deltas):
"""Send all 12 proximity values as a single OSC bundle."""
builder = osc_bundle_builder.OscBundleBuilder(
osc_bundle_builder.IMMEDIATELY # timetag = now
)
for i, delta in enumerate(deltas):
msg = osc_message_builder.OscMessageBuilder(address=f"/proximity/{i}")
msg.add_arg(min(delta / 500.0, 1.0), arg_type="f")
builder.add_content(msg.build())
bundle = builder.build()
client._sock.sendto(bundle.dgram, (client._address, client._port))
Part 4 — Receiving in Max/MSP
- Open a new patch and create a
udpreceive 9000object. - Connect its output to a
route /touch /release /proximityobject. - The
/proximityoutlet produces pairs — append aunpackto separate electrode index from value.
A simple Max patch fragment:
[udpreceive 9000]
|
[OSC-route /touch /release /proximity]
| | |
[print] [print] [unpack i f]
| |
[print] [print]
For RNBO or gen~ integration, connect the float output to a param or in object driving oscillator parameters.
Part 5 — Receiving in Ableton Live
Use the Max for Live device OSCmidi (free on Max for Live Packs) or build a small M4L MIDI generator:
- In Max for Live, create a new MIDI device.
- Add
udpreceive 9000andOSC-route /touch. - Map the outlet (electrode index 0–11) to
makenote→noteout. - For continuous control, route
/proximity/0to alive.dialorlive.remote~object targeting a track parameter.
Alternatively, use the OSCHook AU/VST plugin, which maps OSC paths to MIDI CC with no scripting required.
OSC Address Pattern Reference
| OSC Address | Type | Value Range | Meaning |
|---|---|---|---|
/touch/{n} |
int | 1 | Electrode n just touched |
/release/{n} |
int | 0 | Electrode n just released |
/proximity/{n} |
float | 0.0–1.0 | Normalised proximity for electrode n |
/touch/all |
int[12] | 0 or 1 | Bitmask of all touch states |
Troubleshooting
- “Device not found”: check
ls /dev/tty*(macOS/Linux) or Device Manager (Windows) for the correct serial path. The Touch Board appears asttyACM0on Linux,cu.usbmodem...on macOS, andCOMxon Windows. - Messages arrive but are jittery: increase the
delay()in the Arduino sketch from 20 ms to 40 ms to reduce USB serial congestion. - Max/MSP shows no data: confirm the OSC port number matches on both sides. Firewalls can block UDP even on localhost — temporarily disable the firewall to diagnose.
- Node.js crashes with ENOENT: the SerialPort path is wrong or the board is not plugged in yet. Add a
.on("error", ...)handler for graceful recovery.