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

2 Likes

Hey man, really appreciate all the work here. Can you possibly explain how I could map the MC6 to give me feedback about Play/Pause so that for instance a toggle is in sync? I can’t quite make sense of it above, is this possible yet? Maybe I’m miss understanding haha

I currently have added what I understand to be the full script, and its displaying songs and switching through the set as expected but I’m still a little lost on getting toggle feedback. Any help would be so appreciated! Thanks.

Edit: Okay update, I’ve since wrapped my head around what part of the code needs to be changed I believe and I think I’m close! I’ve set up my play/pause footswitch on “B” to be a toggle with CC 2 and CC3. I’ve set it up as

const loopFootswitch = “A”;

and
const playStopFootswitch = “E”;

Mapped it in midi mapping. But I’m still not getting the toggle to sync if I play from spacebar.. What am I missing I wonder.

Edit number 2: Okay WOW I’ve figured it out. So proud of myself here. I’ve never understood code script at ALL. Last part of the puzzle was setting MC6 to receive on same specified MIDI channel in the editor. Wasn’t getting BPM information before either but now everything’s there.

Hey @kbwall,

Glad you got it working! :raising_hands:

Nice catch on the MIDI channel too, that’s a super common one to miss.

If you run into anything else, just let me know!

Could this be easily adapted to the MC4 Pro? I know it would mean losing some features, but during a show this station only needs Play/stop/next/prev. Thanks!

Hey @RScott,

Since you only need Play/Stop, Next and Prev during the show, you only have to map those three actions on the MC4 Pro Editor.
Next/Prev are momentary actions, so they don’t need any feedback from the script.
The only thing worth syncing back is the Play/Stop toggle state, plus showing the song/section titles and the BPM on the screen. That’s what the stripped-down version does.

I removed the loop and recording sync from the MC6 Pro script and updated the footswitch indexing for the MC4 Pro layout (A–P across your 4 pages instead of A–X).

One important caveat: I couldn’t find the MC4 Pro’s SysEx device ID in Morningstar’s documentation. Knowing that the MC6 Pro uses 0x06 and the MC8 uses 0x08, the MC4 Pro should be 0x04, so that’s what I used in the payload. If the titles don’t show up on the display, that byte is the first thing to check (it’s commented in the code so it’s easy to find).

/**
 * MC4 PRO – Update bank name via temporary SysEx
 *
 * Sends the current song/section name to the MC4 Pro by temporarily
 * overriding the bank name via SysEx. Also forwards BPM and keeps the
 * Play/Stop toggle in sync.
 */

const MIDI_DEVICE  = "YOUR_MIDI_DEVICE"; // Replace with your MIDI output device name
const MIDI_CHANNEL = 1;              // Set to your MC4 Pro's MIDI channel

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

/**
 * Sanitize titles: strip accents/emojis, keep printable ASCII, max 32 chars.
 */
function sanitizeTitle(str) {
  if (!str) return "No Song";
  let s = String(str)
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "") // remove accent marks
    .replace(/[^\x20-\x7E]/g, "")    // remove emojis / non-ASCII
    .replace(/\s+/g, " ")
    .trim();
  return s ? s.slice(0, 32) : "No Song";
}

/**
 * Send a bank-name update to the MC4 Pro.
 * NOTE: byte 4 (0x04) is the assumed MC4 Pro device ID — change it here if
 * titles don't appear on the display.
 */
function sendBankName(name) {
  const songBytes = makeAscii(sanitizeTitle(name ?? "No Song").padEnd(32));
  const payload = [
    0x00, 0x21, 0x24, 0x04, 0x00, 0x70, 0x10,  // <-- 0x04 = assumed MC4 Pro ID
    0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
    ...songBytes
  ];
  payload.push(calculateChecksum(payload));
  sendMidiSysex(MIDI_DEVICE, payload);
}

/**
 * Send BPM via CC5 (MSB) + CC6 (LSB). Avoids MIDI Clock (unstable over BT).
 * Clamped 20–300 BPM.
 */
function sendBpm(bpm) {
  const b = Math.max(20, Math.min(300, Math.round(Number(bpm))));
  sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 5, Math.floor(b / 128)); // MSB
  sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, 6, b % 128);             // LSB
  log(`BPM → ${b}`);
}

/* ---------- Title display state ---------- */

let currentSong = osc("/setlist/activeSongName") ?? "No Song";
let queuedSong  = osc("/setlist/queuedName") ?? null;
let displayToken = 0; // prevents stale async flashes from overwriting newer titles

function displayPreferredTitle() {
  sendBankName(queuedSong ?? currentSong);
}

// Briefly flash a name (e.g. a section), then restore the preferred title.
async function flashTitle(name, duration = 2000) {
  const token = ++displayToken;
  sendBankName(name);
  await sleep(duration);
  if (token === displayToken) displayPreferredTitle();
}

// Active song changed → update title + push current tempo once.
onOscChange("/setlist/activeSongName", ([name]) => {
  currentSong = name ?? "No Song";
  displayPreferredTitle();
  const t = Number(osc("/global/tempo"));
  if (!Number.isNaN(t)) sendBpm(t);
}, true);

// Song/section queued → flash the section name, then fall back to title logic.
onOscChange("/setlist/queuedName", async ([song, section]) => {
  queuedSong = song || null;
  if (section) await flashTitle(section);
  else displayPreferredTitle();
}, true);

// Active section changed while playing → flash it for 2s.
onOscChange("/setlist/activeSectionName", async ([section]) => {
  if (section) await flashTitle(section);
}, true);

// Tempo changes (automation/manual) → forward to the MC4 Pro.
onOscChange("/global/tempo", ([t]) => {
  const n = Number(t);
  if (!Number.isNaN(n)) sendBpm(n);
}, true);

// Mouse-selecting a song exposes a "Loading: ..." name — show it cleanly.
onOscChange("/global/loadingProjectName", ([loadingName]) => {
  if (!loadingName) return;
  sendBankName(loadingName.endsWith(".als") ? loadingName.slice(0, -4) : loadingName);
}, true);

/* ---------- Play/Stop toggle sync ---------- */

/**
 * Convert a footswitch letter (A–P) into its index (0–15).
 * MC4 Pro: 4 switches × 4 pages = presets A–P.
 */
function footswitchLetterToIndex(letter) {
  if (!letter || typeof letter !== "string") return null;
  const code = letter.trim().toUpperCase().charCodeAt(0);
  if (code < 65 || code > 80) { // 'A'=65 .. 'P'=80
    log(`Invalid footswitch letter: "${letter}". Must be A–P.`);
    return null;
  }
  return code - 65;
}

const playStopFootswitch = "A"; // the preset where your Play/Stop toggle lives
const playStopIndex = footswitchLetterToIndex(playStopFootswitch);

// Keep the Play/Stop toggle in sync with Live's playback state.
// CC2 = Set Toggle, CC3 = Clear Toggle; velocity = footswitch index.
onOscChange("/global/isPlaying", ([playing]) => {
  const isPlaying = Number(playing) === 1;
  sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, isPlaying ? 2 : 3, playStopIndex);
  log("Playback →", isPlaying ? "PLAYING" : "STOPPED/PAUSED");
}, true);

Quick setup notes:

  • Set MIDI_DEVICE and MIDI_CHANNEL to match your MC4 Pro.
  • Set playStopFootswitch to whichever preset (A–P) holds your Play/Stop toggle.
  • Just map Prev/Next on the controller to send the commands to AbleSet.

Paste it into Settings → MIDI Mapping, OSC & Scripting → Project Script and give it a run!!

Let me know how this works out on your end!

1 Like

@augustinvolpe you are too kind! This is above and beyond any response expected. The MC4 hasn’t arrived yet, but I’ll be sure to let you know how this works out. Cheers

1 Like