Behringer's X32 OSC Implementation is a bit Quirky
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.
- Enable remote control as well as send and receive of faders and mutes
- 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()