AbleSet 3.0.0-beta.19

Setlist Editor

  • Right-clicking a song in edit mode now opens the song settings popover
  • You can now use ⌘ C to copy the setlist as plaintext and ⌘ V to import a setlist from the text in your clipboard
  • ⇧ H now toggles the visibility of skipped songs
  • ⇧ ⏎ and ⌘ ⏎ now save the setlist and exit the setlist editor
  • You can now rename the current setlist when saving it instead of creating a new file
  • Fixed locators not updating properly when renaming them in Ableton Live

Canvas

  • Added a new Embed element that can display any website
  • Added an option to hide the close/edit buttons unless the key is pressed

Lyrics

  • Fixed inconsistent lyrics scrolling when jumping between songs

MIDI Mapping

  • You can now add MIDI triggers manually without having to use the MIDI controller
  • MIDI triggers can now be mapped to custom JavaScript scripts, and you can now define custom JavaScript scripts that receive all MIDI packets from a given input
    • These scripts have access to all OSC values and the raw MIDI data and they can send MIDI and OSC, giving you complete flexibility with MIDI mapping
  • Program Change MIDI mappings can now be set to trigger on any PC value

Project Script

  • Added a new kind of script that runs each time AbleSet starts or your project is loaded. These scripts can listen to OSC value changes and send MIDI and OSC

Settings

  • Added tooltips for all settings items and improved the description of some items

You can download this beta here:
Mac (Intel): https://ableset.app/download/mac/3.0.0-beta.19
Mac (Apple Silicon): https://ableset.app/download/mac-arm64/3.0.0-beta.19
Win: https://ableset.app/download/win/3.0.0-beta.19

10 Likes

To demonstrate the flexibility of the new scripting options, here’s a project script that sends the current song and section name to a connected MC6 PRO:

Here’s the MIDI mapping:

And here’s the full project script:

/**
 * The checksum is used by the MC6 Pro to check
 * if all bytes were received correctly
 */ 
function calculateChecksum(bytes) {
  return bytes.reduce((acc, cur) => {
    return acc ^ cur;
  }, 0xF0) & 0x7F;
}

/** 
 * Sends a string to the MC6 PRO
 */
function sendBankName(name) {
  const normalizedBankName = name.slice(0, 32).padEnd(32);
  const songBytes = makeAscii(normalizedBankName);

  // This payload tells the MC6 Pro to update its bank name temporarily
  const payload = [
    0x00, 0x21, 0x24, 0x06, 0x00, 0x70, 0x10,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
    ...songBytes
  ];

  payload.push(calculateChecksum(payload));
  sendMidiSysex("Morningstar MC6 Pro Port 1", payload);
}

let lastSongNameUpdate = 0;
let songName = osc("/setlist/activeSongName");

// When the song name changes, display it on the screen
onOscChange("/setlist/activeSongName", ([songName]) => {
  songName = songName ?? "No Song";
  lastSongNameUpdate = Date.now();
  sendBankName(songName);
  log("Song name:", {songName});
}, true);

// When the section name changes, and it's not immediately
// after the song name changes, display it on the screen for
// one second and then display the song name again
onOscChange("/setlist/activeSectionName", async ([sectionName]) => {
  if (!sectionName || Date.now() - lastSongNameUpdate < 100) {
    return;
  }

  sendBankName(sectionName);
  await sleep(1000);
  sendBankName(songName);
}, true)
3 Likes

Copying the setlist in plain text is HUGE! What a time-saver. Thanks so much for adding this.

1 Like

@leolabs, every time life carries me away for a little while, I return to this forum to see that you’ve been hard at work on making Ableset even better. Can’t wait to dive into scripting options. I was doing a lot with Ableset and GigPerformer last year, and utilizing a lot of custom scripting through GP. This looks fantastic.

2 Likes

So excited for this implementation. I just did a 30 show run using the MC6 Pro with the setlist M4L script. Knowing how reliable this will be once fully integrated and supported by Ableset is going to be a game changer.

Never felt like I could let go of the iPad as it occasionally got disconnected on M4L but if this ends up being as solid as everything I’ve come to know with your work, I’ll never look back. That MC6 workflow is the near perfect controller for what I do.
Even more amazing, if that frees up the iPad, I can make that my artist’s new confidence monitor. Such a flexible software Leo, great work man.

1 Like

@leolabs, am I misunderstanding the point of the Project Scripting area? I thought I would be able to declare constants here for IP addresses and port numbers that I could use throughout my canvas, but that doesn’t seem to be the case. Is the project script sandboxed from the rest of the application?

Secondly (and unrelated), any plans for knobs/rotaries, and toggle buttons in Canvas?

Thank you!

Hey @mrdrennan, all scripts are sandboxed but they can share data using the shared functions. I still need to document everything properly, but you could define constants in your project script like this:

setShared("myIpAddress", "192.168.1.2");
setShared("projectName", "Test");

Then in your canvas, you can access these variables like this:

shared("myIpAddress");
shared("projectName");

Or in a template:

Project: ${shared("projectName")}

Let me know if this works for you :slight_smile:

Works like a charm…thanks Leo! I’ll keep my fingers crossed for encoders, tabs, and toggles in Canvas! :wink:

1 Like

You can already kind of do tabs using buttons set to URL mode that point to different canvases, though this would switch the entire UI, not just a part :slight_smile:

1 Like

Yes I’ve experimented with that…I’m trying o build something as close to my old Open Stage Control layout as possible. Maybe in the future? I’ve already created a radio button experience using javascript. Cool stuff!

oh my! I’m seeing such important updates and it’s not even the latest beta.

1 Like

Hey everyone! :waving_hand:

I’ve been working on an extended version of the MC6 Pro script that @leolabs originally shared. Over the last few days I’ve been developing it together with him — adding more features, refining the workflow, and testing different use cases.

I’m sharing it here as a work in progress, in case it’s helpful for anyone integrating AbleSet with a Morningstar MC6 Pro.


What this version does

  • Sends song names and section names to the MC6 Pro by temporarily overriding the bank name via SysEx — this part was already present in Léo’s original script.

  • Sends the tempo of each song encoded as CC5 (MSB) and CC6 (LSB), so the MC6 can show the BPM without using MIDI Clock.
    This is especially useful in setups like mine where MIDI over Bluetooth is involved. MIDI Clock over BT isn’t stable, so sending the tempo as discrete CC values (instead of a continuous clock stream) avoids drift and keeps the BPM display reliable. (If you’re using a fully wired MIDI connection, you can simply rely on regular MIDI Clock for tempo display. In that case, you can omit the BPM-related part of this script altogether.)

  • Keeps the loop state in sync with AbleSet / Live:

    • Whenever the loop is turned on or off in AbleSet/Live, the script sends a CC message to the MC6 Pro so it can update the corresponding toggle on your footswitch.

    • In this example it’s wired to footswitch B, but you can easily change it to any switch.


Tempo automation tip (for reliable BPM “fallback” on the MC6 Pro)

Whether you’re working in a single Live project with many songs on the same timeline, or using a multi-file set, I strongly recommend this:

:backhand_index_pointing_right: Make sure each song has at least one tempo automation point somewhere in its timeline — even if the tempo never changes.

A simple way is to add one tempo automation point slightly after the end of each song (outside the audible part of the track). The reason:

  • When AbleSet triggers “Re-enable automation”, Live will re-read tempo automation.

  • If there is at least one tempo point, the script can resend the correct BPM and the MC6 will “fall back” to the original tempo after you’ve changed it.

  • If there are no automation points at all for that song, Live will still re-enable automation internally, but the MC6 won’t receive a refreshed BPM — so its display won’t update to the correct tempo.

So: one automation point per song (even if it matches the existing tempo, and even if it’s placed after the end of the track) will make BPM behavior consistent.


Loop / footswitch mapping (MC6)

For the loop state, the script uses:

  • CC2 as a “Set Toggle” command

  • CC3 as a “Clear Toggle” command

Morningstar treats the velocity as the footswitch index:

  • 0 → footswitch A

  • 1B

  • 2C, etc., up to X.

To make this easier to configure, the script lets you pick the loop footswitch by letter instead of by index. For example:

  • "A" → index 0

  • "B" → index 1

  • "X" → index 23

So if you want the loop indicator on footswitch L, you can simply set:

const loopFootswitch = "L";

and the script will convert that letter to the correct index internally.


Script

Here’s the full script — feel free to try it out, tweak it, or build on top of it.
Just remember to:

  • Replace "YOUR MIDI DEVICE NAME" (including the quotes) with the exact name of your MIDI output device, and

  • Set MIDI_CHANNEL to the number of the MIDI channel your MC6 Pro is using. Just type the number (1–16). For example, if your MC6 is set to receive on channel 3, write 3 here. No quotes, no extra text — just the number.

/**
 * MC6 PRO – Update bank name via temporary SysEx
 *
 * Sends the current song or section name to the MC6 Pro by temporarily
 * overriding the bank name via SysEx.
 */

const MIDI_DEVICE  = "YOUR MIDI DEVICE NAME"; // Replace with your MIDI output device name
const MIDI_CHANNEL = 1;                        // Example: MIDI channel 1 (change as needed)

/**
 * Calculate checksum for the SysEx payload according to Morningstar's spec.
 */
function calculateChecksum(bytes) {
  return bytes.reduce((acc, cur) => (acc ^ cur), 0xF0) & 0x7F;
}

/**
 * Send a bank name update to the MC6 Pro.
 * Normalizes the string to 32 characters (padded with spaces if shorter).
 */
function sendBankName(name) {
  const normalized = (name ?? "No Song").slice(0, 32).padEnd(32);
  const songBytes = makeAscii(normalized);

  const payload = [
    0x00, 0x21, 0x24, 0x06, 0x00, 0x70, 0x10,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
    ...songBytes
  ];

  payload.push(calculateChecksum(payload));
  sendMidiSysex(MIDI_DEVICE, payload);
}

/**
 * Send BPM to the MC6 Pro by encoding it into CC5 (MSB) + CC6 (LSB).
 *
 * This avoids using MIDI Clock (which can be unstable over Bluetooth)
 * while still allowing a numeric tempo readout on the MC6.
 *
 * Range is clamped between 20–300 BPM.
 */
function sendMC6Bpm(bpm) {
  const b = Math.max(20, Math.min(300, Math.round(Number(bpm))));
  const msb = Math.floor(b / 128);
  const lsb = b % 128;

  sendOsc("/midi/send/cc", MIDI_DEVICE, MIDI_CHANNEL, 5, msb);
  sendOsc("/midi/send/cc", MIDI_DEVICE, MIDI_CHANNEL, 6, lsb);

  log(`MC6 BPM Update → ${b} (MSB=${msb}, LSB=${lsb})`);
}

/**
 * State tracking for current and queued songs.
 */
let currentSong = osc("/setlist/activeSongName") ?? "No Song";
let queuedSong  = osc("/setlist/queuedName") ?? null;

/**
 * Decide which title should be shown on the MC6:
 * - If a song is queued, prefer that name.
 * - Otherwise, show the currently active song.
 */
function displayPreferredTitle() {
  const title = queuedSong ?? currentSong;
  sendBankName(title);
}

/**
 * Initial display when the script loads.
 * (onOscChange handlers with `true` will also fire once, so this mostly
 * ensures we send something immediately at startup.)
 */
displayPreferredTitle();

/**
 * When Live loads a new active song.
 * - Update internal state.
 * - Refresh the title on the MC6.
 * - Send the current tempo once for this song.
 */
onOscChange("/setlist/activeSongName", ([name]) => {
  currentSong = name ?? "No Song";
  log("Got active song:", name);
  displayPreferredTitle();

  const t = Number(osc("/global/tempo"));
  if (!Number.isNaN(t)) sendMC6Bpm(t);
}, true);

/**
 * When a song or section is queued.
 *
 * AbleSet sends [songName, sectionName] on /setlist/queuedName.
 * - If a section is queued, briefly show its name on the MC6.
 * - Then fall back to the normal title logic (queued > active).
 */
onOscChange("/setlist/queuedName", async ([song, section]) => {
  queuedSong = song || null;
  log("Got queued song/section:", song, section);

  if (section) {
    // Show the section name briefly when it is queued
    sendBankName(section);
    await sleep(1000);
  }

  // Restore the preferred title after the brief section display
  displayPreferredTitle();
}, true);

/**
 * When the active section changes while playing.
 * - Show the section name for 1 second.
 * - Then restore the preferred title (queued > active).
 */
onOscChange("/setlist/activeSectionName", async ([section]) => {
  if (!section) return;

  sendBankName(section);
  await sleep(1000);
  displayPreferredTitle();
}, true);

/* =======================================================================
   Footswitch letter → Morningstar index (A–X)
   ======================================================================= */

/**
 * Convert a footswitch letter (A–X) into its Morningstar index (0–23).
 * Example: 'A' → 0, 'B' → 1, ..., 'X' → 23.
 */
function footswitchLetterToIndex(letter) {
  if (!letter || typeof letter !== "string") return null;

  const l = letter.trim().toUpperCase();
  const code = l.charCodeAt(0);

  // 'A' = 65, 'X' = 88
  if (code < 65 || code > 88) {
    log(`Invalid footswitch letter: "${letter}". Must be A–X.`);
    return null;
  }

  return code - 65;
}

/**
 * Loop state mapping
 * Choose the loop footswitch with a letter (A–X).
 * MC6 Pro MIDI Implementation:
 * - CC number selects the action (e.g. 2 = Set Toggle, 3 = Clear Toggle).
 * - Velocity selects the footswitch index:
 *   0 = A, 1 = B, 2 = C, ..., 23 = X.
 */
const loopFootswitch = "B"; // Example: "B" → index 1
const loopFootswitchIndex = footswitchLetterToIndex(loopFootswitch);

/* =======================================================================
   Loop state → MC6 Pro
   - Whenever the loop bracket is enabled/disabled in AbleSet/Live,
     send a CC to the MC6 Pro so it can update a Set/Clear Toggle state.
   ======================================================================= */

onOscChange("/setlist/loopEnabled", ([enabled]) => {
  const isEnabled = Number(enabled) === 1;

  if (isEnabled) {
    // Loop ON  → send "Set Toggle" (example: CC2) for the chosen footswitch
    sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 2, loopFootswitchIndex);
  } else {
    // Loop OFF → send "Clear Toggle" (example: CC3) for the chosen footswitch
    sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 3, loopFootswitchIndex);
  }

  log("Loop state changed →", isEnabled ? "ENABLED" : "DISABLED");
}, true);

/**
 * Live tempo changes.
 * - Whenever the global tempo changes (automation, manual, etc.),
 *   forward the updated BPM to the MC6.
 */
onOscChange("/global/tempo", ([t]) => {
  const n = Number(t);
  if (!Number.isNaN(n)) sendMC6Bpm(n);
}, true);

/* =======================================================================
   Handle loading-project-name so mouse selection behaves like arrows/MC6
   ======================================================================= */

/**
 * Remove trailing ".als" from a project name (if present).
 */
function cleanAlsName(str) {
  if (!str) return null;
  return str.endsWith(".als") ? str.slice(0, -4) : str;
}

/**
 * When clicking on any other song with the mouse:
 * - AbleSet will expose a "Loading: ..." project name.
 * - We strip "Loading:" on AbleSet's side and ".als" here so the MC6
 *   always shows a clean song title, even when selecting with the mouse.
 */
onOscChange("/global/loadingProjectName", ([loadingName]) => {
  if (!loadingName) return;

  const clean = cleanAlsName(loadingName);
  log("Loading project:", clean);

  // Show the title directly (no "Loading:" and no ".als")
  sendBankName(clean);
}, true);


And that’s it!
Take it for a test drive — and if anyone has ideas, improvements, or questions, feel free to jump in.
Happy to refine this further :slightly_smiling_face:

Agus

2 Likes