PreSonus StudioLive API
An unofficial Node.js API for the PreSonus StudioLive III consoles.
Tested against the following models
- StudioLive 16 Mixer
- StudioLive 16R Rack Mixer
- StudioLive 24R Rack Mixer
Read more here
Installation
This node.js package is only hosted on GitHub, however you can install packages from npm
and yarn
in the same way as you would with any other package.
npm install featherbear/presonus-studiolive-api#v1.4.2
yarn add featherbear/presonus-studiolive-api#v1.4.2
Documentation
Data Types
payload:SubscriptionOptions
Property | Type | Default | Description |
---|---|---|---|
clientDescription |
string |
"User" |
Display name |
clientIdentifier |
string |
"133d066a919ea0ea" |
Identifier |
Used during API client connection to configure identity.
If using multiple instances of this API, it is recommended to modify the clientIdentifier
property
enum:MessageCode
import { MessageCode } from 'presonus-studiolive-api'
// ...
client.on(MessageCode.JSON, (evt) => {
// ...
})
// or
client.on("JM", (evt) => {
// ...
})
Name | Code | Description |
---|---|---|
JSON |
JM |
JSON packet |
Setting |
PV |
Settings packet |
DeviceList |
PL |
Device listing packet |
FileResource |
FR |
File data packet |
FileResource2 |
FD |
File data packet |
ZLIB |
ZB |
Zlib data packet |
Unknown1 |
BO |
??? |
Unknown2 |
CK |
Compressed data packet |
Unknown3 |
MB |
Mute status packet |
FaderPosition |
MS |
Fader position packet |
Payloads emitted from these events are of various data types
type:DiscoveryType
Example
{
"name": "StudioLive 16R",
"serial": "RA1E21060260",
"ip": "10.0.0.18",
"port": 53000,
"timestamp": [Date object]
}
Property | Type | Description |
---|---|---|
name |
string |
Device model |
serial |
string |
Device serial |
ip |
string |
Device IP |
port |
number |
Device port |
timestamp |
Date |
Time the device was (last) discovered |
See Discovery
key:ChannelTypes
LINE
MAIN
TALKBACK
AUX
SUB
FX
FXRETURN
type:ChannelSelector
Property | Type | Description |
---|---|---|
type |
ChannelTypes |
Target channel type |
channel |
number |
Target channel |
mixType |
AUX or FX |
(optional) Target mix type |
mixNumber |
number |
(optional) Target mix number |
// Set channel 7 to 50%
<Client>.setChannelVolumeLinear({
type: 'LINE',
channel: 7
}, 50)
// Mute channel 13 on Aux 2
<Client>.mute({
type: 'LINE',
channel: 13,
mixType: 'AUX',
mixNumber: 2
})
If type
is MAIN
or TALKBACK
, the value of channel
will be implicitly set to 1
type:MeterData
Example
client.on('meter', (meterData) => {
meterData.input[0] // Input signal meter for channel 1
meterData.input[10] // Input signal meter for channel 9
meterData.main[0] // Left channel signal meter for main output
meterData.main[0] // Right channel signal meter for main output
})
client.meterSubscribe()
input
-number[]
- Input channel metersmainMixFaders
-number[]
- Main mix channel metersmain
- Main mix metersaux_metering
-number[]
- Auxiliary bus meterschannelStrip
- Signal processor metersstripA
-number[]
stripB
-number[]
stripC
-number[]
stripD
-number[]
stripE
-number[]
aux_chstrip
- Auxiliary bus signal processor metersstripA
-number[]
stripB
-number[]
stripC
-number[]
stripD
-number[]
main_chstrip
- Main mix signal processor metersstageA
-number[]
stageB
-number[]
stageC
-number[]
stageD
-number[]
fx_chstrip
- FX bus signal processor metersinputs
-number[]
stripA
-number[]
stripB
-number[]
stripC
-number[]
fxreturn_strip
- FX bus return signal processor metersinput
-number[]
stripA
-number[]
stripB
-number[]
stripC
-number[]
See Metering
Events
🔥 Also see presonus-studiolive-api-simple-api
Events are split into data events and lifecycle events.
Data Events
import { MessageCode } from 'presonus-studiolive-api'
// Listen for changes to settings
<Client>.on(MessageCode.Setting, function(data) {
console.log(data)
})
or
// Listen for changes to settings
<Client>.on("PV", (data) => {
console.log(data)
})
Specific events can be listened to by attaching an event handler with <Client>.on(type, callback)
.
A list of possible event types are given in enum:MessageCode
.
To listen to all events, attach an event handler to the data
event.
This event will be emitted after the specific event is emitted.
To listen to meter events, attach an event handler to the meter
event.
See Metering
Lifecycle Events
Lifecycle Events
client.on('connected', () => {
console.log("Connected to the device!")
})
To trigger a function when client has successfully connected to the console, listen to the connected
event.
Other events: closed
, error
, reconnecting
client.connect().then(() => {
console.log("Also connected to the device!")
})
Note: The <Client>.connect(...)
function returns a Promise, so you may use .then(cb)
to continue execution after connection
Client API
ES5
const { Client, MessageCode } = require('presonus-studiolive-api')
ES6
import Client, { MessageCode } from 'presonus-studiolive-api'
Example
import Client from 'presonus-studiolive-api'
// Also import some enums
import { MessageCode } from 'presonus-studiolive-api'
const client = new Client({host: "10.0.0.18", port: 53000})
client.connect({
clientDescription: 'Console Remote'
}).then(() => {
// Set channel 1 fader to -6 dB over a duration of 3 seconds
client.setChannelVolumeLogarithmic({
type: 'LINE',
channel: 1
}, -6, 3000).then(() => {
// Unmute channel 2
client.unmute({
type: 'LINE',
channel: 2
})
})
console.log(`Version ${client.state.get('global.mixer_version')}`)
})
new Client({host: string, port?: number = 53000}, {autoreconnect?: boolean, logLevel: [bunyan]})
Creates a Client object that will later connect to the console
<Client>.connect(subscribeData?: SubscriptionOptions)
Connect to the console. See SubscriptionOptions
<Client>.close()
Disconnect from the console
<Client>.on(evtName: str, fn: function)
Attach a handler function fn
to an event given by evtName
.
See Events
<Client>.mute(selector: ChannelSelector)
Mute a channel
<Client>.unmute(selector: ChannelSelector)
Unmute a channel
<Client>.toggleMute(selector: ChannelSelector)
Toggle the mute status of a channel
<Client>.setMute(selector: ChannelSelector, status: boolean)
Set the mute status of a channel
<Client>.setChannelVolumeLogarithmic(selector: ChannelSelector, decibel: number, duration?: number)
Set the level of a channel to a certain decibel (between -84
and 10
).
Optionally provide a duration
(in milliseconds) to set the transition time.
<Client>.setChannelVolumeLinear(selector: ChannelSelector, linearLevel: number, duration?: number)
Set the level of a channel to a certain percentage (between 0
and 100
).
Optionally provide a duration
(in milliseconds) to set the transition time.
<Client>.setLink(selector: ChannelSelector, link: boolean)
Link/unlink a stereo channel pair
<Client>.setColor(selector: ChannelSelector, hex: string, alpha?: number)
<Client>.setColour(selector: ChannelSelector, hex: string, alpha?: number)
Set the colour of a channel, provide a colour hex string (e.g. ffa7a4
), and optionally an alpha (transparency) value (between 0
and 255
)
<Client>.setPan(selector: ChannelSelector, pan: number)
Pan a channel (mono) (L = 0
, C = 50
, R = 100
), or set the stereo width of a stereo chanel pair (between 0
and 100
).
<Client>.getChannelPresets()
Get channel presets stored on the console
<Client>.getProjects(complete?: boolean)
Get projects stored on the console, and optionally scenes as well
<Client>.getScenesOfProject(projFile: string)
Get scenes of a project
<Client>.recallProject(projFile: string)
Recall a project
<Client>.recallProjectScene(projFile: string, sceneFile: string)
Recall a scene
<Client>.currentScene
Current project scene
<Client>.currentProject
Current project
Metering
client.on('meter', function(metering) {
console.log(metering.input[0])
})
client.meterSubscribe()
<Client>.on('meter', Function<MeterData>)
Listen to meter events
<Client>.meterSubscribe(port?: number)
Start the metering server
<Client>.stop()
Stop the metering server
See type:MeterData
Device State
client.state.get('global.mixer_name')
client.state.get('line.ch1.mute')
<Client>.state.get(key)
Get a device state given by its key
.
This function is used internally by other methods, but can be accessed by the user to extract other data that might not yet be used.
Refer to the zlib.parsed
file for known keys.
Alternatively, refer to the IntelliSense information at <Client>.state._data
Discovery
// Discover for 30 seconds
client.discover(30 * 1000).then(devices => {
for (let device of devices) {
// device.name
// device.serial
// device.ip
// device.port
// device.timestamp
console.log(device)
}
})
<Client>.discover(timeout: number = 10 * 1000)
Perform a console discovery for timeout
milliseconds.
Returns an array containing the devices found within the discovery period
Standalone Discovery
import { Discovery } from 'presonus-studiolive-api
If you only want to search for devices on the network without the intention of connecting to them, you can run the discovery client independently.
const discoveryClient = new Discovery()
discoveryClient.on('discover',
(device) => {
// device.name
// device.serial
// device.ip
// device.port
// device.timestamp
}
})
// Discover indefinitely
discoveryClient.start()
// Discover for 10 seconds
discoveryClient.start(10 * 1000)
new Discovery()
Instantiate the client
<Client>.on('discover', Function<DiscoveryType>)
Attach discover
event handler
<Client>.start(timeout?: ms)
Listen for discovery events (optional timeout
)
Internal Functions
<Client>.sendList(key)
Request list resource
<Client>.dumpState()
(DEBUG) Extract the data structure and cache layer values.
Research
Protocol research was performed on a StudioLive Series III 16R rack mixer, StudioLive Series III 24R rack mixer and a StudioLive Series III 16 console, using the UC Surface application on Android and Windows.
Old Python code: github.com/featherbear/presonus-studiolive-api/tree/research_python
High-Level Overview
- TCP connections are used for control
- The console listens on TCP port
53000
- The console listens on TCP port
UDP connections are used for metering data
- The client opens up a UDP server on a random port
- The client notifies the console of the port
Every 3 seconds, the console emits a
Discovery
packetThe client sends a
KA
(KeepAlive
) packet every few secondsWhen the client connects it sends a Hello and Subscribe message to the console
- The console replies with a
SubscriptionReply
and Zlib data containing the console state
- The console replies with a
General
Packet Headers
All packets start with the four bytes \x55\x43\x00\x01
(UC..)
Payload Types
There are different formats of payloads, identified by a two byte preamble preceding the payload data
Code | Description |
---|---|
KA |
KeepAlive |
UM |
Hello |
JM |
JSON |
PV |
Parameter Value |
PS |
Parameter String |
PL |
Parameter String List |
FR |
File Request |
FD |
File Data |
BO |
Binary Object |
CK |
Chunk |
ZB |
ZLIB Compressed Data |
MS |
Fader Position |
Payload Packing
In most situations, the packet is sent as follows
Note
Packet Size = length(Payload Type) + length(Payload Data)
Offset | Length | Type | Description |
---|---|---|---|
0 | 4 | \x55\x43\x00\x01 |
Header |
2 | 2 | uint16 (LE) |
Payload Size |
4 | 2 | char[2] |
Payload Type |
6 | ... | ... | Data |
C-Bytes
There are four bytes that appear after the payload type in the payload data.
The first and third positions have some byte value (either 0x6a
, 0x68
, or 0x65
), with the other positions being NULL.
i.e. 68 00 65 00
, or 6a 00 65 00
I will refer to this group of four bytes as C-Bytes (custom bytes).
The first byte will be C-Byte A, and the third as C-Byte B
i.e If a message is sent with
{A: j, B: e}
a response will be sent containing{A: e, B: j}
so that the response can be matched with the request.
They seem to be used for matching responses to requests.
For most cases it looks like they're not too important to keep synchronised - But as I keep researching and developing this API, I might find the need to actually implement proper request matching
Sent Payloads
Hello
The first packet that gets sent to the console
55 43 00 01 08 00 55 4d 00 00 65 00 15 fa UC....UM..e...
Offset | Length | Description | Note |
---|---|---|---|
0 | 4 | Header | |
4 | 2 | Payload Size | |
6 | 2 | Payload Type | UM |
8 | 4 | C-Bytes | |
12 | 2 | UDP Metering Port | uint16 (LE) |
Subscribe
Register the client to the console
55 43 00 01 fe 00 4a 4d 6a 00 65 00 f4 00 00 00 UC....JMj.e.....
7b 22 69 64 22 3a 20 22 53 75 62 73 63 72 69 62 {"id": "Subscrib
65 22 2c 22 63 6c 69 65 6e 74 4e 61 6d 65 22 3a e","clientName":
20 22 55 6e 69 76 65 72 73 61 6c 20 43 6f 6e 74 "Universal Cont
72 6f 6c 22 2c 22 63 6c 69 65 6e 74 49 6e 74 65 rol","clientInte
72 6e 61 6c 4e 61 6d 65 22 3a 20 22 75 63 61 70 rnalName": "ucap
70 22 2c 22 63 6c 69 65 6e 74 54 79 70 65 22 3a p","clientType":
20 22 50 43 22 2c 22 63 6c 69 65 6e 74 44 65 73 "PC","clientDes
63 72 69 70 74 69 6f 6e 22 3a 20 22 46 45 41 54 cription": "FEAT
48 45 52 4e 45 54 2d 50 43 22 2c 22 63 6c 69 65 HERNET-PC","clie
6e 74 49 64 65 6e 74 69 66 69 65 72 22 3a 20 22 ntIdentifier": "
46 45 41 54 48 45 52 4e 45 54 2d 50 43 22 2c 22 FEATHERNET-PC","
63 6c 69 65 6e 74 4f 70 74 69 6f 6e 73 22 3a 20 clientOptions":
22 70 65 72 6d 20 75 73 65 72 73 20 6c 65 76 6c "perm users levl
20 72 65 64 75 20 72 74 61 6e 22 2c 22 63 6c 69 redu rtan","cli
65 6e 74 45 6e 63 6f 64 69 6e 67 22 3a 20 32 33 entEncoding": 23
31 30 36 7d 106}
Offset | Length | Description | Note |
---|---|---|---|
0 | 4 | Header | |
4 | 2 | Payload Size | |
6 | 2 | Payload Type | JM |
8 | 4 | C-Bytes | |
12 | 4 | JSON Size | uint32 (LE) |
16 | ... | JSON Data |
The console will then reply with a response JSON payload {"id": "SubscriptionReply"}
, and a ZLIB payload*
JSON Fields
Key | Type | Description | Known Values |
---|---|---|---|
id |
string | Action | "Subscribe" |
clientName |
string | Name | "Universal Control" /"UC-Surface" |
clientInternalName |
string | Internal name | "ucapp" /"ucremoteapp" |
clientType |
string | Client platform | |
clientDescription |
string | Visible name | |
clientIdentifier |
string | ID | |
clientOptions |
string | ??? | "perm users levl redu rtan" |
clientEncoding |
integer | ??? | 23106 |
File Request
Request a file from the console.
File path is terminated with a NULL character.
55 43 00 01 1d 00 46 52 6a 00 65 00 01 00 4c 69 UC....FRj.e...Li
73 74 70 72 65 73 65 74 73 2f 63 68 61 6e 6e 65 stpresets/channe
6c 00 00 l..
Offset | Length | Description | Note |
---|---|---|---|
0 | 4 | Header | |
4 | 2 | Payload Size | |
6 | 2 | Payload Type | FR |
8 | 4 | C-Bytes | |
12 | 2 | ?? | 01 00 |
14 | ... | Request path | |
x | Null byte | 00 |
Received Packets
(Maybe they're the same?)
Settings
Setting packets have a payload type code of 0x50 0x56
(PV
).
Format
key +
\x00
+partA
(+partB
)
key
- The name of the setting valuepartA
-0x00 0x00
or0x00 0x01
partB
- Optional 4-byte value
For most responses, partA
is 0x00 0x00
, and partB
is appended.
However, for keys related to filter groups, partA
is 0x00 0x01
and partB
is omitted.
Discovery
Note
Discovery packets do not follow the standard format
Every 3 seconds, the console will broadcast a UDP packet (from port
53000
) to255.255.255.255:47809
.
55 43 00 01 08 cf 44 41 65 00 00 00 00 04 00 80 UC....DAe.......
48 1c 48 67 23 60 51 4f 92 4e 1e 46 91 50 51 d1 H.Hg#`QO.N.F.PQ.
53 74 75 64 69 6f 4c 69 76 65 20 32 34 52 00 41 StudioLive 24R.A
55 44 00 52 41 32 45 31 38 30 33 30 32 32 36 00 UD.RA2E18030226.
53 74 75 64 69 6f 4c 69 76 65 20 32 34 52 00 StudioLive 24R.
Offset | Length | Description | Note |
---|---|---|---|
0 | 4 | Header | |
4 | 2 | Source Port | uint16 (LE) |
6 | ... | ... | |
24 | ... | Console Model* |
*The first "StudioLive 24R" is used to identify the icon in UC Surface. The second instance appears to be some sort of friendly name
55 43 00 01 07 c2 44 51 00 00 65 00 UC....DQ..e.
or
55 43 00 01 00 00 44 51 00 00 65 00 UC....DQ..e.
When a client (Universal Control, etc) receives this broadcast, it will attempt to communicate with the console
ZLIB Compressed Data
Example decompressed payload: zlib.dump
Example decoded payload: zlib.parsed
ZB
payloads contain a zlib-compressed copy of the mixer state.
These payloads have their own 4-byte little-endian size header.
Offset | Length | Description | Note |
---|---|---|---|
0 | 4 | Header | |
4 | 2 | Payload Size | |
6 | 2 | Payload Type | ZB |
8 | 4 | C-Bytes | |
12 | 4 | Compressed Payload Size | uint32 (LE) |
16 | ... | Compressed Payload Data |
The decompressed payload is a UBJSON payload
Chunk Data
Example assembled payload: CK_zlib.dump
Example packet trace: connect_and_idle_UC_SL16R.pcapng
CK
payloads are multi-part portions of a single larger payload. The underlying type of the larger payload is provided in the chunk.
Whilst a single CK
payload can span multiple packets (whose TCP contiguity is guaranteed), CK
payloads belonging to the same larger payload can be dispersed between other payloads
Offset | Length | Description | Note |
---|---|---|---|
0 | 4 | Header | |
4 | 2 | Payload Size | |
6 | 2 | Payload Type | CK |
8 | 4 | C-Bytes | |
12 | 2 | ?? | |
14 | 2 | Larger Data Type | |
16 | 4 | ?? | |
20 | 4 | Larger Data Offset | uint32 (LE) |
24 | 4 | Larger Data Size | uint32 (LE) |
28 | 4 | Current Chunk Size | uint32 (LE) |
32 | ... | Current Chunk Data |
File Data
FD
packets are multi-part portions of a single larger data payload. Whilst similar to Chunk Data (CK
), File Data payloads are prefixed with an identifier number to associate a response to a a request. The underlying nature of the underlying data is arbitrary, and is left to the the initiator of a File Request (FR
) packet.
Offset | Length | Description | Note |
---|---|---|---|
0 | 4 | Header | |
4 | 2 | Payload Size | |
6 | 2 | Payload Type | FD |
8 | 4 | C-Bytes | |
12 | 2 | ?? | |
14 | 2 | Request ID | |
16 | 2 | Bytes Read | uint16 (LE) |
18 | 2 | ?? | |
20 | 2 | Larger Data Size | uint16 (LE) |
22 | 4 | ?? | |
26 | 2 | Current Payload Size | uint16 (LE) |
28 | ... | Current Payload Data |
Related
Demo Projects
For demo projects refer to the below repositories
Simple API
Simplified wrapper to provide subscribable event like mute
, and level
- presonus-studiolive-api-simple-api
SL Edit
StudioLive Series III configuration editor with the ability to edit a configuration offline - SL Edit
MIDI Integration
Experimental support to use MIDI input devices as control surfaces to control the console.
Read more on the February 2022 update blog post.
Bitfocus Companion
An experimental Bitfocus Companion module has been developed to integrate the PreSonus StudioLive API.
See github.com/featherbear/bitfocus-companion-module-presonus-studiolive-iii
Console Advertisement Utility
Experimental tool to spoof a discovery packet, so that UC Surface can 'find' consoles that live on difficult network topologies
See github.com/featherbear/presonus-studiolive-console-advertisement
Other
Blog
Updates about my development progress can be found on my blog
Source Code
The TypeScript source code for this library is available on GitHub.
See github.com/featherbear/presonus-studiolive-api
License
Copyright © 2019 - 2022 Andrew Wong
This software is licensed under the MIT License.
You are free to redistribute it and/or modify it under the terms of the license.