Repository: midilab/uClock Branch: main Commit: 6cf39cc242e1 Files: 33 Total size: 111.8 KB Directory structure: gitextract_wxko6giz/ ├── LICENSE ├── README.md ├── examples/ │ ├── AVRUartSlaveMidiClockMonitor/ │ │ └── AVRUartSlaveMidiClockMonitor.ino │ ├── AcidStepSequencer/ │ │ ├── AcidStepSequencer.ino │ │ ├── DefaultUserInterface.ino │ │ ├── HardwareInterface.ino │ │ └── README.md │ ├── ESP32UartMasterMidiClock/ │ │ └── ESP32UartMasterMidiClock.ino │ ├── GenericMasterOrExternalSync/ │ │ └── GenericMasterOrExternalSync.ino │ ├── LeonardoUsbSlaveMidiClockMonitor/ │ │ └── LeonardoUsbSlaveMidiClockMonitor.ino │ ├── MidiClock/ │ │ └── MidiClock.ino │ ├── RP2040UsbUartMasterClock/ │ │ ├── RP2040UsbUartMasterClock.ino │ │ └── builtin_led.ino │ ├── STM32UartMasterMidiClock/ │ │ └── STM32UartMasterMidiClock.ino │ ├── TeensyUsbMasterMidiClock/ │ │ ├── TeensyUsbMasterMidiClock.ino │ │ └── name.c │ ├── TeensyUsbSlaveMidiClock/ │ │ ├── TeensyUsbSlaveMidiClock.ino │ │ └── name.c │ ├── TeensyUsbSlaveMidiClockMonitor/ │ │ ├── TeensyUsbSlaveMidiClockMonitor.ino │ │ └── name.c │ └── XiaoUsbMasterMidiClock/ │ └── XiaoUsbMasterMidiClock.ino ├── library.json ├── library.properties └── src/ ├── platforms/ │ ├── avr.h │ ├── esp32-nofrertos.h │ ├── esp32.h │ ├── rp2040.h │ ├── samd.h │ ├── software.h │ ├── stm32.h │ └── teensy.h ├── uClock.cpp └── uClock.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 midilab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # uClock - BPM Clock Generator Library 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. ## Overview **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 library leverages hardware timer interrupts to ensure accurate BPM generation and synchronization, making it suitable for professional music production, live performance, and creative installations. The absence of real-time features necessary for creating professional-level embedded devices for music and video on open-source community-based 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. ## Supported Platforms - **AVR**: ATmega168/328, ATmega16u4/32u4, ATmega2560 - **ARM**: Teensy (all versions), STM32XX, Seeed Studio XIAO M0 - **ESP32**: All ESP32 family boards - **RP2040**: Raspberry Pi Pico and compatible boards ## Why uClock? Open-source platforms like Arduino and PlatformIO traditionally lack the real-time capabilities required for professional music and video applications. uClock bridges this gap by providing: - **Precise timing** through hardware interrupts - **Flexible clock resolutions** from 1 to 960 PPQN - **External sync support** for external sync configurations - **Shuffle and groove** capabilities for humanized timing - **Multi-track sequencing** with independent shuffle per track - **Multiple sync outputs** for different device standards ## Installation ### PlatformIO 1. Open platformio.ini, a project configuration file located in the root of PlatformIO project. 2. Add the following line to the lib_deps option of [env:] section: midilab/uClock@^2.3.0 ```ini [env:...] lib_deps = midilab/uClock@^2.3.0 ``` 3. Build a project, PlatformIO will automatically install dependencies. ### Arduino IDE 1. Open your Arduino IDE 2. Select the Library Manager Tab on left side 3. Type "uclock" at the search box 4. Click Install for latest version ## Core Concepts ### Clock Resolutions (PPQN) PPQN (Pulses Per Quarter Note) determines the timing resolution of your clock: - **PPQN_1, 2, 4, 8, 12**: Modular synthesis sync standards - **PPQN_24**: Standard MIDI sync (24 pulses per beat) - **PPQN_48**: Common in vintage drum machines - **PPQN_96**: High-resolution internal clock (default) - **PPQN_384, 480, 960**: Ultra-high resolution for modern sequencing and DAWs ### Callback Architecture uClock operates through a callback system that triggers your code at precise intervals: | Callback | Purpose | Use Case | |----------|---------|----------| | `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 | Pause handling | | `setOnClockContinue()` | Clock continue event | Resume from pause | ## Quick Start ### Basic 96 PPQN Clock ```cpp #include void onPPQNCallback(uint32_t tick) { // Called at each clock pulse (default 96 PPQN) // Drive your sequencer logic here } void setup() { // set main clock rate for output(sequencer resolution) uClock.setOutputPPQN(uClock.PPQN_96); // Configure callbacks uClock.setOnOutputPPQN(onPPQNCallback); // Set tempo uClock.setTempo(120.0); // Initialize and start uClock.init(); uClock.start(); } void loop() { // Your main code here } ``` ### Basic MIDI Clock box with External Sync Send clock message and sync to external midi clock. ```cpp #include // MIDI clock, start and stop byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC // The callback function called by Clock each Pulse of 24PPQN clock resolution. void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears Serial.write(MIDI_CLOCK); } // The callback function called when clock starts by using Clock.start() method. void onClockStart() { Serial.write(MIDI_START); } // The callback function called when clock stops by using Clock.stop() method. void onClockStop() { Serial.write(MIDI_STOP); } void setup() { // Initialize serial communication at 31250 bits per second, the default MIDI serial speed communication: Serial.begin(31250); // set main clock rate for output(sequencer resolution) uClock.setOutputPPQN(uClock.PPQN_96); // set main clock rate for input(expected sync signal rate) uClock.setInputPPQN(uClock.PPQN_24); // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // set external clock mode uClock.setClockMode(uClock.EXTERNAL_CLOCK); // Set the clock BPM to 126 BPM //uClock.setTempo(126); //uClock.start(); } void loop() { // call clockMe() each time you receive an external clock pulse // in this example we set inputPPQN to 24 PPQN // wich expects a signal clock rate comming from MIDI device // PS: Idealy you should do midi sync with another interruption schema // the more code on loop() the less accuracy the MIDI_CLOCK signal reads 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; } } } ``` ## Advanced Features ### Multiple Sync Outputs Generate different clock resolutions simultaneously for various devices: ```cpp #include #define SYNC_OUT_PIN 8 #define MIDI_CLOCK_BYTE 0xF8 void onSync1(uint32_t tick) { // Send modular sync (1 pulse per quarter note) triggerModularPulse(); } void onSync2(uint32_t tick) { // Send Pocket Operators sync (2 pulse per quarter note) triggerPocketOperatorsPulse(); } void onSync24(uint32_t tick) { // Send MIDI clock (24 PPQN) Serial.write(MIDI_CLOCK_BYTE); } void onSync48(uint32_t tick) { // Send 48 PPQN for vintage gear like Korg DIN Sync 48 digitalWrite(SYNC_OUT_PIN, !digitalRead(SYNC_OUT_PIN)); } void setup() { // set main clock rate for output(sequencer resolution) uClock.setOutputPPQN(uClock.PPQN_96); // set sync callbacks uClock.setOnSync(uClock.PPQN_1, onSync1); uClock.setOnSync(uClock.PPQN_2, onSync2); uClock.setOnSync(uClock.PPQN_24, onSync24); uClock.setOnSync(uClock.PPQN_48, onSync48); // do only init after all setup is done uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); uClock.start(); } void loop() { // Your main code here } ``` **Available Sync Resolutions**: All possible Clock Resolutions (PPQN) where setOnSync(resolution) <= setOutputPPQN(resolution) ### Step Sequencer Extension uClock includes a built-in extension specifically designed for creating step sequencers for synthesizers and drum machines. The extension provides multi-track support with independent per-track control, making it ideal for building complete rhythm machines and melodic sequencers. #### Features **Current Features**: - ✅ **16th note orientation**: Natural step sequencer workflow - ✅ **Multi-track support**: Independent sequences with individual callbacks - ✅ **Per-track shuffle**: Each track can have its own groove templatexs **Roadmap**: - 🔄 **Per-track shift**: Offset patterns in time (coming soon) - 🔄 **Per-track direction**: Forward, reverse, ping-pong playback (coming soon) ```cpp #include #define MAX_STEPS 16 //#define TRACK_NUMBER 8 // a pattern example for drums mainly uint8_t pattern[MAX_STEPS] = {1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}; // Called every 16th note // not dependent on internal or external clock resolution // single track callback(doesn't mean you can't code a multirack sequencer here) void onStepCallback(uint32_t step) { if (pattern[step % MAX_STEPS]) playNote(); } // the multirack callback //void onStepCallback(uint32_t step, uint8_t track) { //} void setup() { // Configure callbacks uClock.setOnStep(onStepCallback); // with multitrack support? //uClock.setOnStep(onStepCallback, TRACK_NUMBER); uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); uClock.start(); } void loop() { // Your main code here } ``` ### Shuffle and Groove Add humanization and swing to your sequences with shuffle support. Re-create timeless groove signatures of legends like MPC60 or TR-909, or experiment with your own custom groove templates. #### How Shuffle Works ![Ableton Shuffle Example](https://raw.githubusercontent.com/midilab/uclock/main/doc/shuflle-example.gif) Shuffle operates by shifting individual steps earlier or later in time, along with adjusting note lengths to maintain musical coherence. As shown in the Ableton example above: - **Positive shuffle values**: Delay the note trigger and shorten note length (to avoid overlapping with the next note) - **Negative shuffle values**: Trigger the note earlier and extend note length (filling the time gap) - **Zero values**: Play straight with no shuffle effect This is the fundamental principle behind groove templating in step sequencers. Each 16th note step can be individually time-shifted to create swing, shuffle, or completely custom rhythmic feels. #### Technical Specifications **Shuffle templates are PPQN-dependent**: The range of shuffle values depends on your output resolution: ```cpp // Shuffle range calculation - in ticks min_shuffle = -(output_ppqn/4) - 1 max_shuffle = +(output_ppqn/4) - 1 // Example with PPQN_96 (default) // Range: -23 to +23 ticks per step ``` **Template Size Configuration**: Adjust in `uClock.h` if needed: ```cpp #define MAX_SHUFFLE_TEMPLATE_SIZE 16 ``` Modify this value to support longer patterns (memory permitting) or reduce it to save memory if you only need simpler grooves. MPC60 Groove implmentation example based on [Roger Linn Groove Magic Interview](https://www.attackmagazine.com/features/interview/roger-linn-swing-groove-magic-mpc-timing/) ```cpp #include #define TRACKS_SIZE 8 // The shuffle effect is applied transparently for shuffled steps void onStepCallback(uint32_t step, uint8_t track) { // get the length diff to apply to current shuffled step length // if the step is not shuffled, then shuffle_len == 0 int8_t shuffle_len = uClock.getShuffleLength(track); // sequencer procesing... // ... uint8_t step_len = getNoteLength(step, track); step_len += shuffle_len; // use step_len as the new length of note for this step // ... } // no multirack support gives you a global shuffle flavor //void onStepCallback(uint32_t step) { //} void setup() { // MPC60 groove signature (Same used for TR909) // Based on Roger Linn Groove Magic Interview // Internal clock: 96 PPQN // Each step is 24 ticks long uint8_t current_shuffle = 0; 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}; String shuffle_name[7] = {"50%", "54%", "58%", "62%", "66%", "71%", "75%"}; int8_t* shuffle_templates[7] = {shuffle_50, shuffle_54, shuffle_58, shuffle_62, shuffle_66, shuffle_71, shuffle_75}; // set main clock rate for output(sequencer resolution) and input(expected sync signal) uClock.setOutputPPQN(uClock.PPQN_96); // the second argument of setOnStep is used to request multitrack support // multitrack individual shuffle support for this example uClock.setOnStep(onStepCallback, TRACKS_SIZE); // global shuffle? no need to multitrack handle of step sequencer extension // set the callback without second param //uClock.setOnStep(onStepCallback); uClock.init(); // set a template for shuffle for specific tracks uClock.setShuffleTemplate(shuffle_templates[current_shuffle], 0); uClock.setShuffleTemplate(shuffle_templates[current_shuffle], 1); uClock.setShuffleTemplate(shuffle_templates[current_shuffle], 2); uClock.setShuffleTemplate(shuffle_templates[current_shuffle], 3); // if no multirack support set on setOnStep() then the suffle is global //uClock.setShuffleTemplate(shuffle_templates[current_shuffle]); // enable/disable shuffle for specific tracks uClock.setShuffle(true, 0); uClock.setShuffle(true, 1); uClock.setShuffle(true, 2); uClock.setShuffle(true, 3); uClock.start(); } ``` #### Shuffle API Reference ```cpp // Enable/disable shuffle for a track void setShuffle(bool active, uint8_t track = 0); // Check if shuffle is active bool isShuffled(uint8_t track = 0); // Set shuffle pattern size (max 16 steps) void setShuffleSize(uint8_t size, uint8_t track = 0); // Set individual shuffle values void setShuffleData(uint8_t step, int8_t tick, uint8_t track = 0); // Set complete shuffle template void setShuffleTemplate(int8_t* shuff, uint8_t size, uint8_t track = 0); // Get shuffle offset for note length compensation int8_t getShuffleLength(uint8_t track = 0); ``` **Shuffle Values**: - Range: `-(output_ppqn/4)-1` to `+(output_ppqn/4)-1` ticks - Negative values = play earlier - Positive values = delay - Zero = no shuffle (straight timing) ### External Sync and Phase Lock Fine-tune synchronization with external clocks: ```cpp void setup() { // sets external clock uClock.setClockMode(uClock.EXTERNAL_CLOCK); // get back to internal clock //uClock.setClockMode(uClock.INTERNAL_CLOCK); // Lock phase every N quarters (default: 1) // Higher values = looser sync but more stable // Lower values = tighter sync but may jitter uClock.setPhaseLockQuartersCount(1); // Smooth tempo reading for uClock.getTempo() // Buffer size: 1-254 (larger = smoother but slower lock time response) // The higher the value the longer it takes to sync getTempo() value to actual tempo // PS: the tempo taken to bpm sync here it is for getTempo() value only // it does not affect the main clock. uClock.setExtIntervalBuffer(64); } ``` ## Configuration API ### Clock Control ```cpp // Set internal tempo (1-500 BPM) void setTempo(float bpm); // Get current tempo (works with external sync too) float getTempo(); // Clock transport control void start(); // Start clock void stop(); // Stop clock void pause(); // Toggle pause/continue // Clock mode void setClockMode(ClockMode mode); ClockMode getClockMode(); // Modes: INTERNAL_CLOCK, EXTERNAL_CLOCK ``` ### Resolution Configuration ```cpp // Set main output resolution void setOutputPPQN(PPQNResolution resolution); // Set expected input resolution (external sync) void setInputPPQN(PPQNResolution resolution); // Available resolutions: // PPQN_1, PPQN_2, PPQN_4, PPQN_8, PPQN_12, PPQN_24 // PPQN_48, PPQN_96, PPQN_384, PPQN_480, PPQN_960 ``` **Important**: `inputPPQN` must be ≤ `outputPPQN` ### Timing Utilities ```cpp // Convert BPM to microseconds uint32_t bpmToMicroSeconds(float bpm); // Elapsed time functions uint8_t getNumberOfSeconds(uint32_t time); uint8_t getNumberOfMinutes(uint32_t time); uint8_t getNumberOfHours(uint32_t time); uint8_t getNumberOfDays(uint32_t time); // Timers 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 `USE_UCLOCK_SOFTWARE_TIMER` build flag ```cpp void loop() { // REQUIRED in software timer mode uClock.run(); // Part of your code here // ... // Call run() frequently for best accuracy uClock.run(); // Other parts of your code here // ... uClock.run(); } ``` ⚠️ **Note**: Software timer mode provides less accurate timing than hardware interrupts. ## 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, onSync24)` | | `setOnSync48()` | `setOnSync(uClock.PPQN_48, onSync48)` | | `setOnSyncXX()` | `setOnSync(uClock.PPQN_XX, onSyncXX)` | ### Resolution Changes v2.0 introduces flexible PPQN configuration. If you relied on the old 96 PPQN default: ```cpp // Old (implicit 96 PPQN) uClock.setClock96PPQNOutput(callback); // New (explicit configuration) uClock.setOutputPPQN(uClock.PPQN_96); uClock.setOnOutputPPQN(callback); ``` ### Tick Counting If you used `setClock16PPQNOutput()` (16th notes), simply replace with `setOnStep()` - no other changes needed. If you relied on specific tick values from `setClock96PPQNOutput()`, verify your timing calculations match your chosen PPQN resolution. ## Examples Complete examples are available in the `examples/` directory: - **BasicClock**: Simple internal clock setup - **ExternalSync**: Slave to external clock - **MidiClockSync**: Full MIDI sync box implementation - **AcidStepSequencer**: TB-303 style sequencer engine - **MultiTrackSequencer**: Independent track sequencing with shuffle ## Technical Details ### Concurrency Safety uClock operates at interruption or thread(when avaliable) level, wich means your microcontroller will be executing uClock callbacks processing detached from your main `loop()` code, that creates a concurrency programming context where you need to properly protect read and write access to any shared resource between uClock callback code and `loop()` code. All clock operations that involve shared variables or resources use atomic operations to ensure concurrency-safe access between interrupt/threads contexts and your main `loop()` code. * interruption/thread: Depending on micrcontroller platform we levarage a FreeRTOS implementation when multi core avaliable. In this scenario, a threaded context will be used for callbacks instead of interruption. #### Understanding Shared Resources **What are shared variables or resources?** Any data that is accessed both inside uClock callbacks (which run in interrupt/thread context) and inside your `loop()` function (which runs in the main program context) is considered a shared resource. **Examples of shared resources:** - Pattern data arrays modified in `loop()` and read in `onStepCallback()` - Sequencer state variables (mute, arp, etc.) modified in `loop()` and read in `onStepCallback()` - MIDI buffers or output queues used in `loop()` and `onStepCallback()` #### Safe Modification Using ATOMIC When modifying shared variables from your main code, always use the `ATOMIC()` macro: ```cpp void onStepCallback(uint32_t step) { // Read pattern state in interrupt context if (!sequencer.mute) { playNote(); } } // Example: Safely updating sequencer state from user input void loop() { if (buttonPressed()) { uint8_t mute = readButton(); ATOMIC( sequencer.mute = mute; ) } } ``` ```cpp // Example: Safely modifying pattern data uint8_t pattern[16] = {1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}; void onStepCallback(uint32_t step) { // Read pattern in interrupt context if (pattern[step % 16]) { playNote(); } } void loop() { if (userEditedPattern()) { // Safely modify from main loop ATOMIC( pattern[4] = 1; pattern[8] = 0; ) } } ``` #### When to Use ATOMIC **You MUST use ATOMIC when:** - Modifying variables at `loop()` that are readed inside uClock callbacks - Updating multiple related variables that must change together atomically at `loop()` - Writing or Reading variables that are `volatile` (often used with interrupts/threads) at `loop()` **You DON'T need ATOMIC when:** - Coding inside any uClock callback - Using uClock's built-in methods like `setTempo()`, `start()`, `stop()` (all uClock public API are already protected) #### Built-in Thread Safety uClock's API methods are already interrupt/thread-safe and handle atomic operations internally: ```cpp // These are SAFE to call from loop() without ATOMIC uClock.setTempo(120.0); uClock.start(); uClock.stop(); uClock.pause(); uClock.setShuffle(true, 0); uClock.setShuffleTemplate(template, 16, 0); ``` #### Common Pitfalls **❌ Wrong - Race condition:** ```cpp uint8_t stepCount = 0; void onStepCallback(uint32_t step) { stepCount++; // Modified in interrupt/thread context } void loop() { if (stepCount >= 16) { // Read in main loop - NOT ATOMIC! stepCount = 0; // Modified in main loop - DANGER! doSomething(); } } ``` **✅ Correct - Atomic access:** ```cpp volatile uint8_t stepCount = 0; void onStepCallback(uint32_t step) { stepCount++; // Modified in interrupt/thread context } void loop() { uint8_t currentCount; ATOMIC( currentCount = stepCount; // Atomic read ) if (currentCount >= 16) { ATOMIC( stepCount = 0; // Atomic write ) doSomething(); } } ``` #### Advanced: Platform-Specific Implementation For AVR platforms, `ATOMIC()` is defined as: ```cpp #define ATOMIC(X) noInterrupts(); X; interrupts(); ``` For some ARM's and other platforms with multi-core architecture, uClock uses platform-appropriate atomic mechanisms. From user perspective the ATOMIC() macro is transparent and safe to use when you need, independently from you micro-controller architecture. **Note:** Keep atomic sections as short as possible to minimize interrupt latency. Only include the actual shared variable access inside the `ATOMIC()` block: ```cpp // Good - minimal atomic section ATOMIC(pattern = newPattern;) updateDisplay(pattern); // Outside atomic section // Less ideal - unnecessary code in atomic section ATOMIC( pattern = newPattern; updateDisplay(pattern); // This doesn't need to be atomic! ) ``` ## License MIT License - Copyright (c) 2025 Romulo Silva ## Support & Community - **Documentation**: [GitHub Repository](https://github.com/midilab/uClock) - **Issues**: Report bugs via GitHub Issues - **Contact**: contact@midilab.co --- **uClock** - Precision timing for creative minds. ================================================ FILE: examples/AVRUartSlaveMidiClockMonitor/AVRUartSlaveMidiClockMonitor.ino ================================================ /* Uart MIDI Sync Slave Box Monitor * * This example demonstrates how to create a * MIDI slave clock box with * monitor support using oled display * * MIDI in must be provided via an opto-isolator to pin RX/D0 * Tested on an Arduino Uno. * * You need the following libraries to make it work * - Midi Library * - u8g2 * - uClock * This example code is in the public domain. * * Code by midilab contact@midilab.co * Example modified by Jackson Devices contact@jacksondevices.com */ #include #include // // BPM Clock support // #include MIDI_CREATE_DEFAULT_INSTANCE(); U8X8 * u8x8; // MIDI clock, start, stop, note on and note off byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC float bpm = 126.0; uint8_t bpm_blink_timer = 1; uint8_t clock_state = 1; uint8_t clock_mode = 0; void handle_bpm_led(uint32_t tick) { // BPM led indicator if (!(tick % (96)) || (tick == 1)) { // first compass step will flash longer bpm_blink_timer = 8; digitalWrite(LED_BUILTIN, HIGH); } else if (!(tick % (24))) { // each quarter led on digitalWrite(LED_BUILTIN, HIGH); } else if (!(tick % bpm_blink_timer)) { // get led off digitalWrite(LED_BUILTIN, LOW); bpm_blink_timer = 1; } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears MIDI.sendRealTime(MIDI_CLOCK); handle_bpm_led(tick); } void onClockStart() { MIDI.sendRealTime(MIDI_START); } void onClockStop() { MIDI.sendRealTime(MIDI_STOP); digitalWrite(LED_BUILTIN, LOW); } // External clock handlers void onExternalClock() { uClock.clockMe(); } void onExternalStart() { uClock.start(); } void onExternalStop() { uClock.stop(); digitalWrite(LED_BUILTIN, LOW); } void setup() { // // MIDI setup // // Initiate MIDI communications, listen to all channels, disable soft MIDI thru MIDI.begin(); MIDI.turnThruOff(); MIDI.setHandleClock(onExternalClock); MIDI.setHandleStart(onExternalStart); MIDI.setHandleStop(onExternalStop); // An led to display MIDI reception pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); // // OLED setup // Please check you oled model to correctly init him // The complete list is available here: https://github.com/olikraus/u8g2/wiki/u8g2setupcpp // //u8x8 = new U8X8_SH1106_128X64_NONAME_HW_I2C(U8X8_PIN_NONE); u8x8 = new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE); u8x8->begin(); u8x8->setFont(u8x8_font_pressstart2p_r); u8x8->clear(); u8x8->setFlipMode(true); u8x8->drawUTF8(0, 0, "uClock"); // // uClock Setup // // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // For MIDI Sync Start and Stop uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); uClock.setClockMode(uClock.EXTERNAL_CLOCK); // for smooth slave tempo calculate display you should raise the // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. // note: this doesn't impact on sync time, only display time getTempo() // if you dont want to use it, it is default set it to 1 for memory save uClock.setExtIntervalBuffer(128); // inits uClock uClock.init(); //uClock.setTempo(136.5); //uClock.start(); } void loop() { while(MIDI.read()) {} // DO NOT ADD MORE PROCESS HERE AT THE COST OF LOSING CLOCK SYNC // Since arduino make use of Serial RX interruption we need to // read Serial as fast as we can on the loop if (bpm != uClock.getTempo()) { bpm = uClock.getTempo(); u8x8->drawUTF8(8, 7, String(bpm, 1).c_str()); u8x8->drawUTF8(8 + 5, 7, "BPM"); // clear display ghost number for 2 digit // coming from 3 digit bpm changes if (bpm < 100) { u8x8->drawUTF8(8 + 4, 7, " "); } } if (clock_state != uClock.clock_state) { clock_state = uClock.clock_state; if (clock_state >= 1) { u8x8->drawUTF8(0, 7, "Playing"); } else { u8x8->drawUTF8(0, 7, "Stopped "); } } if (clock_mode != uClock.getClockMode()) { clock_mode = uClock.getClockMode(); if (clock_mode == uClock.EXTERNAL_CLOCK) { u8x8->drawUTF8(10, 0, "Slave "); } else { u8x8->drawUTF8(10, 0, "Master"); } } } ================================================ FILE: examples/AcidStepSequencer/AcidStepSequencer.ino ================================================ // Acid StepSequencer, a Roland TB303 step sequencer engine clone // author: midilab contact@midilab.co // under MIT license #include "Arduino.h" #include // Sequencer config #define STEP_MAX_SIZE 16 #define NOTE_LENGTH 12 // min: 1 max: 23 DO NOT EDIT BEYOND!!! 12 = 50% on 96ppqn, same as original tb303. 62.5% for triplets time signature #define NOTE_VELOCITY 90 #define ACCENT_VELOCITY 127 // MIDI modes #define MIDI_CHANNEL 0 // 0 = channel 1 #define MIDI_MODE //#define SERIAL_MODE // do not edit from here! #define NOTE_STACK_SIZE 3 // MIDI clock, start, stop, note on and note off byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC #define NOTE_ON 0x90 #define NOTE_OFF 0x80 // Sequencer data typedef struct { uint8_t note; bool accent; bool glide; bool rest; } SEQUENCER_STEP_DATA; typedef struct { uint8_t note; int8_t length; } STACK_NOTE_DATA; // main sequencer data SEQUENCER_STEP_DATA _sequencer[STEP_MAX_SIZE]; STACK_NOTE_DATA _note_stack[NOTE_STACK_SIZE]; uint16_t _step_length = STEP_MAX_SIZE; // make sure all above sequencer data are modified atomicly only // eg. ATOMIC(_sequencer[0].accent = true); ATOMIC(_step_length = 7); #define ATOMIC(X) noInterrupts(); X; interrupts(); // shared data to be used for user interface feedback bool _playing = false; uint16_t _step = 0; void sendMidiMessage(uint8_t command, uint8_t byte1, uint8_t byte2) { // send midi message command = command | (uint8_t)MIDI_CHANNEL; Serial.write(command); Serial.write(byte1); Serial.write(byte2); } // Each call represents exactly one step. void onStepCallback(uint32_t tick) { uint16_t step; uint16_t length = NOTE_LENGTH; // get actual step. _step = tick % _step_length; // send note on only if this step are not in rest mode if ( _sequencer[_step].rest == false ) { // check for glide event ahead of _step step = _step; for ( uint16_t i = 1; i < _step_length; i++ ) { ++step; step = step % _step_length; if ( _sequencer[step].glide == true && _sequencer[step].rest == false ) { length = NOTE_LENGTH + (i * 24); break; } else if ( _sequencer[step].rest == false ) { break; } } // find a free note stack to fit in for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { if ( _note_stack[i].length == -1 ) { _note_stack[i].note = _sequencer[_step].note; _note_stack[i].length = length; // send note on sendMidiMessage(NOTE_ON, _sequencer[_step].note, _sequencer[_step].accent ? ACCENT_VELOCITY : NOTE_VELOCITY); return; } } } } // The callback function wich will be called by uClock each Pulse of 96PPQN clock resolution. void onOutputPPQNCallback(uint32_t tick) { // handle note on stack for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { if ( _note_stack[i].length != -1 ) { --_note_stack[i].length; if ( _note_stack[i].length == 0 ) { sendMidiMessage(NOTE_OFF, _note_stack[i].note, 0); _note_stack[i].length = -1; } } } // user feedback about sequence time events tempoInterface(tick); } void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears Serial.write(MIDI_CLOCK); } // The callback function wich will be called when clock starts by using Clock.start() method. void onClockStart() { Serial.write(MIDI_START); _playing = true; } // The callback function wich will be called when clock stops by using Clock.stop() method. void onClockStop() { Serial.write(MIDI_STOP); // send all note off on sequencer stop for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { sendMidiMessage(NOTE_OFF, _note_stack[i].note, 0); _note_stack[i].length = -1; } _playing = false; } void setup() { // Initialize serial communication #ifdef MIDI_MODE // the default MIDI serial speed communication at 31250 bits per second Serial.begin(31250); #endif #ifdef SERIAL_MODE // for usage with a PC with a serial to MIDI bridge Serial.begin(115200); #endif // Set the callback function for the clock output to send MIDI Sync message. uClock.setOnOutputPPQN(onOutputPPQNCallback); // for MIDI sync // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for the step sequencer on 16ppqn uClock.setOnStep(onStepCallback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); // initing sequencer data for ( uint16_t i = 0; i < STEP_MAX_SIZE; i++ ) { _sequencer[i].note = 48; _sequencer[i].accent = false; _sequencer[i].glide = false; _sequencer[i].rest = false; } // initing note stack data for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { _note_stack[i].note = 0; _note_stack[i].length = -1; } // pins, buttons, leds and pots config configureInterface(); } // User interaction goes here void loop() { processInterface(); } ================================================ FILE: examples/AcidStepSequencer/DefaultUserInterface.ino ================================================ #define SEQUENCER_MIN_BPM 50 #define SEQUENCER_MAX_BPM 177 // Ui config #define LOCK_POT_SENSTIVITY 3 // hardware setup to fit different kinda of setups and arduino models #define OCTAVE_POT_PIN A3 #define NOTE_POT_PIN A2 #define STEP_LENGTH_POT_PIN A1 #define TEMPO_POT_PIN A0 #define PREVIOUS_STEP_BUTTON_PIN 2 #define NEXT_STEP_BUTTON_PIN 3 #define REST_BUTTON_PIN 4 #define GLIDE_BUTTON_PIN 5 #define ACCENT_BUTTON_PIN 6 #define PLAY_STOP_BUTTON_PIN 7 #define PREVIOUS_STEP_LED_PIN 8 #define NEXT_STEP_LED_PIN 9 #define REST_LED_PIN 10 #define GLIDE_LED_PIN 11 #define ACCENT_LED_PIN 12 #define PLAY_STOP_LED_PIN 13 // User Interface data uint16_t _step_edit = 0; uint8_t _last_octave = 3; uint8_t _last_note = 0; uint8_t _bpm_blink_timer = 1; void configureInterface() { // Buttons config // use internal pullup for buttons pinMode(PREVIOUS_STEP_BUTTON_PIN, INPUT_PULLUP); pinMode(NEXT_STEP_BUTTON_PIN, INPUT_PULLUP); pinMode(REST_BUTTON_PIN, INPUT_PULLUP); pinMode(GLIDE_BUTTON_PIN, INPUT_PULLUP); pinMode(ACCENT_BUTTON_PIN, INPUT_PULLUP); pinMode(PLAY_STOP_BUTTON_PIN, INPUT_PULLUP); // Leds config pinMode(PREVIOUS_STEP_LED_PIN, OUTPUT); pinMode(NEXT_STEP_LED_PIN, OUTPUT); pinMode(REST_LED_PIN, OUTPUT); pinMode(GLIDE_LED_PIN, OUTPUT); pinMode(ACCENT_LED_PIN, OUTPUT); pinMode(PLAY_STOP_LED_PIN, OUTPUT); digitalWrite(PREVIOUS_STEP_LED_PIN, LOW); digitalWrite(NEXT_STEP_LED_PIN, LOW); digitalWrite(REST_LED_PIN, LOW); digitalWrite(GLIDE_LED_PIN, LOW); digitalWrite(ACCENT_LED_PIN, LOW); digitalWrite(PLAY_STOP_LED_PIN, LOW); // getting first value state pressed(PREVIOUS_STEP_BUTTON_PIN); pressed(NEXT_STEP_BUTTON_PIN); pressed(REST_BUTTON_PIN); pressed(GLIDE_BUTTON_PIN); pressed(ACCENT_BUTTON_PIN); pressed(PLAY_STOP_BUTTON_PIN); // getting first values getPotChanges(OCTAVE_POT_PIN, 0, 10); getPotChanges(NOTE_POT_PIN, 0, 11); getPotChanges(STEP_LENGTH_POT_PIN, 1, STEP_MAX_SIZE); getPotChanges(TEMPO_POT_PIN, SEQUENCER_MIN_BPM, SEQUENCER_MAX_BPM); lockPotsState(true); //acidRandomize(); } void processInterface() { processButtons(); processLeds(); processPots(); } void tempoInterface(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 0) ) { // first compass step will flash longer _bpm_blink_timer = 8; digitalWrite(PLAY_STOP_LED_PIN , HIGH); } else if ( !(tick % (24)) ) { // each quarter led on digitalWrite(PLAY_STOP_LED_PIN , HIGH); } else if ( !(tick % _bpm_blink_timer) ) { // get led off digitalWrite(PLAY_STOP_LED_PIN , LOW); _bpm_blink_timer = 1; } } void sendPreviewNote(uint16_t step) { unsigned long milliTime, preMilliTime; sendMidiMessage(NOTE_ON, _sequencer[step].note, _sequencer[step].accent ? ACCENT_VELOCITY : NOTE_VELOCITY); // avoid delay() call because of uClock timmer1 usage //delay(200); preMilliTime = millis(); while ( true ) { milliTime = millis(); if (abs(milliTime - preMilliTime) >= 200) { break; } } sendMidiMessage(NOTE_OFF, _sequencer[step].note, 0); } void processPots() { static int8_t octave, note, step_note; static int16_t tempo, step_length; octave = getPotChanges(OCTAVE_POT_PIN, 0, 10); if ( octave != -1 ) { _last_octave = octave; } note = getPotChanges(NOTE_POT_PIN, 0, 11); if ( note != -1 ) { _last_note = note; } // changes on octave or note pot? if ( octave != -1 || note != -1 ) { ATOMIC(_sequencer[_step_edit].note = (_last_octave * 8) + _last_note); if ( _playing == false && _sequencer[_step_edit].rest == false ) { sendPreviewNote(_step_edit); } } step_length = getPotChanges(STEP_LENGTH_POT_PIN, 1, STEP_MAX_SIZE); if ( step_length != -1 ) { ATOMIC(_step_length = step_length); if ( _step_edit >= _step_length ) { _step_edit = _step_length-1; } } tempo = getPotChanges(TEMPO_POT_PIN, SEQUENCER_MIN_BPM, SEQUENCER_MAX_BPM); if ( tempo != -1 ) { //uClock.setTempo(tempo); } } void processButtons() { // play/stop if ( pressed(PLAY_STOP_BUTTON_PIN) ) { if ( _playing == false ) { // Starts the clock, tick-tac-tick-tac... uClock.start(); } else { // stop the clock uClock.stop(); } } // ramdom test //if ( pressed(PREVIOUS_STEP_BUTTON_PIN) && pressed(NEXT_STEP_BUTTON_PIN) ) { //acidRandomize(); //return; //} // previous step edit if ( pressed(PREVIOUS_STEP_BUTTON_PIN) ) { if ( _step_edit != 0 ) { // add a lock here for octave and note to not mess with edit mode when moving steps around lockPotsState(true); --_step_edit; } else { // TODO: just for tests.. take this guy off here and put it on second page acidRandomize(); } if ( _playing == false && _sequencer[_step_edit].rest == false ) { sendPreviewNote(_step_edit); } } // next step edit if ( pressed(NEXT_STEP_BUTTON_PIN) ) { if ( _step_edit < _step_length-1 ) { // add a lock here for octave and note to not mess with edit mode when moving steps around lockPotsState(true); ++_step_edit; } if ( _playing == false && _sequencer[_step_edit].rest == false ) { sendPreviewNote(_step_edit); } } // step rest if ( pressed(REST_BUTTON_PIN) ) { ATOMIC(_sequencer[_step_edit].rest = !_sequencer[_step_edit].rest); if ( _playing == false && _sequencer[_step_edit].rest == false ) { sendPreviewNote(_step_edit); } } // step glide if ( pressed(GLIDE_BUTTON_PIN) ) { ATOMIC(_sequencer[_step_edit].glide = !_sequencer[_step_edit].glide); } // step accent if ( pressed(ACCENT_BUTTON_PIN) ) { ATOMIC(_sequencer[_step_edit].accent = !_sequencer[_step_edit].accent); if ( _playing == false && _sequencer[_step_edit].rest == false ) { sendPreviewNote(_step_edit); } } } void processLeds() { // Editing First Step? if ( _step_edit == 0 ) { digitalWrite(PREVIOUS_STEP_LED_PIN , HIGH); } else { digitalWrite(PREVIOUS_STEP_LED_PIN , LOW); } // Editing Last Step? if ( _step_edit == _step_length-1 ) { digitalWrite(NEXT_STEP_LED_PIN , HIGH); } else { digitalWrite(NEXT_STEP_LED_PIN , LOW); } // Rest if ( _sequencer[_step_edit].rest == true ) { digitalWrite(REST_LED_PIN , HIGH); } else { digitalWrite(REST_LED_PIN , LOW); } // Glide if ( _sequencer[_step_edit].glide == true ) { digitalWrite(GLIDE_LED_PIN , HIGH); } else { digitalWrite(GLIDE_LED_PIN , LOW); } // Accent if ( _sequencer[_step_edit].accent == true ) { digitalWrite(ACCENT_LED_PIN , HIGH); } else { digitalWrite(ACCENT_LED_PIN , LOW); } // shut down play led if we are stoped if ( _playing == false ) { digitalWrite(PLAY_STOP_LED_PIN , LOW); } } void acidRandomize() { // ramdom it all for ( uint16_t i = 0; i < STEP_MAX_SIZE; i++ ) { ATOMIC(_sequencer[i].note = random(36, 70)); // octave 2 to 4. octave 3 to 5 (40 - 83) ATOMIC(_sequencer[i].accent = random(0, 2)); ATOMIC(_sequencer[i].glide = random(0, 2)); ATOMIC(_sequencer[i].rest = random(0, 1)); } } ================================================ FILE: examples/AcidStepSequencer/HardwareInterface.ino ================================================ #define POT_NUMBER 4 #define BUTTON_NUMBER 6 // pot data typedef struct { uint8_t pin; uint16_t state; bool lock; } POT_DATA; // button data typedef struct { uint8_t pin; bool state; } BUTTON_DATA; POT_DATA _pot[POT_NUMBER]; BUTTON_DATA _button[BUTTON_NUMBER]; void lockPotsState(bool lock) { for ( uint8_t i = 0; i < POT_NUMBER; i++ ) { _pot[i].lock = lock; } } bool pressed(uint8_t button_pin) { bool value; bool * last_value; switch(button_pin) { case PREVIOUS_STEP_BUTTON_PIN: last_value = &_button[0].state; break; case NEXT_STEP_BUTTON_PIN: last_value = &_button[1].state; break; case REST_BUTTON_PIN: last_value = &_button[2].state; break; case GLIDE_BUTTON_PIN: last_value = &_button[3].state; break; case ACCENT_BUTTON_PIN: last_value = &_button[4].state; break; case PLAY_STOP_BUTTON_PIN: last_value = &_button[5].state; break; default: return false; } value = digitalRead(button_pin); // check, using pullup pressed button goes LOW if ( value != *last_value && value == LOW ) { *last_value = value; return true; } else { *last_value = value; return false; } } int16_t getPotChanges(uint8_t pot_pin, uint16_t min_value, uint16_t max_value) { uint16_t value, value_ranged, last_value_ranged; uint16_t * last_value; bool * lock_pot; uint8_t pot_sensitivity = 1; switch(pot_pin) { case OCTAVE_POT_PIN: last_value = &_pot[0].state; lock_pot = &_pot[0].lock; break; case NOTE_POT_PIN: last_value = &_pot[1].state; lock_pot = &_pot[1].lock; break; case STEP_LENGTH_POT_PIN: last_value = &_pot[2].state; lock_pot = &_pot[2].lock; break; case TEMPO_POT_PIN: last_value = &_pot[3].state; lock_pot = &_pot[3].lock; break; default: return -1; } // get absolute value value = analogRead(pot_pin); // range that value and our last_value value_ranged = (value / (1024 / ((max_value - min_value) + 1))) + min_value; last_value_ranged = (*last_value / (1024 / ((max_value - min_value) + 1))) + min_value; // a lock system to not mess with some data(pots are terrible for some kinda of user interface data controls, but lets keep it low cost!) if ( *lock_pot == true ) { pot_sensitivity = LOCK_POT_SENSTIVITY; } if ( abs(value_ranged - last_value_ranged) >= pot_sensitivity ) { *last_value = value; if ( *lock_pot == true ) { *lock_pot = false; } return value_ranged; } else { return -1; } } ================================================ FILE: examples/AcidStepSequencer/README.md ================================================ # TB303 Step Sequencer engine clone With some user interface changes for cheap construction of a functional TB303 engine we present you the interface: ![Image of uMODULAR ctrl16 pcb top view](https://raw.githubusercontent.com/midilab/uClock/development/examples/AcidStepSequencer/acid_step_sequencer-protoboard-v001.png) ## Interface from left to rigth POT1: Octave POT2: Note POT3: Sequence Length POT4: Sequencer BPM Tempo Button1: Prev Step Button2: Next Step Button3: Rest Button4: Glide Button5: Accent Buttons6: Play/Stop ================================================ FILE: examples/ESP32UartMasterMidiClock/ESP32UartMasterMidiClock.ino ================================================ /* Uart MIDI Sync Box * * This example demonstrates how to change the Uart MIDI * device name on ESP32 family. * * This example code is in the public domain. * * ... * */ #include // MIDI clock, start and stop byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC // the blue led #define LED_BUILTIN 2 uint8_t bpm_blink_timer = 1; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first compass step will flash longer bpm_blink_timer = 8; digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % (24)) ) { // each quarter led on bpm_blink_timer = 1; digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % bpm_blink_timer) ) { // get led off digitalWrite(LED_BUILTIN, LOW); } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears Serial.write(MIDI_CLOCK); handle_bpm_led(tick); } void onClockStart() { Serial.write(MIDI_START); } void onClockStop() { Serial.write(MIDI_STOP); } void setup() { // Initialize serial communication at 31250 bits per second, the default MIDI serial speed communication: Serial.begin(31250); // A led to count bpms pinMode(LED_BUILTIN, OUTPUT); // Setup our clock system // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); // Starts the clock, tick-tac-tick-tac... uClock.start(); } // Do it whatever to interface with Clock.stop(), Clock.start(), Clock.setTempo() and integrate your environment... void loop() { } ================================================ FILE: examples/GenericMasterOrExternalSync/GenericMasterOrExternalSync.ino ================================================ #include // external or internal sync? bool _external_sync_on = false; // the main uClock PPQN resolution ticking void onOutputPPQNCallback(uint32_t tick) { // tick your sequencers or tickable devices... } void onStepCallback(uint32_t step) { // triger step data for sequencer device... } // The callback function called by uClock each Pulse of 1PPQN clock resolution. void onSync1Callback(uint32_t tick) { // send sync signal to... } // The callback function called by uClock each Pulse of 2PPQN clock resolution. void onSync2Callback(uint32_t tick) { // send sync signal to... } // The callback function called by uClock each Pulse of 4PPQN clock resolution. void onSync4Callback(uint32_t tick) { // send sync signal to... } // The callback function called by uClock each Pulse of 24PPQN clock resolution. void onSync24Callback(uint32_t tick) { // send sync signal to... } // The callback function called by uClock each Pulse of 48PPQN clock resolution. void onSync48Callback(uint32_t tick) { // send sync signal to... } // The callback function called when clock starts by using uClock.start() method. void onClockStartCallback() { // send start signal to... } // The callback function called when clock stops by using uClock.stop() method. void onClockStopCallback() { // send stop signal to... } void setup() { // setup clock library // avaliable output resolutions // [ uClock.PPQN_4, uClock.PPQN_8, uClock.PPQN_12, uClock.PPQN_24, uClock.PPQN_48, uClock.PPQN_96, uClock.PPQN_384, uClock.PPQN_480, uClock.PPQN_960 ] // not mandatory to call, the default is 96PPQN if not set uClock.setOutputPPQN(uClock.PPQN_96); // you need to use at least one! uClock.setOnOutputPPQN(onOutputPPQNCallback); uClock.setOnStep(onStepCallback); // multi sync output signatures avaliable // normaly used by eurorack modular modules uClock.setOnSync(uClock.PPQN_1, onSync1Callback); uClock.setOnSync(uClock.PPQN_2, onSync2Callback); uClock.setOnSync(uClock.PPQN_4, onSync4Callback); // midi sync standard uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // some korg machines does 48ppqn uClock.setOnSync(uClock.PPQN_48, onSync48Callback); uClock.setOnClockStart(onClockStartCallback); uClock.setOnClockStop(onClockStopCallback); // set external sync mode? if (_external_sync_on) { uClock.setClockMode(uClock.EXTERNAL_CLOCK); // what is the clock of incomming signal to sync with? // not mandatory to call, the default is 24PPQN if not set // avaliable input resolutions - should be always InputPPQN <= OutputPPQN // [ uClock.PPQN_1, uClock.PPQN_2, uClock.PPQN_4, uClock.PPQN_8, uClock.PPQN_12, uClock.PPQN_24, uClock.PPQN_48, uClock.PPQN_96, uClock.PPQN_384, uClock.PPQN_480, uClock.PPQN_960 ] uClock.setInputPPQN(uClock.PPQN_24); } // inits the clock library uClock.init(); // starts clock uClock.start(); } void loop() { // do we need to external sync? if (_external_sync_on) { // watch for external sync signal income bool signal_income = true; // your external input signal check will be this condition result if (signal_income) { // at each clockMe call uClock will process and handle external/internal syncronization uClock.clockMe(); } } } ================================================ FILE: examples/LeonardoUsbSlaveMidiClockMonitor/LeonardoUsbSlaveMidiClockMonitor.ino ================================================ /* USB MIDI Sync Slave Box Monitor * * This example demonstrates how to create a * MIDI hid compilant slave clock box with * monitor support using oled displays * * You need the following libraries to make it work * - Midi Library * - USB-MIDI and MIDIUSB * - u8g2 * - uClock * This example code is in the public domain. */ #include #include // // BPM Clock support // #include USBMIDI_CREATE_DEFAULT_INSTANCE(); U8X8 * u8x8; // MIDI clock, start, stop, note on and note off byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC float bpm = 126.0; uint8_t bpm_blink_timer = 1; uint8_t clock_state = 1; uint8_t clock_mode = 0; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first compass step will flash longer bpm_blink_timer = 8; TXLED1; } else if ( !(tick % (24)) ) { // each quarter led on TXLED1; } else if ( !(tick % bpm_blink_timer) ) { // get led off TXLED0; bpm_blink_timer = 1; } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears MIDI.sendRealTime(MIDI_CLOCK); handle_bpm_led(tick); } void onClockStart() { MIDI.sendRealTime(MIDI_START); } void onClockStop() { MIDI.sendRealTime(MIDI_STOP); } // External clock handlers void onExternalClock() { uClock.clockMe(); } void onExternalStart() { uClock.start(); } void onExternalStop() { uClock.stop(); } void setup() { // // MIDI setup // MIDI.begin(); MIDI.setHandleClock(onExternalClock); MIDI.setHandleStart(onExternalStart); MIDI.setHandleStop(onExternalStop); // // OLED setup // Please check you oled model to correctly init him // //u8x8 = new U8X8_SH1106_128X64_NONAME_HW_I2C(U8X8_PIN_NONE); u8x8 = new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE); u8x8->begin(); u8x8->setFont(u8x8_font_pressstart2p_r); u8x8->clear(); u8x8->setFlipMode(true); u8x8->drawUTF8(0, 0, "uClock"); // // uClock Setup // // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // set main clock rate for input(expected sync signal rate) MIDI 24PPQN clock based uClock.setInputPPQN(uClock.PPQN_24); // For MIDI Sync Start and Stop uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); uClock.setClockMode(uClock.EXTERNAL_CLOCK); // for smooth slave tempo calculate display you should raise the // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. // note: this doesn't impact on sync time, only display time getTempo() // if you dont want to use it, it is default set it to 1 for memory save uClock.setExtIntervalBuffer(128); // init uClock uClock.init(); //uClock.setTempo(136.5); //uClock.start(); } void loop() { while(MIDI.read()) {} // DO NOT ADD MORE PROCESS HERE AT THE COST OF LOSING CLOCK SYNC // Since arduino make use of Serial RX interruption we need to // read Serial as fast as we can on the loop if (bpm != uClock.getTempo()) { bpm = uClock.getTempo(); u8x8->drawUTF8(8, 7, String(bpm, 1).c_str()); u8x8->drawUTF8(8+5, 7, "BPM"); // clear display ghost number for 2 digit // coming from 3 digit bpm changes if (bpm < 100) { u8x8->drawUTF8(8+4, 7, " "); } } if (clock_state != uClock.clock_state) { clock_state = uClock.clock_state; if (clock_state >= 1) { u8x8->drawUTF8(0, 7, "Playing"); } else { u8x8->drawUTF8(0, 7, "Stopped"); } } if (clock_mode != uClock.getClockMode()) { clock_mode = uClock.getClockMode(); if (clock_mode == uClock.EXTERNAL_CLOCK) { u8x8->drawUTF8(10, 0, "Slave "); } else { u8x8->drawUTF8(10, 0, "Master"); } } } ================================================ FILE: examples/MidiClock/MidiClock.ino ================================================ #include "Arduino.h" #include // MIDI clock, start and stop byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC // The callback function wich will be called by Clock each Pulse of 24PPQN clock resolution. void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears Serial.write(MIDI_CLOCK); } // The callback function wich will be called when clock starts by using Clock.start() method. void onClockStart() { Serial.write(MIDI_START); } // The callback function wich will be called when clock stops by using Clock.stop() method. void onClockStop() { Serial.write(MIDI_STOP); } void setup() { // Initialize serial communication at 31250 bits per second, the default MIDI serial speed communication: Serial.begin(31250); // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); // Starts the clock, tick-tac-tick-tac... uClock.start(); } // Do it whatever to interface with Clock.stop(), Clock.start(), Clock.setTempo() and integrate your environment... void loop() { } ================================================ FILE: examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino ================================================ /* * USB/Uart MIDI Sync Box * * This example code is in the public domain. * */ #include #include #include // Instantiate the MIDI interfaces Adafruit_USBD_MIDI usb_midi; MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI_USB); MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); // Do your rpi 2040 has a ws2812 RGB LED? set the pin! // otherwise keep it commented for normal LED_BUILTIN led blinking #define WS2812_BUILTIN_LED 16 uint8_t bpm_blink_timer = 1; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first of 4 quarter pulse will flash longer bpm_blink_timer = 8; ledOn(); } else if ( !(tick % (24)) ) { // each quarter led on bpm_blink_timer = 1; ledOn(); } else if ( !(tick % bpm_blink_timer) ) { // get led off ledOff(); } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears MIDI.sendRealTime(midi::Clock); MIDI_USB.sendRealTime(midi::Clock); // blink tempo handle_bpm_led(tick); } void onClockStart() { MIDI.sendRealTime(midi::Start); MIDI_USB.sendRealTime(midi::Start); } void onClockStop() { MIDI.sendRealTime(midi::Stop); MIDI_USB.sendRealTime(midi::Stop); } void setup() { #if defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040) // Manual begin() is required on core without built-in support for TinyUSB such as mbed rp2040 TinyUSB_Device_Init(0); #endif // Initialize USB midi stack MIDI_USB.begin(MIDI_CHANNEL_OMNI); // Initialize UART midi stack MIDI.begin(MIDI_CHANNEL_OMNI); // Initialize builtin led for clock timer blinking initBlinkLed(); // Setup our clock system // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); // Starts the clock, tick-tac-tick-tac.. uClock.start(); } // Do it whatever to interface with Clock.stop(), Clock.start(), Clock.setTempo() and integrate your environment... void loop() { // handle midi input? MIDI.read(); MIDI_USB.read(); } ================================================ FILE: examples/RP2040UsbUartMasterClock/builtin_led.ino ================================================ #if defined(WS2812_BUILTIN_LED) #include #define NUMPIXELS 1 Adafruit_NeoPixel pixels(NUMPIXELS, WS2812_BUILTIN_LED, NEO_GRB + NEO_KHZ800); #endif // check the pinage for BUILTIN LED of your model in case LED_BUILTIN wont ligth up // this is valid only if you're not using rgb version ws2812 (WS2812_BUILTIN_LED) //#define LED_BUILTIN PIN_LED_B void initBlinkLed() { #if defined(WS2812_BUILTIN_LED) // use adafruit neo pixel pixels.begin(); #else // normal led pin pinMode(LED_BUILTIN, OUTPUT); #endif } void ledOn() { #if defined(WS2812_BUILTIN_LED) pixels.setPixelColor(0, pixels.Color(0, 0, 20)); pixels.show(); // turn the LED on (HIGH is the voltage level) #else digitalWrite(LED_BUILTIN, LOW); #endif } void ledOff() { #if defined(WS2812_BUILTIN_LED) pixels.setPixelColor(0, pixels.Color(0, 0, 0)); pixels.show(); #else digitalWrite(LED_BUILTIN, HIGH); #endif } ================================================ FILE: examples/STM32UartMasterMidiClock/STM32UartMasterMidiClock.ino ================================================ /* Uart MIDI out * * This example demonstrates how to send MIDI data via Uart * interface on STM32 family. * * This example code is in the public domain. * * Requires STM32Duino board manager to be installed. * * Define HardwareSerial using any available UART/USART. * Nucleo boards have UART/USART pins that are used by the ST-LINK interface (unless using solder bridging). * * Tested on Nucleo-F401RE and Nucleo-F072RB (PA9=D8 PA10=D2 on the Arduino pins) * * Code by midilab contact@midilab.co * Example modified by Jackson Devices contact@jacksondevices.com */ #include // MIDI clock, start and stop byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC HardwareSerial Serial1(PA10, PA9); uint8_t bpm_blink_timer = 1; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first compass step will flash longer bpm_blink_timer = 8; digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % (24)) ) { // each quarter led on bpm_blink_timer = 1; digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % bpm_blink_timer) ) { // get led off digitalWrite(LED_BUILTIN, LOW); } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gear Serial1.write(MIDI_CLOCK); handle_bpm_led(tick); } void onClockStart() { // Send MIDI_START to external gear Serial1.write(MIDI_START); } void onClockStop() { // Send MIDI_STOP to external gear Serial1.write(MIDI_STOP); } void setup() { // Initialize Serial1 communication at 31250 bits per second, the default MIDI Serial1 speed communication: Serial1.begin(31250); // An led to display BPM pinMode(LED_BUILTIN, OUTPUT); // Setup our clock system // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(120); // Starts the clock, tick-tac-tick-tac... uClock.start(); } // Do it whatever to interface with Clock.stop(), Clock.start(), Clock.setTempo() and integrate your environment... void loop() { } ================================================ FILE: examples/TeensyUsbMasterMidiClock/TeensyUsbMasterMidiClock.ino ================================================ /* USB MIDI Sync Box * * This example demonstrates how to change the USB MIDI * device name on Teensy LC, 3.x and 4.x. When creating more * that one MIDI device, custom names are much easier to * use when selecting each device in MIDI software on * your PC or Mac. The custom name is in the "name.c" tab. * * Windows and Macintosh systems often cache USB info. * After changing the name, you may need to test on a * different computer to observe the new name, or take * steps to get your operating system to "forget" the * cached info. (TODO: wanted... can anyone contribute * instructions for these systems) * * You must select MIDI from the "Tools > USB Type" menu * * This example code is in the public domain. */ #include uint8_t bpm_blink_timer = 1; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first compass step will flash longer bpm_blink_timer = 8; digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % (24)) ) { // each quarter led on digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % bpm_blink_timer) ) { // get led off digitalWrite(LED_BUILTIN, LOW); bpm_blink_timer = 1; } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears usbMIDI.sendRealTime(usbMIDI.Clock); handle_bpm_led(tick); } void onClockStart() { usbMIDI.sendRealTime(usbMIDI.Start); } void onClockStop() { usbMIDI.sendRealTime(usbMIDI.Stop); } void setup() { // A led to count bpms pinMode(LED_BUILTIN, OUTPUT); // Setup our clock system // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); // Starts the clock, tick-tac-tick-tac... uClock.start(); } // Do it whatever to interface with Clock.stop(), Clock.start(), Clock.setTempo() and integrate your environment... void loop() { } ================================================ FILE: examples/TeensyUsbMasterMidiClock/name.c ================================================ // To give your project a unique name, this code must be // placed into a .c file (its own tab). It can not be in // a .cpp file or your main sketch (the .ino file). #include "usb_names.h" // Edit these lines to create your own name. The length must // match the number of characters in your custom name. #define MIDI_NAME {'u','c','l','o','c','k','_','1'} #define MIDI_NAME_LEN 8 // Do not change this part. This exact format is required by USB. struct usb_string_descriptor_struct usb_string_product_name = { 2 + MIDI_NAME_LEN * 2, 3, MIDI_NAME }; ================================================ FILE: examples/TeensyUsbSlaveMidiClock/TeensyUsbSlaveMidiClock.ino ================================================ /* USB MIDI Sync Slave Box * * This example demonstrates how to change the USB MIDI * device name on Teensy LC, 3.x and 4.x. When creating more * that one MIDI device, custom names are much easier to * use when selecting each device in MIDI software on * your PC or Mac. The custom name is in the "name.c" tab. * * Windows and Macintosh systems often cache USB info. * After changing the name, you may need to test on a * different computer to observe the new name, or take * steps to get your operating system to "forget" the * cached info. (TODO: wanted... can anyone contribute * instructions for these systems) * * You must select MIDI from the "Tools > USB Type" menu * * This example code is in the public domain. */ #include uint8_t bpm_blink_timer = 1; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first compass step will flash longer bpm_blink_timer = 8; digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % (24)) ) { // each quarter led on digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % bpm_blink_timer) ) { // get led off digitalWrite(LED_BUILTIN, LOW); bpm_blink_timer = 1; } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears on other port? //usbMIDI.sendRealTime(usbMIDI.Clock); handle_bpm_led(tick); } void onClockStart() { //usbMIDI.sendRealTime(usbMIDI.Start); } void onClockStop() { //usbMIDI.sendRealTime(usbMIDI.Stop); } // External clock handlers void onExternalClock() { uClock.clockMe(); } void onExternalStart() { uClock.start(); } void onExternalStop() { uClock.stop(); } void setup() { // A led to count bpms pinMode(LED_BUILTIN, OUTPUT); // Setup realtime midi event handlers usbMIDI.setHandleClock(onExternalClock); usbMIDI.setHandleStart(onExternalStart); usbMIDI.setHandleStop(onExternalStop); // Setup our clock system // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // set main clock rate for input(expected sync signal rate) MIDI 24PPQN clock based uClock.setInputPPQN(uClock.PPQN_24); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // set to external sync mode uClock.setClockMode(uClock.EXTERNAL_CLOCK); // Inits the clock uClock.init(); } void loop() { // Grab all midi data as fast as we can! while (usbMIDI.read()) {} } ================================================ FILE: examples/TeensyUsbSlaveMidiClock/name.c ================================================ // To give your project a unique name, this code must be // placed into a .c file (its own tab). It can not be in // a .cpp file or your main sketch (the .ino file). #include "usb_names.h" // Edit these lines to create your own name. The length must // match the number of characters in your custom name. #define MIDI_NAME {'u','c','l','o','c','k','_','1'} #define MIDI_NAME_LEN 8 // Do not change this part. This exact format is required by USB. struct usb_string_descriptor_struct usb_string_product_name = { 2 + MIDI_NAME_LEN * 2, 3, MIDI_NAME }; ================================================ FILE: examples/TeensyUsbSlaveMidiClockMonitor/TeensyUsbSlaveMidiClockMonitor.ino ================================================ /* USB MIDI Sync Slave Box Monitor * * This example demonstrates how to create a * MIDI hid compilant slave clock box using * Teensy LC, 3.x and 4.x with * monitor support using oled displays * * Making use of a 250 usceconds timer to * handle MIDI input to avoid jitter on clock * * You need the following libraries to make it work * - u8g2 * - uClock * * This example code is in the public domain. */ #include // // BPM Clock support // #include U8X8 * u8x8; IntervalTimer teensyTimer; // MIDI clock, start, stop, note on and note off byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 #define MIDI_START 0xFA #define MIDI_STOP 0xFC float bpm = 126; uint8_t bpm_blink_timer = 1; uint8_t clock_state = 1; uint8_t clock_mode = 0; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first compass step will flash longer bpm_blink_timer = 8; digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % (24)) ) { // each quarter led on digitalWrite(LED_BUILTIN, HIGH); } else if ( !(tick % bpm_blink_timer) ) { // get led off digitalWrite(LED_BUILTIN, LOW); bpm_blink_timer = 1; } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears usbMIDI.sendRealTime(MIDI_CLOCK); handle_bpm_led(tick); } void onClockStart() { usbMIDI.sendRealTime(MIDI_START); } void onClockStop() { usbMIDI.sendRealTime(MIDI_STOP); digitalWrite(LED_BUILTIN, LOW); } // External clock handlers void onExternalClock() { uClock.clockMe(); } void onExternalStart() { uClock.start(); } void onExternalStop() { uClock.stop(); } void setup() { // A led to count bpms pinMode(LED_BUILTIN, OUTPUT); // // MIDI setup // usbMIDI.begin(); usbMIDI.setHandleClock(onExternalClock); usbMIDI.setHandleStart(onExternalStart); usbMIDI.setHandleStop(onExternalStop); // // OLED setup // Please check you oled model to correctly init him // //u8x8 = new U8X8_SH1106_128X64_NONAME_HW_I2C(U8X8_PIN_NONE); u8x8 = new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE); u8x8->begin(); u8x8->setFont(u8x8_font_pressstart2p_r); u8x8->clear(); u8x8->setFlipMode(true); u8x8->drawUTF8(0, 0, "uClock"); // // uClock Setup // // Setup our clock system // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // set main clock rate for input(expected sync signal rate) MIDI 24PPQN clock based uClock.setInputPPQN(uClock.PPQN_24); // For MIDI Sync Start and Stop uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); uClock.setClockMode(uClock.EXTERNAL_CLOCK); // for smooth slave tempo calculate display you should raise the // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. // note: this doesn't impact on sync time, only display time getTempo() // if you dont want to use it, it is default set it to 1 for memory save uClock.setExtIntervalBuffer(128); // inits uClock uClock.init(); // make use of 250us timer to handle midi input sync teensyTimer.begin(handleMidiInput, 250); teensyTimer.priority(80); } void handleMidiInput() { while (usbMIDI.read()) { } } void loop() { if (bpm != uClock.getTempo()) { bpm = uClock.getTempo(); u8x8->drawUTF8(8, 7, String(bpm, 1).c_str()); u8x8->drawUTF8(8+5, 7, "BPM"); // clear display ghost number for 2 digit // coming from 3 digit bpm changes if (bpm < 100) { u8x8->drawUTF8(8+4, 7, " "); } } if (clock_state != uClock.clock_state) { clock_state = uClock.clock_state; if (clock_state >= 1) { u8x8->drawUTF8(0, 7, "Playing"); } else { u8x8->drawUTF8(0, 7, "Stopped"); } } if (clock_mode != uClock.getClockMode()) { clock_mode = uClock.getClockMode(); if (clock_mode == uClock.EXTERNAL_CLOCK) { u8x8->drawUTF8(10, 0, "Slave "); } else { u8x8->drawUTF8(10, 0, "Master"); } } } ================================================ FILE: examples/TeensyUsbSlaveMidiClockMonitor/name.c ================================================ // To give your project a unique name, this code must be // placed into a .c file (its own tab). It can not be in // a .cpp file or your main sketch (the .ino file). #include "usb_names.h" // Edit these lines to create your own name. The length must // match the number of characters in your custom name. #define MIDI_NAME {'u','c','l','o','c','k','_','1'} #define MIDI_NAME_LEN 8 // Do not change this part. This exact format is required by USB. struct usb_string_descriptor_struct usb_string_product_name = { 2 + MIDI_NAME_LEN * 2, 3, MIDI_NAME }; ================================================ FILE: examples/XiaoUsbMasterMidiClock/XiaoUsbMasterMidiClock.ino ================================================ /* USB MIDI Sync Box * * This example demonstrates how to change the USB MIDI * device name on Seeedstudio XIAO M0. * * This example code is in the public domain. * * Tested with Adafruit TinyUSB version 0.10.5 * */ #include #include Adafruit_USBD_MIDI usb_midi; MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI_USB); //MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); #include uint8_t bpm_blink_timer = 1; void handle_bpm_led(uint32_t tick) { // BPM led indicator if ( !(tick % (96)) || (tick == 1) ) { // first compass step will flash longer bpm_blink_timer = 8; digitalWrite(LED_BUILTIN, LOW); } else if ( !(tick % (24)) ) { // each quarter led on bpm_blink_timer = 1; digitalWrite(LED_BUILTIN, LOW); } else if ( !(tick % bpm_blink_timer) ) { // get led off digitalWrite(LED_BUILTIN, HIGH); } } // Internal clock handlers void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears MIDI_USB.sendRealTime(midi::Clock); handle_bpm_led(tick); } void onClockStart() { MIDI_USB.sendRealTime(midi::Start); } void onClockStop() { MIDI_USB.sendRealTime(midi::Stop); } void setup() { MIDI_USB.begin(MIDI_CHANNEL_OMNI); // A led to count bpms pinMode(LED_BUILTIN, OUTPUT); // Setup our clock system // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN uClock.setOnSync(uClock.PPQN_24, onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStart(onClockStart); uClock.setOnClockStop(onClockStop); // Inits the clock uClock.init(); // Set the clock BPM to 126 BPM uClock.setTempo(126); // Starts the clock, tick-tac-tick-tac... uClock.start(); } // Do it whatever to interface with Clock.stop(), Clock.start(), Clock.setTempo() and integrate your environment... void loop() { } ================================================ FILE: library.json ================================================ { "name": "uClock", "version": "2.3.0", "description": "A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040)", "keywords": "bpm, clock, timing, tick, music, generator", "repository": { "type": "git", "url": "https://github.com/midilab/uClock.git" }, "authors": [ { "name": "Romulo Silva", "email": "contact@midilab.co", "url": "https://midilab.co", "maintainer": true } ], "license": "MIT", "homepage": "https://midilab.co/umodular/", "headers": "uClock.h", "dependencies": {}, "frameworks": "Arduino", "platforms": "atmelavr,atmelmegaavr,espressif32,ststm32,teensy,atmelsam,raspberrypi" } ================================================ FILE: library.properties ================================================ name=uClock version=2.3.0 author=Romulo Silva maintainer=Romulo Silva sentence=BPM clock generator for Arduino platform. paragraph=A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040) category=Timing url=https://github.com/midilab/uClock architectures=avr,arm,samd,stm32,esp32,rp2040 includes=uClock.h ================================================ FILE: src/platforms/avr.h ================================================ #include #define ATOMIC(X) noInterrupts(); X; interrupts(); // want a different avr clock support? // TODO: we should do this using macro guards for avrs different clocks freqeuncy setup at compile time #define AVR_CLOCK_FREQ 16000000 // forward declaration of uClockHandler void uClockHandler(); // AVR ISR Entrypoint ISR(TIMER1_COMPA_vect) { uClockHandler(); } void initTimer(uint32_t init_clock) { ATOMIC( // 16bits Timer1 init // begin at 120bpm (48.0007680122882 Hz) TCCR1A = 0; // set entire TCCR1A register to 0 TCCR1B = 0; // same for TCCR1B TCNT1 = 0; // initialize counter value to 0 // set compare match register for 48.0007680122882 Hz increments OCR1A = 41665; // = 16000000 / (8 * 48.0007680122882) - 1 (must be <65536) // turn on CTC mode TCCR1B |= (1 << WGM12); // Set CS12, CS11 and CS10 bits for 8 prescaler TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10); // enable timer compare interrupt TIMSK1 |= (1 << OCIE1A); ) } void setTimer(uint32_t us_interval) { float tick_hertz_interval = 1/((float)us_interval/1000000); uint32_t ocr; uint8_t tccr = 0; // 16bits avr timer setup if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1 )) < 65535) { // Set CS12, CS11 and CS10 bits for 1 prescaler tccr |= (0 << CS12) | (0 << CS11) | (1 << CS10); } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 8 )) < 65535) { // Set CS12, CS11 and CS10 bits for 8 prescaler tccr |= (0 << CS12) | (1 << CS11) | (0 << CS10); } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 64 )) < 65535) { // Set CS12, CS11 and CS10 bits for 64 prescaler tccr |= (0 << CS12) | (1 << CS11) | (1 << CS10); } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 256 )) < 65535) { // Set CS12, CS11 and CS10 bits for 256 prescaler tccr |= (1 << CS12) | (0 << CS11) | (0 << CS10); } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1024 )) < 65535) { // Set CS12, CS11 and CS10 bits for 1024 prescaler tccr |= (1 << CS12) | (0 << CS11) | (1 << CS10); } else { // tempo not achiavable return; } ATOMIC( TCCR1B = 0; OCR1A = ocr-1; TCCR1B |= (1 << WGM12); TCCR1B |= tccr; ) } ================================================ FILE: src/platforms/esp32-nofrertos.h ================================================ #include #define TIMER_ID 0 hw_timer_t * _uclockTimer = NULL; portMUX_TYPE _uclockTimerMux = portMUX_INITIALIZER_UNLOCKED; #define ATOMIC(X) portENTER_CRITICAL_ISR(&_uclockTimerMux); X; portEXIT_CRITICAL_ISR(&_uclockTimerMux); // forward declaration of uClockHandler void uClockHandler(); // ISR handler void ARDUINO_ISR_ATTR handlerISR(void) { uClockHandler(); } void initTimer(uint32_t init_clock) { _uclockTimer = timerBegin(init_clock); // attach to generic uclock ISR timerAttachInterrupt(_uclockTimer, &handlerISR); // init clock tick time timerAlarm(_uclockTimer, init_clock, true, 0); } void setTimer(uint32_t us_interval) { timerAlarmWrite(_uclockTimer, us_interval, true); } ================================================ FILE: src/platforms/esp32.h ================================================ #include #include #include // esp32-specific timer hw_timer_t * _uclockTimer = NULL; // FreeRTOS main clock task size in bytes #define CLOCK_STACK_SIZE 5*1024 // adjust for your needs, a sequencer with heavy serial handling should be large in size TaskHandle_t taskHandle; // mutex to protect the shared resource SemaphoreHandle_t _mutex; // mutex control for task #define ATOMIC(X) xSemaphoreTake(_mutex, portMAX_DELAY); X; xSemaphoreGive(_mutex); // forward declaration of uClockHandler void uClockHandler(); // ISR handler void ARDUINO_ISR_ATTR handlerISR(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // Send a notification to task1 vTaskNotifyGiveFromISR(taskHandle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } // task for user clock process void clockTask(void *pvParameters) { while (1) { // wait for a notification from ISR ulTaskNotifyTake(pdTRUE, portMAX_DELAY); uClockHandler(); } } void initTimer(uint32_t init_clock) { // initialize the mutex for shared resource access _mutex = xSemaphoreCreateMutex(); // create the clockTask xTaskCreate(clockTask, "clockTask", CLOCK_STACK_SIZE, NULL, 1, &taskHandle); _uclockTimer = timerBegin(init_clock); // attach to generic uclock ISR timerAttachInterrupt(_uclockTimer, &handlerISR); // init clock tick time timerAlarm(_uclockTimer, init_clock, true, 0); } void setTimer(uint32_t us_interval) { timerAlarm(_uclockTimer, us_interval, true, 0); } ================================================ FILE: src/platforms/rp2040.h ================================================ #include #include "pico/sync.h" // RPi-specific timer struct repeating_timer timer; #define ATOMIC(X) { uint32_t __interrupt_mask = save_and_disable_interrupts(); X; restore_interrupts(__interrupt_mask); } // forward declaration of uClockHandler void uClockHandler(); // ISR handler -- called when tick happens bool handlerISR(repeating_timer *timer) { uClockHandler(); return true; } void initTimer(uint32_t init_clock) { // set up RPi interrupt timer // todo: actually should be -init_clock so that timer is set to start init_clock us after last tick, instead of init_clock us after finished processing last tick! add_repeating_timer_us(init_clock, &handlerISR, NULL, &timer); } void setTimer(uint32_t us_interval) { cancel_repeating_timer(&timer); // todo: actually should be -us_interval so that timer is set to start init_clock us after last tick, instead of init_clock us after finished processing last tick! add_repeating_timer_us(us_interval, &handlerISR, NULL, &timer); } ================================================ FILE: src/platforms/samd.h ================================================ #include // 24 bits timer #include // uses TimerTcc0 // 16 bits timer //#include // uses TimerTc3 #define ATOMIC(X) noInterrupts(); X; interrupts(); // forward declaration of ISR void uClockHandler(); void initTimer(uint32_t init_clock) { TimerTcc0.initialize(init_clock); // attach to generic uclock ISR TimerTcc0.attachInterrupt(uClockHandler); } void setTimer(uint32_t us_interval) { TimerTcc0.setPeriod(us_interval); } ================================================ FILE: src/platforms/software.h ================================================ #include /* Generic fallback approach that doesn't rely on any particular MCU's interrupts or RTOS threads etc. Simply checks micros() and compares last time tick happened and interval size to determine when a tick is due. requires calling softwareTimerHandler(micros()); inside loop() in order to trigger tick processing. function signature: void softwareTimerHandler(uint32_t micros_time); @author Doctea */ #define ATOMIC(X) X; // forward declaration of ISR void uClockHandler(); uint32_t uclock_last_time_ticked; uint32_t uclock_us_interval; // call this as often as possible to tick the uClock void softwareTimerHandler(uint32_t micros_time) { if (micros_time - uclock_last_time_ticked >= uclock_us_interval) { uclock_last_time_ticked = micros_time; uClockHandler(); } } void initTimer(uint32_t init_clock) { // basically nothing to do for software-implemented version..? uclock_last_time_ticked = micros(); } void setTimer(uint32_t us_interval) { uclock_us_interval = us_interval; } ================================================ FILE: src/platforms/stm32.h ================================================ #include #if !defined(STM32_CORE_VERSION) || (STM32_CORE_VERSION < 0x01090000) #error "Due to API change, this library is compatible with STM32_CORE_VERSION >= 0x01090000. Please update/install stm32duino core." #endif #if defined(TIM1) TIM_TypeDef * TimerInstance = TIM1; #else TIM_TypeDef * TimerInstance = TIM2; #endif // instantiate HardwareTimer object HardwareTimer * _uclockTimer = new HardwareTimer(TimerInstance); #define ATOMIC(X) noInterrupts(); X; interrupts(); // forward declaration of ISR void uClockHandler(); void initTimer(uint32_t us_interval) { _uclockTimer->setOverflow(us_interval, MICROSEC_FORMAT); _uclockTimer->attachInterrupt(uClockHandler); _uclockTimer->resume(); } void setTimer(uint32_t us_interval) { _uclockTimer->setOverflow(us_interval, MICROSEC_FORMAT); _uclockTimer->refresh(); } ================================================ FILE: src/platforms/teensy.h ================================================ #include #define ATOMIC(X) noInterrupts(); X; interrupts(); IntervalTimer _uclockTimer; // forward declaration of ISR void uClockHandler(); void initTimer(uint32_t init_clock) { _uclockTimer.begin(uClockHandler, init_clock); // Set the interrupt priority level, controlling which other interrupts // this timer is allowed to interrupt. Lower numbers are higher priority, // with 0 the highest and 255 the lowest. Most other interrupts default to 128. // As a general guideline, interrupt routines that run longer should be given // lower priority (higher numerical values). _uclockTimer.priority(80); } void setTimer(uint32_t us_interval) { _uclockTimer.update(us_interval); } ================================================ FILE: src/uClock.cpp ================================================ /*! * @file uClock.cpp * Project BPM clock generator for Arduino * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) * @version 2.3.0 * @author Romulo Silva * @date 10/06/2017 * @license MIT - (c) 2025 - Romulo Silva - contact@midilab.co * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "uClock.h" // // Compile time selection of Platform implementation of timer setup/control/handler // #if !defined(USE_UCLOCK_SOFTWARE_TIMER) // // General Arduino AVRs port // #if defined(ARDUINO_ARCH_AVR) #include "platforms/avr.h" #define UCLOCK_PLATFORM_FOUND #endif // // Teensyduino ARMs port // #if defined(TEENSYDUINO) #include "platforms/teensy.h" #define UCLOCK_PLATFORM_FOUND #endif // // Seedstudio XIAO M0 port // #if defined(SEEED_XIAO_M0) #include "platforms/samd.h" #define UCLOCK_PLATFORM_FOUND #endif // // ESP32 family // #if defined(ARDUINO_ARCH_ESP32) || defined(ESP32) #include "platforms/esp32.h" #define UCLOCK_PLATFORM_FOUND #endif // // STM32XX family // #if defined(ARDUINO_ARCH_STM32) #include "platforms/stm32.h" #define UCLOCK_PLATFORM_FOUND #endif // // RP2040 (Raspberry Pico) family // #if defined(ARDUINO_ARCH_RP2040) #include "platforms/rp2040.h" #define UCLOCK_PLATFORM_FOUND #endif #endif // // Software Timer for generic, board-agnostic, not-accurate, no-interrupt, software-only port // No hardware timer support? fallback to USE_UCLOCK_SOFTWARE_TIMER // #if !defined(UCLOCK_PLATFORM_FOUND) #pragma message ("NOTE: uClock is using the 'software timer' approach instead of specific board interrupted support, because board is not supported or because of USE_UCLOCK_SOFTWARE_TIMER build flag. Remember to call uClock.run() inside your loop().") #include "platforms/software.h" #if !defined(USE_UCLOCK_SOFTWARE_TIMER) #define USE_UCLOCK_SOFTWARE_TIMER #endif #endif // // Platform specific timer handler/setup/control wrappers // // global timer counter volatile uint32_t _millis = 0; // called each tick genarated from platform specific timer void uClockHandler() { _millis = millis(); uClock.handleInternalClock(); } // initTimer(uint32_t us_interval) and setTimer(uint32_t us_interval) // are defined at platform specific code and are platform dependent void uClockInitTimer() { // initialize at 120bpm as default initTimer(uClock.bpmToMicroSeconds(120.00)); } void uClockSetTimerTempo(float bpm) { setTimer(uClock.bpmToMicroSeconds(bpm)); } namespace umodular { namespace clock { static inline uint32_t clock_diff(uint32_t old_clock, uint32_t new_clock) { if (new_clock >= old_clock) { return new_clock - old_clock; } else { return new_clock + (4294967295UL - old_clock); } } uClockClass::uClockClass() { resetCounters(); } uClockClass::~uClockClass() { if (sync_callbacks) delete[] sync_callbacks; if (ext_interval_buffer) delete[] ext_interval_buffer; if (tracks) delete[] tracks; } void uClockClass::init() { if (ext_interval_buffer == nullptr) setExtIntervalBuffer(1); // initialize reference data calculateReferencedata(); // initialize hardware timer uClockInitTimer(); // first interval calculus setTempo(tempo); } void uClockClass::handleInternalClock() { static uint32_t counter = 0; static uint32_t sync_interval = 0; // for debug usage while developing any application under uClock ++int_overflow_counter; if (clock_state <= STARTING) // STOPED=0, PAUSED=1, STARTING=2, SYNCING=3, STARTED=4 return; // tick phase lock and external tempo match for EXTERNAL_CLOCK mode if (clock_mode == EXTERNAL_CLOCK) { // Tick Phase-lock if (labs(int_clock_tick - ext_clock_tick) > 1) { // only update tick at a full quarter or phase_lock_quarters * a quarter // how many quarters to count until we phase-lock? if ((ext_clock_tick * mod_clock_ref) % (output_ppqn*phase_lock_quarters) == 0) { tick = ext_clock_tick * mod_clock_ref; int_clock_tick = ext_clock_tick; // update any counter reference to lock with int_clock_tick for (uint8_t track=0; track < track_slots_size; track++) { tracks[track].step_counter = tick/mod_step_ref; tracks[track].mod_step_counter = 0; } // update counter reference for sync callbacks for (uint8_t i = 0; i < sync_callback_size; i++) { if (sync_callbacks[i].callback) { sync_callbacks[i].tick = tick/sync_callbacks[i].sync_ref; sync_callbacks[i].mod_counter = 0; } } } } // any external interval avaliable to start sync timer? if (ext_interval > 0) { counter = ext_interval; sync_interval = clock_diff(ext_clock_us, micros()); // phase-multiplier interval if (int_clock_tick <= ext_clock_tick) { counter -= (sync_interval * PHASE_FACTOR) >> 8; } else { if (counter > sync_interval) { counter += ((counter - sync_interval) * PHASE_FACTOR) >> 8; } } external_tempo = constrainBpm(freqToBpm(counter)); if (external_tempo != tempo) { tempo = external_tempo; uClockSetTimerTempo(tempo); } } } // main input clock counter control if (mod_clock_counter == mod_clock_ref) mod_clock_counter = 0; // process internal clock signal // int_clock_tick is the internal clock reference. mainly used for external clock phase lock if (mod_clock_counter == 0) ++int_clock_tick; ++mod_clock_counter; // sync callbacks for (uint8_t i = 0; i < sync_callback_size; i++) { if (sync_callbacks[i].mod_counter == sync_callbacks[i].sync_ref) sync_callbacks[i].mod_counter = 0; if (sync_callbacks[i].mod_counter == 0) { sync_callbacks[i].callback(sync_callbacks[i].tick); // tick sync callback ++sync_callbacks[i].tick; } ++sync_callbacks[i].mod_counter; } // StepSeq extension: step callback to support 16th old school style sequencers // with builtin shuffle - process onStepCallback() if (tracks) stepSeqTick(); // main PPQNCallback if (onOutputPPQNCallback) onOutputPPQNCallback(tick); // internal ticking ++tick; // for debug usage while developing any application under uClock --int_overflow_counter; } void uClockClass::handleExternalClock() { static uint32_t now_clock_us = 0; static uint8_t start_sync_counter = 0; // for debug usage while developing any application under uClock ++ext_overflow_counter; // calculate and store ext_interval now_clock_us = micros(); if (ext_clock_us > 0) ext_interval = clock_diff(ext_clock_us, now_clock_us); ext_clock_us = now_clock_us; // external clock tick me! ext_clock_tick++; switch (clock_state) { case STARTING: clock_state = SYNCING; start_sync_counter = 4; break; case SYNCING: if (--start_sync_counter == 0) clock_state = STARTED; break; default: // accumulate interval incomming ticks data for getTempo() smooth reads on slave clock_mode if (ext_interval > 0) { ext_interval_buffer[ext_interval_idx] = ext_interval; if(++ext_interval_idx >= ext_interval_buffer_size) ext_interval_idx = 0; } break; } // for debug usage while developing any application under uClock --ext_overflow_counter; } void uClockClass::clockMe() { ATOMIC(handleExternalClock()) } void uClockClass::start() { ATOMIC(resetCounters()) start_timer = millis(); if (clock_mode == INTERNAL_CLOCK) { ATOMIC(clock_state = STARTED) } else { ATOMIC(clock_state = STARTING) } if (onClockStartCallback) onClockStartCallback(); } void uClockClass::stop() { ATOMIC(clock_state = STOPED) start_timer = 0; if (onClockStopCallback) onClockStopCallback(); } void uClockClass::pause() { if (clock_state == STARTED) { ATOMIC(clock_state = PAUSED) if (onClockPauseCallback) onClockPauseCallback(); } else if (clock_state == PAUSED) { if (clock_mode == INTERNAL_CLOCK) { ATOMIC(clock_state = STARTED) } else if (clock_mode == EXTERNAL_CLOCK) { ATOMIC(clock_state = STARTING) } if (onClockContinueCallback) onClockContinueCallback(); } } void uClockClass::setClockMode(ClockMode tempo_mode) { ATOMIC( clock_mode = tempo_mode; // trying to set external clock while playing? if (clock_mode == EXTERNAL_CLOCK && clock_state == STARTED) clock_state = STARTING; ) } uClockClass::ClockMode uClockClass::getClockMode() { return clock_mode; } // for software timer implementation(fallback for no timer board support) void uClockClass::run() { #if defined(USE_UCLOCK_SOFTWARE_TIMER) // call software timer implementation softwareTimerHandler(micros()); #endif } void uClockClass::stepSeqTick() { for (uint8_t track=0; track < track_slots_size; track++) { bool stepProcess = false; if (tracks[track].mod_step_counter == mod_step_ref) tracks[track].mod_step_counter = 0; if (!tracks[track].shuffle.tmplt.active) { if (tracks[track].mod_step_counter == 0) stepProcess = true; } else if (processShuffle(track)) { stepProcess = true; } if (stepProcess) { if (onStepGlobalCallback) onStepGlobalCallback(tracks[track].step_counter); if (onStepMultiCallback) onStepMultiCallback(tracks[track].step_counter, track); // going forward to the next step call ++tracks[track].step_counter; } ++tracks[track].mod_step_counter; } } void uClockClass::setShuffle(bool active, uint8_t track) { if (tracks == nullptr) return; ATOMIC(tracks[track].shuffle.tmplt.active = active) } bool uClockClass::isShuffled(uint8_t track) { if (tracks == nullptr) return false; return tracks[track].shuffle.tmplt.active; } void uClockClass::setShuffleSize(uint8_t size, uint8_t track) { if (tracks == nullptr) return; if (size > MAX_SHUFFLE_TEMPLATE_SIZE) size = MAX_SHUFFLE_TEMPLATE_SIZE; ATOMIC(tracks[track].shuffle.tmplt.size = size) } void uClockClass::setShuffleData(uint8_t step, int8_t tick, uint8_t track) { if (tracks == nullptr) return; if (step >= MAX_SHUFFLE_TEMPLATE_SIZE) return; ATOMIC(tracks[track].shuffle.tmplt.step[step] = tick) } void uClockClass::setShuffleTemplate(int8_t * shuff, uint8_t size, uint8_t track) { if (tracks == nullptr) return; //uint8_t size = sizeof(shuff) / sizeof(shuff[0]); if (size > MAX_SHUFFLE_TEMPLATE_SIZE) size = MAX_SHUFFLE_TEMPLATE_SIZE; ATOMIC(tracks[track].shuffle.tmplt.size = size) for (uint8_t i=0; i < size; i++) { setShuffleData(i, shuff[i], track); } } int8_t uClockClass::getShuffleLength(uint8_t track) { if (tracks == nullptr) return 0; return tracks[track].shuffle.shuffle_length_ctrl; } bool inline uClockClass::processShuffle(uint8_t track) { if (tracks == nullptr) return false; int8_t mod_shuffle = 0; // check shuffle template of current int8_t shff = tracks[track].shuffle.tmplt.step[tracks[track].step_counter%tracks[track].shuffle.tmplt.size]; if (tracks[track].shuffle.shuffle_shoot_ctrl == false && tracks[track].mod_step_counter == 0) tracks[track].shuffle.shuffle_shoot_ctrl = true; if (shff >= 0) { mod_shuffle = tracks[track].mod_step_counter - shff; // any late shuffle? we should skip next tracks[track].mod_step_counter == 0 if (tracks[track].shuffle.last_shff < 0 && tracks[track].mod_step_counter != 1) { if (tracks[track].shuffle.shuffle_shoot_ctrl == true) tracks[track].shuffle.shuffle_shoot_ctrl = false; return false; } } else if (shff < 0) { mod_shuffle = tracks[track].mod_step_counter - (mod_step_ref + shff); tracks[track].shuffle.shuffle_shoot_ctrl = true; } tracks[track].shuffle.last_shff = shff; // shuffle_shoot_ctrl helps keep track if we have shoot or not a note for the step space of output_ppqn/4 pulses if (mod_shuffle == 0 && tracks[track].shuffle.shuffle_shoot_ctrl == true) { // keep track of next note shuffle for current note lenght control tracks[track].shuffle.shuffle_length_ctrl = tracks[track].shuffle.tmplt.step[(tracks[track].step_counter+1)%tracks[track].shuffle.tmplt.size]; if (shff > 0) tracks[track].shuffle.shuffle_length_ctrl -= shff; if (shff < 0) tracks[track].shuffle.shuffle_length_ctrl += shff; tracks[track].shuffle.shuffle_shoot_ctrl = false; return true; } return false; } uint32_t uClockClass::bpmToMicroSeconds(float bpm) { return (60000000.0f / (float)output_ppqn / bpm); } void uClockClass::calculateReferencedata() { mod_clock_ref = output_ppqn / input_ppqn; mod_step_ref = output_ppqn / 4; // sync callback references update for (uint8_t i = 0; i < sync_callback_size; i++) sync_callbacks[i].sync_ref = output_ppqn / sync_callbacks[i].resolution; } void uClockClass::setOutputPPQN(PPQNResolution resolution) { // dont allow PPQN lower than PPQN_4 for output clock (to avoid problems with mod_step_ref) if (resolution < PPQN_4) return; // dont allow output_ppqn lower than input_ppqn if (resolution < input_ppqn) return; ATOMIC( output_ppqn = resolution; calculateReferencedata(); ) } void uClockClass::setInputPPQN(PPQNResolution resolution) { // dont allow input_ppqn greater than output_ppqn if (resolution > output_ppqn) return; ATOMIC( input_ppqn = resolution; calculateReferencedata(); ) } void uClockClass::setOnSync(PPQNResolution resolution, void (*callback)(uint32_t tick)) { // sets sync callback only if the resolution is lower or equal main clock rate if (resolution > output_ppqn) return; // alloc once and forever policy! // reallocate by creating a new array, copying data, and deleting the old one SyncCallback * new_sync_callbacks = new SyncCallback[sync_callback_size+1]; if (sync_callbacks != nullptr) { memcpy(new_sync_callbacks, sync_callbacks, sizeof(SyncCallback) * (sync_callback_size+1)); delete[] sync_callbacks; } sync_callbacks = new_sync_callbacks; sync_callbacks[sync_callback_size].callback = callback; sync_callbacks[sync_callback_size].resolution = resolution; ++sync_callback_size; } void uClockClass::setTempo(float bpm) { if (clock_mode == EXTERNAL_CLOCK) return; if (bpm < MIN_BPM || bpm > MAX_BPM) return; ATOMIC(tempo = bpm) uClockSetTimerTempo(bpm); } float uClockClass::getTempo() { if (clock_mode == EXTERNAL_CLOCK) { uint64_t acc = 0; uint8_t valid_buffer_size = 0; for (uint8_t i=0; i < ext_interval_buffer_size; i++) { if (ext_interval_buffer[i] > 0) { ATOMIC(acc += ext_interval_buffer[i]) ++valid_buffer_size; } } if (acc == 0) return tempo; return constrainBpm(freqToBpm(acc / valid_buffer_size)); } return tempo; } float inline uClockClass::freqToBpm(uint32_t freq) { float usecs = 1/((float)freq/1000000.0); return (float)((float)(usecs/(float)input_ppqn) * 60.0); } float inline uClockClass::constrainBpm(float bpm) { return (bpm < MIN_BPM) ? MIN_BPM : ( bpm > MAX_BPM ? MAX_BPM : bpm ); } void uClockClass::setExtIntervalBuffer(size_t buffer_size) { if (ext_interval_buffer != nullptr) return; // alloc once and forever policy ext_interval_buffer_size = buffer_size; ext_interval_buffer = new uint32_t[ext_interval_buffer_size]; for (uint8_t i=0; i < ext_interval_buffer_size; i++) ext_interval_buffer[i] = 0; } void uClockClass::setPhaseLockQuartersCount(uint8_t count) { ATOMIC(phase_lock_quarters = count) } void uClockClass::resetCounters() { tick = 0; mod_clock_counter = 0; int_clock_tick = 0; ext_clock_tick = 0; ext_clock_us = 0; ext_interval = 0; //ext_interval_idx = 0; // sync output counters for (uint8_t i = 0; i < sync_callback_size; i++) { sync_callbacks[i].mod_counter = 0; sync_callbacks[i].tick = 0; } // stepseq counters for (uint8_t track=0; track < track_slots_size; track++) { tracks[track].step_counter = 0; tracks[track].mod_step_counter = 0; } // external bpm read buffer //for (uint8_t i=0; i < ext_interval_buffer_size; i++) // ext_interval_buffer[i] = 0; } void uClockClass::tap() { // we can make use of mod_sync1_ref for tap //uint8_t mod_tap_ref = output_ppqn / PPQN_1; // we only set tap if ClockMode is INTERNAL_CLOCK } // elapsed time support uint8_t uClockClass::getNumberOfSeconds(uint32_t time) { if ( time == 0 ) { return time; } return ((_millis - time) / 1000) % SECS_PER_MIN; } uint8_t uClockClass::getNumberOfMinutes(uint32_t time) { if ( time == 0 ) { return time; } return (((_millis - time) / 1000) / SECS_PER_MIN) % SECS_PER_MIN; } uint8_t uClockClass::getNumberOfHours(uint32_t time) { if ( time == 0 ) { return time; } return (((_millis - time) / 1000) % SECS_PER_DAY) / SECS_PER_HOUR; } uint8_t uClockClass::getNumberOfDays(uint32_t time) { if ( time == 0 ) { return time; } return ((_millis - time) / 1000) / SECS_PER_DAY; } uint32_t uClockClass::getNowTimer() { return _millis; } uint32_t uClockClass::getPlayTime() { return start_timer; } uint16_t uClockClass::getIntOverflowCounter() { uint16_t counter = 0; ATOMIC(counter = int_overflow_counter) return counter; } uint16_t uClockClass::getExtOverflowCounter() { uint16_t counter = 0; ATOMIC(counter = ext_overflow_counter) return counter; } } } // end namespace umodular::clock umodular::clock::uClockClass uClock; ================================================ FILE: src/uClock.h ================================================ /*! * @file uClock.h * Project BPM clock generator for Arduino * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) * @version 2.3.0 * @author Romulo Silva * @date 10/06/2017 * @license MIT - (c) 2025 - Romulo Silva - contact@midilab.co * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #ifndef __U_CLOCK_H__ #define __U_CLOCK_H__ #include #include namespace umodular { namespace clock { // Shuffle templates are specific for each PPQN output resolution // min: -(output_ppqn/4)-1 ticks // max: (output_ppqn/4)-1 ticks // adjust the size of you template if more than 16 shuffle step info needed #define MAX_SHUFFLE_TEMPLATE_SIZE 16 #define MIN_BPM 1 #define MAX_BPM 500 #define PHASE_FACTOR 16 #define MICROS_PER_MIN (60000000UL) #define SECS_PER_MIN (60UL) #define SECS_PER_HOUR (3600UL) #define SECS_PER_DAY (SECS_PER_HOUR * 24L) class uClockClass { public: enum ClockMode { INTERNAL_CLOCK = 0, EXTERNAL_CLOCK }; enum ClockState { STOPED = 0, PAUSED, STARTING, SYNCING, STARTED }; enum PPQNResolution { PPQN_1 = 1, PPQN_2 = 2, PPQN_4 = 4, PPQN_8 = 8, PPQN_12 = 12, PPQN_24 = 24, PPQN_48 = 48, PPQN_96 = 96, PPQN_384 = 384, PPQN_480 = 480, PPQN_960 = 960 }; ClockState clock_state = STOPED; uClockClass(); ~uClockClass(); // set main input and output clock rates void setOutputPPQN(PPQNResolution resolution); void setInputPPQN(PPQNResolution resolution); // callbacks setup void setOnOutputPPQN(void (*callback)(uint32_t tick)) { onOutputPPQNCallback = callback; } // multiple output sync clock signatures callback register void setOnSync(PPQNResolution resolution, void (*callback)(uint32_t tick)); void setOnClockStart(void (*callback)()) { onClockStartCallback = callback; } void setOnClockStop(void (*callback)()) { onClockStopCallback = callback; } void setOnClockPause(void (*callback)()) { onClockPauseCallback = callback; } void setOnClockContinue(void (*callback)()) { onClockContinueCallback = callback; } // Step Seq extension support? // Step Seq supports per track control: // shuffle: YES // shift: ROADMAP // direction: ROADMAP // keep API compatibility for setOnStep global track void setOnStep(void (*callback)(uint32_t step)) { if (tracks == nullptr) { track_slots_size = 1; // alloc once and forever policy tracks = new TRACK_SLOT[track_slots_size]; onStepGlobalCallback = callback; } } // extended stepSeq multitrack support for setOnStep void setOnStep(void (*callback)(uint32_t step, uint8_t track), uint8_t track_number) { if (tracks == nullptr) { track_slots_size = track_number; // alloc once and forever policy tracks = new TRACK_SLOT[track_slots_size]; onStepMultiCallback = callback; } } void init(); // the main processing of internal and external tick system void handleInternalClock(); void handleExternalClock(); // external class control void start(); void stop(); void pause(); void setTempo(float bpm); float getTempo(); // Step Seq extension for global and multi track sequences control // void setShift(int8_t shift, uint8_t track = 0); // void setDirection(uint8_t direction, uint8_t track = 0); 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); // use this to know how many positive or negative ticks to add to current note length int8_t getShuffleLength(uint8_t track = 0); // for software timer implementation(fallback for no board support) void run(); // external timming control void setClockMode(ClockMode tempo_mode); ClockMode getClockMode(); void clockMe(); void setPhaseLockQuartersCount(uint8_t count); // for smooth slave tempo calculate display you should raise the // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. // note: this doesn't impact on sync time, only display time getTempo() // if you dont want to use it, it is default set it to 1 for memory save void setExtIntervalBuffer(size_t buffer_size); // todo! void tap(); // elapsed time support 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(); // interrupt overflow debug uint16_t getIntOverflowCounter(); uint16_t getExtOverflowCounter(); uint32_t bpmToMicroSeconds(float bpm); void resetCounters(); private: float inline freqToBpm(uint32_t freq); float inline constrainBpm(float bpm); void calculateReferencedata(); // callbacks void (*onOutputPPQNCallback)(uint32_t tick) = nullptr; void (*onClockStartCallback)() = nullptr; void (*onClockStopCallback)() = nullptr; void (*onClockPauseCallback)() = nullptr; void (*onClockContinueCallback)() = nullptr; // step seq extension for global and multi track sequences control void (*onStepGlobalCallback)(uint32_t step) = nullptr; void (*onStepMultiCallback)(uint32_t step, uint8_t track) = nullptr; typedef struct { bool active = false; uint8_t size = MAX_SHUFFLE_TEMPLATE_SIZE; int8_t step[MAX_SHUFFLE_TEMPLATE_SIZE] = {0}; // int8 supports max PPQN_480 of internal clock resolution } SHUFFLE_TEMPLATE; typedef struct { volatile SHUFFLE_TEMPLATE tmplt; int8_t last_shff = 0; // int8 supports max PPQN_480 of internal clock resolution bool shuffle_shoot_ctrl = true; volatile int8_t shuffle_length_ctrl = 0; } SHUFFLE_DATA; typedef struct { SHUFFLE_DATA shuffle; //int8_t shift = 0; //uint8_t direction = 0; uint32_t step_counter = 0; uint8_t mod_step_counter; } TRACK_SLOT; // sync callback structure for dynamic multiple sync outputs support struct SyncCallback { void (*callback)(uint32_t tick) = nullptr; uint8_t mod_counter = 0; uint16_t sync_ref = 0; uint32_t tick = 0; PPQNResolution resolution; }; // sync callback data SyncCallback * sync_callbacks = nullptr; uint8_t sync_callback_size = 0; // clock core // input/output tick resolution PPQNResolution output_ppqn = PPQN_96; PPQNResolution input_ppqn = PPQN_24; volatile float tempo = 120.0; volatile ClockMode clock_mode = INTERNAL_CLOCK; uint32_t start_timer = 0; // output and internal counters, ticks and references volatile uint32_t tick = 0; volatile uint32_t int_clock_tick = 0; uint8_t mod_step_ref = 0; uint8_t mod_clock_counter = 0; uint16_t mod_clock_ref = 0; // external clock control volatile uint32_t ext_clock_us = 0; volatile uint32_t ext_clock_tick = 0; volatile uint32_t ext_interval = 0; volatile float external_tempo = tempo; uint8_t phase_lock_quarters = 1; // debug interrupts overflow volatile uint16_t int_overflow_counter = 0; volatile uint16_t ext_overflow_counter = 0; // StepSeq extension // main stepseq tick processor void stepSeqTick(); // stepseq shuffle processor bool inline processShuffle(uint8_t track = 0); TRACK_SLOT * tracks = nullptr; size_t track_slots_size = 0; // external clock bpm read calculus getTempo() volatile uint32_t * ext_interval_buffer = nullptr; size_t ext_interval_buffer_size = 0; uint16_t ext_interval_idx = 0; }; } } // end namespace umodular::clock extern umodular::clock::uClockClass uClock; extern "C" { extern volatile uint32_t _millis; } #endif /* __U_CLOCK_H__ */