Skip to main content

uClock

A professional-grade BPM clock generator library for Arduino and PlatformIO, designed for musicians, artists, and engineers creating sequencers, sync boxes, and real-time musical devices. It is built to be multi-architecture, portable, and easy to use within the open-source ecosystem.

uClock delivers precise, hardware-interrupt-driven clock timing for music and media applications. Whether you're building a MIDI sequencer, modular synthesizer clock, or synchronizing multiple devices, uClock provides the rock-solid timing foundation your project needs.

The absence of real-time features necessary for creating professional-level embedded devices for music and video on open-source platforms like Arduino led to the development of uClock. By leveraging timer hardware interrupts, the library can schedule and manage real-time processing with safe shared resource access through its API.


Features

  • Hardware-interrupt timing for precise BPM generation
  • Flexible PPQN resolutions — from 1 to 960 pulses per quarter note
  • Callback architecture — PPQN output, step, sync, start/stop/pause/continue
  • External sync — slave to MIDI clock or other external sources
  • Shuffle & groove — per-track swing templates (MPC60, TR-909, custom)
  • Multi-track step sequencer — independent sequences with individual shuffle
  • Multiple sync outputs — drive different devices at different resolutions simultaneously
  • Concurrency-safe API — built-in atomic operations + ATOMIC() macro for shared resources
  • Software timer mode — for unsupported boards or to avoid interrupt conflicts

Supported Platforms

FamilyBoards
AVRATmega168/328, ATmega16u4/32u4, ATmega2560
ARMTeensy (all versions), STM32XX, Seeed Studio XIAO M0
ESP32All ESP32 family boards
RP2040Raspberry Pi Pico and compatible boards

Installation

PlatformIO

Add to your platformio.ini:

[env:...]
lib_deps =
midilab/uClock@^2.3.0

Build the project — PlatformIO installs dependencies automatically.

Arduino IDE

  1. Open Library Manager (Tools → Manage Libraries)
  2. Search for uclock
  3. Click Install on the latest version

Core Concepts

PPQN (Pulses Per Quarter Note)

Determines the timing resolution of your clock:

ResolutionTypical Use
PPQN_1, 2, 4, 8, 12Modular synthesis sync standards
PPQN_24Standard MIDI sync (24 pulses per beat)
PPQN_48Vintage drum machines, Korg DIN Sync
PPQN_96High-resolution internal clock (default)
PPQN_384, 480, 960Ultra-high resolution for modern sequencing and DAWs

Callback Architecture

uClock triggers your code at precise intervals through callbacks:

CallbackPurpose
setOnOutputPPQN()Main clock pulse — drive sequencers, process MIDI
setOnStep()16th note intervals — step sequencers, drum patterns
setOnSync()Custom sync outputs — multiple device sync, modular CV
setOnClockStart()Clock start event — initialize sequences, send MIDI start
setOnClockStop()Clock stop event — reset states, send MIDI stop
setOnClockPause()Clock pause event
setOnClockContinue()Clock continue event — resume from pause

Quick Start

Basic 96 PPQN Clock

#include <uClock.h>

void onPPQNCallback(uint32_t tick) {
// Called at each clock pulse (96 PPQN)
// Drive your sequencer logic here
}

void setup() {
uClock.setOutputPPQN(uClock.PPQN_96);
uClock.setOnOutputPPQN(onPPQNCallback);

uClock.setTempo(120.0);

uClock.init();
uClock.start();
}

void loop() {
// Your main code here
}

MIDI Sync Box with External Sync

Send MIDI clock and sync to external gear, with the ability to slave to an external master clock.

#include <uClock.h>

#define MIDI_CLOCK 0xF8
#define MIDI_START 0xFA
#define MIDI_STOP 0xFC

void onSync24Callback(uint32_t tick) {
Serial.write(MIDI_CLOCK);
}

void onClockStart() {
Serial.write(MIDI_START);
}

void onClockStop() {
Serial.write(MIDI_STOP);
}

void setup() {
Serial.begin(31250);

uClock.setOutputPPQN(uClock.PPQN_96);
uClock.setInputPPQN(uClock.PPQN_24);

uClock.setOnSync(uClock.PPQN_24, onSync24Callback);
uClock.setOnClockStart(onClockStart);
uClock.setOnClockStop(onClockStop);

uClock.init();
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
}

void loop() {
if (Serial.available() > 0) {
uint8_t midi_byte = Serial.read();
switch (midi_byte) {
case MIDI_CLOCK:
uClock.clockMe();
break;
case MIDI_START:
uClock.start();
break;
case MIDI_STOP:
uClock.stop();
break;
}
}
}

Multiple Sync Outputs

Generate different clock resolutions simultaneously for different devices:

#include <uClock.h>

#define SYNC_OUT_PIN 8

void onSync1(uint32_t tick) {
triggerModularPulse();
}

void onSync24(uint32_t tick) {
Serial.write(0xF8);
}

void onSync48(uint32_t tick) {
digitalWrite(SYNC_OUT_PIN, !digitalRead(SYNC_OUT_PIN));
}

void setup() {
uClock.setOutputPPQN(uClock.PPQN_96);
uClock.setOnSync(uClock.PPQN_1, onSync1);
uClock.setOnSync(uClock.PPQN_24, onSync24);
uClock.setOnSync(uClock.PPQN_48, onSync48);

uClock.init();
uClock.setTempo(126);
uClock.start();
}

void loop() {}

Available sync resolutions: any PPQN value ≤ the configured outputPPQN.


Step Sequencer Extension

uClock includes a built-in extension for step sequencers with multi-track support and per-track shuffle.

Single Track

#include <uClock.h>

#define MAX_STEPS 16

uint8_t pattern[MAX_STEPS] = {1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0};

void onStepCallback(uint32_t step) {
if (pattern[step % MAX_STEPS])
playNote();
}

void setup() {
uClock.setOnStep(onStepCallback);
uClock.init();
uClock.setTempo(126);
uClock.start();
}

void loop() {}

Multi-Track

Pass a track count as the second argument to setOnStep():

void onStepCallback(uint32_t step, uint8_t track) {
// track 0..TRACK_NUMBER-1
if (pattern[track][step % 16])
playTrackNote(track);
}

void setup() {
uClock.setOnStep(onStepCallback, 8); // 8 tracks
uClock.init();
uClock.setTempo(126);
uClock.start();
}

Features

  • 16th note orientation — natural step sequencer workflow
  • Multi-track support with independent callbacks
  • Per-track shuffle templates

Shuffle & Groove

Add humanization and swing to your sequences. Shuffle shifts individual steps earlier or later in time and adjusts note lengths to maintain musical coherence.

Shuffle example

Shuffle shifts individual steps earlier or later — positive values delay and shorten, negative values advance and extend.

  • Positive shuffle values: delay the note trigger and shorten note length
  • Negative shuffle values: trigger earlier and extend note length
  • Zero: play straight with no shuffle effect

This is the fundamental principle behind groove templating in step sequencers.

Shuffle Range

The range depends on your output PPQN:

min_shuffle = -(output_ppqn / 4) - 1
max_shuffle = +(output_ppqn / 4) - 1

Example with PPQN_96: -23 to +23 ticks per step

MPC60 Groove Example

#include <uClock.h>

#define TRACKS_SIZE 8

void onStepCallback(uint32_t step, uint8_t track) {
int8_t shuffle_len = uClock.getShuffleLength(track);
uint8_t step_len = getNoteLength(step, track);
step_len += shuffle_len;
// use step_len as the new note length
}

void setup() {
// MPC60 groove signatures at 96 PPQN (24 ticks per step)
int8_t shuffle_50[2] = {0, 0};
int8_t shuffle_54[2] = {0, 2};
int8_t shuffle_58[2] = {0, 4};
int8_t shuffle_62[2] = {0, 6};
int8_t shuffle_66[2] = {0, 8};
int8_t shuffle_71[2] = {0, 10};
int8_t shuffle_75[2] = {0, 12};
int8_t* templates[7] = {shuffle_50, shuffle_54, shuffle_58,
shuffle_62, shuffle_66, shuffle_71, shuffle_75};

uClock.setOutputPPQN(uClock.PPQN_96);
uClock.setOnStep(onStepCallback, TRACKS_SIZE);

uClock.init();

uClock.setShuffleTemplate(templates[3], 0); // 62% on track 0
uClock.setShuffle(true, 0);

uClock.start();
}

Shuffle API

void setShuffle(bool active, uint8_t track = 0);
bool isShuffled(uint8_t track = 0);
void setShuffleSize(uint8_t size, uint8_t track = 0);
void setShuffleData(uint8_t step, int8_t tick, uint8_t track = 0);
void setShuffleTemplate(int8_t* shuff, uint8_t size, uint8_t track = 0);
int8_t getShuffleLength(uint8_t track = 0);

Template size is configurable in uClock.h (default: MAX_SHUFFLE_TEMPLATE_SIZE 16).


External Sync & Phase Lock

Fine-tune synchronization with external clocks:

void setup() {
uClock.setClockMode(uClock.EXTERNAL_CLOCK);

// Lock phase every N quarter notes (default: 1)
// Higher values = looser sync but more stable
// Lower values = tighter sync but may jitter
uClock.setPhaseLockQuartersCount(1);

// Smooth tempo reading buffer (1-254)
// Larger = smoother but slower lock time response
uClock.setExtIntervalBuffer(64);
}

API Reference

Clock Control

void setTempo(float bpm); // 1-500 BPM
float getTempo(); // works with external sync too
void start();
void stop();
void pause(); // toggle pause/continue
void setClockMode(ClockMode mode); // INTERNAL_CLOCK or EXTERNAL_CLOCK
ClockMode getClockMode();

Resolution Configuration

void setOutputPPQN(PPQNResolution res); // main output resolution
void setInputPPQN(PPQNResolution res); // expected external sync resolution

// Available: PPQN_1, 2, 4, 8, 12, 24, 48, 96, 384, 480, 960
// Note: inputPPQN must be ≤ outputPPQN

Timing Utilities

uint32_t bpmToMicroSeconds(float bpm);
uint8_t getNumberOfSeconds(uint32_t time);
uint8_t getNumberOfMinutes(uint32_t time);
uint8_t getNumberOfHours(uint32_t time);
uint8_t getNumberOfDays(uint32_t time);
uint32_t getNowTimer();
uint32_t getPlayTime();

Software Timer Mode

For unsupported boards or to avoid interrupt conflicts, uClock can run in software mode.

Automatic fallback: activates automatically on unsupported boards.

Manual activation: define the USE_UCLOCK_SOFTWARE_TIMER build flag.

void loop() {
uClock.run(); // REQUIRED in software timer mode
// your code...
uClock.run();
}
warning

Software timer mode provides less accurate timing than hardware interrupts. Call uClock.run() frequently for best accuracy.


Concurrency Safety

uClock operates at interrupt or thread level, meaning callbacks run detached from your main loop(). Any data accessed both inside callbacks and in loop() is a shared resource and must be protected.

The ATOMIC Macro

Use ATOMIC() when modifying shared variables from loop():

void onStepCallback(uint32_t step) {
if (!sequencer.mute) {
playNote();
}
}

void loop() {
if (buttonPressed()) {
ATOMIC(
sequencer.mute = readButton();
)
}
}

When to Use ATOMIC

MUST use ATOMIC:

  • Modifying variables in loop() that are read inside uClock callbacks
  • Updating multiple related variables that must change atomically

DON'T need ATOMIC:

  • Inside any uClock callback
  • When using uClock's built-in methods (setTempo(), start(), stop(), setShuffle(), etc.) — these are already thread-safe

Common Pitfall

// ❌ WRONG — race condition
void onStepCallback(uint32_t step) { stepCount++; }
void loop() {
if (stepCount >= 16) {
stepCount = 0; // DANGER — modified outside ATOMIC
}
}

// ✅ CORRECT — atomic access
volatile uint8_t stepCount = 0;
void loop() {
uint8_t currentCount;
ATOMIC(currentCount = stepCount;)
if (currentCount >= 16) {
ATOMIC(stepCount = 0;)
doSomething();
}
}

Keep atomic sections as short as possible to minimize interrupt latency.


Migration Guide (v1.x → v2.3)

Breaking Changes

Old API (v1.x)New API (v2.3+)
setClock96PPQNOutput()setOnOutputPPQN()
setClock16PPQNOutput()setOnStep()
setOnClockStartOutput()setOnClockStart()
setOnClockStopOutput()setOnClockStop()
setOnSync24()setOnSync(uClock.PPQN_24, callback)
setOnSync48()setOnSync(uClock.PPQN_48, callback)
setOnSyncXX()setOnSync(uClock.PPQN_XX, callback)

Resolution Changes

v2.0 introduces flexible PPQN configuration:

// Old (implicit 96 PPQN)
uClock.setClock96PPQNOutput(callback);

// New (explicit)
uClock.setOutputPPQN(uClock.PPQN_96);
uClock.setOnOutputPPQN(callback);

If you used setClock16PPQNOutput(), replace with setOnStep() — no other changes needed.


Examples

Complete examples in the examples directory:

ExampleDescription
BasicClockSimple internal clock setup
ExternalSyncSlave to external clock
MidiClockSyncFull MIDI sync box implementation
AcidStepSequencerTB-303 style sequencer engine
MultiTrackSequencerIndependent track sequencing with shuffle

License

MIT License — Copyright (c) 2025 Romulo Silva

Support