TouchOSC While playing around with my Behringer X32 Rack, I stumbled upon some quirks.

Setup

I want to use an half-broken old iPad to control my personal monitoring settings and mute my microphone. I want one button to mute the microphone while holding it, and one button to permanently mute the microphone. For personal monitoring, I use two of the AUX outputs, which I have linked to a stereo pair. So if you adjust the volume of one of those channels, both are updated simultaneously.

To achive this, I used TouchOSC. There is an unofficial documentation of the OSC commands provided by the X32, which we can use to configure our buttons in the TouchOSC Editor.

Getting Status of Faders and Mutes

There are two steps required, to get the level and status of our channels via OSC.

  1. Enable remote control as well as send and receive of faders and mutes Setup -> Remote -> MIDI Control Interface
  2. To enable sending of data by the mixer, we need to send the OSC command /xremote at least every 10 seconds

The second step creates some headache, as TouchOSC does not allow (as for now) to periodically send commands besides /ping.

So I wrote a simple proxy in Python to keep the X32 sending.

Receiving Updates

Soon I noticed, that the X32 does not send fader updates, if they are caused by an OSC command. This causes trouble for my two mute buttons and linked channels. The toggle state of the buttons and the faders are not synced properly, if updated via the iPad. To fix this, I simply reflect all commands I send to the proxy back to the client to register all updates.

Initial State of Faders and Buttons in TouchOSC

To receive the value of faders and mutes, you can simply send the mute and fader command without parameters. Unfortunately, TouchOSC does not do this to initilize its state.

As I already had a periodic send of the /xremote command, I just added the fader poll commands in this loop and enabled the periodic /ping in the ThochOSC settings. Now if I start the app, the status of the faders and mutes appear after at least 5 seconds.

The Script

Disclaimer: As of my laziness, the script does not support multiple TouchOSC clients and contains the address of the X32 hardcoded. I may genereralize this in the future and throw it on GitHub.

Requirement: python-osc

import argparse

from pythonosc import udp_client
from pythonosc import dispatcher
from pythonosc import osc_server

from pythonosc import osc_packet
from typing import overload, List, Union, Any, Generator, Tuple
from types import FunctionType
from pythonosc.osc_message import OscMessage
import time
import socket
import threading

last_recv_addr = None
behringer_addr = '<BEHRINGER_IP>'
client = udp_client.SimpleUDPClient(behringer_addr, 10023)

def keep_behringer_awake():
  while True:
    print("send xremote and mtx fader poll")
    client.send_message('/xremote', None)
    client.send_message('/mtx/02/mix/fader', None)
    client.send_message('/mtx/01/mix/fader', None)
    client.send_message('/mtx/01/mix/on', None)
    client.send_message('/ch/01/mix/on', None)
    client.send_message('/mtx/02/mix/on', None)
    client.send_message('/main/st/mix/on', None)
    client.send_message('/main/st/mix/fader', None)
    time.sleep(5)

class MyDispatcher(dispatcher.Dispatcher):
    def call_handlers_for_packet(self, data: bytes, client_address: Tuple[str, int]) -> None:
        # ugly. Needs refactoring
        global last_recv_addr
        global behringer_addr
        # Get OSC messages from all bundles or standalone message.
        try:
            # Loop prevention
            if client_address[0] != behringer_addr:
              client._sock.sendto(data, (behringer_addr, 10023))
              last_recv_addr = client_address
            if last_recv_addr is not None:
              client._sock.sendto(data, last_recv_addr)
            packet = osc_packet.OscPacket(data)
            for timed_msg in packet.messages:
              now = time.time()
              handlers = self.handlers_for_address(timed_msg.message.address)
              if not handlers:
                continue
                # If the message is to be handled later, then so be it.
              if timed_msg.time > now:
                time.sleep(timed_msg.time - now)
                for handler in handlers:
                  handler.invoke(client_address, timed_msg.message)
        except osc_packet.ParseError:
            pass

if __name__ == "__main__":
  parser = argparse.ArgumentParser()
  parser.add_argument("--ip",
      default="0.0.0.0", help="The ip to listen on")
  parser.add_argument("--port",
      type=int, default=10023, help="The port to listen on")
  args = parser.parse_args()

  dispatcher = MyDispatcher()

  server = osc_server.ThreadingOSCUDPServer(
      (args.ip, args.port), dispatcher)
  print("Serving on {}".format(server.server_address))
  client._sock = server.socket
  x = threading.Thread(target=keep_behringer_awake)
  x.start()
  server.serve_forever()