Hey @kylecmoore,
You can already build this with Canvas (available in Pro) using Shared Variables. Shared Variables are synced across all devices, so the same timer shows up on every remote at once.
The timer is stored as two variables: timer1Elapsed (time accumulated so far, in milliseconds) and timer1Start (the timestamp of the current running segment, empty when paused). That way Start and Stop act as a real pause/resume — only Reset takes it back to zero.
1. The timer display (Label)
Add a Label element and set its value to:
${formatDuration((shared("timer1Elapsed", 0) + (shared("timer1Start") ? now() - shared("timer1Start") : 0)) / 1000)}
While the timer is running it adds the current segment to whatever has already been accumulated; when it’s paused it just shows the accumulated total. now() updates every second inside a template, so the Label counts up on its own.
Optionally, switch the Label’s Background Color to Dynamic so it lights up while running:
${shared("timer1Start") ? "green-700" : "gray-800"}
2. Start / Stop / Reset buttons
Add three Buttons with Button Type set to Script.
Start (or Resume) — Script on Press:
// Only starts a new segment if the timer isn't already running
if (!shared("timer1Start")) {
setShared("timer1Start", now());
}
Stop (or Pause) — Script on Press:
// Banks the current segment into the accumulated total, then stops
if (shared("timer1Start")) {
setShared(
"timer1Elapsed",
shared("timer1Elapsed", 0) + (now() - shared("timer1Start"))
);
resetShared("timer1Start");
}
Reset — Script on Press:
resetShared("timer1Start");
resetShared("timer1Elapsed");
That alone gives you a working stopwatch you can control from any device — and since Stop banks the time instead of discarding it, you can pause during a break and pick right back up where you left off.
3. Starting and stopping automatically at a given song or section
If you’d rather have it fire on its own, you could try driving the same variables from the Project Script (Settings → MIDI Mapping, OSC & Scripting → Project Script). It runs whenever AbleSet or the project loads, and onOscChange() lets you react to the active song changing:
// One entry per timer. Names must match your song locators exactly.
const TIMERS = [
{ key: "timer1", startSong: "Opener", endSong: "Act One Closer" },
{ key: "timer2", startSong: "Act Two Opener", endSong: "Encore" },
];
let prevSong = null;
onOscChange("/setlist/activeSongName", ([song]) => {
for (const t of TIMERS) {
// Start when the start song becomes active (and hasn't run yet tonight)
if (
song === t.startSong &&
!shared(`${t.key}Start`) &&
!shared(`${t.key}Elapsed`)
) {
setShared(`${t.key}Start`, now());
}
// Stop when we leave the end song
if (
prevSong === t.endSong &&
song !== t.endSong &&
shared(`${t.key}Start`)
) {
setShared(
`${t.key}Elapsed`,
shared(`${t.key}Elapsed`, 0) + (now() - shared(`${t.key}Start`))
);
resetShared(`${t.key}Start`);
}
}
prevSong = song;
}, true);
To trigger on sections instead of songs, swap /setlist/activeSongName for /setlist/activeSectionName and use your section names.
One thing worth knowing: there’s no “song ended” event, so the stop fires the moment the active song changes away from your end song. That works perfectly for timing an act in the middle of a show, but if your end song is the very last one in the set, you’ll want to hit the manual Stop button for that one.
4. Multiple timers
Each timer is just a pair of Shared Variables, so you can run as many as you like side by side — timer1Elapsed/timer1Start, timer2Elapsed/timer2Start, and so on. Add one Label per timer (swapping the key in the template), one set of buttons if you want manual control over it, and a matching entry in the TIMERS array above.
You could also run a “Total Show” timer alongside the per-act ones — same setup, just starting on your first song and ending on your last.
And since it’s all Shared Variables, don’t forget to hit Reset before the next night, or the new show will keep counting on top of the previous one.
Would that work for your use case?