Compare commits
18 Commits
v2.0.0beta
...
update-doc
| Author | SHA1 | Date | |
|---|---|---|---|
| c3ad3f0027 | |||
| 6e7a648c24 | |||
| 1acc9ac126 | |||
| 19473db67e | |||
| dd7217d04e | |||
| d1c8ee16a4 | |||
| 65dde4d62e | |||
| c7a3277b5f | |||
| fb44601707 | |||
| ec34bc3a7b | |||
| c5bddef66d | |||
| b0accdc83a | |||
| 1c0fb86bc1 | |||
| 01f32407f6 | |||
| 4f04137f67 | |||
| 1bf90e1674 | |||
| 5729eef037 | |||
| 05cf6022ed |
@ -1,88 +0,0 @@
|
||||
/**
|
||||
* @file analog_input.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Class for interacting with analog inputs.
|
||||
* @version 0.1
|
||||
* @date 2025-05-23
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
*/
|
||||
#ifndef ANALOG_INPUT_H
|
||||
#define ANALOG_INPUT_H
|
||||
|
||||
const int MAX_INPUT = (1 << 10) - 1; // Max 10 bit analog read resolution.
|
||||
|
||||
// estimated default calibration value
|
||||
const int CALIBRATED_LOW = -566;
|
||||
const int CALIBRATED_HIGH = 512;
|
||||
|
||||
class AnalogInput {
|
||||
public:
|
||||
AnalogInput() {}
|
||||
~AnalogInput() {}
|
||||
|
||||
/**
|
||||
* Initializes a analog input object.
|
||||
*
|
||||
* @param pin gpio pin for the analog input.
|
||||
*/
|
||||
void Init(uint8_t pin) {
|
||||
pinMode(pin, INPUT);
|
||||
pin_ = pin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the value of the analog input and set instance state.
|
||||
*
|
||||
*/
|
||||
void Process() {
|
||||
old_read_ = read_;
|
||||
int raw = analogRead(pin_);
|
||||
read_ = map(raw, 0, MAX_INPUT, low_, high_);
|
||||
read_ = constrain(read_ - offset_, -512, 512);
|
||||
if (inverted_) read_ = -read_;
|
||||
}
|
||||
|
||||
// Set calibration values.
|
||||
|
||||
void AdjustCalibrationLow(int amount) { low_ += amount; }
|
||||
|
||||
void AdjustCalibrationHigh(int amount) { high_ += amount; }
|
||||
|
||||
void SetOffset(float percent) { offset_ = -(percent)*512; }
|
||||
|
||||
void SetAttenuation(float percent) {
|
||||
low_ = abs(percent) * CALIBRATED_LOW;
|
||||
high_ = abs(percent) * CALIBRATED_HIGH;
|
||||
inverted_ = percent < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of the analog input within a range of +/-512.
|
||||
*
|
||||
* @return read value within a range of +/-512.
|
||||
*
|
||||
*/
|
||||
inline int16_t Read() { return read_; }
|
||||
|
||||
/**
|
||||
* Return the analog read value as voltage.
|
||||
*
|
||||
* @return A float representing the voltage (-5.0 to +5.0).
|
||||
*
|
||||
*/
|
||||
inline float Voltage() { return ((read_ / 512.0) * 5.0); }
|
||||
|
||||
private:
|
||||
uint8_t pin_;
|
||||
int16_t read_;
|
||||
uint16_t old_read_;
|
||||
// calibration values.
|
||||
int offset_ = 0;
|
||||
int low_ = CALIBRATED_LOW;
|
||||
int high_ = CALIBRATED_HIGH;
|
||||
bool inverted_ = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -17,7 +17,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include "gravity.h"
|
||||
#include <libGravity.h>
|
||||
|
||||
// Firmware state variables.
|
||||
struct Channel {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* @file Gravity.ino
|
||||
* @author Adam Wonak (https://github.com/awonak/)
|
||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||
* @version v2.0.1 - June 2025 awonak - Full rewrite
|
||||
* @version v2.0.0 - June 2025 awonak - Full rewrite
|
||||
* @version v1.0 - August 2023 Oleksiy H - Initial release
|
||||
* @date 2025-07-04
|
||||
*
|
||||
@ -25,7 +25,7 @@
|
||||
* quantization of features like duty cycle (pulse width) or offset.
|
||||
* Additionally, this firmware replaces the sequencer with a Euclidean Rhythm
|
||||
* generator.
|
||||
*
|
||||
*
|
||||
* ENCODER:
|
||||
* Press: change between selecting a parameter and editing the parameter.
|
||||
* Hold & Rotate: change current selected output channel.
|
||||
@ -33,20 +33,22 @@
|
||||
* BTN1:
|
||||
* Play/pause - start or stop the internal clock.
|
||||
*
|
||||
* BTN2:
|
||||
* BTN2:
|
||||
* Shift - hold and rotate encoder to change current selected output channel.
|
||||
*
|
||||
* EXT:
|
||||
* External clock input. When Gravity is set to INTERNAL clock mode, this
|
||||
* input is used to reset clocks.
|
||||
*
|
||||
* External clock input. When Gravity is set to INTERNAL or MIDI clock
|
||||
* source, this input is used to reset clocks.
|
||||
*
|
||||
* CV1:
|
||||
* CV2:
|
||||
* External analog input used to provide modulation to any channel parameter.
|
||||
*
|
||||
* CV2:
|
||||
* External analog input used to provide modulation to any channel parameter.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <gravity.h>
|
||||
#include <libGravity.h>
|
||||
|
||||
#include "app_state.h"
|
||||
#include "channel.h"
|
||||
@ -64,6 +66,10 @@ void setup() {
|
||||
// Start Gravity.
|
||||
gravity.Init();
|
||||
|
||||
// Show bootsplash when initializing firmware.
|
||||
Bootsplash();
|
||||
delay(2000);
|
||||
|
||||
// Initialize the state manager. This will load settings from EEPROM
|
||||
stateManager.initialize(app);
|
||||
InitGravity(app);
|
||||
@ -135,13 +141,12 @@ void HandleIntClockTick(uint32_t tick) {
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t pulse_high_ticks = CLOCK_MOD_PULSES[clock_index];
|
||||
const uint16_t pulse_high_ticks = pgm_read_word_near(&CLOCK_MOD_PULSES[clock_index]);
|
||||
const uint32_t pulse_low_ticks = tick + max((pulse_high_ticks / 2), 1L);
|
||||
|
||||
if (tick % pulse_high_ticks == 0) {
|
||||
gravity.pulse.High();
|
||||
}
|
||||
if (pulse_low_ticks % pulse_high_ticks == 0) {
|
||||
} else if (pulse_low_ticks % pulse_high_ticks == 0) {
|
||||
gravity.pulse.Low();
|
||||
}
|
||||
}
|
||||
@ -152,13 +157,16 @@ void HandleIntClockTick(uint32_t tick) {
|
||||
}
|
||||
|
||||
void HandleExtClockTick() {
|
||||
if (gravity.clock.InternalSource()) {
|
||||
// Use EXT as Reset when internally clocked.
|
||||
ResetOutputs();
|
||||
gravity.clock.Reset();
|
||||
} else {
|
||||
// Register clock tick.
|
||||
gravity.clock.Tick();
|
||||
switch (app.selected_source) {
|
||||
case Clock::SOURCE_INTERNAL:
|
||||
case Clock::SOURCE_EXTERNAL_MIDI:
|
||||
// Use EXT as Reset when not used for clock source.
|
||||
ResetOutputs();
|
||||
gravity.clock.Reset();
|
||||
break;
|
||||
default:
|
||||
// Register EXT cv clock tick.
|
||||
gravity.clock.Tick();
|
||||
}
|
||||
app.refresh_screen = true;
|
||||
}
|
||||
@ -168,6 +176,21 @@ void HandleExtClockTick() {
|
||||
//
|
||||
|
||||
void HandlePlayPressed() {
|
||||
// Check if SHIFT is pressed to mute all/current channel.
|
||||
if (gravity.shift_button.On()) {
|
||||
if (app.selected_channel == 0) {
|
||||
// Mute all channels
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
app.channel[i].toggleMute();
|
||||
}
|
||||
} else {
|
||||
// Mute selected channel
|
||||
auto& ch = GetSelectedChannel();
|
||||
ch.toggleMute();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
gravity.clock.IsPaused()
|
||||
? gravity.clock.Start()
|
||||
: gravity.clock.Stop();
|
||||
@ -181,17 +204,17 @@ void HandleEncoderPressed() {
|
||||
if (app.selected_channel == 0) { // main page
|
||||
// TODO: rewrite as switch
|
||||
if (app.selected_param == PARAM_MAIN_ENCODER_DIR) {
|
||||
bool reversed = app.selected_sub_param == 1;
|
||||
gravity.encoder.SetReverseDirection(reversed);
|
||||
app.encoder_reversed = app.selected_sub_param == 1;
|
||||
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
||||
}
|
||||
if (app.selected_param == PARAM_MAIN_SAVE_DATA) {
|
||||
if (app.selected_sub_param < MAX_SAVE_SLOTS) {
|
||||
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||
app.selected_save_slot = app.selected_sub_param;
|
||||
stateManager.saveData(app);
|
||||
}
|
||||
}
|
||||
if (app.selected_param == PARAM_MAIN_LOAD_DATA) {
|
||||
if (app.selected_sub_param < MAX_SAVE_SLOTS) {
|
||||
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||
app.selected_save_slot = app.selected_sub_param;
|
||||
stateManager.loadData(app, app.selected_save_slot);
|
||||
InitGravity(app);
|
||||
@ -203,6 +226,14 @@ void HandleEncoderPressed() {
|
||||
InitGravity(app);
|
||||
}
|
||||
}
|
||||
if (app.selected_param == PARAM_MAIN_FACTORY_RESET) {
|
||||
if (app.selected_sub_param == 0) { // Erase
|
||||
// Show bootsplash during slow erase operation.
|
||||
Bootsplash();
|
||||
stateManager.factoryReset(app);
|
||||
InitGravity(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only mark dirty and reset selected_sub_param when leaving editing mode.
|
||||
stateManager.markDirty();
|
||||
@ -272,11 +303,14 @@ void editMainParameter(int val) {
|
||||
break;
|
||||
case PARAM_MAIN_SAVE_DATA:
|
||||
case PARAM_MAIN_LOAD_DATA:
|
||||
updateSelection(app.selected_sub_param, val, MAX_SAVE_SLOTS + 1);
|
||||
updateSelection(app.selected_sub_param, val, StateManager::MAX_SAVE_SLOTS + 1);
|
||||
break;
|
||||
case PARAM_MAIN_RESET_STATE:
|
||||
updateSelection(app.selected_sub_param, val, 2);
|
||||
break;
|
||||
case PARAM_MAIN_FACTORY_RESET:
|
||||
updateSelection(app.selected_sub_param, val, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,16 +12,14 @@
|
||||
#ifndef APP_STATE_H
|
||||
#define APP_STATE_H
|
||||
|
||||
#include <gravity.h>
|
||||
#include <libGravity.h>
|
||||
|
||||
#include "channel.h"
|
||||
|
||||
// Global state for settings and app behavior.
|
||||
struct AppState {
|
||||
int tempo = Clock::DEFAULT_TEMPO;
|
||||
bool encoder_reversed = false;
|
||||
bool refresh_screen = true;
|
||||
bool editing_param = false;
|
||||
Channel channel[Gravity::OUTPUT_COUNT];
|
||||
byte selected_param = 0;
|
||||
byte selected_sub_param = 0; // Temporary value for editing params.
|
||||
byte selected_channel = 0; // 0=tempo, 1-6=output channel
|
||||
@ -29,7 +27,9 @@ struct AppState {
|
||||
byte selected_save_slot = 0; // The currently active save slot.
|
||||
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
|
||||
Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24;
|
||||
Channel channel[Gravity::OUTPUT_COUNT];
|
||||
bool editing_param = false;
|
||||
bool encoder_reversed = false;
|
||||
bool refresh_screen = true;
|
||||
};
|
||||
|
||||
extern AppState app;
|
||||
@ -38,28 +38,4 @@ static Channel& GetSelectedChannel() {
|
||||
return app.channel[app.selected_channel - 1];
|
||||
}
|
||||
|
||||
enum ParamsMainPage : uint8_t {
|
||||
PARAM_MAIN_TEMPO,
|
||||
PARAM_MAIN_SOURCE,
|
||||
PARAM_MAIN_PULSE,
|
||||
PARAM_MAIN_ENCODER_DIR,
|
||||
PARAM_MAIN_SAVE_DATA,
|
||||
PARAM_MAIN_LOAD_DATA,
|
||||
PARAM_MAIN_RESET_STATE,
|
||||
PARAM_MAIN_LAST,
|
||||
};
|
||||
|
||||
enum ParamsChannelPage : uint8_t {
|
||||
PARAM_CH_MOD,
|
||||
PARAM_CH_PROB,
|
||||
PARAM_CH_DUTY,
|
||||
PARAM_CH_OFFSET,
|
||||
PARAM_CH_SWING,
|
||||
PARAM_CH_EUC_STEPS,
|
||||
PARAM_CH_EUC_HITS,
|
||||
PARAM_CH_CV1_DEST,
|
||||
PARAM_CH_CV2_DEST,
|
||||
PARAM_CH_LAST,
|
||||
};
|
||||
|
||||
#endif // APP_STATE_H
|
||||
@ -13,7 +13,7 @@
|
||||
#define CHANNEL_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <gravity.h>
|
||||
#include <libGravity.h>
|
||||
|
||||
#include "euclidean.h"
|
||||
|
||||
@ -34,28 +34,28 @@ static const byte MOD_CHOICE_SIZE = 25;
|
||||
|
||||
// Negative numbers are multipliers, positive are divisors.
|
||||
static const int CLOCK_MOD[MOD_CHOICE_SIZE] PROGMEM = {
|
||||
// Multipliers
|
||||
-24, -16, -12, -8, -6, -4, -3, -2,
|
||||
// Internal Clock Unity
|
||||
1,
|
||||
// Divisors
|
||||
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 24, 32, 64, 128};
|
||||
128, 64, 32, 24, 16, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2,
|
||||
// Internal Clock Unity (quarter note)
|
||||
1,
|
||||
// Multipliers
|
||||
-2, -3, -4, -6, -8, -12, -16, -24};
|
||||
|
||||
// This represents the number of clock pulses for a 96 PPQN clock source
|
||||
// that match the above div/mult mods.
|
||||
static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = {
|
||||
// Multiplier Pulses (96 / X)
|
||||
4, 6, 8, 12, 16, 24, 32, 48,
|
||||
// Divisor Pulses (96 * X)
|
||||
12288, 6144, 3072, 2304, 1536, 1152, 1056, 960, 864, 768, 672, 576, 480, 384, 288, 192,
|
||||
// Internal Clock Pulses
|
||||
96,
|
||||
// Divisor Pulses (96 * X)
|
||||
192, 288, 384, 480, 576, 672, 768, 864, 960, 1056, 1152, 1536, 2304, 3072, 6144, 12288};
|
||||
// Multiplier Pulses (96 / X)
|
||||
48, 32, 24, 16, 12, 8, 6, 4};
|
||||
|
||||
static const byte DEFAULT_CLOCK_MOD_INDEX = 8; // x1 or 96 PPQN.
|
||||
static const byte DEFAULT_CLOCK_MOD_INDEX = 16; // x1 or 96 PPQN.
|
||||
|
||||
static const byte PULSE_PPQN_24_CLOCK_MOD_INDEX = 0;
|
||||
static const byte PULSE_PPQN_4_CLOCK_MOD_INDEX = 4;
|
||||
static const byte PULSE_PPQN_1_CLOCK_MOD_INDEX = 8;
|
||||
static const byte PULSE_PPQN_24_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 1;
|
||||
static const byte PULSE_PPQN_4_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 6;
|
||||
static const byte PULSE_PPQN_1_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 9;
|
||||
|
||||
class Channel {
|
||||
public:
|
||||
@ -161,6 +161,8 @@ class Channel {
|
||||
byte getSteps(bool withCvMod = false) const { return withCvMod ? pattern.GetSteps() : base_euc_steps; }
|
||||
byte getHits(bool withCvMod = false) const { return withCvMod ? pattern.GetHits() : base_euc_hits; }
|
||||
|
||||
void toggleMute() { mute = !mute; }
|
||||
|
||||
/**
|
||||
* @brief Processes a clock tick and determines if the output should be high or low.
|
||||
* Note: this method is called from an ISR and must be kept as simple as possible.
|
||||
@ -168,6 +170,12 @@ class Channel {
|
||||
* @param output The output object to be modified.
|
||||
*/
|
||||
void processClockTick(uint32_t tick, DigitalOutput& output) {
|
||||
// Mute check
|
||||
if (mute) {
|
||||
output.Low();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]);
|
||||
|
||||
// Conditionally apply swing on down beats.
|
||||
@ -247,7 +255,7 @@ class Channel {
|
||||
int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN);
|
||||
pattern.SetSteps(base_euc_steps + step_mod);
|
||||
|
||||
int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN);
|
||||
int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, pattern.GetSteps());
|
||||
pattern.SetHits(base_euc_hits + hit_mod);
|
||||
|
||||
// After all cvmod values are updated, recalculate clock pulse modifiers.
|
||||
@ -298,6 +306,9 @@ class Channel {
|
||||
// Euclidean pattern
|
||||
Pattern pattern;
|
||||
|
||||
// Mute channel flag
|
||||
bool mute;
|
||||
|
||||
// Pre-calculated pulse values for ISR performance
|
||||
uint16_t _duty_pulses;
|
||||
uint16_t _offset_pulses;
|
||||
|
||||
@ -96,6 +96,33 @@ constexpr uint8_t CHANNEL_BOXES_Y = 50;
|
||||
constexpr uint8_t CHANNEL_BOX_WIDTH = 18;
|
||||
constexpr uint8_t CHANNEL_BOX_HEIGHT = 14;
|
||||
|
||||
// Menu items for editing global parameters.
|
||||
enum ParamsMainPage : uint8_t {
|
||||
PARAM_MAIN_TEMPO,
|
||||
PARAM_MAIN_SOURCE,
|
||||
PARAM_MAIN_PULSE,
|
||||
PARAM_MAIN_ENCODER_DIR,
|
||||
PARAM_MAIN_SAVE_DATA,
|
||||
PARAM_MAIN_LOAD_DATA,
|
||||
PARAM_MAIN_RESET_STATE,
|
||||
PARAM_MAIN_FACTORY_RESET,
|
||||
PARAM_MAIN_LAST,
|
||||
};
|
||||
|
||||
// Menu items for editing channel parameters.
|
||||
enum ParamsChannelPage : uint8_t {
|
||||
PARAM_CH_MOD,
|
||||
PARAM_CH_PROB,
|
||||
PARAM_CH_DUTY,
|
||||
PARAM_CH_OFFSET,
|
||||
PARAM_CH_SWING,
|
||||
PARAM_CH_EUC_STEPS,
|
||||
PARAM_CH_EUC_HITS,
|
||||
PARAM_CH_CV1_DEST,
|
||||
PARAM_CH_CV2_DEST,
|
||||
PARAM_CH_LAST,
|
||||
};
|
||||
|
||||
// Helper function to draw centered text
|
||||
void drawCenteredText(const char* text, int y, const uint8_t* font) {
|
||||
gravity.display.setFont(font);
|
||||
@ -187,10 +214,10 @@ void swingDivisionMark() {
|
||||
|
||||
// Human friendly display value for save slot.
|
||||
String displaySaveSlot(int slot) {
|
||||
if (slot >= 0 && slot < MAX_SAVE_SLOTS / 2) {
|
||||
if (slot >= 0 && slot < StateManager::MAX_SAVE_SLOTS / 2) {
|
||||
return String("A") + String(slot + 1);
|
||||
} else if (slot >= MAX_SAVE_SLOTS / 2 && slot <= MAX_SAVE_SLOTS) {
|
||||
return String("B") + String(slot - (MAX_SAVE_SLOTS / 2) + 1);
|
||||
} else if (slot >= StateManager::MAX_SAVE_SLOTS / 2 && slot <= StateManager::MAX_SAVE_SLOTS) {
|
||||
return String("B") + String(slot - (StateManager::MAX_SAVE_SLOTS / 2) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,7 +283,7 @@ void DisplayMainPage() {
|
||||
break;
|
||||
case PARAM_MAIN_SAVE_DATA:
|
||||
case PARAM_MAIN_LOAD_DATA:
|
||||
if (app.selected_sub_param == MAX_SAVE_SLOTS) {
|
||||
if (app.selected_sub_param == StateManager::MAX_SAVE_SLOTS) {
|
||||
mainText = F("x");
|
||||
subText = F("BACK TO MAIN");
|
||||
} else {
|
||||
@ -278,13 +305,23 @@ void DisplayMainPage() {
|
||||
mainText = F("x");
|
||||
subText = F("BACK TO MAIN");
|
||||
}
|
||||
break;
|
||||
case PARAM_MAIN_FACTORY_RESET:
|
||||
if (app.selected_sub_param == 0) {
|
||||
mainText = F("DEL");
|
||||
subText = F("FACTORY RESET");
|
||||
} else {
|
||||
mainText = F("x");
|
||||
subText = F("BACK TO MAIN");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT);
|
||||
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("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET")};
|
||||
String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET"), F("ERASE")};
|
||||
drawMenuItems(menu_items, PARAM_MAIN_LAST);
|
||||
}
|
||||
|
||||
@ -428,7 +465,25 @@ void UpdateDisplay() {
|
||||
DisplayChannelPage();
|
||||
}
|
||||
// Global channel select UI.
|
||||
DisplaySelectedChannel();
|
||||
DisplaySelectedChannel();
|
||||
} while (gravity.display.nextPage());
|
||||
}
|
||||
|
||||
void Bootsplash() {
|
||||
gravity.display.firstPage();
|
||||
do {
|
||||
int textWidth;
|
||||
String loadingText = F("LOADING....");
|
||||
gravity.display.setFont(TEXT_FONT);
|
||||
|
||||
textWidth = gravity.display.getStrWidth(StateManager::SKETCH_NAME);
|
||||
gravity.display.drawStr(16 + (textWidth / 2), 20, StateManager::SKETCH_NAME);
|
||||
|
||||
textWidth = gravity.display.getStrWidth(StateManager::SEMANTIC_VERSION);
|
||||
gravity.display.drawStr(16 + (textWidth / 2), 32, StateManager::SEMANTIC_VERSION);
|
||||
|
||||
textWidth = gravity.display.getStrWidth(loadingText.c_str());
|
||||
gravity.display.drawStr(26 + (textWidth / 2), 44, loadingText.c_str());
|
||||
} while (gravity.display.nextPage());
|
||||
}
|
||||
|
||||
|
||||
@ -15,65 +15,86 @@
|
||||
|
||||
#include "app_state.h"
|
||||
|
||||
// Define the constants for the current firmware.
|
||||
const char StateManager::SKETCH_NAME[] = "ALT GRAVITY";
|
||||
const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA3"; // NOTE: This should match the version in the library.properties file.
|
||||
|
||||
// Number of available save slots.
|
||||
const byte StateManager::MAX_SAVE_SLOTS = 10;
|
||||
const byte StateManager::TRANSIENT_SLOT = 10;
|
||||
|
||||
// Define the minimum amount of time between EEPROM writes.
|
||||
const unsigned long StateManager::SAVE_DELAY_MS = 2000;
|
||||
|
||||
// Calculate the starting address for EepromData, leaving space for metadata.
|
||||
static const int EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata);
|
||||
const int StateManager::METADATA_START_ADDR = 0;
|
||||
const int StateManager::EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata);
|
||||
|
||||
StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {}
|
||||
|
||||
bool StateManager::initialize(AppState& app) {
|
||||
if (_isDataValid()) {
|
||||
// Load data from the transient slot.
|
||||
return loadData(app, MAX_SAVE_SLOTS);
|
||||
} else {
|
||||
// EEPROM does not contain save data for this firmware & version.
|
||||
// Initialize eeprom and save default patter to all save slots.
|
||||
reset(app);
|
||||
_saveMetadata();
|
||||
// MAX_SAVE_SLOTS slot is reserved for transient state.
|
||||
for (int i = 0; i <= MAX_SAVE_SLOTS; i++) {
|
||||
app.selected_save_slot = i;
|
||||
_saveState(app, i);
|
||||
}
|
||||
// Load global settings.
|
||||
_loadMetadata(app);
|
||||
// Load app data from the transient slot.
|
||||
_loadState(app, TRANSIENT_SLOT);
|
||||
return true;
|
||||
}
|
||||
// EEPROM does not contain save data for this firmware & version.
|
||||
else {
|
||||
// Erase EEPROM and initialize state. Save default pattern to all save slots.
|
||||
factoryReset(app);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool StateManager::loadData(AppState& app, byte slot_index) {
|
||||
if (slot_index >= MAX_SAVE_SLOTS) return false;
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
if (slot_index >= MAX_SAVE_SLOTS + 1) return false;
|
||||
|
||||
// Load the state data from the specified EEPROM slot and update the app state save slot.
|
||||
_loadState(app, slot_index);
|
||||
app.selected_save_slot = slot_index;
|
||||
// Persist this change in the global metadata.
|
||||
_saveMetadata(app);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save app state to user specified save slot.
|
||||
void StateManager::saveData(const AppState& app) {
|
||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS) return;
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
||||
|
||||
_saveState(app, app.selected_save_slot);
|
||||
_saveMetadata(app);
|
||||
_isDirty = false;
|
||||
}
|
||||
|
||||
// Save transient state if it has changed and enough time has passed since last save.
|
||||
void StateManager::update(const AppState& app) {
|
||||
if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) {
|
||||
// MAX_SAVE_SLOTS slot is reserved for transient state.
|
||||
_saveState(app, MAX_SAVE_SLOTS);
|
||||
_saveState(app, TRANSIENT_SLOT);
|
||||
_saveMetadata(app);
|
||||
_isDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void StateManager::reset(AppState& app) {
|
||||
app.tempo = Clock::DEFAULT_TEMPO;
|
||||
app.encoder_reversed = false;
|
||||
app.selected_param = 0;
|
||||
app.selected_channel = 0;
|
||||
app.selected_source = Clock::SOURCE_INTERNAL;
|
||||
app.selected_pulse = Clock::PULSE_PPQN_24;
|
||||
app.selected_save_slot = 0;
|
||||
AppState default_app;
|
||||
app.tempo = default_app.tempo;
|
||||
app.selected_param = default_app.selected_param;
|
||||
app.selected_channel = default_app.selected_channel;
|
||||
app.selected_source = default_app.selected_source;
|
||||
app.selected_pulse = default_app.selected_pulse;
|
||||
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
app.channel[i].Init();
|
||||
}
|
||||
|
||||
// Load global settings from Metadata
|
||||
_loadMetadata(app);
|
||||
|
||||
_isDirty = false;
|
||||
}
|
||||
|
||||
@ -82,28 +103,48 @@ void StateManager::markDirty() {
|
||||
_lastChangeTime = millis();
|
||||
}
|
||||
|
||||
// Erases all data in the EEPROM by writing 0 to every address.
|
||||
void StateManager::factoryReset(AppState& app) {
|
||||
noInterrupts();
|
||||
for (unsigned int i = 0; i < EEPROM.length(); i++) {
|
||||
EEPROM.write(i, 0);
|
||||
}
|
||||
// Initialize eeprom and save default patter to all save slots.
|
||||
_saveMetadata(app);
|
||||
reset(app);
|
||||
for (int i = 0; i < MAX_SAVE_SLOTS; i++) {
|
||||
app.selected_save_slot = i;
|
||||
_saveState(app, i);
|
||||
}
|
||||
_saveState(app, TRANSIENT_SLOT);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
bool StateManager::_isDataValid() {
|
||||
Metadata load_meta;
|
||||
EEPROM.get(0, load_meta);
|
||||
bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0);
|
||||
bool version_match = (load_meta.version == SKETCH_VERSION);
|
||||
Metadata metadata;
|
||||
EEPROM.get(METADATA_START_ADDR, metadata);
|
||||
bool name_match = (strcmp(metadata.sketch_name, SKETCH_NAME) == 0);
|
||||
bool version_match = (strcmp(metadata.version, SEMANTIC_VERSION) == 0);
|
||||
return name_match && version_match;
|
||||
}
|
||||
|
||||
void StateManager::_saveState(const AppState& app, byte slot_index) {
|
||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS) return;
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
||||
|
||||
noInterrupts();
|
||||
static EepromData save_data;
|
||||
|
||||
save_data.tempo = app.tempo;
|
||||
save_data.encoder_reversed = app.encoder_reversed;
|
||||
save_data.selected_param = app.selected_param;
|
||||
save_data.selected_channel = app.selected_channel;
|
||||
save_data.selected_source = static_cast<byte>(app.selected_source);
|
||||
save_data.selected_pulse = static_cast<byte>(app.selected_pulse);
|
||||
save_data.selected_save_slot = app.selected_save_slot;
|
||||
|
||||
// TODO: break this out into a separate function. Save State should be
|
||||
// broken out into global / per-channel save methods. When saving via
|
||||
// "update" only save state for the current channel since other channels
|
||||
// will not have changed when saving user edits.
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
const auto& ch = app.channel[i];
|
||||
auto& save_ch = save_data.channel_data[i];
|
||||
@ -124,6 +165,9 @@ void StateManager::_saveState(const AppState& app, byte slot_index) {
|
||||
}
|
||||
|
||||
void StateManager::_loadState(AppState& app, byte slot_index) {
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
if (slot_index >= MAX_SAVE_SLOTS + 1) return;
|
||||
|
||||
noInterrupts();
|
||||
static EepromData load_data;
|
||||
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
|
||||
@ -131,12 +175,10 @@ void StateManager::_loadState(AppState& app, byte slot_index) {
|
||||
|
||||
// Restore app state from loaded data.
|
||||
app.tempo = load_data.tempo;
|
||||
app.encoder_reversed = load_data.encoder_reversed;
|
||||
app.selected_param = load_data.selected_param;
|
||||
app.selected_channel = load_data.selected_channel;
|
||||
app.selected_source = static_cast<Clock::Source>(load_data.selected_source);
|
||||
app.selected_pulse = static_cast<Clock::Pulse>(load_data.selected_pulse);
|
||||
app.selected_save_slot = slot_index;
|
||||
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
auto& ch = app.channel[i];
|
||||
@ -155,11 +197,25 @@ void StateManager::_loadState(AppState& app, byte slot_index) {
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void StateManager::_saveMetadata() {
|
||||
void StateManager::_saveMetadata(const AppState& app) {
|
||||
noInterrupts();
|
||||
Metadata current_meta;
|
||||
strcpy(current_meta.sketch_name, SKETCH_NAME);
|
||||
current_meta.version = SKETCH_VERSION;
|
||||
EEPROM.put(0, current_meta);
|
||||
strcpy(current_meta.version, SEMANTIC_VERSION);
|
||||
|
||||
// Global user settings
|
||||
current_meta.selected_save_slot = app.selected_save_slot;
|
||||
current_meta.encoder_reversed = app.encoder_reversed;
|
||||
|
||||
EEPROM.put(METADATA_START_ADDR, current_meta);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void StateManager::_loadMetadata(AppState& app) {
|
||||
noInterrupts();
|
||||
Metadata metadata;
|
||||
EEPROM.get(METADATA_START_ADDR, metadata);
|
||||
app.selected_save_slot = metadata.selected_save_slot;
|
||||
app.encoder_reversed = metadata.encoder_reversed;
|
||||
interrupts();
|
||||
}
|
||||
@ -13,21 +13,11 @@
|
||||
#define SAVE_STATE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <gravity.h>
|
||||
#include <libGravity.h>
|
||||
|
||||
// Forward-declare AppState to avoid circular dependencies.
|
||||
struct AppState;
|
||||
|
||||
// Define the constants for the current firmware.
|
||||
const char SKETCH_NAME[] = "Gravity";
|
||||
const byte SKETCH_VERSION = 7;
|
||||
|
||||
// Number of available save slots.
|
||||
const byte MAX_SAVE_SLOTS = 10;
|
||||
|
||||
// Define the minimum amount of time between EEPROM writes.
|
||||
static const unsigned long SAVE_DELAY_MS = 2000;
|
||||
|
||||
/**
|
||||
* @brief Manages saving and loading of the application state to and from EEPROM.
|
||||
* The number of user slots is defined by MAX_SAVE_SLOTS, and one additional slot
|
||||
@ -38,6 +28,11 @@ static const unsigned long SAVE_DELAY_MS = 2000;
|
||||
*/
|
||||
class StateManager {
|
||||
public:
|
||||
static const char SKETCH_NAME[];
|
||||
static const char SEMANTIC_VERSION[];
|
||||
static const byte MAX_SAVE_SLOTS;
|
||||
static const byte TRANSIENT_SLOT;
|
||||
|
||||
StateManager();
|
||||
|
||||
// Populate the AppState instance with values from EEPROM if they exist.
|
||||
@ -52,11 +47,16 @@ class StateManager {
|
||||
void update(const AppState& app);
|
||||
// Indicate that state has changed and we should save.
|
||||
void markDirty();
|
||||
// Erase all data stored in the EEPROM.
|
||||
void factoryReset(AppState& app);
|
||||
|
||||
// This struct holds the data that identifies the firmware version.
|
||||
struct Metadata {
|
||||
byte version;
|
||||
char sketch_name[16];
|
||||
char version[16];
|
||||
// Additional global/hardware settings
|
||||
byte selected_save_slot;
|
||||
bool encoder_reversed;
|
||||
};
|
||||
struct ChannelState {
|
||||
byte base_clock_mod_index;
|
||||
@ -72,21 +72,24 @@ class StateManager {
|
||||
// This struct holds all the parameters we want to save.
|
||||
struct EepromData {
|
||||
int tempo;
|
||||
bool encoder_reversed;
|
||||
byte selected_param;
|
||||
byte selected_channel;
|
||||
byte selected_source;
|
||||
byte selected_pulse;
|
||||
byte selected_save_slot;
|
||||
ChannelState channel_data[Gravity::OUTPUT_COUNT];
|
||||
};
|
||||
|
||||
private:
|
||||
bool _isDataValid();
|
||||
void _saveMetadata();
|
||||
void _saveMetadata(const AppState& app);
|
||||
void _loadMetadata(AppState& app);
|
||||
void _saveState(const AppState& app, byte slot_index);
|
||||
void _loadState(AppState& app, byte slot_index);
|
||||
|
||||
static const unsigned long SAVE_DELAY_MS;
|
||||
static const int METADATA_START_ADDR;
|
||||
static const int EEPROM_DATA_START_ADDR;
|
||||
|
||||
bool _isDirty;
|
||||
unsigned long _lastChangeTime;
|
||||
};
|
||||
|
||||
10
library.properties
Normal file
10
library.properties
Normal file
@ -0,0 +1,10 @@
|
||||
name=libGravity
|
||||
version=2.0.0beta3
|
||||
author=Adam Wonak
|
||||
maintainer=awonak <github.com/awonak>
|
||||
sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module
|
||||
category=Other
|
||||
license=MIT
|
||||
url=https://github.com/awonak/libGravity
|
||||
architectures=avr
|
||||
depends=uClock,RotaryEncoder,U8g2
|
||||
343
src/README.md
Normal file
343
src/README.md
Normal file
@ -0,0 +1,343 @@
|
||||
# libGravity API Reference
|
||||
|
||||
This document provides API documentation for `libGravity`, a library for building custom scripts for the Sitka Instruments Gravity module.
|
||||
|
||||
## `Gravity` Class
|
||||
|
||||
The `Gravity` class is the main hardware abstraction wrapper for the module. It provides a central point of access to all of the module's hardware components like the display, clock, inputs, and outputs.
|
||||
|
||||
A global instance of this class, `gravity`, is created for you to use in your scripts.
|
||||
|
||||
```cpp
|
||||
// Global instance
|
||||
extern Gravity gravity;
|
||||
```
|
||||
|
||||
### Public Methods
|
||||
|
||||
#### `void Init()`
|
||||
|
||||
Initializes the Arduino and all the Gravity hardware components. This should be called once in your `setup()` function.
|
||||
|
||||
#### `void Process()`
|
||||
|
||||
Performs a polling check for state changes on all inputs and outputs. This should be called repeatedly in your main `loop()` function to ensure all components are responsive.
|
||||
|
||||
### Public Properties
|
||||
|
||||
* `U8G2_SSD1306_128X64_NONAME_1_HW_I2C display`
|
||||
* OLED display object from the `U8g2lib` library. Use this to draw to the screen.
|
||||
* `Clock clock`
|
||||
* The main clock source wrapper. See the [Clock Class](https://www.google.com/search?q=%23clock-class) documentation for details.
|
||||
* `DigitalOutput outputs[OUTPUT_COUNT]`
|
||||
* An array of `DigitalOutput` objects, where `OUTPUT_COUNT` is 6. Each element corresponds to one of the six gate/trigger outputs.
|
||||
* `DigitalOutput pulse`
|
||||
* A `DigitalOutput` object for the MIDI Expander module's pulse output.
|
||||
* `Encoder encoder`
|
||||
* The rotary encoder with a built-in push button. See the [Encoder Class](https://www.google.com/search?q=%23encoder-class) documentation for details.
|
||||
* `Button shift_button`
|
||||
* A `Button` object for the 'Shift' button.
|
||||
* `Button play_button`
|
||||
* A `Button` object for the 'Play' button.
|
||||
* `AnalogInput cv1`
|
||||
* An `AnalogInput` object for the CV1 input jack.
|
||||
* `AnalogInput cv2`
|
||||
* An `AnalogInput` object for the CV2 input jack.
|
||||
|
||||
## `AnalogInput` Class
|
||||
|
||||
This class handles reading and processing the analog CV inputs. It includes features for calibration, offsetting, and attenuation.
|
||||
|
||||
### Public Methods
|
||||
|
||||
#### `void Init(uint8_t pin)`
|
||||
|
||||
Initializes the analog input on a specific pin.
|
||||
|
||||
* **Parameters:**
|
||||
* `pin`: The GPIO pin for the analog input.
|
||||
|
||||
#### `void Process()`
|
||||
|
||||
Reads the raw value from the ADC, applies calibration, offset, and attenuation/inversion. This must be called regularly in the main loop.
|
||||
|
||||
#### `void AdjustCalibrationLow(int amount)`
|
||||
|
||||
Adjusts the low calibration point to fine-tune the input mapping.
|
||||
|
||||
* **Parameters:**
|
||||
* `amount`: The amount to add to the current low calibration value.
|
||||
|
||||
#### `void AdjustCalibrationHigh(int amount)`
|
||||
|
||||
Adjusts the high calibration point to fine-tune the input mapping.
|
||||
|
||||
* **Parameters:**
|
||||
* `amount`: The amount to add to the current high calibration value.
|
||||
|
||||
#### `void SetOffset(float percent)`
|
||||
|
||||
Sets a DC offset for the input signal.
|
||||
|
||||
* **Parameters:**
|
||||
* `percent`: A percentage (e.g., `0.5` for 50%) to shift the signal.
|
||||
|
||||
#### `void SetAttenuation(float percent)`
|
||||
|
||||
Sets the attenuation (scaling) of the input signal. A negative percentage will also invert the signal.
|
||||
|
||||
* **Parameters:**
|
||||
* `percent`: The attenuation level, typically from `0.0` to `1.0`.
|
||||
|
||||
#### `int16_t Read()`
|
||||
|
||||
Gets the current processed value of the analog input.
|
||||
|
||||
* **Returns:** The read value, scaled to a range of +/-512.
|
||||
|
||||
#### `float Voltage()`
|
||||
|
||||
Gets the analog read value as a voltage.
|
||||
|
||||
* **Returns:** A `float` representing the calculated voltage (-5.0V to +5.0V).
|
||||
|
||||
## `Button` Class
|
||||
|
||||
A wrapper class for handling digital inputs like push buttons, including debouncing and long-press detection.
|
||||
|
||||
### Enums
|
||||
|
||||
#### `enum ButtonChange`
|
||||
|
||||
Constants representing a change in the button's state.
|
||||
|
||||
* `CHANGE_UNCHANGED`
|
||||
* `CHANGE_PRESSED`
|
||||
* `CHANGE_RELEASED` (a normal, short press)
|
||||
* `CHANGE_RELEASED_LONG` (a long press)
|
||||
|
||||
### Public Methods
|
||||
|
||||
#### `void Init(uint8_t pin)`
|
||||
|
||||
Initializes the button on a specific GPIO pin.
|
||||
|
||||
* **Parameters:**
|
||||
* `pin`: The GPIO pin for the button.
|
||||
|
||||
#### `void AttachPressHandler(void (*f)())`
|
||||
|
||||
Attaches a callback function to be executed on a short button press.
|
||||
|
||||
* **Parameters:**
|
||||
* `f`: The function to call.
|
||||
|
||||
#### `void AttachLongPressHandler(void (*f)())`
|
||||
|
||||
Attaches a callback function to be executed on a long button press.
|
||||
|
||||
* **Parameters:**
|
||||
* `f`: The function to call.
|
||||
|
||||
#### `void Process()`
|
||||
|
||||
Reads the button's state and handles debouncing and press detection. Call this repeatedly in the main loop.
|
||||
|
||||
#### `ButtonChange Change()`
|
||||
|
||||
Gets the last state change of the button.
|
||||
|
||||
* **Returns:** A `ButtonChange` enum value indicating the last detected change.
|
||||
|
||||
#### `bool On()`
|
||||
|
||||
Checks the current physical state of the button.
|
||||
|
||||
* **Returns:** `true` if the button is currently being held down, `false` otherwise.
|
||||
|
||||
## `Clock` Class
|
||||
|
||||
A wrapper for all clock and timing functions, supporting internal, external, and MIDI clock sources.
|
||||
|
||||
### Enums
|
||||
|
||||
#### `enum Source`
|
||||
|
||||
Defines the possible clock sources.
|
||||
|
||||
* `SOURCE_INTERNAL`
|
||||
* `SOURCE_EXTERNAL_PPQN_24` (24 pulses per quarter note)
|
||||
* `SOURCE_EXTERNAL_PPQN_4` (4 pulses per quarter note)
|
||||
* `SOURCE_EXTERNAL_MIDI`
|
||||
|
||||
#### `enum Pulse`
|
||||
|
||||
Defines the possible pulse-per-quarter-note rates for the pulse output.
|
||||
|
||||
* `PULSE_NONE`
|
||||
* `PULSE_PPQN_1`
|
||||
* `PULSE_PPQN_4`
|
||||
* `PULSE_PPQN_24`
|
||||
|
||||
### Public Methods
|
||||
|
||||
#### `void Init()`
|
||||
|
||||
Initializes the clock, sets up MIDI serial, and sets default values.
|
||||
|
||||
#### `void AttachExtHandler(void (*callback)())`
|
||||
|
||||
Attaches a user-defined callback to the external clock input. This is triggered by a rising edge on the external clock pin or by an incoming MIDI clock message.
|
||||
|
||||
* **Parameters:**
|
||||
* `callback`: The function to call on an external clock event.
|
||||
|
||||
#### `void AttachIntHandler(void (*callback)(uint32_t))`
|
||||
|
||||
Sets a callback function that is triggered at the high-resolution internal clock rate (PPQN\_96). This is the main internal timing callback.
|
||||
|
||||
* **Parameters:**
|
||||
* `callback`: The function to call on every internal clock tick. It receives the tick count as a `uint32_t` parameter.
|
||||
|
||||
#### `void SetSource(Source source)`
|
||||
|
||||
Sets the clock's driving source.
|
||||
|
||||
* **Parameters:**
|
||||
* `source`: The new clock source from the `Source` enum.
|
||||
|
||||
#### `bool ExternalSource()`
|
||||
|
||||
Checks if the clock source is external.
|
||||
|
||||
* **Returns:** `true` if the source is external (PPQN or MIDI).
|
||||
|
||||
#### `bool InternalSource()`
|
||||
|
||||
Checks if the clock source is internal.
|
||||
|
||||
* **Returns:** `true` if the source is the internal master clock.
|
||||
|
||||
#### `int Tempo()`
|
||||
|
||||
Gets the current tempo.
|
||||
|
||||
* **Returns:** The current tempo in beats per minute (BPM).
|
||||
|
||||
#### `void SetTempo(int tempo)`
|
||||
|
||||
Sets the clock tempo when in internal mode.
|
||||
|
||||
* **Parameters:**
|
||||
* `tempo`: The new tempo in BPM.
|
||||
|
||||
#### `void Tick()`
|
||||
|
||||
Manually triggers a clock tick. This should be called from your external clock handler to drive the internal timing when in an external clock mode.
|
||||
|
||||
#### `void Start()`
|
||||
|
||||
Starts the clock.
|
||||
|
||||
#### `void Stop()`
|
||||
|
||||
Stops (pauses) the clock.
|
||||
|
||||
#### `void Reset()`
|
||||
|
||||
Resets all clock counters to zero.
|
||||
|
||||
#### `bool IsPaused()`
|
||||
|
||||
Checks if the clock is currently paused.
|
||||
|
||||
* **Returns:** `true` if the clock is stopped.
|
||||
|
||||
## `DigitalOutput` Class
|
||||
|
||||
This class is used to control the digital gate/trigger outputs.
|
||||
|
||||
### Public Methods
|
||||
|
||||
#### `void Init(uint8_t cv_pin)`
|
||||
|
||||
Initializes a digital output on a specific pin.
|
||||
|
||||
* **Parameters:**
|
||||
* `cv_pin`: The GPIO pin for the CV/Gate output.
|
||||
|
||||
#### `void SetTriggerDuration(uint8_t duration_ms)`
|
||||
|
||||
Sets the duration for triggers. When `Trigger()` is called, the output will remain high for this duration.
|
||||
|
||||
* **Parameters:**
|
||||
* `duration_ms`: The trigger duration in milliseconds.
|
||||
|
||||
#### `void Update(uint8_t state)`
|
||||
|
||||
Sets the output state directly.
|
||||
|
||||
* **Parameters:**
|
||||
* `state`: `HIGH` or `LOW`.
|
||||
|
||||
#### `void High()`
|
||||
|
||||
Sets the output to HIGH (approx. 5V).
|
||||
|
||||
#### `void Low()`
|
||||
|
||||
Sets the output to LOW (0V).
|
||||
|
||||
#### `void Trigger()`
|
||||
|
||||
Begins a trigger. The output goes HIGH and will automatically be set LOW after the configured trigger duration has elapsed (handled by `Process()`).
|
||||
|
||||
#### `void Process()`
|
||||
|
||||
Handles the timing for triggers. If an output was triggered, this method checks if the duration has elapsed and sets the output LOW if necessary. Call this in the main loop.
|
||||
|
||||
#### `bool On()`
|
||||
|
||||
Returns the current on/off state of the output.
|
||||
|
||||
* **Returns:** `true` if the output is currently HIGH.
|
||||
|
||||
## `Encoder` Class
|
||||
|
||||
Handles all interaction with the rotary encoder, including rotation, button presses, and rotation while pressed.
|
||||
|
||||
**Header:** `encoder_dir.h`
|
||||
|
||||
### Public Methods
|
||||
|
||||
#### `void SetReverseDirection(bool reversed)`
|
||||
|
||||
Sets the direction of the encoder.
|
||||
|
||||
* **Parameters:**
|
||||
* `reversed`: Set to `true` to reverse the direction of rotation.
|
||||
|
||||
#### `void AttachPressHandler(void (*f)())`
|
||||
|
||||
Attaches a callback for a simple press-and-release of the encoder button.
|
||||
|
||||
* **Parameters:**
|
||||
* `f`: The function to call on a button press.
|
||||
|
||||
#### `void AttachRotateHandler(void (*f)(int val))`
|
||||
|
||||
Attaches a callback for when the encoder is rotated (while the button is not pressed).
|
||||
|
||||
* **Parameters:**
|
||||
* `f`: The callback function. It receives an `int` representing the change in position (can be positive or negative).
|
||||
|
||||
#### `void AttachPressRotateHandler(void (*f)(int val))`
|
||||
|
||||
Attaches a callback for when the encoder is rotated while the button is being held down.
|
||||
|
||||
* **Parameters:**
|
||||
* `f`: The callback function. It receives an `int` representing the change in position.
|
||||
|
||||
#### `void Process()`
|
||||
|
||||
Processes encoder and button events. This method must be called repeatedly in the main loop to check for state changes and dispatch the appropriate callbacks.
|
||||
117
src/analog_input.h
Normal file
117
src/analog_input.h
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @file analog_input.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Class for interacting with analog inputs.
|
||||
* @version 0.1
|
||||
* @date 2025-05-23
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
*/
|
||||
#ifndef ANALOG_INPUT_H
|
||||
#define ANALOG_INPUT_H
|
||||
|
||||
const int MAX_INPUT = (1 << 10) - 1; // Max 10 bit analog read resolution.
|
||||
|
||||
// Estimated default calibration value
|
||||
// TODO: This should be set by metadata via calibration.
|
||||
const int CALIBRATED_LOW = -566;
|
||||
const int CALIBRATED_HIGH = 512;
|
||||
|
||||
/**
|
||||
* @brief Class for interacting with analog inputs (CV).
|
||||
*/
|
||||
class AnalogInput {
|
||||
public:
|
||||
AnalogInput() {}
|
||||
~AnalogInput() {}
|
||||
|
||||
/**
|
||||
* @brief Initializes an analog input object.
|
||||
*
|
||||
* @param pin The GPIO pin for the analog input.
|
||||
*/
|
||||
void Init(uint8_t pin) {
|
||||
pinMode(pin, INPUT);
|
||||
pin_ = pin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads and processes the analog input.
|
||||
*
|
||||
* This method reads the raw value from the ADC, applies the current
|
||||
* calibration, offset, and attenuation/inversion settings. It should be
|
||||
* called regularly in the main loop to update the input's state.
|
||||
*/
|
||||
void Process() {
|
||||
old_read_ = read_;
|
||||
int raw = analogRead(pin_);
|
||||
read_ = map(raw, 0, MAX_INPUT, low_, high_);
|
||||
read_ = constrain(read_ - offset_, -512, 512);
|
||||
if (inverted_) read_ = -read_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adjusts the low calibration point.
|
||||
*
|
||||
* This is used to fine-tune the mapping of the raw analog input to the output range.
|
||||
*
|
||||
* @param amount The amount to add to the current low calibration value.
|
||||
*/
|
||||
void AdjustCalibrationLow(int amount) { low_ += amount; }
|
||||
|
||||
/**
|
||||
* @brief Adjusts the high calibration point.
|
||||
*
|
||||
* This is used to fine-tune the mapping of the raw analog input to the output range.
|
||||
*
|
||||
* @param amount The amount to add to the current high calibration value.
|
||||
*/
|
||||
void AdjustCalibrationHigh(int amount) { high_ += amount; }
|
||||
|
||||
/**
|
||||
* @brief Sets a DC offset for the input.
|
||||
*
|
||||
* @param percent A percentage (e.g., 0.5 for 50%) to shift the signal.
|
||||
*/
|
||||
void SetOffset(float percent) { offset_ = -(percent)*512; }
|
||||
|
||||
/**
|
||||
* @brief Sets the attenuation (scaling) of the input signal.
|
||||
*
|
||||
* This scales the input signal. A negative percentage will also invert the signal.
|
||||
*
|
||||
* @param percent The attenuation level, typically from 0.0 to 1.0.
|
||||
*/
|
||||
void SetAttenuation(float percent) {
|
||||
low_ = abs(percent) * CALIBRATED_LOW;
|
||||
high_ = abs(percent) * CALIBRATED_HIGH;
|
||||
inverted_ = percent < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the current processed value of the analog input.
|
||||
*
|
||||
* @return The read value within a range of +/-512.
|
||||
*/
|
||||
inline int16_t Read() { return read_; }
|
||||
|
||||
/**
|
||||
* @brief Return the analog read value as a voltage.
|
||||
*
|
||||
* @return A float representing the calculated voltage (-5.0 to +5.0).
|
||||
*/
|
||||
inline float Voltage() { return ((read_ / 512.0) * 5.0); }
|
||||
|
||||
private:
|
||||
uint8_t pin_;
|
||||
int16_t read_;
|
||||
uint16_t old_read_;
|
||||
// calibration values.
|
||||
int offset_ = 0;
|
||||
int low_ = CALIBRATED_LOW;
|
||||
int high_ = CALIBRATED_HIGH;
|
||||
bool inverted_ = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -4,7 +4,7 @@
|
||||
* @brief Wrapper class for interacting with trigger / gate inputs.
|
||||
* @version 0.1
|
||||
* @date 2025-04-20
|
||||
*
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
*/
|
||||
@ -13,14 +13,14 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const uint8_t DEBOUNCE_MS = 10;
|
||||
const uint16_t LONG_PRESS_DURATION_MS = 750;
|
||||
|
||||
class Button {
|
||||
protected:
|
||||
typedef void (*CallbackFunction)(void);
|
||||
|
||||
public:
|
||||
static const uint8_t DEBOUNCE_MS = 10;
|
||||
static const uint16_t LONG_PRESS_DURATION_MS = 750;
|
||||
|
||||
// Enum constants for active change in button state.
|
||||
enum ButtonChange {
|
||||
CHANGE_UNCHANGED,
|
||||
@ -84,7 +84,7 @@ class Button {
|
||||
if (on_long_press_ != NULL) on_long_press_();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update variables for next loop
|
||||
last_press_ = (pressed || released) ? millis() : last_press_;
|
||||
old_read_ = read;
|
||||
@ -4,7 +4,7 @@
|
||||
* @brief Wrapper Class for clock timing functions.
|
||||
* @version 0.1
|
||||
* @date 2025-05-04
|
||||
*
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
*/
|
||||
@ -15,7 +15,7 @@
|
||||
#include <NeoHWSerial.h>
|
||||
|
||||
#include "peripherials.h"
|
||||
#include "uClock.h"
|
||||
#include "uClock/uClock.h"
|
||||
|
||||
// MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards.
|
||||
#define MIDI_CLOCK 0xF8
|
||||
@ -27,6 +27,9 @@ typedef void (*ExtCallback)(void);
|
||||
static ExtCallback extUserCallback = nullptr;
|
||||
static void serialEventNoop(uint8_t msg, uint8_t status) {}
|
||||
|
||||
/**
|
||||
* @brief Wrapper Class for clock timing functions.
|
||||
*/
|
||||
class Clock {
|
||||
public:
|
||||
static constexpr int DEFAULT_TEMPO = 120;
|
||||
@ -47,12 +50,12 @@ class Clock {
|
||||
PULSE_LAST,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Initializes the clock, MIDI serial, and sets default values.
|
||||
*/
|
||||
void Init() {
|
||||
NeoSerial.begin(31250);
|
||||
|
||||
// Static pin definition for pulse out.
|
||||
pinMode(PULSE_OUT_PIN, OUTPUT);
|
||||
|
||||
// Initialize the clock library
|
||||
uClock.init();
|
||||
uClock.setClockMode(uClock.INTERNAL_CLOCK);
|
||||
@ -67,18 +70,36 @@ class Clock {
|
||||
uClock.start();
|
||||
}
|
||||
|
||||
// Handle external clock tick and call user callback when receiving clock trigger (PPQN_4, PPQN_24, or MIDI).
|
||||
/**
|
||||
* @brief Attach a handler for external clock ticks.
|
||||
*
|
||||
* This function attaches a user-defined callback to the external clock input pin interrupt.
|
||||
* It is also called for incoming MIDI clock events.
|
||||
*
|
||||
* @param callback Function to call on an external clock event.
|
||||
*/
|
||||
void AttachExtHandler(void (*callback)()) {
|
||||
extUserCallback = callback;
|
||||
attachInterrupt(digitalPinToInterrupt(EXT_PIN), callback, RISING);
|
||||
}
|
||||
|
||||
// Internal PPQN96 callback for all clock timer operations.
|
||||
/**
|
||||
* @brief Attach a handler for the internal high-resolution clock.
|
||||
*
|
||||
* Sets a callback function that is triggered at the internal PPQN_96 rate. This is the
|
||||
* main internal timing callback for all clock operations.
|
||||
*
|
||||
* @param callback Function to call on every internal clock tick. It receives the tick count as a parameter.
|
||||
*/
|
||||
void AttachIntHandler(void (*callback)(uint32_t)) {
|
||||
uClock.setOnOutputPPQN(callback);
|
||||
}
|
||||
|
||||
// Set the source of the clock mode.
|
||||
/**
|
||||
* @brief Set the source of the clock.
|
||||
*
|
||||
* @param source The new source for driving the clock. See the `Source` enum.
|
||||
*/
|
||||
void SetSource(Source source) {
|
||||
bool was_playing = !IsPaused();
|
||||
uClock.stop();
|
||||
@ -110,47 +131,81 @@ class Clock {
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if the current selected source is externl (PPQN_4, PPQN_24, or MIDI).
|
||||
/**
|
||||
* @brief Checks if the clock source is external.
|
||||
*
|
||||
* @return true if the current source is external (PPQN_4, PPQN_24, or MIDI).
|
||||
* @return false if the source is internal.
|
||||
*/
|
||||
bool ExternalSource() {
|
||||
return uClock.getClockMode() == uClock.EXTERNAL_CLOCK;
|
||||
}
|
||||
|
||||
// Return true if the current selected source is the internal master clock.
|
||||
/**
|
||||
* @brief Checks if the clock source is internal.
|
||||
*
|
||||
* @return true if the current source is the internal master clock.
|
||||
* @return false if the source is external.
|
||||
*/
|
||||
bool InternalSource() {
|
||||
return uClock.getClockMode() == uClock.INTERNAL_CLOCK;
|
||||
}
|
||||
|
||||
// Returns the current BPM tempo.
|
||||
/**
|
||||
* @brief Gets the current tempo.
|
||||
*
|
||||
* @return int The current tempo in beats per minute (BPM).
|
||||
*/
|
||||
int Tempo() {
|
||||
return uClock.getTempo();
|
||||
}
|
||||
|
||||
// Set the clock tempo to a int between 1 and 400.
|
||||
/**
|
||||
* @brief Set the clock tempo.
|
||||
*
|
||||
* @param tempo The new tempo in beats per minute (BPM).
|
||||
*/
|
||||
void SetTempo(int tempo) {
|
||||
return uClock.setTempo(tempo);
|
||||
}
|
||||
|
||||
// Record an external clock tick received to process external/internal syncronization.
|
||||
/**
|
||||
* @brief Manually trigger a clock tick.
|
||||
*
|
||||
* This should be called when in an external clock mode to register an incoming
|
||||
* clock pulse and drive the internal timing.
|
||||
*/
|
||||
void Tick() {
|
||||
uClock.clockMe();
|
||||
}
|
||||
|
||||
// Start the internal clock.
|
||||
/**
|
||||
* @brief Starts the clock.
|
||||
*/
|
||||
void Start() {
|
||||
uClock.start();
|
||||
}
|
||||
|
||||
// Stop internal clock clock.
|
||||
/**
|
||||
* @brief Stops (pauses) the clock.
|
||||
*/
|
||||
void Stop() {
|
||||
uClock.stop();
|
||||
}
|
||||
|
||||
// Reset all clock counters to 0.
|
||||
/**
|
||||
* @brief Resets all clock counters to zero.
|
||||
*/
|
||||
void Reset() {
|
||||
uClock.resetCounters();
|
||||
}
|
||||
|
||||
// Returns true if the clock is not running.
|
||||
/**
|
||||
* @brief Checks if the clock is currently paused.
|
||||
*
|
||||
* @return true if the clock is stopped/paused.
|
||||
* @return false if the clock is running.
|
||||
*/
|
||||
bool IsPaused() {
|
||||
return uClock.clock_state == uClock.PAUSED;
|
||||
}
|
||||
@ -13,10 +13,10 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const byte DEFAULT_TRIGGER_DURATION_MS = 5;
|
||||
|
||||
class DigitalOutput {
|
||||
public:
|
||||
static const byte DEFAULT_TRIGGER_DURATION_MS = 5;
|
||||
|
||||
/**
|
||||
* Initializes an CV Output paired object.
|
||||
*
|
||||
@ -4,10 +4,11 @@
|
||||
* @brief Class for interacting with encoders.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
*
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ENCODER_DIR_H
|
||||
#define ENCODER_DIR_H
|
||||
|
||||
@ -16,6 +17,9 @@
|
||||
#include "button.h"
|
||||
#include "peripherials.h"
|
||||
|
||||
/**
|
||||
* @brief Class for interacting with a rotary encoder that has a push button.
|
||||
*/
|
||||
class Encoder {
|
||||
protected:
|
||||
typedef void (*CallbackFunction)(void);
|
||||
@ -32,22 +36,57 @@ class Encoder {
|
||||
}
|
||||
~Encoder() {}
|
||||
|
||||
// Set to true if the encoder read direction should be reversed.
|
||||
/**
|
||||
* @brief Set the direction of the encoder.
|
||||
*
|
||||
* @param reversed Set to true to reverse the direction of rotation.
|
||||
*/
|
||||
void SetReverseDirection(bool reversed) {
|
||||
reversed_ = reversed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attach a handler for the encoder button press.
|
||||
*
|
||||
* This callback is triggered on a simple press and release of the button,
|
||||
* without any rotation occurring during the press.
|
||||
*
|
||||
* @param f The callback function to execute when a button press.
|
||||
*/
|
||||
void AttachPressHandler(CallbackFunction f) {
|
||||
on_press = f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attach a handler for encoder rotation.
|
||||
*
|
||||
* This callback is triggered when the encoder is rotated while the button is not pressed.
|
||||
*
|
||||
* @param f The callback function to execute on rotation. It receives an integer
|
||||
* representing the change in position (can be positive or negative).
|
||||
*/
|
||||
void AttachRotateHandler(RotateCallbackFunction f) {
|
||||
on_rotate = f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attach a handler for rotation while the button is pressed.
|
||||
*
|
||||
* This callback is triggered when the encoder is rotated while the button is being held down.
|
||||
*
|
||||
* @param f The callback function to execute. It receives an integer
|
||||
* representing the change in position.
|
||||
*/
|
||||
void AttachPressRotateHandler(RotateCallbackFunction f) {
|
||||
on_press_rotate = f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Processes encoder and button events.
|
||||
*
|
||||
* This method should be called repeatedly in the main loop to check for state
|
||||
* changes (rotation, button presses) and dispatch the appropriate callbacks.
|
||||
*/
|
||||
void Process() {
|
||||
// Get encoder position change amount.
|
||||
int encoder_rotated = _rotate_change() != 0;
|
||||
@ -91,7 +130,6 @@ class Encoder {
|
||||
int position = encoder_.getPosition();
|
||||
unsigned long ms = encoder_.getMillisBetweenRotations();
|
||||
|
||||
// Validation (TODO: add debounce check).
|
||||
if (previous_pos_ == position) {
|
||||
return 0;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file gravity.cpp
|
||||
* @file libGravity.cpp
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
||||
* @version 0.1
|
||||
@ -9,7 +9,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include "gravity.h"
|
||||
#include "libGravity.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.
|
||||
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file gravity.h
|
||||
* @file libGravity.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
||||
* @version 0.1
|
||||
@ -32,7 +32,7 @@
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#include "uClock.h"
|
||||
#include "uClock/platforms/avr.h"
|
||||
#include "platforms/avr.h"
|
||||
|
||||
//
|
||||
// Platform specific timer setup/control
|
||||
180
uClock/uClock.h
180
uClock/uClock.h
@ -1,180 +0,0 @@
|
||||
/*!
|
||||
* @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 <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
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__ */
|
||||
Reference in New Issue
Block a user