NAV

PreSonus StudioLive API

An unofficial Node.js API for the PreSonus StudioLive III consoles.

Tested against the following models

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.

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

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()

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

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)

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) to 255.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.