Morningstar MC4 Pro Project Script Integration

Hey everyone! :waving_hand:

After the MC6 Pro Project Script thread, a few people asked about other Morningstar controllers — so here’s a version adapted for the MC4 Pro.

This one was a real team effort. Big thanks to @andypenn, who did all the testing on an actual MC4 Pro and patiently worked through several rounds of trial and error with me. Quite a few things only came to light because of his reports. Without that testing this would still be guesswork, so credit where it’s due. :raising_hands:

:white_check_mark: What this script does

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

The MC4 Pro has a single display, so instead of flashing one name at a time, this version splits the bank name into two lines:

  • Top line → current song
  • Bottom line → current section

Both stay on screen and update as you move through the set.

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

Same approach as the MC6 Pro version — the tempo is encoded as:

  • CC5 = MSB
  • CC6 = LSB

This lets the MC4 Pro show the BPM without relying on MIDI Clock, which is useful in setups where MIDI over Bluetooth is involved.

:backhand_index_pointing_right: One thing to be aware of: CC#5 and CC#6 are Morningstar’s “Set MIDI BPM” messages, so sending the tempo also sets the controller’s internal MIDI clock. You’ll see the BPM readout with its blinking indicator. Whether it keeps running depends on the MIDI Clock Persist setting in Controller Settings. If you’re on a fully wired setup and would rather use regular MIDI Clock, just remove the two sendBpm calls.

3️⃣ Keeps Play/Stop and Loop states in sync

The script keeps the MC4 Pro toggles in sync with AbleSet / Live — no matter where the action is triggered from (controller, keyboard, mouse).

:backhand_index_pointing_right: There’s no Record toggle in this version, but it follows the exact same pattern if you want one — add a recordFootswitch constant and an onOscChange("/global/isRecording", ...) handler calling setToggle().

:level_slider: Tempo automation tip

Whether you’re working in a single Live project with many songs on one 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 is to add one point after the end of each song, outside the audible part.

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. If a song has no tempo automation at all, the controller won’t receive a refreshed BPM value.

:control_knobs: Footswitch mapping for the MC4 Pro

The MC4 Pro has 4 switches across 4 pages, so presets run A–P (indexes 0–15).

As in the original, you define footswitches by letter, not index:

const loopFootswitch = "B";

The script converts that to the correct index internally.

:warning: The one thing that will break everything: MODEL_ID

Every Morningstar SysEx message carries a Device Model ID, and the controller silently ignores any message whose ID doesn’t match. This is exactly what we hit first: the BPM and toggles worked perfectly (CC messages carry no model ID) while the display never updated at all.

The MC4 Pro’s ID is 0x09. For reference: 0x03 (MC6), 0x04 (MC8), 0x05 (MC3), 0x06 (MC6 Pro), 0x08 (MC8 Pro).

Morningstar’s SysEx reference: SysEx Documentation for External Applications | Morningstar Engineering

Here's the full script
/**
 * MC4 PRO – AbleSet Project Script
 *
 * Shows the current song and section on the MC4 Pro bank name (two lines),
 * forwards BPM, and keeps the Play/Stop and Loop toggles in sync.
 */

/* ---------- Configuration ---------- */

const MIDI_DEVICE  = "YOUR MIDI DEVICE"; // Replace with your MIDI output device name
const MIDI_CHANNEL = 1;                  // The MIDI channel your MC4 Pro listens on

const MODEL_ID = 0x09; // MC4 Pro (MC6 Pro = 0x06, MC8 Pro = 0x08)

const playStopFootswitch = "A"; // preset holding your Play/Stop toggle
const loopFootswitch     = "B"; // preset holding your Loop On/Off toggle

// How the two lines are separated. If the name doesn't split in two,
// change this to "\n" instead and try again.
const LINE_BREAK = "\\n";

// Max characters per line. The two lines plus the separator have to add
// up to 32, so if you change one, adjust the other.
const LINE1_MAX = 20; // song
const LINE2_MAX = 10; // section

log("Project Script loaded");

/* ---------- Helpers ---------- */

/**
 * 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 display:
 * - Remove accents/diacritics (áéíóúñ → aeioun, etc.)
 * - Strip emojis and any non-printable / non-ASCII characters
 * - Collapse extra spaces
 * - Limit to the given number of characters
 */
function sanitizeTitle(str, max) {
  if (!str) return "";

  const 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.slice(0, max);
}

/**
 * Send a bank name update to the MC4 Pro (temporary override).
 * Line 1 = song, line 2 = section. Line 2 is optional.
 */
function sendBankName(line1, line2) {
  const top    = sanitizeTitle(line1, LINE1_MAX) || "No Song";
  const bottom = sanitizeTitle(line2, LINE2_MAX);

  const text = bottom ? top + LINE_BREAK + bottom : top;
  const normalized = text.slice(0, 32).padEnd(32);

  const payload = [
    0x00, 0x21, 0x24, MODEL_ID, 0x00, 0x70, 0x10,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
    ...makeAscii(normalized)
  ];

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

/**
 * Send BPM to the MC4 Pro by encoding it into CC5 (MSB) + CC6 (LSB).
 * Range is clamped between 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}`);
}

/* ---------- Display state ---------- */

let currentSong    = osc("/setlist/activeSongName") ?? "No Song";
let currentSection = osc("/setlist/activeSectionName") ?? "";
let queuedSong     = osc("/setlist/queuedName") ?? null;

/**
 * Decide what to show:
 * - Top line: queued song if there is one, otherwise the active song.
 * - Bottom line: the current section.
 */
function updateDisplay() {
  sendBankName(queuedSong ?? currentSong, currentSection);
}

// Initial display when the script loads.
updateDisplay();

/* ---------- AbleSet → MC4 Pro ---------- */

// Active song changed → push current tempo, then refresh the display.
onOscChange("/setlist/activeSongName", ([name]) => {
  currentSong = name ?? "No Song";

  const t = Number(osc("/global/tempo"));
  if (!Number.isNaN(t)) sendBpm(t);

  updateDisplay();
}, true);

// Active section changed → refresh the bottom line.
onOscChange("/setlist/activeSectionName", ([section]) => {
  currentSection = section ?? "";
  updateDisplay();
}, true);

// Song queued → show the queued name on the top line.
onOscChange("/setlist/queuedName", ([song]) => {
  queuedSong = song || null;
  updateDisplay();
}, true);

// Tempo changes (automation, manual, etc.) → 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,
// without the prefix and without the ".als" extension.
onOscChange("/global/loadingProjectName", ([loadingName]) => {
  if (!loadingName) return;

  const clean = String(loadingName);
  sendBankName(clean.endsWith(".als") ? clean.slice(0, -4) : clean, "");
}, true);

/* =======================================================================
   MC4 Pro presets index (A to P)
   ======================================================================= */

/**
 * Convert a footswitch letter (A–P) into its Morningstar 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;
}

/**
 * CC2 = Engage Toggle, CC3 = Disengage Toggle.
 * The value selects the preset index: 0 = A, 1 = B, ..., 15 = P.
 */
function setToggle(index, on) {
  if (index === null) return;
  sendMidiCc(MIDI_DEVICE, MIDI_CHANNEL, on ? 2 : 3, index);
}

const playStopIndex = footswitchLetterToIndex(playStopFootswitch);
const loopIndex     = footswitchLetterToIndex(loopFootswitch);

/* =======================================================================
   Play/Stop + Loop state → MC4 Pro (toggle sync)
   ======================================================================= */

// PLAYBACK toggle (A) – uses /global/isPlaying
onOscChange("/global/isPlaying", ([playing]) => {
  const isPlaying = Number(playing) === 1;
  setToggle(playStopIndex, isPlaying);
  log("Playback →", isPlaying ? "PLAYING" : "STOPPED/PAUSED");
}, true);

/**
 * LOOP toggle (B) – uses /setlist/isInActiveLoop
 *
 * This follows whether you're actually inside an active loop, rather than
 * whether a loop bracket exists somewhere in the project — otherwise the
 * toggle stays lit after jumping away from a looped song.
 */
function updateLoopToggle() {
  const inLoop = Number(osc("/setlist/isInActiveLoop")) === 1;
  setToggle(loopIndex, inLoop);
  log("Loop →", inLoop ? "ENABLED" : "DISABLED");
}

onOscChange("/setlist/isInActiveLoop", updateLoopToggle, true);
onOscChange("/setlist/loopEnabled", updateLoopToggle, true);

Just remember to:

  • Replace "YOUR MIDI DEVICE" with your exact MIDI output device name, as AbleSet lists it
  • Set MIDI_CHANNEL to the MIDI channel your MC4 Pro is listening on (1–16)
  • Set playStopFootswitch and loopFootswitch to the presets where those toggles live
  • The presets themselves still need to be programmed in the Morningstar Editor — this script only keeps their states in sync

One last note: LINE_BREAK is set to a literal \n sequence, which is what worked in testing. If your display shows both names on one line with the characters visible in the middle, change it to a real newline and reload.

Feel free to try it out, adapt it to your setup, and build on top of it!

1 Like