Morningstar MC6 Pro Project Script Integration

Hey everyone! :waving_hand:

I originally shared an early version of this script inside the AbleSet 3 Beta 19 thread, but since it has grown quite a bit and proven useful, it made sense to give it its own dedicated thread.

This is an extended version of the MC6 Pro project script that @leolabs originally shared, and that @joshcrosby later neatly adapted for the MC8 Pro.
Over the last few days I kept developing it and adding more features.

I’m sharing it here as a work in progress, in case it helps anyone integrating AbleSet + Ableton Live with a Morningstar MC6 Pro.


:white_check_mark: What this script does

1️⃣ Sends song & section names to the MC6 Pro (SysEx bank override)

The script sends the current song name (and temporarily the section name) to the MC6 Pro by overriding the bank name via SysEx.

This part was already present in Léo’s original script, but it’s now extended with:

  • Sanitizing names (no accents, emojis, or non-ASCII characters)
  • Safe trimming and padding for the MC6 display
  • Automatic fallback Logic:
    • Queued song > active song
    • Brief section display when sections are queued or become active

Result: the MC6 always shows meaningful, readable context while navigating a set.

2️⃣ Sends BPM to the MC6 Pro using CC (no MIDI Clock required)

The script sends the song tempo encoded as:

  • CC5 = MSB
  • CC6 = LSB

This allows the MC6 Pro to show the BPM without relying on MIDI Clock.

Why this matters:

  • In setups where MIDI over Bluetooth is involved.
  • MIDI Clock over BT is not always reliable.
  • Sending tempo as discrete CC values avoids drift and keeps the BPM display promptly updated.

:backhand_index_pointing_right: If you’re using a fully wired MIDI setup, you can safely rely on regular MIDI Clock and remove the BPM-related part of this script.

3️⃣ Keeps Play, Stop, Record and Loop states in sync

The script keeps the MC6 Pro footswitch LEDs/toggles perfectly in sync with AbleSet / Live — no matter where the action is triggered from (MC6, keyboard, mouse, etc.).

:repeat_button: Loop state

  • Listens to:
    /setlist/loopEnabled
  • Sends:
    • CC2 → Set Toggle
    • CC3 → Clear Toggle
  • Velocity = footswitch index (A–X → 0–23)

So whenever the loop bracket is enabled or disabled in AbleSet / Live, the corresponding MC6 toggle updates automatically.

In the example script, the loop is mapped to footswitch B, but you can change it to any other footswitch.


:play_button: Play / Stop state

  • Listens to:
    /global/isPlaying
  • Syncs a Play/Stop toggle on the MC6 (example: footswitch A)

This means:

  • Press Play from the keyboard → MC6 toggle turns ON
  • Press Stop from the UI → MC6 toggle turns OFF
  • Press Play/Stop from the MC6 → everything stays in sync

No guessing, the controller always reflects the real playback state.


:red_circle: Record state

  • Listens to:
    /global/isRecording
  • Syncs a Record ON/OFF toggle on the MC6 (example: footswitch E)

Important Logic here:

  • Whenever AbleSet is recording, it is also playing
  • But not every playback state implies recording

By keeping Record on its own dedicated toggle, the MC6 accurately reflects:

  • Playing but not recording
  • Playing and recording
  • Recording stopped while playback continues

Again, this works regardless of whether recording is started from the MC6 or directly from the AbleSet UI.


:level_slider: Tempo automation tip (important for reliable BPM fallback)

Whether you’re working:

  • In a single Live project with many songs on the same timeline, or
  • With a multi-file set

I strongly recommend this:

:backhand_index_pointing_right: Make sure each song has at least one tempo automation point, even if the tempo never changes.

A simple approach:

  • Add one tempo automation point after the end of each song, outside the audible part.

Why this matters:

  • When AbleSet triggers Re-Enable Automation, Live re-reads tempo automation.
  • If at least one tempo point exists, the script can resend the correct BPM to the MC6.
  • If a song has no tempo automation at all, Live still re-enables automation internally, but the MC6 won’t receive a refreshed BPM value.

🎛 Loop / footswitch mapping for MC6 Pro

For loop, play, and record state sync, the script uses Morningstar’s toggle Logic:

  • CC2 → Set Toggle
  • CC3 → Clear Toggle

Velocity selects the footswitch index:

  • 0 → A
  • 1 → B
  • 2 → C
  • 23 → X

To keep this user-friendly, the script lets you define footswitches by letter, not index.

Example:

const loopFootswitch = "L";

The script automatically converts that to the correct index internally.


:scroll: Script

Here's the full script
/**
 * 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"; // Replace with your MIDI output device name
const MIDI_CHANNEL = 1;              // Example: MIDI channel 8 (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;
}

/**
 * Sanitize titles for the MC6 Pro:
 * - Remove accents/diacritics (áéíóúñ → aeioun, etc.)
 * - Strip emojis and any non-printable / non-ASCII characters
 * - Collapse extra spaces
 * - Limit to 32 characters
 */
function sanitizeForMC6Title(str) {
  if (!str) return "No Song";

  let s = String(str);

  // 1) Remove non-ASCII printable characters (this nukes emojis, etc.)
  //    Range 0x20–0x7E = space through "~"
  s = s.replace(/[^\x20-\x7E]/g, "");

  // 2) Collapse multiple spaces and trim ends
  s = s.replace(/\s+/g, " ").trim();

  if (!s) return "No Song";

  // 3) Limit to 32 characters for the MC6 display
  return s.slice(0, 32);
}

/**
 * Send a bank name update to the MC6 Pro.
 * - Normalizes to ASCII (no accents/emojis)
 * - Pads to 32 characters with spaces if shorter.
 */
function sendBankName(name) {
  const safeTitle = sanitizeForMC6Title(name ?? "No Song");
  const normalized = safeTitle.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);

/* =======================================================================
   MC6 Pro Footswitches index (A to 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
 * User-friendly: choose the loop footswitch with a letter (A–X).
 * MC6 Pro's 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)

/* =======================================================================
   Play/Stop + Recording state → MC6 Pro (toggle sync)
   ======================================================================= */

const playStopFootswitch = "A";
const playStopFootswitchIndex = footswitchLetterToIndex(playStopFootswitch);

const recordFootswitch = "E";
const recordFootswitchIndex = footswitchLetterToIndex(recordFootswitch);

// PLAYBACK toggle (A) – uses /global/isPlaying
onOscChange("/global/isPlaying", ([playing]) => {
  const isPlaying = Number(playing) === 1;

  if (isPlaying) {
    sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 2, playStopFootswitchIndex); // Set Toggle
  } else {
    sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 3, playStopFootswitchIndex); // Clear Toggle
  }

  log("Playback state changed →", isPlaying ? "PLAYING" : "STOPPED/PAUSED");
}, true);

onOscChange("/global/isRecording", ([recording]) => {
  const isRecording = Number(recording) === 1;

  if (isRecording) {
    sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 2, recordFootswitchIndex); // Set Toggle
  } else {
    sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 3, recordFootswitchIndex); // Clear Toggle
  }

  log("Recording state changed →", isRecording ? "RECORDING" : "NOT RECORDING");
}, true);

Just remember to:

  1. Replace "YOUR MIDI DEVICE" with your exact MIDI output device name
  2. Set MIDI_CHANNEL to the MIDI channel your MC6 Pro is listening on (1–16)

Feel free to try it out, adapt it to your setup, and build on top of it.
I’m happy to keep iterating on this as new ideas come up.

Agus

1 Like