- Open Sound Control Bridge
- Install
- Overview
- Configuration
- Development
OSCBridge is a tool to help automate operations with audio/streaming gear.
Input could come from various sources, such as:
- Digital Audio Mixer Console state (such as Behringer X32 or other that supports OSC)
- OBS Studio state
- A HTTP Request
- Time
OSCBridge currently supports the following "tasks":
- HTTP Request
- Delay (just wait)
- OBS Change preview scene
- OBS Change program scene
- OBS Send "vendor" message to any plugin that cares, e.g. to the amazingly excellent Advanced Scene Switcher.
- Excecute a command
- Send an OSC message
Here is just a few idea:
- When a microphone is unmuted, turn the PTZ camera to the speaker.
- When the stage is unmuted, turn the PTZ camera to the stage.
- When a special HTTP request arrives, mute/unmute something.
- When a special HTTP request arrives, set the volume of a channel to the specified value.
- At a specified time, unmute a microphone.
- At a specified time, switch to an OBS Scene.
- At a specified time, send an HTTP Request.
- When something is unmuted, switch to a scene in OBS.
- When a scene is activated in OBS unmute certain channels.
- When a microphone is unmuted, then turn the camera but only if a ceratin OBS scene is active.
- When ... send a command to Advanced Scene Switcher, to do a zillion other things
- When ... then make Advanced Scene Switcher do an http request to execute some other actions through the oscbridge. ( Btw A.S.S. can send OSC messages too.)
I think now you got the point!
- Download the binary from the latest release
- Create a config.yml next to the binary.
- Execute!
$:oscbridge$ ls
config.yml oscbridge-6acaf3b4-linux-amd64.bin
$:oscbridge$ chmod +x oscbridge-6acaf3b4-linux-amd64.bin
$:oscbridge$ ./oscbridge-6acaf3b4-linux-amd64.bin
2023-11-13 07:30:51 [ INFO] OPEN SOUND CONTROL BRIDGE is starting.
2023-11-13 07:30:51 [ INFO] Version: v1.0.0 Revision: 6acaf3b4
2023-11-13 07:30:51 [ INFO] Initializing OBS connections...
2023-11-13 07:30:51 [ INFO] Connecting to streaming_pc_obs...
2023-11-13 07:30:51 [ INFO] Initializing OBS bridges...
2023-11-13 07:30:51 [ INFO] Initializing Open Sound Control (mixer consoles, etc) connections...
...
You may override the config.yml location with the environment variable APP_CONFIG_FILE
, e.g.: APP_CONFIG_FILE=/a/b/c/d/osc.yml
.
A docker-hub version will be coming soon when my time permits.
From a birds eye view, oscbridge provides a central "message store", to which "osc sources" can publish messages. Every time a new message arrives, each action is checked, if their trigger_chain conditions are resolving to true based on the current store. If every the trigger chain resolves to true, then the action's tasks are executed.
So this is the control flow: [OSC SOURCES] -> [OSC MESSAGE STORE] -> [ACTION TRIGGER CHAIN] -> [ACTION TASK]
Below is the simplest example to showcase how the system works.
Click to see YAML
obs_connections:
- name: "streaming_pc_obs"
host: 192.168.1.75
port: 4455
password: "foobar"
osc_sources:
console_bridges:
- name: "behringer_x32"
enabled: false
prefix: ""
host: 192.168.2.99
port: 10023
osc_implementation: l
init_command:
address: /xinfo
check_address: /ch/01/mix/on
check_pattern: "^0|1$"
subscriptions:
- osc_command:
address: /subscribe
arguments:
- type: string
value: /ch/01/mix/on
- type: int32
value: 10
repeat_millis: 8000
dummy_connections:
- name: "behringer_x32_dummy"
enabled: true
prefix: ""
iteration_speed_secs: 1
message_groups:
- name: mic_1_on
osc_commands:
- address: /ch/01/mix/on
comment: "headset mute (0: muted, 1: unmuted)"
arguments:
- type: int32
value: 1
- name: mic_1_off
osc_commands:
- address: /ch/01/mix/on
comment: "headset mute (0: muted, 1: unmuted)"
arguments:
- type: int32
value: 0
actions:
to_pulpit:
trigger_chain:
type: osc_match
parameters:
address: /ch/01/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
tasks:
- type: http_request
parameters:
url: "http://127.0.0.1:8888/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&0&__TURN_TO_PULPIT"
method: "get"
timeout_secs: 1
- type: obs_scene_change
parameters:
scene: "PULPIT"
scene_match_type: regexp
target: "program"
connection: "streaming_pc_obs"
- type: obs_scene_change
parameters:
scene: "STAGE"
scene_match_type: regexp
target: "preview"
connection: "streaming_pc_obs"
to_stage:
trigger_chain:
type: osc_match
parameters:
address: /ch/01/mix/on
arguments:
- index: 0
type: "int32"
value: "0"
tasks:
- type: http_request
parameters:
url: "http://127.0.0.1:8888/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&1&__TURN_TO_STAGE"
method: "get"
timeout_secs: 1
- type: obs_scene_change
parameters:
scene: "STAGE"
scene_match_type: regexp
target: "program"
connection: "streaming_pc_obs"
- type: obs_scene_change
parameters:
scene: "PULPIT"
scene_match_type: regexp
target: "preview"
connection: "streaming_pc_obs"
In this configuration there are two OSC sources:
- Dummy (enabled)
- A Behringer X32 digital console (disabled)
The dummy source acts as if someone would press Ch1's mute button every second to toggle it.
Then there are two actions defined, "to_pulpit" and "to_stage". Each has a single trigger, that matches /ch/01/mix/on to be 0 or 1.
Then for each action, there are three tasks:
- An HTTP request that would recall a PTZ Optics camera preset (0 and 1 respectively).
- An obs_scene_change to change the program scene.
- An obs_scene_change to change the preview scene.
You can see the results on this gif:
OBS is switching scenes based on the mute status, and at the bottom you can see the arriving requests.
You can just switch from the dummy to the console one, and your mute button is then tied to OBS scenes and the camera.
Actions encapsulate a so called trigger_chain
and a list of tasks
together.
This is how actions look like:
actions:
change_to_pulpit:
trigger_chain:
# ... tree of conditions
tasks:
# ... 1 dimensional list of tasks to be executed in order, serially
change_to_stage:
trigger_chain:
# ... tree of conditions
tasks:
# ... 1 dimensional list of tasks to be executed in order, serially
start_live_stream:
trigger_chain:
# ... tree of conditions
tasks:
# ... 1 dimensional list of tasks to be executed in order, serially
Each action has it's own name, that is shown in the logs upon evaluation/execution.
Whenever the internal store receives an update, OSCBridge checks each action's trigger_chain, the tree of conditions if they match the store or not. If the trigger_chain is evaluated to be true, then the tasks will be executed.
There is an option, that can be specified for each action, called debounce_millis
,
if provided then the logic changes a bit. Upon store change, if the trigger_chain resolves to true,
then after the specified ammount of milliseconds the trigger_chain is re-evaluated.
If it is still true, only then will the tasks be executed.
For example:
actions:
change_to_pulpit:
trigger_chain:
# ... tree of conditions
tasks:
# ... 1 dimensional list of tasks to be executed in order, serially
debounce_millis: 500
This could protect against quick transients, e.g. an accidental unmute/mute. For example here, if the trigger chain is watching for ch1's unmute, then it will only execute the tasks if it is unmute for more than 0.5seconds. This can help avoid accidents, where you accidentally unmute something but then you immediately mute it back.
The trigger chain is a tree of conditions. Some conditions can be nested, some of them are just leafs on a tree, without any children.
You can build very complex conditions into here, e.g. (in pseudo code):
IF
(mic1-is-muted AND mic2-is-unmuted) OR
(ch10-is-unmuted AND
(
ch11fader > 0.5 OR
ch12fader > 0.5
)
) THEN
...
But the way to express these are a bit more complicated due to the YAML configuration we use.
The osc_match
condition can nothave any children, and it is checking for a single message in the store.
It can check based on address, address regexp and also based on arguments.
Here is an example:
actions:
change_to_pulpit:
trigger_chain:
- type: osc_match
parameters:
address: /ch/01/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
tasks:
# ...
This is a single condition on an action's trigger_chain. This checks for a message with an exact address of "/ch/01/mix/on" and with a single first argument, that is int32 and the value is 1.
If such a message exists in the store, the tasks will be executed.
Parameters:
Parameter | Default value | Possible values | Description | Example values |
---|---|---|---|---|
address | none, required | The value for matching a message's address. Can be a regexp, see next option. | /ch/01/mix/on, /ch/0[0-9]/mix/on | |
address_match_type | eq |
eq , regexp |
Determines the way of address matching. | regexp |
trigger_on_change | true |
true , false |
See the trigger on change paragraph. | true |
arguments | none, optional | See the next table. | List of arguments |
Arguments:
Parameter | Default value | Possible values | Description | Example values |
---|---|---|---|---|
index | none, required | 0 |
The 0 based index for the argument. | 0 , 1 , 2 |
type | none, required | string , int32 , float32 |
The type of the argument. | string |
value | none, required | The value of the argument. | 1 |
|
value_match_type | = |
regexp , <= ,< ,> ,>= ,!= |
The comparison method. In case of regexp, the value can be a regexp expression. | = |
The trigger_on_change
option is a special one. Whenever a new message arrives that changes the store, every
trigger_chain is checked.
Now, during the execution of the trigger_chain, it is being monitored what messages those conditions accessed.
By default (when trigger_on_change: true
) if the trigger chain did not access the NEWLY UPDATED message, so the one
that just arrived,
the tasks aren't going to be executed. This avoids unneccessary re-execution just because an unrelevant message updated
the store.
But this is also usable, to avoid re-execution in a case when a relevant message updated the store.
Practically this option decouples a condition from being a trigger. The condition is still required to match in order to execute the tasks, but that single condition's change will not trigger execution.
You want to set this to false, when you don't want to re-execute the action upon the toggling of one of the parameters your trigger_chain is watching for. This is an edge case, that comes handy sometimes.
For example, let's say you have the following trigger_chain (in pseudo-ish code):
IF ( OBS-scene-name-contains-foobar AND
OR (ch1-unmuted OR ch2-unmuted OR stage-is-muted)
)
THEN
...
So you want to only execute the tasks, when certain things on the console match, but don't wanna re-execute just because of an OBS scene change. But you only want to execute the tasks, when certain things on the console match AND obs scene name contains foobar.
Then you can mark the OBS-scene-name-contains condition with trigger_on_change: false
.
That will cause the tasks to be executed when the console state changes (and obs scene contains foobar), but will not
trigger if only obs changes would otherwise match.
E.g. you might switch from one scene to another that contains foobar in our pseudo example, but that would not
re-execute the tasks.
And
as it's name implies requires all children to resolve to true.
The following example action requires both ch1 AND ch2 to be on.
actions:
change_to_pulpit:
trigger_chain:
type: and
children:
- type: osc_match
parameters:
address: /ch/01/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
- type: osc_match
parameters:
address: /ch/02/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
tasks:
# ...
Now you see how conditions can be nested.
Or
as it's name implies requires that at least one of the childrens would resolve to true.
The following example action executes the tasks if ch1 OR ch2 is be on.
actions:
change_to_pulpit:
trigger_chain:
type: or
children:
- type: osc_match
parameters:
address: /ch/01/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
- type: osc_match
parameters:
address: /ch/02/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
tasks:
# ...
The NOT
condition simply negates it's single child's result.
Here is how you would achieve this pseudo code:
AND(ch1-unmuted; NOT(OR(ch10-unmuted,ch20-unmuted)))
In yaml:
actions:
change_to_pulpit:
trigger_chain:
type: and
children:
- type: osc_match
parameters:
address: /ch/01/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
- type: not
children:
- type: or
children:
- type: osc_match
parameters:
address: /ch/10/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
- type: osc_match
parameters:
address: /ch/20/mix/on
arguments:
- index: 0
type: "int32"
value: "1"
tasks:
# ...
Now that you know how to compose conditions, you need input sources, that would add messages to the internal store, against which you can match your trigger chains.
Many digital mixing consoles support a protocol called "Open Sound Control", this is a UDP based simple protocol. It is based on "Messages", where each message has an address, and 0 or more arguments, and each argument can be a string, a float, an int, etc.
I have tested on Behringer X32, so most examples are based on this console. See pmalliot's excellent work here on X32's OSC implementation.
In the case of X32, we need to regularly(8-10 sec) issue a /subscribe command with proper arguments, to show that we are interested in updates of a certain value from the console. Then the mixer is flooding us with the requested parameter.
So below is a real world example for behringer x32 OSC connection:
Click to see YAML
osc_sources:
console_bridges:
# The name of this mixer
- name: "behringer_x32"
# If enabled, OSCBRIDGE will try to connect, and restart if fails.
enabled: true
# Prefix determines the message address prefix as it will be stored to the store.
# E.g. if you'd have multiple consoles, you could prefix them "/console1", "/console2",
# and you could match for /console1/ch/01/mix/on for example.
prefix: ""
host: 192.168.2.99
port: 10023
# The driver to use. We only have "l" for now.
osc_implementation: l
# This command is sent right after the connection is opened.
# It can be used for authentication, or anything that is required.
# X32 does not require anything, but for this it returns it's own name.
init_command:
address: /xinfo
# You could specify arguments also.
# arguments:
# - type: string
# value: "foobar"
# There is a regular query running, for checking if the connection is still alive.
# Specify an address here, and a regexp that matches the returned value.
# If there is no response, or the response doesn't match, the connection is counted as broken and the app restarts.
check_address: /ch/01/mix/on
check_pattern: "^0|1$"
# Subscriptions are commands that are sent regularly (repeat_millis) that cause the mixer to update us with the lates values for the subscribed thing.
# Research your own mixer for the exact syntax, but this is how you do it for X32.
subscriptions:
- osc_command:
# This command subscribes for channel 1's mute status. 0 is muted, 1 is unmuted.
address: /subscribe
arguments:
- type: string
value: /ch/01/mix/on
- type: int32
value: 10
repeat_millis: 8000
The dummy console implementation is just what it's name implies.
It has message_groups
, and each message_group
contains messages
.
The dummy console iterates infinitely through the groups, and executes the messages in them.
Between each group it waits the configured ammount of time.
The below example configures two groups, called "mic_1_on" and "mic_1_off".
Therefore, it provides a way to test the logic even without a real connection to a mixer. You can have a dummy emitting the same messages the real console would, and you can freely enable/disable any source, so you can test, or you can switch to the real operation mode by enabling the console connection.
Click to see YAML
osc_sources:
dummy_connections:
- name: "behringer_x32_dummy"
# Use this source, or not.
enabled: true
# Prefix determines the message address prefix as it will be stored to the store.
prefix: ""
# How much delay should be between each group?
iteration_speed_secs: 1
# Message groups are set of messages being emitted at once.
message_groups:
- name: mic_1_on
osc_commands:
- address: /ch/01/mix/on
comment: "headset mute (0: muted, 1: unmuted)"
arguments:
- type: int32
value: 1
- name: mic_1_off
osc_commands:
- address: /ch/01/mix/on
comment: "headset mute (0: muted, 1: unmuted)"
arguments:
- type: int32
value: 0
OSCBridge can be configured to connect to an OBS Studio instance via websocket, and it will subscribe to some events in OBS.
These events are the following:
- CurrentPreviewSceneChanged
- Message:
- Address: /obs/preview_scene
- Argument[0]: string, value: NAME_OF_SCENE
- Message:
- CurrentProgramSceneChanged
- Message:
- Address: /obs/program_scene
- Argument[0]: string, value: NAME_OF_SCENE
- Message:
- RecordStateChanged
- Message:
- Address: /obs/recording
- Argument[0]: int32, value: 0 or 1
- Message:
- StreamStateChanged
- Message:
- Address: /obs/streaming
- Argument[0]: int32, value: 0 or 1
- Message:
In order to configure an OBS Bridge, you'll also need to configure an OBS Connection.
Click to see YAML
obs_connections:
- name: "streampc_obs"
host: 192.168.1.75
port: 4455
password: "foobar12345"
osc_sources:
obs_bridges:
- name: "obsbridge1"
# You may choose to disable it.
enabled: true
# Prefix determines the message address prefix as it will be stored to the store.
prefix: ""
# The name of the obs connection, see above.
connection: "streampc_obs"
HTTP Bridges in OSCBridge enables you to open a port on a network interface and start a HTTP server on them. The server can receive special HTTP GET requests, and converts them to OSC messages and stores them in the message store. Then you can write actions that check for that value, and may even execute tasks based on it.
The message can be put away under some namespace by using the prefix option, but you could also use it to override an existing message.
To insert an OSC Message like this:
Message(address: /foo/bar/baz, arguments: [Argument(string:hello), Argument(int32:1)])
Execute a GET request like this:
curl "127.0.0.1:7878/?address=/foo/bar/baz&args[]=string,hello&args[]=int32,1"
Click to see YAML
osc_sources:
http_bridges:
- name: "httpbridge1"
# You may choose to disable it.
enabled: true
# Prefix determines the message address prefix as it will be stored to the store.
prefix: ""
port: 7878
host: 0.0.0.0
You can enable "Tickers", that would regularly update the store with messages representing the current date/time.
The ticker publishes several packages under "/time/" (if you don't specify a prefix), with names that might be weird for the first time, if you are not familiar with how golang's time formatting works.
You may see the full reference here.
Currently these messages are being emitted in every iteration:
Message(address: /time/rfc3339, arguments: [Argument(string:2023-11-07T08:53:06Z)])
Message(address: /time/parts/2006, arguments: [Argument(string:2023)])
Message(address: /time/parts/06, arguments: [Argument(string:23)])
Message(address: /time/parts/Jan, arguments: [Argument(string:Nov)])
Message(address: /time/parts/January, arguments: [Argument(string:November)])
Message(address: /time/parts/01, arguments: [Argument(string:11)])
Message(address: /time/parts/1, arguments: [Argument(string:11)])
Message(address: /time/parts/Mon, arguments: [Argument(string:Tue)])
Message(address: /time/parts/Monday, arguments: [Argument(string:Tuesday)])
Message(address: /time/parts/2, arguments: [Argument(string:7)])
Message(address: /time/parts/_2, arguments: [Argument(string: 7)])
Message(address: /time/parts/02, arguments: [Argument(string:07)])
Message(address: /time/parts/__2, arguments: [Argument(string:311)])
Message(address: /time/parts/002, arguments: [Argument(string:311)])
Message(address: /time/parts/15, arguments: [Argument(string:08)])
Message(address: /time/parts/3, arguments: [Argument(string:8)])
Message(address: /time/parts/03, arguments: [Argument(string:08)])
Message(address: /time/parts/4, arguments: [Argument(string:53)])
Message(address: /time/parts/04, arguments: [Argument(string:53)])
Message(address: /time/parts/5, arguments: [Argument(string:6)])
Message(address: /time/parts/05, arguments: [Argument(string:06)])
Message(address: /time/parts/PM, arguments: [Argument(string:AM)])
So if you want to match for hour:minute, then you want to match the values of /time/15 and /time/04 respectively in the trigger chain (to be explained later).
Click to see YAML
osc_sources:
tickers:
- name: "ticker1"
# You may choose to disable it.
enabled: true
# Prefix determines the message address prefix as it will be stored to the store.
prefix: ""
# How often updates should occur
refresh_rate_millis: 1000
Now you have actions, trigger_chains and sources, the final piece is to have tasks that will be executed if the trigger_chain evaluates to true.
The http_request
task executes a specific http request upon evaluation.
Parameters:
Parameter | Default value | Possible values | Description | Example values |
---|---|---|---|---|
url | none, required | The URL for the request. | http://127.0.0.1/?foo=bar | |
body | empty string | The request body. | {"json":"or something else"} | |
timeout_secs | 30 | The timeout for the request. | 1 | |
method | GET |
GET , POST |
The method for the request. | POST |
headers | empty | A list of "Key: value" pairs. | - "Content-Type: text/json" |
Example:
actions:
to_pulpit:
trigger_chain:
# ...
tasks:
- type: http_request
parameters:
url: "http://127.0.0.1:8888/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&0&__TURN_TO_PULPIT"
method: "get"
timeout_secs: 1
headers:
- "X-Foo: bar"
- "X-Foo2: baz"
body: "O HAI"
The request that will be made:
GET /cgi-bin/ptzctrl.cgi?ptzcmd&poscall&0&__TURN_TO_PULPIT HTTP/1.1
Host: 127.0.0.1:8888
User-Agent: Go-http-client/1.1
Content-Length: 5
X-Foo: bar
X-Foo2: baz
Accept-Encoding: gzip
O HAI
The obs_scene_change
task changes the live or program scene on a remote OBS instance.
Parameters:
Parameter | Default value | Possible values | Description | Example values |
---|---|---|---|---|
scene | none, required | The name of the scene to which we need to switch. | PULPIT , STAGE |
|
connection | none, required | The name of the obs connection that this task should use. | streampc_obs |
|
scene_match_type | exact |
exact , regexp |
How to match the scene name. | regexp |
target | none, required | program , preview |
Which side of OBS should be switched. | program |
Example:
obs_connections:
- name: "streampc_obs"
host: 192.168.1.75
port: 4455
password: "foobar12345"
actions:
to_pulpit:
trigger_chain:
# ...
tasks:
- type: obs_scene_change
parameters:
scene: "PULPIT.*"
scene_match_type: regexp
target: "program"
connection: "streaming_pc_obs"
- type: obs_scene_change
parameters:
scene: "STAGE"
scene_match_type: exact
target: "preview"
connection: "streaming_pc_obs"
It is possible to send a VendorEvent to OBS via a websocket connection. Different plugins can listen for these events, one example is the marvelous Advanced Scene Switcher, which supports this.
So given that you are listening in that plugin for "IF Websocket Message waas received: foobar_notice", you can execute macros remotely with OSCBridge:
obs_connections:
- name: "streampc_obs"
host: 192.168.1.75
port: 4455
password: "foobar12345"
actions:
to_pulpit:
trigger_chain:
# ...
tasks:
- type: obs_vendor_request
parameters:
connection: "streampc_obs"
vendorName: "AdvancedSceneSwitcher"
requestType: "AdvancedSceneSwitcherMessage"
requestData:
message: "foobar_notice"
Parameters:
Parameter | Default value | Description | Example values |
---|---|---|---|
connection | none, required | The name of the obs connection that this task should use. | streampc_obs |
vendorName | none, required | AdvancedSceneSwitcher |
|
requestType | none, required | AdvancedSceneSwitcherMessage |
|
requestData | none, required | message: whatever |
The delay
simply delays the serial execution of the tasks, taking up as much time as you configure.
Parameters:
Parameter | Default value | Description | Example values |
---|---|---|---|
delay_millis | none, required | How much milliseconds to wait. | 1500 (for 1.5 second) |
Example:
actions:
to_pulpit:
trigger_chain:
# ...
tasks:
- type: delay
parameters:
delay_millis: 1500
The run_command
task simply executes the given command.
Parameter | Default value | Description | Example values |
---|---|---|---|
command | none, required | The path to the binary to execute. | /usr/bin/bash |
arguments | optional | The list of arguments. | - "-l" |
run_in_background | false | Whether or not the serial execution of tasks should wait for the command to finish. | |
directory | optional | The execution folder for the command. |
You need to follow the classical way of specifying a binary and it's
arguments.
So you can not use date > /tmp/date.txt
as the command, you need to specify /usr/bin/bash
as the command, and then
the parameters.
Example:
actions:
to_pulpit:
trigger_chain:
# ...
tasks:
- type: run_command
parameters:
command: "/usr/bin/bash"
arguments: [ "-l","-c","date > /tmp/date.txt" ]
The send_osc_message
sends an open sound control message through the specified connection.
Currently only the console_bridges
support sending a message. E.g. you can send a message back to your console.
Parameter | Default value | Description | Example values |
---|---|---|---|
connection | none, required | The OSC connection to use (the name from one of your console_bridges ) |
behringer_x32 |
address | none, required | The address of the message. | /ch/10/mix/on |
arguments | optional | The arguments of the message. | - type: int32 |
Example:
(Unmute channel 10)
actions:
to_pulpit:
trigger_chain:
# ...
tasks:
- type: send_osc_message
parameters:
connection: "behringer_x32"
address: "/ch/10/mix/on"
arguments:
- type: int32
value: 1
You'll need "make" and "docker" installed. After cloning the repository, run "make" to see the available commands.
Run make dev_start
to start the development environment.
It'll look for a config.yml in the source root.