- Notifications
You must be signed in to change notification settings - Fork51
Control Ableton Live with Node.js
License
leolabs/ableton-js
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Ableton.js lets you control your instance or instances of Ableton using Node.js.It tries to cover as many functions as possible.
This package is still a work-in-progress. My goal is to expose all ofAbleton's MIDI Remote Scriptfunctions to TypeScript. If you'd like to contribute, please feel free to do so.
I've used Ableton.js to build a setlist manager calledAbleSet. AbleSet allows you to easily manage and controlyour Ableton setlists from any device, re-order songs and add notes to them, andget an overview of the current state of your set.
To use this library, you'll need to install and activate the MIDI Remote Scriptin Ableton.js. To do that, copy themidi-script
folder of this repo toAbleton's Remote Scripts folder and rename it toAbletonJS
. The MIDI RemoteScripts folder is usually located at~/Music/Ableton/User Library/Remote Scripts
After starting Ableton Live, add the script to your list of control surfaces:
If you've forked this project on macOS, you can also use yarn to do that foryou. Runningyarn ableton10:start
oryarn ableton11:start
(depending on yourapp version) will copy themidi-script
folder, open Ableton and show a streamof log messages until you kill it.
This library exposes anAbleton
class which lets you control the entireapplication. You can instantiate it once and use TS to explore availablefeatures.
Example:
import{Ableton}from"ableton-js";// Log all messages to the consoleconstableton=newAbleton({logger:console});consttest=async()=>{// Establishes a connection with Liveawaitableton.start();// Observe the current playback state and tempoableton.song.addListener("is_playing",(p)=>console.log("Playing:",p));ableton.song.addListener("tempo",(t)=>console.log("Tempo:",t));// Get the current tempoconsttempo=awaitableton.song.get("tempo");console.log("Current tempo:",tempo);// Set the tempoawaitableton.song.set("tempo",85);};test();
There are a few events you can use to get more under-the-hood insights:
// A connection to Ableton is establishedab.on("connect",(e)=>console.log("Connect",e));// Connection to Ableton was lost,// also happens when you load a new projectab.on("disconnect",(e)=>console.log("Disconnect",e));// A raw message was received from Abletonab.on("message",(m)=>console.log("Message:",m));// A received message could not be parsedab.on("error",(e)=>console.error("Error:",e));// Fires on every response with the current pingab.on("ping",(ping)=>console.log("Ping:",ping,"ms"));
Ableton.js uses UDP to communicate with the MIDI Script. Each message is a JSONobject containing required data and a UUID so request and response can beassociated with each other.
Both the client and the server bind to a random available port and store thatport in a local file so the other side knows which port to send messages to.
To allow sending large JSON payloads, requests to and responses from the MIDIScript are compressed using gzip and chunked to fit into the maximum allowedpackage size. The first byte of every message chunk contains the chunk index(0x00-0xFF) followed by the gzipped chunk. The last chunk always has the index0xFF. This indicates to the JS library that the previous received messagesshould be stiched together, unzipped, and processed.
Certain props are cached on the client to reduce the bandwidth over UDP. To dothis, the Ableton plugin generates an MD5 hash of the prop, called ETag, andsends it to the client along with the data.
The client stores both the ETag and the data in an LRU cache and sends thelatest stored ETag to the plugin the next time the same prop is requested. Ifthe data still matches the ETag, the plugin responds with a placeholder objectand the client returns the cached data.
A command payload consists of the following properties:
{"uuid":"a20f25a0-83e2-11e9-bbe1-bd3a580ef903",// A unique command id"ns":"song",// The command namespace"nsid":null,// The namespace id, for example to address a specific track or device"name":"get_prop",// Command name"args":{"prop":"current_song_time"},// Command arguments"etag":"4e0794e44c7eb58bdbbbf7268e8237b4",// MD5 hash of the data if it might be cached locally"cache":true// If this is true, the plugin will calculate an etag and return a placeholder if it matches the provided one}
The MIDI Script answers with a JSON object looking like this:
{"data":0.0,// The command's return value, can be of any JSON-compatible type"event":"result",// This can be 'result' or 'error'"uuid":"a20f25a0-83e2-11e9-bbe1-bd3a580ef903"// The same UUID that was used to send the command}
If you're getting a cached prop, the JSON object could look like this:
{"data":{"data":0.0,"etag":"4e0794e44c7eb58bdbbbf7268e8237b4"},"event":"result",// This can be 'result' or 'error'"uuid":"a20f25a0-83e2-11e9-bbe1-bd3a580ef903"// The same UUID that was used to send the command}
Or, if the data hasn't changed, it looks like this:
{"data":{"__cached":true},"event":"result",// This can be 'result' or 'error'"uuid":"a20f25a0-83e2-11e9-bbe1-bd3a580ef903"// The same UUID that was used to send the command}
To attach an event listener to a specific property, the client sends a commandobject:
{"uuid":"922d54d0-83e3-11e9-ba7c-917478f8b91b",// A unique command id"ns":"song",// The command namespace"name":"add_listener",// The command to add an event listener"args":{"prop":"current_song_time",// The property that should be watched"eventId":"922d2dc0-83e3-11e9-ba7c-917478f8b91b"// A unique event id}}
The MIDI Script answers with a JSON object looking like this to confirm that thelistener has been attached:
{"data":"922d2dc0-83e3-11e9-ba7c-917478f8b91b",// The unique event id"event":"result",// Should be result, is error when something goes wrong"uuid":"922d54d0-83e3-11e9-ba7c-917478f8b91b"// The unique command id}
From now on, when the observed property changes, the MIDI Script sends an eventobject:
{"data":68.0,// The new value, can be any JSON-compatible type"event":"922d2dc0-83e3-11e9-ba7c-917478f8b91b",// The event id"uuid":null// Is always null and may be removed in future versions}
Note that for some values, this event is emitted multiple times per second.20-30 updates per second are not unusual.
The MIDI Script sends events when it starts and when it shuts down. These looklike this:
{"data":null,// Is always null"event":"connect",// Can be connect or disconnect"uuid":null// Is always null and may be removed in future versions}
When you open a new Project in Ableton, the script will shut down and startagain.
When Ableton.js receives a disconnect event, it clears all current eventlisteners and pending commands. It is usually a good idea to attach all eventlisteners and get properties each time theconnect
event is emitted.
In this section, I'll note interesting pieces of information related toAbleton's Python framework that I stumble upon during the development of thislibrary.
- It seems like Ableton's listener to
output_meter_level
doesn't quite work aswell as expected, hanging every few 100ms. Listening tooutput_meter_left
oroutput_meter_right
works better. SeeIssue #4 - The
playing_status
listener of clip slots never fires in Ableton. SeeIssue #25
If you'd like to add features to this project or submit a bugfix, please feelfree to open a pull request. Before committing changes to any of the TypeScriptfiles, please runyarn format
to format the code using Prettier.
About
Control Ableton Live with Node.js