AbleSet 3.0.0-beta.19

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