Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Copyright (c) 2015 Wolfgang Herget
Copyright (c) 2025 Felix Gertz

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
50 changes: 45 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
Behringer 3216 over HTTP
Behringer DDX3216 over HTTP
========================

This is a small node.js based thing to control a Behringer DDX3216 with a web browser.
This is a Node.js based server and user interface to control a Behringer DDX3216 with a web browser.

Ideally, this would run on an embedded system (e.g. a Raspberry Pi) that also provides a WiFi interface, so you could use a phone or tablet to control the desk (which supposedly sits at FOH) from, say, the stage.
Use cases
---------

Ideally this would run on an embedded system (e.g. a Raspberry Pi) that also provides a WiFi interface, so you could use a phone or tablet to control the desk (which probably sits at FOH) from, say, the stage.

Or if you integrate your MIDI devices with a MIDI router, f.ex. the MioXL, via RTP into a LAN or even WAN, this script can run on any server in your RTP network. You can use [rtpmidid](https://github.com/davidmoreno/rtpmidid) on the server to make a RTP midi port available for this script.

It is inspired by the various control apps available for newer digital mixers.

Features
----------

* 64 volume faders on 4 pages (Ch 1-16, Ch 17-23, Bus 1-16, Aux/Fx)
* dB fine tune buttons
* 8 sends (Aux/Fx) on each channel
* Pan
* Mute
* Editable channel names
* Multitouch faders
* Fullscreen mode
* Great UI name

![Uli Master 3216 UI Screenshot](screenshot-ulimaster.png "Uli Master DDX3216 UI Screenshot")

How to use
----------

You need to have a MIDI output device on your computer. (An input device is optional.)
You need to have a MIDI output device on your computer/server. (An input device is optional.)
You also need a C++ compiler, to allow the required `midi` module to compile.
Then, just run:

Expand All @@ -24,7 +44,22 @@ Due to quirks on the development setup, it will try to receive updates from the
If a device description is available, it will be printed on startup.
You can select other devices using the command line. Run `node main --help` for a short help.

When the server is running, navigate a browser to http://localhost:9080/.
```
usage: main.js [-h] [-l] [-d DEV] [-i INPUT] [-o OUTPUT]

DDX3216http

optional arguments:
-h, --help show this help message and exit
-l, --list List MIDI devices
-d DEV, --dev DEV Use Device number (1..16)
-i INPUT, --input INPUT
Use Device <INPUT> for input
-o OUTPUT, --output OUTPUT
Use Device <INPUT> for output
```

When the server is running, navigate a browser to http://localhost:9080/ or respectively your private LAN address.

Parameters are picked up as they are changed.
You can load a preset on the desk to make all settings known to the HTTP interface.
Expand All @@ -38,6 +73,11 @@ How to set up your desk
- Connect the MIDI Out port of the desk to the input of your computer's MIDI device
- Connect the output of your computer's MIDI to the MIDI In port of the desk.

Compability
-----------

For __mobile devices__ you need __at least Chrome 140, Firefox Mobile or a recent Safari for iOS__. The web ui should work on every modern desktop browser though.

Debugging
---------

Expand Down
107 changes: 101 additions & 6 deletions behringer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ var toHexString = function(intArray) {
const ALL_DEVICES = 0x60;
const PARAMETER_CHANGE = 0x20;

function getDefaultVolume(channel_number) {
if (channel_number >= 33 && channel_number <= 48) {
return 0;
}
if (channel_number >= 49 && channel_number <= 52) {
return -80;
}
if (channel_number >= 53 && channel_number <= 56) {
return 0;
}
if (channel_number >= 57 && channel_number <= 64) {
return -12;
}

return -80;
}

/**
*
* @param {number} channel_number
Expand All @@ -20,13 +37,21 @@ var Channel = function (channel_number, connection) {
this.events = Mitt();
this.channel = channel_number - 1;
this.connection = connection;
this.volume_db = -80;
this.volume_db = getDefaultVolume(channel_number);
this.mute = 0;
this.pan_db = 0;
this.aux = [
{db: -80, pre: false},
{db: -80, pre: false},
{db: -80, pre: false},
{db: -80, pre: false}
];
this.fx = [
{db: -80, pre: false},
{db: -80, pre: false},
{db: -80, pre: false},
{db: -80, pre: false}
];
};

Channel.prototype.setAuxSend = function(aux_ch, db) {
Expand All @@ -50,6 +75,27 @@ Channel.prototype.setAuxPre = function(aux_ch, isPre) {
this.connection.sendCommand(sysex);
};

Channel.prototype.setFxSend = function(fx_ch, db) {
this.fx[fx_ch - 1].db = db;
// fx1=80, fx2=82, ...
var parameterNumber = ((fx_ch - 1) * 2) + 80;
var dBValue = this.fullrangeValue(db);
var sysex = this.paramChange(parameterNumber, dBValue);
this.connection.sendCommand(sysex);
};

Channel.prototype.getFxSend = function(fx_ch) {
return this.fx[fx_ch - 1].db;
};

Channel.prototype.setFxPre = function(fx_ch, isPre) {
this.fx[fx_ch - 1].pre = isPre;
// pre-post fx1=81, fx2=83, ...
var parameterNumber = ((fx_ch - 1) * 2) + 81;
var sysex = this.paramChange(parameterNumber, isPre ? 1 : 0);
this.connection.sendCommand(sysex);
};

Channel.prototype.setVolume = function(dB) {
this.volume_db = dB;
var sysex = this.paramChange(1, this.fullrangeValue(dB));
Expand All @@ -60,6 +106,26 @@ Channel.prototype.getVolume = function() {
return this.volume_db;
}

Channel.prototype.setMute = function(value) {
this.mute = value;
var sysex = this.paramChange(2, Number(value));
this.connection.sendCommand(sysex);
}

Channel.prototype.getMute = function() {
return this.mute;
}

Channel.prototype.setPan = function(dB) {
this.pan_db = dB;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the SysEx manual calls it "dB" but I find it misleading as it's just 0 to 60 with 30 presumably being center?
AFAICS this currently should be called with values from -30 to +30.

I'm not enforcing inputs with other parameters, but a comment and name change might be helpful for future developers (i.e. probably future me).

var sysex = this.paramChange(3, Math.round(Number(dB) + 30));
this.connection.sendCommand(sysex);
}

Channel.prototype.getPan = function() {
return this.pan_db;
}

Channel.prototype.fullrangeValue = function(db_fraction) {
var unboundedValue = Math.round((db_fraction + 80) * 16);
// clamp to 0-1472
Expand All @@ -83,22 +149,32 @@ Channel.prototype.setFromMidi = function(param, high, low) {
var rawValue = (high << 7) | low;
switch(param) {
case 1: // volume
var db = Math.round((rawValue / 16) - 80);
var db = (rawValue / 16) - 80;
this.volume_db = db;
debug("Channel", this.channel, "set volume", db, "dB");
this.emitMidiEvent('vol', undefined, db);
break;
case 70:
case 2: // mute
this.mute = rawValue;
debug("Channel", this.channel, "set mute", rawValue);
this.emitMidiEvent('mute', undefined, rawValue);
break;
case 3: // pan
this.pan_db = -30 + rawValue;
debug("Channel", this.channel, "set pan", this.pan_db);
this.emitMidiEvent('pan', undefined, this.pan_db);
break;
case 70: // Aux volume
case 72:
case 74:
case 76:
var aux = (param - 70) / 2;
var db = Math.round((rawValue / 16) - 80);
var db = (rawValue / 16) - 80;
this.aux[aux].db = db;
debug("Channel", this.channel, "set aux", aux, "send", this.aux[aux].db, "dB");
this.emitMidiEvent('aux', aux, db);
break;
case 71:
case 71: // Aux pre/post
case 73:
case 75:
case 77:
Expand All @@ -107,6 +183,25 @@ Channel.prototype.setFromMidi = function(param, high, low) {
debug("Channel", this.channel, "set aux", aux, "pre", this.aux[aux].pre);
this.emitMidiEvent('aux_pre');
break;
case 80: // Fx volume
case 82:
case 84:
case 86:
var fx = (param - 80) / 2;
var db = (rawValue / 16) - 80;
this.fx[fx].db = db;
debug("Channel", this.channel, "set fx", fx, "send", this.fx[fx].db, "dB");
this.emitMidiEvent('fx', fx, db);
break;
case 81: // Fx pre/post
case 83:
case 85:
case 87:
var fx = (param - 81) / 2;
this.fx[fx].pre = (rawValue === 1);
debug("Channel", this.channel, "set fx", fx, "pre", this.fx[fx].pre);
this.emitMidiEvent('fx_pre');
break;
}
};

Expand Down Expand Up @@ -163,7 +258,7 @@ Behringer.prototype.forwardChannelEvent = function(type, param) {

Behringer.prototype.createChannels = function() {
this.channels = [];
for (var channel_number = 1; channel_number < 33; channel_number++) {
for (var channel_number = 1; channel_number < 65; channel_number++) {
this.channels[channel_number] = new Channel(channel_number, this);
this.channels[channel_number].events.on('*', this.eventForward);
}
Expand Down
30 changes: 27 additions & 3 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,26 @@ io.on('connection', function(socket){
debug("Ch", cmd.channel, "Aux", cmd.parameter, "@", cmd.value);
desk.channel(cmd.channel).setAuxSend(cmd.parameter, cmd.value);
});
socket.on('fx', function (message) {
var cmd = JSON.parse(message);
debug("Ch", cmd.channel, "Fx", cmd.parameter, "@", cmd.value);
desk.channel(cmd.channel).setFxSend(cmd.parameter, cmd.value);
});
socket.on('vol', function (message) {
var cmd = JSON.parse(message);
debug("Ch", cmd.channel, "@", cmd.value);
debug("Ch", cmd.channel, "@ vol", cmd.value);
desk.channel(cmd.channel).setVolume(cmd.value);
});
socket.on('mute', function (message) {
var cmd = JSON.parse(message);
debug("Ch", cmd.channel, "@ mute", cmd.value);
desk.channel(cmd.channel).setMute(cmd.value);
});
socket.on('pan', function (message) {
var cmd = JSON.parse(message);
debug("Ch", cmd.channel, "@ pan", cmd.value);
desk.channel(cmd.channel).setPan(cmd.value);
});
socket.on('get', function (message, cb) {
var cmd = JSON.parse(message);
debug("Get", cmd);
Expand All @@ -71,8 +86,17 @@ io.on('connection', function(socket){
case "vol":
cb(channel.getVolume());
break;
case "mute":
cb(channel.getMute());
break;
case "pan":
cb(channel.getPan());
break;
case "aux":
cb(channel.getAuxSend(cmd.parameter));
cb(channel.getAuxSend(cmd.parameter), cmd.parameter);
break;
case "fx":
cb(channel.getFxSend(cmd.parameter), cmd.parameter);
break;
}
});
Expand All @@ -81,7 +105,7 @@ io.on('connection', function(socket){
desk.ping();
desk.events.on('midi', function(param) {
var ch = param.channel.channel+1;
debug('Forward Midi to Web: ', ch);
debug('Forward Midi to Web: ', ch, param.setting, param.parameter, param.value);
var parameter = param.parameter;
if (parameter !== undefined) {
parameter += 1;
Expand Down
Loading