From dd1228be00bd739300ffb846aa7c8ce3afdb302f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 2 Jul 2025 02:45:39 +0000 Subject: [PATCH] Vendorize uClock (#10) Add copy of uClock to the repo including memory optimization changes. Also add user config setting for changing Pulse Out resolution. Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/10 Co-authored-by: Adam Wonak Co-committed-by: Adam Wonak --- README.md | 5 +- clock.h | 17 +- examples/Gravity/Gravity.ino | 33 +++ examples/Gravity/app_state.h | 2 + examples/Gravity/channel.h | 25 +- examples/Gravity/display.h | 41 +++- examples/Gravity/save_state.cpp | 5 +- examples/Gravity/save_state.h | 1 + gravity.cpp | 4 +- gravity.h | 3 +- uClock.cpp | 409 ++++++++++++++++++++++++++++++++ uClock.h | 180 ++++++++++++++ uClock/platforms/avr.h | 98 ++++++++ uClock/uClock.h | 180 ++++++++++++++ 14 files changed, 966 insertions(+), 37 deletions(-) create mode 100755 uClock.cpp create mode 100755 uClock.h create mode 100644 uClock/platforms/avr.h create mode 100755 uClock/uClock.h diff --git a/README.md b/README.md index 58d0e58..f244487 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,9 @@ Common directory locations: ## Required Third-party Libraries -* [uClock](https://github.com/midilab/uClock) [MIT] - Handle clock tempo, external clock input, and internal clock timer handler. +* [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) Handle clock tempo, external clock input, and internal clock timer handler. * [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation. -* [Adafruit_GFX](https://github.com/adafruit/Adafruit-GFX-Library) [BSD] - Graphics helper library. -* [Adafruit_SSD1306](https://github.com/adafruit/Adafruit_SSD1306) [BSD] - Library for interacting with the SSD1306 OLED display. +* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library. ## Example diff --git a/clock.h b/clock.h index 19ed250..00bd213 100644 --- a/clock.h +++ b/clock.h @@ -13,9 +13,9 @@ #define CLOCK_H #include -#include #include "peripherials.h" +#include "uClock.h" // MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 @@ -39,6 +39,14 @@ class Clock { SOURCE_LAST, }; + enum Pulse { + PULSE_NONE, + PULSE_PPQN_1, + PULSE_PPQN_4, + PULSE_PPQN_24, + PULSE_LAST, + }; + void Init() { NeoSerial.begin(31250); @@ -55,7 +63,6 @@ class Clock { uClock.setOnClockStart(sendMIDIStart); uClock.setOnClockStop(sendMIDIStop); uClock.setOnSync24(sendMIDIClock); - uClock.setOnSync48(sendPulseOut); uClock.start(); } @@ -75,7 +82,7 @@ class Clock { void SetSource(Source source) { bool was_playing = !IsPaused(); uClock.stop(); - // If source is currently MIDI, disable the serial interrupt handler. + // If we are changing the source from MIDI, disable the serial interrupt handler. if (source_ == SOURCE_EXTERNAL_MIDI) { NeoSerial.attachInterrupt(serialEventNoop); } @@ -175,10 +182,6 @@ class Clock { static void sendMIDIClock(uint32_t tick) { NeoSerial.write(MIDI_CLOCK); } - - static void sendPulseOut(uint32_t tick) { - digitalWrite(PULSE_OUT_PIN, !digitalRead(PULSE_OUT_PIN)); - } }; #endif \ No newline at end of file diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 8a46749..99c33a5 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -101,6 +101,32 @@ void HandleIntClockTick(uint32_t tick) { } } + // Pulse Out gate + if (app.selected_pulse != Clock::PULSE_NONE) { + int clock_index; + switch (app.selected_pulse) { + case Clock::PULSE_PPQN_24: + clock_index = 0; + break; + case Clock::PULSE_PPQN_4: + clock_index = 4; + break; + case Clock::PULSE_PPQN_1: + clock_index = 7; + break; + } + + const uint16_t pulse_high_ticks = clock_mod_pulses[clock_index]; + const uint32_t pulse_low_ticks = tick + max((long)(pulse_high_ticks / 2), 1L); + + if (tick % pulse_high_ticks == 0) { + gravity.pulse.High(); + } + if (pulse_low_ticks % pulse_high_ticks == 0) { + gravity.pulse.Low(); + } + } + if (!app.editing_param) { app.refresh_screen |= refresh; } @@ -197,6 +223,13 @@ void editMainParameter(int val) { gravity.clock.SetSource(app.selected_source); break; } + case PARAM_MAIN_PULSE: + byte pulse = static_cast(app.selected_pulse); + updateSelection(pulse, val, Clock::PULSE_LAST); + app.selected_pulse = static_cast(pulse); + if (app.selected_pulse == Clock::PULSE_NONE) { + gravity.pulse.Low(); + } case PARAM_MAIN_ENCODER_DIR: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 201ac72..062ac3c 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -16,6 +16,7 @@ struct AppState { byte selected_channel = 0; // 0=tempo, 1-6=output channel byte selected_shuffle = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; + Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; Channel channel[Gravity::OUTPUT_COUNT]; }; @@ -28,6 +29,7 @@ static Channel& GetSelectedChannel() { enum ParamsMainPage { PARAM_MAIN_TEMPO, PARAM_MAIN_SOURCE, + PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, PARAM_MAIN_RESET_STATE, PARAM_MAIN_LAST, diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 5578d42..9acbb70 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -3,6 +3,7 @@ #include #include + #include "euclidean.h" // Enums for CV configuration @@ -66,7 +67,7 @@ class Channel { } } - void setProbability(int prob) { + void setProbability(int prob) { base_probability = constrain(prob, 0, 100); if (!isCvModActive()) { cvmod_probability = base_probability; @@ -74,20 +75,20 @@ class Channel { } void setDutyCycle(int duty) { - base_duty_cycle = constrain(duty, 1, 99); + base_duty_cycle = constrain(duty, 1, 99); if (!isCvModActive()) { cvmod_duty_cycle = base_duty_cycle; } } - void setOffset(int off) { + void setOffset(int off) { base_offset = constrain(off, 0, 99); if (!isCvModActive()) { cvmod_offset = base_offset; } } void setSwing(int val) { - base_swing = constrain(val, 50, 95); + base_swing = constrain(val, 50, 95); if (!isCvModActive()) { cvmod_swing = base_swing; } @@ -141,12 +142,12 @@ class Channel { bool hit = cvmod_probability >= random(0, 100); // Euclidean rhythm check switch (pattern.NextStep()) { - case Pattern::REST: // Rest when active or fall back to probability - hit = false; - break; - case Pattern::HIT: // Hit if probability is true - hit &= true; - break; + case Pattern::REST: // Rest when active or fall back to probability + hit = false; + break; + case Pattern::HIT: // Hit if probability is true + hit &= true; + break; } if (hit) { output.High(); @@ -192,11 +193,11 @@ class Channel { (cv_destination == CV_DEST_SWING) ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) : base_swing; - + if (cv_destination == CV_DEST_EUC_STEPS) { pattern.SetSteps(map(value, -512, 512, 0, MAX_PATTERN_LEN)); } - + if (cv_destination == CV_DEST_EUC_HITS) { pattern.SetHits(map(value, -512, 512, 0, pattern.GetSteps())); } diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index c1dc144..038d4bf 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -68,16 +68,16 @@ static const unsigned char pause_icon[28] PROGMEM = { 0x38, 0x0E, 0x00, 0x00}; // Constants for screen layout and fonts -constexpr int SCREEN_CENTER_X = 32; -constexpr int MAIN_TEXT_Y = 26; -constexpr int SUB_TEXT_Y = 40; -constexpr int VISIBLE_MENU_ITEMS = 3; -constexpr int MENU_ITEM_HEIGHT = 14; -constexpr int MENU_BOX_PADDING = 4; -constexpr int MENU_BOX_WIDTH = 64; -constexpr int CHANNEL_BOXES_Y = 50; -constexpr int CHANNEL_BOX_WIDTH = 18; -constexpr int CHANNEL_BOX_HEIGHT = 14; +constexpr uint8_t SCREEN_CENTER_X = 32; +constexpr uint8_t MAIN_TEXT_Y = 26; +constexpr uint8_t SUB_TEXT_Y = 40; +constexpr uint8_t VISIBLE_MENU_ITEMS = 3; +constexpr uint8_t MENU_ITEM_HEIGHT = 14; +constexpr uint8_t MENU_BOX_PADDING = 4; +constexpr uint8_t MENU_BOX_WIDTH = 64; +constexpr uint8_t CHANNEL_BOXES_Y = 50; +constexpr uint8_t CHANNEL_BOX_WIDTH = 18; +constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; // Helper function to draw centered text void drawCenteredText(const char* text, int y, const uint8_t* font) { @@ -204,6 +204,23 @@ void DisplayMainPage() { break; } break; + case PARAM_MAIN_PULSE: + mainText = F("OUT"); + switch (app.selected_pulse) { + case Clock::PULSE_NONE: + subText = F("PULSE OFF"); + break; + case Clock::PULSE_PPQN_24: + subText = F("24 PPQN PULSE"); + break; + case Clock::PULSE_PPQN_4: + subText = F("4 PPQN PULSE"); + break; + case Clock::PULSE_PPQN_1: + subText = F("1 PPQN PULSE"); + break; + } + break; case PARAM_MAIN_ENCODER_DIR: mainText = F("DIR"); subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); @@ -218,7 +235,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("ENCODER DIR"), F("RESET")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("RESET")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -329,7 +346,7 @@ void DisplayChannelPage() { // Draw Channel Page menu items String menu_items[PARAM_CH_LAST] = { - F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("EUCLID STEPS"), + F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("EUCLID STEPS"), F("EUCLID HITS"), F("CV SOURCE"), F("CV DEST")}; drawMenuItems(menu_items, PARAM_CH_LAST); } diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index aca6977..c435df8 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -17,6 +17,7 @@ bool StateManager::initialize(AppState& app) { app.selected_param = load_data.selected_param; app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); + app.selected_pulse = static_cast(load_data.selected_pulse); // Loop through and restore each channel's state. for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { @@ -54,6 +55,7 @@ void StateManager::reset(AppState& app) { app.selected_param = 0; app.selected_channel = 0; app.selected_source = Clock::SOURCE_INTERNAL; + app.selected_pulse = Clock::PULSE_PPQN_24; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); @@ -61,7 +63,7 @@ void StateManager::reset(AppState& app) { noInterrupts(); _saveMetadata(); // Write the new metadata - _saveState(app); // Write the new (default) app state + _saveState(app); // Write the new (default) app state interrupts(); _isDirty = false; @@ -97,6 +99,7 @@ void StateManager::_saveState(const AppState& app) { save_data.selected_param = app.selected_param; save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); + save_data.selected_pulse = static_cast(app.selected_pulse); // Loop through and populate each channel's state for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index d37a6ae..8a2659e 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -54,6 +54,7 @@ class StateManager { byte selected_param; byte selected_channel; byte selected_source; + byte selected_pulse; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; diff --git a/gravity.cpp b/gravity.cpp index 658a960..3ef6d2b 100644 --- a/gravity.cpp +++ b/gravity.cpp @@ -12,7 +12,7 @@ #include "gravity.h" // Initialize the static pointer for the EncoderDir class to null. We want to -// have a static pointer to decouple the ISR from the global gravity object. +// have a static pointer to decouple the ISR from the global gravity object. Encoder* Encoder::_instance = nullptr; void Gravity::Init() { @@ -52,6 +52,8 @@ void Gravity::initOutputs() { outputs[3].Init(OUT_CH4); outputs[4].Init(OUT_CH5); outputs[5].Init(OUT_CH6); + // Expansion Pulse Output + pulse.Init(PULSE_OUT_PIN); } void Gravity::initDisplay() { // OLED Display configuration. diff --git a/gravity.h b/gravity.h index 1cc424b..5701586 100644 --- a/gravity.h +++ b/gravity.h @@ -32,7 +32,8 @@ class Gravity { U8G2_SSD1306_128X64_NONAME_1_HW_I2C display; // OLED display object. Clock clock; // Clock source wrapper. DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object. - Encoder encoder; // Rotary encoder with button instance + DigitalOutput pulse; // MIDI Expander module pulse output. + Encoder encoder; // Rotary encoder with button instance Button shift_button; Button play_button; AnalogInput cv1; diff --git a/uClock.cpp b/uClock.cpp new file mode 100755 index 0000000..1f53084 --- /dev/null +++ b/uClock.cpp @@ -0,0 +1,409 @@ +/*! + * @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.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @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"), + * 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" +#include "uClock/platforms/avr.h" + +// +// Platform specific timer setup/control +// +// initTimer(uint32_t us_interval) and setTimer(uint32_t us_interval) +// are called from architecture specific module included at the +// header of this file +void uclockInitTimer() +{ + // begin at 120bpm + initTimer(uClock.bpmToMicroSeconds(120.00)); +} + +void setTimerTempo(float bpm) +{ + setTimer(uClock.bpmToMicroSeconds(bpm)); +} + +namespace umodular { namespace clock { + +static inline uint32_t phase_mult(uint32_t val) +{ + return (val * PHASE_FACTOR) >> 8; +} + +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 + (4294967295 - old_clock); + } +} + +uClockClass::uClockClass() +{ + tempo = 120; + start_timer = 0; + last_interval = 0; + sync_interval = 0; + clock_state = PAUSED; + clock_mode = INTERNAL_CLOCK; + resetCounters(); + + onOutputPPQNCallback = nullptr; + onSync24Callback = nullptr; + onClockStartCallback = nullptr; + onClockStopCallback = nullptr; + // initialize reference data + calculateReferencedata(); +} + +void uClockClass::init() +{ + if (ext_interval_buffer == nullptr) + setExtIntervalBuffer(1); + + uclockInitTimer(); + // first interval calculus + setTempo(tempo); +} + +uint32_t uClockClass::bpmToMicroSeconds(float bpm) +{ + return (60000000.0f / (float)output_ppqn / bpm); +} + +void uClockClass::calculateReferencedata() +{ + mod_clock_ref = output_ppqn / input_ppqn; + mod_sync24_ref = output_ppqn / PPQN_24; +} + +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; + + ATOMIC( + output_ppqn = resolution; + calculateReferencedata(); + ) +} + +void uClockClass::setInputPPQN(PPQNResolution resolution) +{ + ATOMIC( + input_ppqn = resolution; + calculateReferencedata(); + ) +} + +void uClockClass::start() +{ + resetCounters(); + start_timer = millis(); + + if (onClockStartCallback) { + onClockStartCallback(); + } + + if (clock_mode == INTERNAL_CLOCK) { + clock_state = STARTED; + } else { + clock_state = STARTING; + } +} + +void uClockClass::stop() +{ + clock_state = PAUSED; + start_timer = 0; + resetCounters(); + if (onClockStopCallback) { + onClockStopCallback(); + } +} + +void uClockClass::pause() +{ + if (clock_mode == INTERNAL_CLOCK) { + if (clock_state == PAUSED) { + start(); + } else { + stop(); + } + } +} + +void uClockClass::setTempo(float bpm) +{ + if (clock_mode == EXTERNAL_CLOCK) { + return; + } + + if (bpm < MIN_BPM || bpm > MAX_BPM) { + return; + } + + ATOMIC( + tempo = bpm + ) + + setTimerTempo(bpm); +} + +float uClockClass::getTempo() +{ + if (clock_mode == EXTERNAL_CLOCK) { + uint32_t acc = 0; + // wait the buffer to get full + if (ext_interval_buffer[ext_interval_buffer_size-1] == 0) { + return tempo; + } + for (uint8_t i=0; i < ext_interval_buffer_size; i++) { + acc += ext_interval_buffer[i]; + } + if (acc != 0) { + return constrainBpm(freqToBpm(acc / ext_interval_buffer_size)); + } + } + return tempo; +} + +// for software timer implementation(fallback for no board support) +void uClockClass::run() {} + +float inline uClockClass::freqToBpm(uint32_t freq) +{ + return 60000000.0f / (float)(freq * input_ppqn); +} + +float inline uClockClass::constrainBpm(float bpm) +{ + return (bpm < MIN_BPM) ? MIN_BPM : ( bpm > MAX_BPM ? MAX_BPM : bpm ); +} + +void uClockClass::setClockMode(ClockMode tempo_mode) +{ + clock_mode = tempo_mode; +} + +uClockClass::ClockMode uClockClass::getClockMode() +{ + return clock_mode; +} + +void uClockClass::clockMe() +{ + if (clock_mode == EXTERNAL_CLOCK) { + ATOMIC( + handleExternalClock() + ) + } +} + +void uClockClass::setExtIntervalBuffer(uint8_t buffer_size) +{ + if (ext_interval_buffer != nullptr) + return; + + // alloc once and forever policy + ext_interval_buffer_size = buffer_size; + ext_interval_buffer = (uint32_t*) malloc( sizeof(uint32_t) * ext_interval_buffer_size ); +} + +void uClockClass::resetCounters() +{ + tick = 0; + int_clock_tick = 0; + mod_clock_counter = 0; + + mod_sync24_counter = 0; + sync24_tick = 0; + + ext_clock_tick = 0; + ext_clock_us = 0; + ext_interval_idx = 0; + + for (uint8_t i=0; i < ext_interval_buffer_size; i++) { + ext_interval_buffer[i] = 0; + } +} + +void uClockClass::handleExternalClock() +{ + switch (clock_state) { + case PAUSED: + break; + + case STARTING: + clock_state = STARTED; + 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; + + // external clock tick me! + ext_clock_tick++; + + // accumulate interval incomming ticks data for getTempo() smooth reads on slave clock_mode + if(++ext_interval_idx >= ext_interval_buffer_size) { + ext_interval_idx = 0; + } + ext_interval_buffer[ext_interval_idx] = last_interval; + + if (ext_clock_tick == 1) { + ext_interval = last_interval; + } else { + ext_interval = (((uint32_t)ext_interval * (uint32_t)PLL_X) + (uint32_t)(256 - PLL_X) * (uint32_t)last_interval) >> 8; + } + break; + } +} + +void uClockClass::handleTimerInt() +{ + // track main input clock counter + if (mod_clock_counter == mod_clock_ref) + mod_clock_counter = 0; + + // process sync signals first please... + if (mod_clock_counter == 0) { + + if (clock_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; + tick = int_clock_tick * mod_clock_ref; + mod_clock_counter = tick % mod_clock_ref; + } + + uint32_t counter = ext_interval; + uint32_t now_clock_us = micros(); + sync_interval = clock_diff(ext_clock_us, now_clock_us); + + 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 = constrainBpm(freqToBpm(counter)); + if (bpm != tempo) { + tempo = bpm; + setTimerTempo(bpm); + } + } + + // internal clock tick me! + ++int_clock_tick; + } + ++mod_clock_counter; + + // Sync24 callback + if (onSync24Callback) { + if (mod_sync24_counter == mod_sync24_ref) + mod_sync24_counter = 0; + if (mod_sync24_counter == 0) { + onSync24Callback(sync24_tick); + ++sync24_tick; + } + ++mod_sync24_counter; + } + + // main PPQNCallback + if (onOutputPPQNCallback) { + onOutputPPQNCallback(tick); + ++tick; + } +} + +// 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; +} + +} } // end namespace umodular::clock + +umodular::clock::uClockClass uClock; + +volatile uint32_t _millis = 0; + +// +// TIMER HANDLER +// +void uClockHandler() +{ + // global timer counter + _millis = millis(); + + if (uClock.clock_state == uClock.STARTED) { + uClock.handleTimerInt(); + } +} diff --git a/uClock.h b/uClock.h new file mode 100755 index 0000000..d8670b0 --- /dev/null +++ b/uClock.h @@ -0,0 +1,180 @@ +/*! + * @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.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @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"), + * 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 { + +#define MIN_BPM 1 +#define MAX_BPM 400 + +#define PHASE_FACTOR 16 +#define PLL_X 220 + +#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 { + PAUSED = 0, + STARTING, + 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; + + uClockClass(); + + void setOnOutputPPQN(void (*callback)(uint32_t tick)) { + onOutputPPQNCallback = callback; + } + + void setOnSync24(void (*callback)(uint32_t tick)) { + onSync24Callback = callback; + } + + void setOnClockStart(void (*callback)()) { + onClockStartCallback = callback; + } + + void setOnClockStop(void (*callback)()) { + onClockStopCallback = callback; + } + + void init(); + void setOutputPPQN(PPQNResolution resolution); + void setInputPPQN(PPQNResolution resolution); + + void handleTimerInt(); + void handleExternalClock(); + void resetCounters(); + + // external class control + void start(); + void stop(); + void pause(); + void setTempo(float bpm); + float getTempo(); + + // for software timer implementation(fallback for no board support) + void run(); + + // external timming control + void setClockMode(ClockMode tempo_mode); + ClockMode getClockMode(); + void clockMe(); + // 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(uint8_t buffer_size); + + // 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); + float inline constrainBpm(float bpm); + void calculateReferencedata(); + + void (*onOutputPPQNCallback)(uint32_t tick); + void (*onSync24Callback)(uint32_t tick); + void (*onClockStartCallback)(); + void (*onClockStopCallback)(); + + // clock input/output control + PPQNResolution output_ppqn = PPQN_96; + PPQNResolution input_ppqn = PPQN_24; + // output and internal counters, ticks and references + uint32_t tick; + uint32_t int_clock_tick; + uint8_t mod_clock_counter; + uint16_t mod_clock_ref; + + uint8_t mod_sync24_counter; + uint16_t mod_sync24_ref; + uint32_t sync24_tick; + + // 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; + ClockMode clock_mode; + + volatile uint32_t * ext_interval_buffer = nullptr; + uint8_t ext_interval_buffer_size; + uint16_t ext_interval_idx; +}; + +} } // end namespace umodular::clock + +extern umodular::clock::uClockClass uClock; + +extern "C" { + extern volatile uint32_t _millis; +} + +#endif /* __U_CLOCK_H__ */ diff --git a/uClock/platforms/avr.h b/uClock/platforms/avr.h new file mode 100644 index 0000000..c6a25d7 --- /dev/null +++ b/uClock/platforms/avr.h @@ -0,0 +1,98 @@ +/*! + * @file avr.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.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @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"), + * 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 + +#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; + ) +} \ No newline at end of file diff --git a/uClock/uClock.h b/uClock/uClock.h new file mode 100755 index 0000000..d8670b0 --- /dev/null +++ b/uClock/uClock.h @@ -0,0 +1,180 @@ +/*! + * @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.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @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"), + * 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 { + +#define MIN_BPM 1 +#define MAX_BPM 400 + +#define PHASE_FACTOR 16 +#define PLL_X 220 + +#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 { + PAUSED = 0, + STARTING, + 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; + + uClockClass(); + + void setOnOutputPPQN(void (*callback)(uint32_t tick)) { + onOutputPPQNCallback = callback; + } + + void setOnSync24(void (*callback)(uint32_t tick)) { + onSync24Callback = callback; + } + + void setOnClockStart(void (*callback)()) { + onClockStartCallback = callback; + } + + void setOnClockStop(void (*callback)()) { + onClockStopCallback = callback; + } + + void init(); + void setOutputPPQN(PPQNResolution resolution); + void setInputPPQN(PPQNResolution resolution); + + void handleTimerInt(); + void handleExternalClock(); + void resetCounters(); + + // external class control + void start(); + void stop(); + void pause(); + void setTempo(float bpm); + float getTempo(); + + // for software timer implementation(fallback for no board support) + void run(); + + // external timming control + void setClockMode(ClockMode tempo_mode); + ClockMode getClockMode(); + void clockMe(); + // 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(uint8_t buffer_size); + + // 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); + float inline constrainBpm(float bpm); + void calculateReferencedata(); + + void (*onOutputPPQNCallback)(uint32_t tick); + void (*onSync24Callback)(uint32_t tick); + void (*onClockStartCallback)(); + void (*onClockStopCallback)(); + + // clock input/output control + PPQNResolution output_ppqn = PPQN_96; + PPQNResolution input_ppqn = PPQN_24; + // output and internal counters, ticks and references + uint32_t tick; + uint32_t int_clock_tick; + uint8_t mod_clock_counter; + uint16_t mod_clock_ref; + + uint8_t mod_sync24_counter; + uint16_t mod_sync24_ref; + uint32_t sync24_tick; + + // 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; + ClockMode clock_mode; + + volatile uint32_t * ext_interval_buffer = nullptr; + uint8_t ext_interval_buffer_size; + uint16_t ext_interval_idx; +}; + +} } // end namespace umodular::clock + +extern umodular::clock::uClockClass uClock; + +extern "C" { + extern volatile uint32_t _millis; +} + +#endif /* __U_CLOCK_H__ */