Skip to content

The GoXLR Utility API

Craig McLure edited this page Mar 12, 2024 · 14 revisions

The GoXLR Utility is an 'API Driven' application, meaning all configuration happens via an API. The goxlr-client binary and Web UI are examples of API clients, and the message format is simple JSON.

Communication Methods

There are currently three methods of communicating with the API:

  • The Unix Socket /tmp/goxlr.socket, or on Windows the Named Pipe @goxlr.socket which are always present
  • Simple HTTP Requests via /api/command on the embedded web server
  • Websocket communication via /api/websocket on the embedded web server.

Note, that all three interfaces have the same JSON message processing and responses attached to them (so any command will work on all three), however the websocket has an additional JSON Patch mechanism, for real-time updates.

We'll go through each of these, and how to communicate with them.

Unix Socket / Named Pipe

These should be your primary point of entry to the utility, even if you intend to use the HTTP channels for actual work. They will always be present, and active, so long as the GoXLR Utility is running. Users can turn off the web server component of the Daemon, or change the port, but this socket can be used to determine if that's the case.

The message send / receive format is relatively straight forward:
[Message Length as unsigned 32bit BigEndian Integer][JSON Message]

These sockets are simple send / receive, you send a request, you get a response in the same format. To parse out the JSON response, you would read the length, then use that to read the message.

If you're writing in rust, you can utilize the interprocess crate, along with the utilities ipc crate to automatically handle connections and marshaling data. Check out the goxlr-client for an example on how to do this.

If you're a C# / Other Language user, there's a VERY basic example on handling this socket available as a GitHub Gist.

If your intent is to use the web based interfaces, you'd want to perform a GetStatus request, which will return http_settings as part of the response json structure detailing the location and state of the web interface.

Simple HTTP Request

Once you have the web server details (or assumed the defaults), you can use HTTP to Send commands to the daemon. These are incredibly simple, send a POST request with Content-Type: application/json to /api/command, with the request in the body and you'll receive a JSON response. (more on messaging later).

So for example, to fetch the Daemon Status, you'd simply send a "GetStatus" message to the endpoint, and will receive the "Status" {..} response.

Websocket Requests

Websockets behave slightly differently than simple requests due to extra async behaviors to consider. The websocket supports asynchronous reuse, allowing you to send commands with potentially different execution lengths, and get the response as soon as it's available (Note that all requests will be processed in the order received). The main issue that this can cause is that the responses may not arrive in the order they are sent, so an ID is required to be attached to all requests, which will be returned with the response.

So an example GetStatus request will look something like this:

{
    "id": 72, /* id is an unsigned 32bit integer */
    "data": "GetStatus",
}

And responses will follow a similar format:

{
    "id": 72, /* Matches the Request */
    "data": { "Status" {..} }
}

How to match up requests and responses is an exercise for the integrator, I use simple javascript promises in the Web UI.

JSON Patching

Whenever anything about the GoXLR changes (be it a physical interaction, or another app changing a setting), a JSON Patch message is emitted to any and all clients connected via websockets.

This patch is intended to be applied to a Status object received from executing a GetStatus message, although how this is achieved may be dependent on your platform or language, some options include:

  • Directly mutating the Status structure (fast-json-patch for javascript, possibly Reflection in type-safe languages)
  • Storing the raw JSON response from GetStatus, patching that, and replacing any de-serialized objects
  • Serialize an object back to JSON, patch the JSON, then de-serialize replacing the original object

For a rust example on handling the websocket and patches, check out this project, it fetches the websocket address from the unix socket, grabs the Status, patches whenever updates arrive, and updates an audio source in OBS whenever a specific channel volume changes (either via fader, or the UI), it could be the basis for a 'VOD Track' implementation.

A Patching Warning..

When working in a UI, you'll need to be considerate of patch events for your own changes. When sending a change to the daemon, in addition to the general 'Ok' response, it will emit a 'Patch' event for that change, confirming it has been made. Depending on the type of interaction, and the speed of updates, the patches may be slightly behind what has been sent (an example is spamming a slider up and down really fast), and if you've mapped your component directly to the Status value there might be some contention over the actual value.

In addition, if you're modifying the volume of a fader on the full sized GoXLR, the GoXLR itself will trigger internal events for the volume changes to the fader as the motorized sliders move, which the daemon will pick up and emit as a new patch (unfortunately, the GoXLR itself is authoritative as far as volumes are concerned, and being physical may overshoot / undershoot an intended target making it difficult to handle daemon-side), sending a command to the Daemon which sets the volume from 0 to 255 could result in many patch events as the fader moves.

In the WebUI, this is solved by temporarily ignoring patch events for sliders while the user is actively manipulating them. Once the user finishes, simply replace the value in the Status object with the new value, which will ostensibly be the same value as the last change sent to the Daemon. While this might not perfectly catch overshoot / undershoot, and could leave the Status object slightly desynced, it's generally close enough.

Messages

While not formally documented (yet!), determining the JSON messages to send across to the GoXLR is relatively straight forward. All of the commands and responses are defined in the ipc crates lib.rs file, DaemonRequest are the requests, and DaemonResponse are the possible responses.

Sending

All requests should be derived from the DaemonRequest enum, if you send a request that's not listed there, or is of an incorrect format, an error will be returned. Serialization of the enums is straight forward, the constant is converted to a String, and the parameters are represented either by themselves, or as an Array if there's more than one. If there ARE parameters, the command needs to be wrapped in a JSON object {} so the DaemonRequest::GetStatus constant is represented in JSON as:

"GetStatus"

With this information, we can infer the behavior of the DaemonRequest::OpenPath command, PathTypes is an enum, and we know the constants are just represented as Strings in the JSON, and there's only one parameter, so an array isn't needed, but it needs to be wrapped in an object, so to open the profiles directory, the JSON would be:

{ "OpenPath": "Profiles" } 

And DaemonRequest::Command has two parameters, which need to be represented as an array, so we end up with:

{ "Command": [String, GoXLRCommand] }

The String in this case is a device serial number (they're included in the GetStatus response).. As for the GoXLRCommand, this is another enum further down the same file, listing all of the device commands that can be sent. These commands all serialize out the same way as the top level elements. It should be noted, that the commands include a lot of types, these can be found in the types crates lib.rs file. They're all enums, so their values get sent as Strings.

So to set a Fader, we look to the GoXLRCommand::SetFader constant, it requires a FaderName and a ChannelName (both defined in the types crate), so we'll change Fader A to the channel Mic, remembering to start at the DaemonRequest level, we end up with:

{ "Command": [Serial, { "SetFader": ["A", "Mic"] } ] }

Receiving

Serialization of responses tends to behave in exactly the same way as serializing requests, except they're based on the DaemonResponse enum. when executing a GoXLRCommand, you'll get one of two responses, either Ok as a string by itself, or an error:

{ "Error": "This is an Error!" }

Obviously, errors can be sent for any Request.

There are three other types of responses, HttpSettings fetched by calling GetHttpState, DaemonStatus fetched by calling GetStatus and Patch, discussed earlier in the websocket section.

Serialisation of these structs is pretty straight forward, although they're defined in the ipc crates device.rs file.

In the case of a struct, the values inside it are represented as key: value pairs, so for example a DaemonConfig inside the DaemonStatus struct would be represented as:

/* "Status" is from DaemonResponse */
"Status": {
  "config": {
    "daemon_version": "0.9.0",

    /* Rust boolean is mapped directly to javascript boolean */
    "autostart_enabled": true,
    "show_tray_icon": true
  },
  ...
}

For anything that's an Option, there will be either a value, or null, this generally applies to feature that aren't present on a Mini.

For Maps (EnumMap, HashMap, BTreeMap), the first type is the key, and the second the value, for example, the mixers are defined in rust as:

    pub mixers: HashMap<String, MixerStatus>,

And map to:

"mixers": {
  "SERIAL_NUMBER": {
    /* Into the MixerStatus object */
    "hardware": {
     ...
    }
  },
  "SERIAL_NUMBER_2": {
    ...
  }
}

For the EnumMap<A, B> type, all keys of type A are GUARANTEED to be present and set in the response, in HashMaps and BTreeMaps that guarantee is not met (note, in future releases, some HashMaps may be converted to EnumMaps).

And finally, for types defined as Vec, they will be presented as a simple Javascript array.

Conclusion

Hopefully that should be enough to get you going with the API, hit me up on discord if you have any questions or need a hand. Often the best way to get a handle on the Requests and Responses would be to open a web browser's development console with the UI open, and watch the Web Socket there do its magic as you make changes.