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.

⏱ 16 min read osc serial python nodejs maxmsp ableton

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

  1. Open a new patch and create a udpreceive 9000 object.
  2. Connect its output to a route /touch /release /proximity object.
  3. The /proximity outlet produces pairs — append a unpack to 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:

  1. In Max for Live, create a new MIDI device.
  2. Add udpreceive 9000 and OSC-route /touch.
  3. Map the outlet (electrode index 0–11) to makenotenoteout.
  4. For continuous control, route /proximity/0 to a live.dial or live.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 as ttyACM0 on Linux, cu.usbmodem... on macOS, and COMx on 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.