This script works with AbleSet 3.1.0-beta.13 or later and allows you to use a LIOBOX2 to control AbleSet. It’s split into two parts: the project script which sends your setlist and playback/loop status to the controller, and a MIDI mapping script which parses incoming MIDI to allow you to control AbleSet with the controller.
This is not an official integration and not endorsed by GOOROO, but I thought it would be interesting to see how well I could make it work with AbleSet.
To set this up, make sure that the LIOBOX plugin is disabled in Live’s MIDI settings and your LIOBOX is set to the “Ableton - All Songs” mode, then add these scripts:
Project Script
/* This script sends data to your LIOBOX */
const COMMANDS = {
/** This is needed for the spare port to work */
spare: 0x03,
/** Sets the loop state */
loop: 0x34,
/** Sets the mode (session/arrangement) */
mode: 0x42,
/** Sends a ping containing the current song index and playback state */
ping: 0x43,
/** Makes the LioBox respond with its device ID */
identify: 0x44,
/** Sends the number of songs */
setlistLength: 0x46,
/** Sends the information for a song */
songInfo: 0x47,
};
/** Used as a separator between values */
const GAP = 0x03;
/** This value indicates that we're in arrangement mode */
const ARRANGEMENT_MODE = 0x32;
/**
* This is sent at the beginning of each message
* @param command {number}
*/
function makeHeader(command) {
return [0x00, 0x22, 0x1d, 0x10, 0x00, 0x00, command];
}
/**
* Sends a SysEx message to the LioBox based on its port.
* In my tests, the LioBox's MASTER port uses ID [0x31, 0x38],
* and the SPARE port uses ID [0x32, 0x30].
*
* @param command {number}
* @param data {number[]}
* @param portOverride {string | null}
*/
function sendCommand(command, data = [], portOverride = null) {
const port = portOverride ?? shared("lioBoxPort");
if (port) {
const header = makeHeader(command);
const id = port.includes("MASTER") ? [0x31, 0x38] : [0x32, 0x30];
sendMidiSysex(port, [...header, ...id, GAP, ...data]);
}
}
const updateSongs = debounce(() => {
const songs = osc("/setlist/songs", "all");
const descriptions = osc("/setlist/songDescriptions", "all");
const durations = osc("/setlist/songDurations", "all");
const tempos = osc("/setlist/songTempos", "all");
log("Updating songs...");
sendCommand(COMMANDS.spare, [0x31, GAP]);
sendCommand(COMMANDS.mode, [ARRANGEMENT_MODE, GAP]);
sendCommand(COMMANDS.setlistLength, [
ARRANGEMENT_MODE,
GAP,
...makeAscii(String(songs.length)),
GAP,
]);
for (const [index, song] of songs.entries()) {
const name = song?.slice(0, 22) ?? "";
const description = descriptions[index]?.slice(0, 5) ?? "";
const duration = String(Math.round(durations[index] ?? 0));
const tempo = String(tempos[index] ?? "");
sendCommand(COMMANDS.songInfo, [
ARRANGEMENT_MODE,
GAP,
...makeAscii(String(index)),
GAP,
...makeAscii(name),
GAP,
...makeAscii(description),
GAP,
...makeAscii(duration),
GAP,
...makeAscii(tempo),
GAP,
]);
}
sendPing();
}, 10);
updateSongs();
onOscChange("/setlist/songs", updateSongs);
onOscChange("/setlist/songDescriptions", updateSongs);
onOscChange("/setlist/songDurations", updateSongs);
onOscChange("/setlist/songTempos", updateSongs);
onOscChange("/shared/lioBoxLastRefresh", updateSongs);
function sendPing() {
const isPlaying = osc("/global/isPlaying");
const songIndex = osc("/setlist/activeSongIndex") ?? 0;
sendCommand(COMMANDS.ping, [
isPlaying ? 0x32 : 0x30,
GAP,
...makeAscii(String(songIndex)),
GAP,
]);
}
onOscChange("/global/isPlaying", sendPing);
onOscChange("/setlist/activeSongIndex", sendPing);
setInterval(sendPing, 1000);
onOscChange(
"/setlist/isInActiveSectionLoop",
([isLooped]) => {
sendCommand(COMMANDS.loop, [isLooped ? 0x31 : 0x30, GAP]);
},
true,
);
onOscChange(
"/midi/outputs",
(outputs) => {
for (const output of outputs) {
if (output?.includes("LioBox")) {
sendCommand(COMMANDS.identify, [], output);
}
}
},
true,
);
MIDI Mapping
You can add this script to the “LioBox MASTER Port 1” input. If you’re using a redundant setup, with the controller connected to both computers, you also need to add it to the “LioBox SPARE Port 1” input. Make sure that the AbleNet toggle is disabled for both inputs.
/* This script processes incoming MIDI data from your LIOBOX */
if (
midi.raw[0] === 0xf0 &&
midi.raw[1] === 0x00 &&
midi.raw[2] === 0x22 &&
midi.raw[3] === 0x1d &&
midi.raw[4] === 0x10
) {
const command = midi.raw[7];
if (command === 0x50 || command === 0x51) {
// Device Identify / Refresh Request
setShared("lioBoxPort", midi.input);
setShared("lioBoxLastRefresh", now());
} else if (command === 0x52) {
// Play a song
const rawIndex = midi.raw.slice(11, midi.raw.indexOf(0x03, 11));
const index = Number(rawIndex.map((c) => String.fromCharCode(c)).join(""));
log("Playing song", index);
sendOsc("/setlist/jumpToSong", index + 1);
sendOsc("/setlist/jumpToQueued", true);
sendOsc("/global/play");
} else if (command === 0x54) {
// Stop playback
log("Stopping playback");
sendOsc("/global/stop");
} else if (command === 0x57) {
// Prev Song
log("Jumping to Prev Song");
sendOsc("/setlist/jumpBySongs", -1);
} else if (command === 0x58) {
// Next Song
log("Jumping to Next Song");
sendOsc("/setlist/jumpBySongs", 1);
} else if (command === 0x5a || command === 0x5b) {
// Loop Control
const type = midi.raw[11];
if (type === 0x30 || type === 0x33) {
log("Toggling Loop");
sendOsc("/loop/toggle");
} else if (type === 0x31) {
log("Enabling Loop");
sendOsc("/loop/enable");
} else if(type === 0x32) {
log("Escaping Loop");
sendOsc("/loop/escape");
}
} else {
const data = midi.raw
.slice(8)
.map((c) => c.toString(16).padStart(2, "0"))
.join(" ");
log("Unknown command", command.toString(16).padStart(2, "0"), { data });
}
}
Let me know if you have any feedback or ideas for improvements! ![]()




