From b35ba50dc1a306ad4520714285eef8c15577a542 Mon Sep 17 00:00:00 2001 From: midilab Date: Fri, 5 Jan 2024 08:43:10 -0300 Subject: [PATCH 1/4] prepare for release, beta test, and missing features to implement --- README.md | 55 ++++++++-- library.json | 2 +- library.properties | 2 +- src/uClock.cpp | 234 +++++++++++++++++++++------------------ src/uClock.h | 267 +++++++++++++++++++++++++-------------------- 5 files changed, 324 insertions(+), 236 deletions(-) diff --git a/README.md b/README.md index 1d5831d..cdcf873 100755 --- a/README.md +++ b/README.md @@ -9,15 +9,46 @@ The absence of real-time features necessary for creating professional-level embe With uClock, you gain the ability to create professional-grade sequencers, sync boxes, or generate a precise BPM clock for external devices in the realms of music, audio/video productions, performances, or tech art installations. The library offers an external synchronization schema that enables you to generate an internal clock based on an external clock source, allowing you to master your entire MIDI setup or any other protocols according to your specific preferences and requirements. ## Interface -The uClock library interfaces through an attached callback function that runs on a hardware interrupt and provides support for the following resolutions: +The uClock library API operates through attached callback functions mechanism: -1. **16PPQN** 16 Pulses Per Quarter Note -2. **32PPQN** 32 Pulses Per Quarter Note -3. **96PPQN** 96 Pulses Per Quarter Note +1. **onPPQNCallback(uint32_t tick)** calls on each new pulse based on selected PPQN resolution(if no PPQN set, the default is 96PPQN) +2. **onStepCallback(uint32_t step)** good way to code old style step sequencer based on 16th note schema(not dependent on PPQN resolution) +3. **onSync24Callback(uint32_t tick)** good way to code a clock machine, or keep your gears synced with your device +4. **onSync48Callback(uint32_t tick)** there are some 48ppqn based sync devices out there +5. **onClockStartCallback()** there are some 48ppqn based sync devices out there +6. **onClockStopCallback()** there are some 48ppqn based sync devices out there -To generate a MIDI sync signal and synchronize external MIDI devices, you can start working with the resolution of 96PPQN, which aligns with the clocking standards of modern MIDI-syncable devices commonly available in the market. This resolution is based on the 24PPQN (24 multiplied by 4 equals 96). By sending a sync signal every 96PPQN interval, you can ensure effective synchronization among your MIDI devices. +```c++ +// avaliable resolutions +// [ 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.setPPQN(uClock.PPQN_96); + +// you need to use at least one! +uClock.setOnPPQN(onPPQNCallback); +uClock.setOnStep(onStepCallback); +uClock.setOnSync24(onSync24Callback); +uClock.setOnSync48(onSync48Callback); + +uClock.setOnClockStart(onClockStartCallback); +uClock.setOnClockStop(onClockStopCallback); + +uClock.init(); +``` + +Resolutions: set youw own resolution for your clock needs + +1. **PPQN_24** 24 Pulses Per Quarter Note +2. **PPQN_48** 48 Pulses Per Quarter Note +3. **PPQN_96** 96 Pulses Per Quarter Note +1. **PPQN_384** 384 Pulses Per Quarter Note +2. **PPQN_480** 480 Pulses Per Quarter Note +3. **PPQN_960** 96 Pulses Per Quarter Note -If you are working on the development of a vintage-style step sequencer, utilizing a resolution of 16PPQN is a fitting option to initiate the coding process. This resolution ensures that each 16PPQN call corresponds to a step played note or event. + +To generate a MIDI sync signal and synchronize external MIDI devices, you can start working with the resolution of 24PPQN, which aligns with the clocking standards of modern MIDI-syncable devices commonly available in the market. By sending 24 pulses per quarter note interval, you can ensure effective synchronization among your MIDI devices. + +If you are working on the development of a vintage-style step sequencer, utilizing a resolution of 96PPQN is a fitting option to initiate the coding process. Then you can use onStepCallback call wich corresponds to a step played note or event. Furthermore, it is possible to utilize all three resolutions simultaneously, allowing for flexibility based on your specific requirements and preferences. @@ -38,7 +69,7 @@ Here is an example on how to create a simple MIDI Sync Box on Arduino boards #define MIDI_STOP 0xFC // The callback function wich will be called by Clock each Pulse of 96PPQN clock resolution. -void ClockOut96PPQN(uint32_t tick) { +void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears Serial.write(MIDI_CLOCK); } @@ -60,8 +91,8 @@ void setup() { // Inits the clock uClock.init(); - // Set the callback function for the clock output to send MIDI Sync message. - uClock.setClock96PPQNOutput(ClockOut96PPQN); + // Set the callback function for the clock output to send MIDI Sync message based on 24PPQN + uClock.setOnSync24(onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStartOutput(onClockStart); uClock.setOnClockStopOutput(onClockStop); @@ -85,7 +116,7 @@ An example on how to create a simple MIDI Sync Box on Teensy boards and USB Midi #include // The callback function wich will be called by Clock each Pulse of 96PPQN clock resolution. -void ClockOut96PPQN(uint32_t tick) { +void onSync24Callback(uint32_t tick) { // Send MIDI_CLOCK to external gears usbMIDI.sendRealTime(usbMIDI.Clock); } @@ -103,8 +134,8 @@ void onClockStop() { void setup() { // Inits the clock uClock.init(); - // Set the callback function for the clock output to send MIDI Sync message. - uClock.setClock96PPQNOutput(ClockOut96PPQN); + // Set the callback function for the clock output to send MIDI Sync message. based on 24PPQN + uClock.setOnSync24(onSync24Callback); // Set the callback function for MIDI Start and Stop messages. uClock.setOnClockStartOutput(onClockStart); uClock.setOnClockStopOutput(onClockStop); diff --git a/library.json b/library.json index 522da9d..e64bcf2 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "uClock", - "version": "1.5.1", + "version": "2.0.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, Seedstudio XIAO M0. ESP32 and STM32)", "keywords": "bpm, clock, timing, tick, music, generator", "repository": diff --git a/library.properties b/library.properties index 92f7b9b..fa4b2e8 100755 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=uClock -version=1.5.1 +version=2.0.0 author=Romulo Silva maintainer=Romulo Silva sentence=BPM clock generator for Arduino platform. diff --git a/src/uClock.cpp b/src/uClock.cpp index fc9226d..c34dc74 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -2,10 +2,10 @@ * @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(Teensy, Seedstudio XIAO M0 and ESP32) - * @version 1.5.1 + * @version 2.0.0 * @author Romulo Silva * @date 10/06/2017 - * @license MIT - (c) 2022 - Romulo Silva - contact@midilab.co + * @license MIT - (c) 2024 - 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"), @@ -66,15 +66,13 @@ // header of this file void uclockInitTimer() { - // begin at 120bpm (20833us) - initTimer(20833); + // begin at 120bpm + initTimer(uClock.bpmToMicroSeconds(120.00)); } void setTimerTempo(float bpm) { - // convert bpm float into 96 ppqn resolution microseconds interval - uint32_t us_interval = (60000000 / 24 / bpm); - setTimer(us_interval); + setTimer(uClock.bpmToMicroSeconds(bpm)); } namespace umodular { namespace clock { @@ -103,11 +101,14 @@ uClockClass::uClockClass() mode = INTERNAL_CLOCK; resetCounters(); - onClock96PPQNCallback = NULL; - onClock32PPQNCallback = NULL; - onClock16PPQNCallback = NULL; - onClockStartCallback = NULL; - onClockStopCallback = NULL; + onPPQNCallback = nullptr; + onSync24Callback = nullptr; + onSync48Callback = nullptr; + onStepCallback = nullptr; + onClockStartCallback = nullptr; + onClockStopCallback = nullptr; + // first ppqn references calculus + setPPQN(PPQN_96); } void uClockClass::init() @@ -117,6 +118,23 @@ void uClockClass::init() setTempo(tempo); } +uint32_t uClockClass::bpmToMicroSeconds(float bpm) +{ + return (60000000 / ppqn / bpm); +} + +void uClockClass::setPPQN(PPQNResolution resolution) +{ + // stop clock to make it safe changing those references + // so we avoid volatile then and ATOMIC everyone + stop(); + ppqn = resolution; + // calculate the mod24, mod48 and mod_step tick reference trigger + mod24_ref = ppqn / 24; + mod48_ref = ppqn / 48; + mod_step_ref = ppqn / 4; +} + void uClockClass::start() { resetCounters(); @@ -169,20 +187,19 @@ void uClockClass::setTempo(float bpm) ) setTimerTempo(bpm); - } float inline uClockClass::freqToBpm(uint32_t freq) { float usecs = 1/((float)freq/1000000.0); - return (float)((float)(usecs/24.0) * 60.0); + return (float)((float)(usecs/(float)ppqn) * 60.0); } float uClockClass::getTempo() { if (mode == EXTERNAL_CLOCK) { uint32_t acc = 0; - // wait the buffer get full + // wait the buffer to get full if (ext_interval_buffer[EXT_INTERVAL_BUFFER_SIZE-1] == 0) { return tempo; } @@ -196,7 +213,7 @@ float uClockClass::getTempo() return tempo; } -void uClockClass::setMode(uint8_t tempo_mode) +void uClockClass::setMode(SyncMode tempo_mode) { mode = tempo_mode; } @@ -217,16 +234,16 @@ void uClockClass::clockMe() void uClockClass::resetCounters() { - external_clock = 0; - internal_tick = 0; - external_tick = 0; - div32th_counter = 0; - div16th_counter = 0; - mod6_counter = 0; - indiv32th_counter = 0; - indiv16th_counter = 0; - inmod6_counter = 0; + tick = 0; + int_clock_tick = 0; + sync48_tick = 0; + ext_clock_tick = 0; + ext_clock_us = 0; ext_interval_idx = 0; + mod24_counter = 0; + mod48_counter = 0; + mod_step_counter = 0; + step_counter = 0; for (uint8_t i=0; i < EXT_INTERVAL_BUFFER_SIZE; i++) { ext_interval_buffer[i] = 0; } @@ -277,9 +294,10 @@ int8_t uClockClass::getShuffleLength() { return shuffle_length_ctrl; } - +/* int8_t inline uClockClass::processShuffle() { + // mod6_counter will become mod_step_counter int8_t mod6_shuffle_counter; if (!shuffle.active) { mod6_shuffle_counter = mod6_counter; @@ -309,41 +327,26 @@ int8_t inline uClockClass::processShuffle() } return mod6_shuffle_counter; } - + */ +// it is expected to be called in 24PPQN void uClockClass::handleExternalClock() { - switch (state) { case PAUSED: break; case STARTING: state = STARTED; - external_clock = micros(); + ext_clock_us = micros(); break; case STARTED: + uint32_t now_clock_us = micros(); + last_interval = clock_diff(ext_clock_us, now_clock_us); + ext_clock_us = now_clock_us; - uint32_t u_timer = micros(); - last_interval = clock_diff(external_clock, u_timer); - external_clock = u_timer; - - if (inmod6_counter == 0) { - indiv16th_counter++; - indiv32th_counter++; - } - - if (inmod6_counter == 3) { - indiv32th_counter++; - } - - // slave tick me! - external_tick++; - inmod6_counter++; - - if (inmod6_counter == 6) { - inmod6_counter = 0; - } + // external clock tick me! + ext_clock_tick++; // accumulate interval incomming ticks data for getTempo() smooth reads on slave mode if(++ext_interval_idx >= EXT_INTERVAL_BUFFER_SIZE) { @@ -351,10 +354,10 @@ void uClockClass::handleExternalClock() } ext_interval_buffer[ext_interval_idx] = last_interval; - if (external_tick == 1) { - interval = last_interval; + if (ext_clock_tick == 1) { + ext_interval = last_interval; } else { - interval = (((uint32_t)interval * (uint32_t)PLL_X) + (uint32_t)(256 - PLL_X) * (uint32_t)last_interval) >> 8; + ext_interval = (((uint32_t)ext_interval * (uint32_t)PLL_X) + (uint32_t)(256 - PLL_X) * (uint32_t)last_interval) >> 8; } break; } @@ -362,65 +365,89 @@ void uClockClass::handleExternalClock() void uClockClass::handleTimerInt() { - if (mode == EXTERNAL_CLOCK) { - // sync tick position with external tick clock - if ((internal_tick < external_tick) || (internal_tick > (external_tick + 1))) { - internal_tick = external_tick; - div32th_counter = indiv32th_counter; - div16th_counter = indiv16th_counter; - mod6_counter = inmod6_counter; - } + // External sync is handled here... test if clock check on each tick instead when + // mod24_counter kicks in will help or worst slave timing sync quality + if (mod24_counter == mod24_ref) { + if (mode == EXTERNAL_CLOCK) { + // sync tick position with external tick clock + if ((int_clock_tick < ext_clock_tick) || (int_clock_tick > (ext_clock_tick + 1))) { + int_clock_tick = ext_clock_tick; + } - uint32_t counter = interval; - uint32_t u_timer = micros(); - sync_interval = clock_diff(external_clock, u_timer); + uint32_t counter = ext_interval; + uint32_t now_clock_us = micros(); + sync_interval = clock_diff(ext_clock_us, now_clock_us); - if (internal_tick <= external_tick) { - counter -= phase_mult(sync_interval); - } else { - if (counter > sync_interval) { - counter += phase_mult(counter - sync_interval); + if (int_clock_tick <= ext_clock_tick) { + counter -= phase_mult(sync_interval); + } else { + if (counter > sync_interval) { + counter += phase_mult(counter - sync_interval); + } } - } - // update internal clock timer frequency - float bpm = freqToBpm(counter); - if (bpm != tempo) { - if (bpm >= MIN_BPM && bpm <= MAX_BPM) { - tempo = bpm; - setTimerTempo(bpm); + // update internal clock timer frequency + float bpm = freqToBpm(counter); + if (bpm != tempo) { + if (bpm >= MIN_BPM && bpm <= MAX_BPM) { + tempo = bpm; + setTimerTempo(bpm); + } } } + // callback to inform about sync24 event + if (onSync24Callback) { + onSync24Callback(int_clock_tick); + } + // reset counter + mod24_counter = 0; + // internal clock tick me! sync24 tick too + ++int_clock_tick; } - if (onClock96PPQNCallback) { - onClock96PPQNCallback(internal_tick); + // sync signals first please... + if (onSync48Callback) { + if (mod48_counter == mod48_ref) { + onSync48Callback(sync48_tick); + // reset counter + mod48_counter = 0; + // sync48 tick me! + ++sync48_tick; + } } - // 16PPQN call and shuffle processing if enabled - if (processShuffle() == 0) { - if (onClock16PPQNCallback) { - onClock16PPQNCallback(div16th_counter); + // PPQNCallback time! + if (onPPQNCallback) { + onPPQNCallback(tick); + } + + if (onStepCallback) { + // we can add a time signature here for call setup based on mod_step_ref + // basic will be 16ths, but let the option to handle unusual sequences + if (mod_step_counter == mod_step_ref) { + onStepCallback(step_counter); + // reset counter + mod_step_counter = 0; + // going forward to the next step call + ++step_counter; } - div16th_counter++; - shuffle_shoot_ctrl = false; } - // 32PPQN call. does anyone uses it? - if (mod6_counter == 3 || mod6_counter == 6) { - if (onClock32PPQNCallback) { - onClock32PPQNCallback(div32th_counter); + /* // TODO: port it from 24PPQN to ppqn set + if (processShuffle() == 0) { + //if (mod_step_counter == signature) { + if (onStepCallback) { + onStepCallback(mod_step_counter); } - div32th_counter++; - } + shuffle_shoot_ctrl = false; + } */ // tick me! - internal_tick++; - mod6_counter++; - - if (mod6_counter == 6) { - mod6_counter = 0; - } + ++tick; + // increment mod counters + ++mod24_counter; + ++mod48_counter; + ++mod_step_counter; } // elapsed time support @@ -429,7 +456,7 @@ uint8_t uClockClass::getNumberOfSeconds(uint32_t time) if ( time == 0 ) { return time; } - return ((_timer - time) / 1000) % SECS_PER_MIN; + return ((_millis - time) / 1000) % SECS_PER_MIN; } uint8_t uClockClass::getNumberOfMinutes(uint32_t time) @@ -437,7 +464,7 @@ uint8_t uClockClass::getNumberOfMinutes(uint32_t time) if ( time == 0 ) { return time; } - return (((_timer - time) / 1000) / SECS_PER_MIN) % SECS_PER_MIN; + return (((_millis - time) / 1000) / SECS_PER_MIN) % SECS_PER_MIN; } uint8_t uClockClass::getNumberOfHours(uint32_t time) @@ -445,7 +472,7 @@ uint8_t uClockClass::getNumberOfHours(uint32_t time) if ( time == 0 ) { return time; } - return (((_timer - time) / 1000) % SECS_PER_DAY) / SECS_PER_HOUR; + return (((_millis - time) / 1000) % SECS_PER_DAY) / SECS_PER_HOUR; } uint8_t uClockClass::getNumberOfDays(uint32_t time) @@ -453,12 +480,12 @@ uint8_t uClockClass::getNumberOfDays(uint32_t time) if ( time == 0 ) { return time; } - return ((_timer - time) / 1000) / SECS_PER_DAY; + return ((_millis - time) / 1000) / SECS_PER_DAY; } uint32_t uClockClass::getNowTimer() { - return _timer; + return _millis; } uint32_t uClockClass::getPlayTime() @@ -470,12 +497,11 @@ uint32_t uClockClass::getPlayTime() umodular::clock::uClockClass uClock; -volatile uint32_t _timer = 0; +volatile uint32_t _millis = 0; // -// TIMER INTERRUPT HANDLER +// TIMER HANDLER // -// #if defined(ARDUINO_ARCH_AVR) ISR(TIMER1_COMPA_vect) #else @@ -483,7 +509,7 @@ void uClockHandler() #endif { // global timer counter - _timer = millis(); + _millis = millis(); if (uClock.state == uClock.STARTED) { uClock.handleTimerInt(); diff --git a/src/uClock.h b/src/uClock.h index b8722d1..461483a 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -2,10 +2,10 @@ * @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(Teensy, Seedstudio XIAO M0 and ESP32) - * @version 1.5.1 + * @version 2.0.0 * @author Romulo Silva * @date 10/06/2017 - * @license MIT - (c) 2022 - Romulo Silva - contact@midilab.co + * @license MIT - (c) 2024 - 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"), @@ -34,6 +34,16 @@ namespace umodular { namespace clock { +// for extended steps in memory style and make use of 96ppqn for record propurse we can +// keep array[step] memory layout and add new information about note possition to be check for the entire ppqn pulse +// example: for a whole 24 pulses we only check array[step].offset that can vary from 0 to 24(ppqn/4) +// time/tick notation and representation notes: +// one quarter note == 4 steps in 16th notes step sequencer style +// PPQN / 4 = pulses in between steps(from step sequencer perspective, a quarter note have 4 steps) +// 24 PPQN (6 pulses per step) +// 48 PPQN (12 pulses per step) +// 96 PPQN (24 pulses per step) + // min: 2 step, max: 16 steps // adjust the size of you template if more than 16 needed // step adjust goes min: -5, max: 5 @@ -61,120 +71,142 @@ typedef struct { #define SECS_PER_DAY (SECS_PER_HOUR * 24L) class uClockClass { -private: - - float inline freqToBpm(uint32_t freq); - - // shuffle - int8_t inline processShuffle(); - - void (*onClock96PPQNCallback)(uint32_t tick); - void (*onClock32PPQNCallback)(uint32_t tick); - void (*onClock16PPQNCallback)(uint32_t tick); - void (*onClockStartCallback)(); - void (*onClockStopCallback)(); - - // internal clock control - uint32_t internal_tick; - uint32_t div32th_counter; - uint32_t div16th_counter; - uint8_t mod6_counter; - - // external clock control - volatile uint32_t external_clock; - volatile uint32_t external_tick; - volatile uint32_t indiv32th_counter; - volatile uint32_t indiv16th_counter; - volatile uint8_t inmod6_counter; - volatile uint32_t interval; - uint32_t last_interval; - uint32_t sync_interval; - - float tempo; - uint32_t start_timer; - uint8_t mode; - - volatile uint32_t ext_interval_buffer[EXT_INTERVAL_BUFFER_SIZE]; - uint16_t ext_interval_idx; - - // shuffle implementation that applies to 16PPQN callback - volatile SHUFFLE_TEMPLATE shuffle; - bool shuffle_shoot_ctrl = true; - volatile int8_t shuffle_length_ctrl = 0; - -public: - - enum { - INTERNAL_CLOCK = 0, - EXTERNAL_CLOCK - }; - - enum { - PAUSED = 0, - STARTING, - STARTED - }; - - uint8_t state; - - uClockClass(); - - void setClock96PPQNOutput(void (*callback)(uint32_t tick)) { - onClock96PPQNCallback = callback; - } - - void setClock32PPQNOutput(void (*callback)(uint32_t tick)) { - onClock32PPQNCallback = callback; - } - - void setClock16PPQNOutput(void (*callback)(uint32_t tick)) { - onClock16PPQNCallback = callback; - } - - void setOnClockStartOutput(void (*callback)()) { - onClockStartCallback = callback; - } - - void setOnClockStopOutput(void (*callback)()) { - onClockStopCallback = callback; - } - - void init(); - void handleTimerInt(); - void handleExternalClock(); - void resetCounters(); - - // external class control - void start(); - void stop(); - void pause(); - void setTempo(float bpm); - float getTempo(); - - // external timming control - void setMode(uint8_t tempo_mode); - uint8_t getMode(); - void clockMe(); - - // shuffle - void setShuffle(bool active); - bool isShuffled(); - void setShuffleSize(uint8_t size); - void setShuffleData(uint8_t step, int8_t tick); - void setShuffleTemplate(int8_t * shuff); - // use this to know how many positive or negative ticks to add to current note length - int8_t getShuffleLength(); - - // 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(); + + public: + enum SyncMode { + INTERNAL_CLOCK = 0, + EXTERNAL_CLOCK + }; + + enum ClockState { + PAUSED = 0, + STARTING, + STARTED + }; + + enum PPQNResolution { + PPQN_24 = 24, + PPQN_48 = 48, + PPQN_96 = 96, + PPQN_384 = 384, + PPQN_480 = 480, + PPQN_960 = 960 + }; + + uint8_t state; + + uClockClass(); + + void setOnPPQN(void (*callback)(uint32_t tick)) { + onPPQNCallback = callback; + } + + void setOnStep(void (*callback)(uint32_t step)) { + onStepCallback = callback; + } + + void setOnSync24(void (*callback)(uint32_t tick)) { + onSync24Callback = callback; + } + + void setOnSync48(void (*callback)(uint32_t tick)) { + onSync48Callback = callback; + } + + void setOnClockStart(void (*callback)()) { + onClockStartCallback = callback; + } + + void setOnClockStop(void (*callback)()) { + onClockStopCallback = callback; + } + + void init(); + void setPPQN(PPQNResolution resolution); + + void handleTimerInt(); + void handleExternalClock(); + void resetCounters(); + + // external class control + void start(); + void stop(); + void pause(); + void setTempo(float bpm); + float getTempo(); + + // external timming control + void setMode(SyncMode tempo_mode); + uint8_t getMode(); + void clockMe(); + + // shuffle + void setShuffle(bool active); + bool isShuffled(); + void setShuffleSize(uint8_t size); + void setShuffleData(uint8_t step, int8_t tick); + void setShuffleTemplate(int8_t * shuff); + // use this to know how many positive or negative ticks to add to current note length + int8_t getShuffleLength(); + + // 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(); + + uint32_t bpmToMicroSeconds(float bpm); + + private: + float inline freqToBpm(uint32_t freq); + + // shuffle + //int8_t inline processShuffle(); + + void (*onPPQNCallback)(uint32_t tick); + void (*onStepCallback)(uint32_t step); + void (*onSync24Callback)(uint32_t tick); + void (*onSync48Callback)(uint32_t tick); + void (*onClockStartCallback)(); + void (*onClockStopCallback)(); + + // internal clock control + // uint16_t ppqn; + PPQNResolution ppqn = PPQN_96; + uint32_t tick; + uint32_t int_clock_tick; + uint32_t sync48_tick; + uint8_t mod24_counter; + uint8_t mod24_ref; + uint8_t mod48_counter; + uint8_t mod48_ref; + uint8_t mod_step_counter; + uint8_t mod_step_ref; + uint32_t step_counter; // should we go uint16_t? + + // external clock control + volatile uint32_t ext_clock_us; + volatile uint32_t ext_clock_tick; + volatile uint32_t ext_interval; + uint32_t last_interval; + uint32_t sync_interval; + + float tempo; + uint32_t start_timer; + uint8_t mode; + + volatile uint32_t ext_interval_buffer[EXT_INTERVAL_BUFFER_SIZE]; + uint16_t ext_interval_idx; + + // shuffle implementation that applies to 16PPQN callback + volatile SHUFFLE_TEMPLATE shuffle; + bool shuffle_shoot_ctrl = true; + volatile int8_t shuffle_length_ctrl = 0; }; } } // end namespace umodular::clock @@ -182,8 +214,7 @@ public: extern umodular::clock::uClockClass uClock; extern "C" { - extern volatile uint16_t _clock; - extern volatile uint32_t _timer; + extern volatile uint32_t _millis; } #endif /* __U_CLOCK_H__ */ From 51b03d55b615bc429ba986a6754e8aaf372f41d3 Mon Sep 17 00:00:00 2001 From: midilab Date: Fri, 5 Jan 2024 11:23:33 -0300 Subject: [PATCH 2/4] fix: callbacks trigger on tick 0 not skiped anymore --- README.md | 32 ++++++++++++++++++-------------- src/platforms/esp32.h | 2 +- src/uClock.cpp | 26 +++++++++++++------------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index cdcf873..e9bd542 100755 --- a/README.md +++ b/README.md @@ -15,25 +15,29 @@ The uClock library API operates through attached callback functions mechanism: 2. **onStepCallback(uint32_t step)** good way to code old style step sequencer based on 16th note schema(not dependent on PPQN resolution) 3. **onSync24Callback(uint32_t tick)** good way to code a clock machine, or keep your gears synced with your device 4. **onSync48Callback(uint32_t tick)** there are some 48ppqn based sync devices out there -5. **onClockStartCallback()** there are some 48ppqn based sync devices out there -6. **onClockStopCallback()** there are some 48ppqn based sync devices out there +5. **onClockStartCallback()** on uClock Start event +6. **onClockStopCallback()** on uClock Stop event ```c++ -// avaliable resolutions -// [ 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.setPPQN(uClock.PPQN_96); +#include -// you need to use at least one! -uClock.setOnPPQN(onPPQNCallback); -uClock.setOnStep(onStepCallback); -uClock.setOnSync24(onSync24Callback); -uClock.setOnSync48(onSync48Callback); +void setup() { + // avaliable resolutions + // [ 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.setPPQN(uClock.PPQN_96); + + // you need to use at least one! + uClock.setOnPPQN(onPPQNCallback); + uClock.setOnStep(onStepCallback); + uClock.setOnSync24(onSync24Callback); + uClock.setOnSync48(onSync48Callback); -uClock.setOnClockStart(onClockStartCallback); -uClock.setOnClockStop(onClockStopCallback); + uClock.setOnClockStart(onClockStartCallback); + uClock.setOnClockStop(onClockStopCallback); -uClock.init(); + uClock.init(); +} ``` Resolutions: set youw own resolution for your clock needs diff --git a/src/platforms/esp32.h b/src/platforms/esp32.h index 99153e3..1344923 100644 --- a/src/platforms/esp32.h +++ b/src/platforms/esp32.h @@ -9,7 +9,7 @@ hw_timer_t * _uclockTimer = NULL; //#define ATOMIC(X) portENTER_CRITICAL_ISR(&_uclockTimerMux); X; portEXIT_CRITICAL_ISR(&_uclockTimerMux); // FreeRTOS main clock task size in bytes -#define CLOCK_STACK_SIZE 5012 // adjust for your needs, a sequencer with heavy serial handling should be large in size +#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; diff --git a/src/uClock.cpp b/src/uClock.cpp index c34dc74..63da5a0 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -367,7 +367,7 @@ void uClockClass::handleTimerInt() { // External sync is handled here... test if clock check on each tick instead when // mod24_counter kicks in will help or worst slave timing sync quality - if (mod24_counter == mod24_ref) { + if (mod24_counter == 0) { if (mode == EXTERNAL_CLOCK) { // sync tick position with external tick clock if ((int_clock_tick < ext_clock_tick) || (int_clock_tick > (ext_clock_tick + 1))) { @@ -399,18 +399,18 @@ void uClockClass::handleTimerInt() if (onSync24Callback) { onSync24Callback(int_clock_tick); } - // reset counter - mod24_counter = 0; + // reset counter reference + mod24_counter = mod24_ref; // internal clock tick me! sync24 tick too ++int_clock_tick; } // sync signals first please... if (onSync48Callback) { - if (mod48_counter == mod48_ref) { + if (mod48_counter == 0) { onSync48Callback(sync48_tick); - // reset counter - mod48_counter = 0; + // reset counter reference + mod48_counter = mod48_ref; // sync48 tick me! ++sync48_tick; } @@ -424,10 +424,10 @@ void uClockClass::handleTimerInt() if (onStepCallback) { // we can add a time signature here for call setup based on mod_step_ref // basic will be 16ths, but let the option to handle unusual sequences - if (mod_step_counter == mod_step_ref) { + if (mod_step_counter == 0) { onStepCallback(step_counter); - // reset counter - mod_step_counter = 0; + // reset counter reference + mod_step_counter = mod_step_ref; // going forward to the next step call ++step_counter; } @@ -444,10 +444,10 @@ void uClockClass::handleTimerInt() // tick me! ++tick; - // increment mod counters - ++mod24_counter; - ++mod48_counter; - ++mod_step_counter; + // decrement mod counters + --mod24_counter; + --mod48_counter; + --mod_step_counter; } // elapsed time support From c5f126a90b695c2105231d072bcc8cf700ed8715 Mon Sep 17 00:00:00 2001 From: midilab Date: Fri, 5 Jan 2024 21:56:51 -0300 Subject: [PATCH 3/4] shuffle positive restored, missing negative review --- src/uClock.cpp | 84 ++++++++++++++++++++++++-------------------------- src/uClock.h | 4 +-- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 63da5a0..a81409c 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -279,9 +279,9 @@ void uClockClass::setShuffleData(uint8_t step, int8_t tick) ATOMIC(shuffle.step[step] = tick) } -void uClockClass::setShuffleTemplate(int8_t * shuff) +void uClockClass::setShuffleTemplate(int8_t * shuff, uint8_t size) { - uint8_t size = sizeof(shuff) / sizeof(shuff[0]); + //uint8_t size = sizeof(shuff) / sizeof(shuff[0]); if (size > MAX_SHUFFLE_TEMPLATE_SIZE) size = MAX_SHUFFLE_TEMPLATE_SIZE; ATOMIC(shuffle.size = size) @@ -294,40 +294,43 @@ int8_t uClockClass::getShuffleLength() { return shuffle_length_ctrl; } -/* -int8_t inline uClockClass::processShuffle() + +bool inline uClockClass::processShuffle() { - // mod6_counter will become mod_step_counter - int8_t mod6_shuffle_counter; if (!shuffle.active) { - mod6_shuffle_counter = mod6_counter; - } else { - // apply shuffle template to step - int8_t shff = shuffle.step[div16th_counter%shuffle.size]; + return mod_step_counter == 0; + } + + // internal reference for mod_counter + uint8_t mod_counter = mod_step_ref - (mod_step_counter == 0 ? mod_step_ref : mod_step_counter); + int8_t mod_shuffle = 0; + + // check shuffle template to step + int8_t shff = shuffle.step[step_counter%shuffle.size]; + + if (shuffle_shoot_ctrl == false && mod_counter == 0) + shuffle_shoot_ctrl = true; + + if (shff >= 0) { + mod_shuffle = mod_counter - shff; + } else if (shff < 0) { + mod_shuffle = mod_counter - (mod_step_ref + shff); + } + + if (mod_shuffle == 0 && shuffle_shoot_ctrl == true) { // keep track of next note shuffle for current note lenght control - shuffle_length_ctrl = shuffle.step[(div16th_counter+1)%shuffle.size]; - // prepare the next mod6 quantize to be called - if (shff == 0) { - mod6_shuffle_counter = mod6_counter; - } else if (shff > 0) { - if (shuffle_shoot_ctrl == false && mod6_counter > shff || (shff == 5 && mod6_counter == 0)) - shuffle_shoot_ctrl = true; - mod6_shuffle_counter = shuffle_shoot_ctrl ? mod6_counter - shff : 1; + shuffle_length_ctrl = shuffle.step[(step_counter+1)%shuffle.size]; + if (shff > 0) shuffle_length_ctrl -= shff; - if (shuffle_length_ctrl == 0) - shuffle_length_ctrl = 1; - } else if (shff < 0) { - if (shuffle_shoot_ctrl == false && mod6_counter == 0) - shuffle_shoot_ctrl = true; - mod6_shuffle_counter = shff - mod6_counter == -6 ? shuffle_shoot_ctrl ? 0 : 1 : 1; + if (shff < 0) shuffle_length_ctrl += shff; - if (shuffle_length_ctrl == 0) - shuffle_length_ctrl = -1; - } + shuffle_shoot_ctrl = false; + return true; } - return mod6_shuffle_counter; + + return false; } - */ + // it is expected to be called in 24PPQN void uClockClass::handleExternalClock() { @@ -421,26 +424,21 @@ void uClockClass::handleTimerInt() onPPQNCallback(tick); } + // step callback to support 16th old school style sequencers + // with builtin shuffle for this callback only if (onStepCallback) { - // we can add a time signature here for call setup based on mod_step_ref - // basic will be 16ths, but let the option to handle unusual sequences - if (mod_step_counter == 0) { + // processShufle make use of mod_step_counter + if (processShuffle()) { onStepCallback(step_counter); - // reset counter reference - mod_step_counter = mod_step_ref; // going forward to the next step call ++step_counter; } - } - - /* // TODO: port it from 24PPQN to ppqn set - if (processShuffle() == 0) { - //if (mod_step_counter == signature) { - if (onStepCallback) { - onStepCallback(mod_step_counter); + // keep track of mod_step_counter + if (mod_step_counter == 0) { + // reset counter reference + mod_step_counter = mod_step_ref; } - shuffle_shoot_ctrl = false; - } */ + } // tick me! ++tick; diff --git a/src/uClock.h b/src/uClock.h index 461483a..f10e664 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -145,7 +145,7 @@ class uClockClass { bool isShuffled(); void setShuffleSize(uint8_t size); void setShuffleData(uint8_t step, int8_t tick); - void setShuffleTemplate(int8_t * shuff); + void setShuffleTemplate(int8_t * shuff, uint8_t size); // use this to know how many positive or negative ticks to add to current note length int8_t getShuffleLength(); @@ -166,7 +166,7 @@ class uClockClass { float inline freqToBpm(uint32_t freq); // shuffle - //int8_t inline processShuffle(); + bool inline processShuffle(); void (*onPPQNCallback)(uint32_t tick); void (*onStepCallback)(uint32_t step); From 459ae7f4e61ff159bd6ba39a5178bc6601849573 Mon Sep 17 00:00:00 2001 From: midilab Date: Sat, 6 Jan 2024 08:40:39 -0300 Subject: [PATCH 4/4] shuffle schema integrated to dinamicly PPQN system. missing reviewing more complex shuffle templates --- src/uClock.cpp | 23 +++++++++++++++++++++++ src/uClock.h | 10 +++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index a81409c..d59d3eb 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -311,12 +311,35 @@ bool inline uClockClass::processShuffle() if (shuffle_shoot_ctrl == false && mod_counter == 0) shuffle_shoot_ctrl = true; + //if (mod_counter == mod_step_ref-1) + if (shff >= 0) { mod_shuffle = mod_counter - shff; + // any late shuffle? we should skip next mod_counter == 0 + if (last_shff < 0 && mod_counter != 1) + return false; } else if (shff < 0) { mod_shuffle = mod_counter - (mod_step_ref + shff); + //if (last_shff < 0 && mod_counter != 1) + // return false; + shuffle_shoot_ctrl = true; } + //Serial.println("-----------------"); + //Serial.print("shff: "); + //Serial.println(shff); + //Serial.print("mod_counter: "); + //Serial.println(mod_counter); + //Serial.print("mod_shuffle: "); + //Serial.println(mod_shuffle); + //Serial.print("shuffle_shoot_ctrl: "); + //Serial.println(shuffle_shoot_ctrl); + //Serial.print("last_shff: "); + //Serial.println(last_shff); + + last_shff = shff; + + // shuffle_shoot_ctrl helps keep track if we have shoot or not a note for the step space of ppqn/4 pulses if (mod_shuffle == 0 && shuffle_shoot_ctrl == true) { // keep track of next note shuffle for current note lenght control shuffle_length_ctrl = shuffle.step[(step_counter+1)%shuffle.size]; diff --git a/src/uClock.h b/src/uClock.h index f10e664..145fb3c 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -44,9 +44,8 @@ namespace umodular { namespace clock { // 48 PPQN (12 pulses per step) // 96 PPQN (24 pulses per step) -// min: 2 step, max: 16 steps -// adjust the size of you template if more than 16 needed -// step adjust goes min: -5, max: 5 +// min: -(ppqn/4)-1 step, max: (ppqn/4)-1 steps +// adjust the size of you template if more than 16 shuffle step info needed #define MAX_SHUFFLE_TEMPLATE_SIZE 16 typedef struct { bool active = false; @@ -198,13 +197,14 @@ class uClockClass { float tempo; uint32_t start_timer; - uint8_t mode; + SyncMode mode; volatile uint32_t ext_interval_buffer[EXT_INTERVAL_BUFFER_SIZE]; uint16_t ext_interval_idx; - // shuffle implementation that applies to 16PPQN callback + // shuffle implementation volatile SHUFFLE_TEMPLATE shuffle; + int8_t last_shff = 0; bool shuffle_shoot_ctrl = true; volatile int8_t shuffle_length_ctrl = 0; };