Add clock run/reset

This commit is contained in:
2026-02-21 10:19:09 -08:00
parent 763d58f411
commit bd08ac4352
12 changed files with 324 additions and 162 deletions

View File

@ -107,6 +107,28 @@ void loop() {
}
}
// Clock Run
if (app.cv_run == 1 || app.cv_run == 2) {
auto &cv = app.cv_run == 1 ? gravity.cv1 : gravity.cv2;
int val = cv.Read();
if (val > AnalogInput::GATE_THRESHOLD && gravity.clock.IsPaused()) {
gravity.clock.Start();
app.refresh_screen = true;
} else if (val < AnalogInput::GATE_THRESHOLD && !gravity.clock.IsPaused()) {
gravity.clock.Stop();
ResetOutputs();
app.refresh_screen = true;
}
}
// Clock Reset
if ((app.cv_reset == 1 &&
gravity.cv1.IsRisingEdge(AnalogInput::GATE_THRESHOLD)) ||
(app.cv_reset == 2 &&
gravity.cv2.IsRisingEdge(AnalogInput::GATE_THRESHOLD))) {
gravity.clock.Reset();
}
// Check for dirty state eligible to be saved.
stateManager.update(app);
@ -285,6 +307,14 @@ void editMainParameter(int val) {
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
app.tempo = gravity.clock.Tempo();
break;
case PARAM_MAIN_RUN:
updateSelection(app.selected_sub_param, val, 3);
app.cv_run = app.selected_sub_param;
break;
case PARAM_MAIN_RESET:
updateSelection(app.selected_sub_param, val, 3);
app.cv_reset = app.selected_sub_param;
break;
case PARAM_MAIN_SOURCE: {
byte source = static_cast<int>(app.selected_source);
updateSelection(source, val, Clock::SOURCE_LAST);
@ -301,6 +331,7 @@ void editMainParameter(int val) {
}
break;
}
// These changes are applied upon encoder button press.
case PARAM_MAIN_ENCODER_DIR:
updateSelection(app.selected_sub_param, val, 2);
break;

View File

@ -27,6 +27,8 @@ 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;
byte cv_run = 0;
byte cv_reset = 0;
bool editing_param = false;
bool encoder_reversed = false;
bool refresh_screen = true;
@ -34,7 +36,7 @@ struct AppState {
extern AppState app;
static Channel& GetSelectedChannel() {
static Channel &GetSelectedChannel() {
return app.channel[app.selected_channel - 1];
}

View File

@ -127,6 +127,8 @@ constexpr uint8_t CHANNEL_BOX_HEIGHT = 14;
// Menu items for editing global parameters.
enum ParamsMainPage : uint8_t {
PARAM_MAIN_TEMPO,
PARAM_MAIN_RUN,
PARAM_MAIN_RESET,
PARAM_MAIN_SOURCE,
PARAM_MAIN_PULSE,
PARAM_MAIN_ENCODER_DIR,
@ -253,6 +255,34 @@ void DisplayMainPage() {
}
subText = F("BPM");
break;
case PARAM_MAIN_RUN:
mainText = F("RUN");
switch (app.cv_run) {
case 0:
subText = F("NONE");
break;
case 1:
subText = F("CV1 GATE");
break;
case 2:
subText = F("CV2 GATE");
break;
}
break;
case PARAM_MAIN_RESET:
mainText = F("RST");
switch (app.cv_reset) {
case 0:
subText = F("NONE");
break;
case 1:
subText = F("CV1 TRIG");
break;
case 2:
subText = F("CV2 TRIG");
break;
}
break;
case PARAM_MAIN_SOURCE:
mainText = F("EXT");
switch (app.selected_source) {
@ -339,8 +369,9 @@ void DisplayMainPage() {
// 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"), F("ERASE")};
F("TEMPO"), F("CLK RUN"), F("CLK RESET"), F("SOURCE"),
F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"),
F("RESET"), F("ERASE")};
drawMenuItems(menu_items, PARAM_MAIN_LAST);
}

View File

@ -18,7 +18,7 @@
// Define the constants for the current firmware.
const char StateManager::SKETCH_NAME[] = "ALT EUCLIDEAN";
const char StateManager::SEMANTIC_VERSION[] =
"V2.0.0BETA2"; // NOTE: This should match the version in the
"V2.0.0BETA3"; // NOTE: This should match the version in the
// library.properties file.
// Number of available save slots.
@ -94,6 +94,8 @@ void StateManager::reset(AppState &app) {
app.selected_channel = default_app.selected_channel;
app.selected_source = default_app.selected_source;
app.selected_pulse = default_app.selected_pulse;
app.cv_run = default_app.cv_run;
app.cv_reset = default_app.cv_reset;
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
app.channel[i].Init();
@ -148,6 +150,8 @@ void StateManager::_saveState(const AppState &app, byte slot_index) {
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.cv_run = app.cv_run;
save_data.cv_reset = app.cv_reset;
// TODO: break this out into a separate function. Save State should be
// broken out into global / per-channel save methods. When saving via
@ -184,6 +188,8 @@ void StateManager::_loadState(AppState &app, byte slot_index) {
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.cv_run = load_data.cv_run;
app.cv_reset = load_data.cv_reset;
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
auto &ch = app.channel[i];

View File

@ -72,6 +72,8 @@ public:
byte selected_channel;
byte selected_source;
byte selected_pulse;
byte cv_run;
byte cv_reset;
ChannelState channel_data[Gravity::OUTPUT_COUNT];
};

View File

@ -107,6 +107,28 @@ void loop() {
}
}
// Clock Run
if (app.cv_run == 1 || app.cv_run == 2) {
auto &cv = app.cv_run == 1 ? gravity.cv1 : gravity.cv2;
int val = cv.Read();
if (val > AnalogInput::GATE_THRESHOLD && gravity.clock.IsPaused()) {
gravity.clock.Start();
app.refresh_screen = true;
} else if (val < AnalogInput::GATE_THRESHOLD && !gravity.clock.IsPaused()) {
gravity.clock.Stop();
ResetOutputs();
app.refresh_screen = true;
}
}
// Clock Reset
if ((app.cv_reset == 1 &&
gravity.cv1.IsRisingEdge(AnalogInput::GATE_THRESHOLD)) ||
(app.cv_reset == 2 &&
gravity.cv2.IsRisingEdge(AnalogInput::GATE_THRESHOLD))) {
gravity.clock.Reset();
}
// Check for dirty state eligible to be saved.
stateManager.update(app);
@ -285,6 +307,14 @@ void editMainParameter(int val) {
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
app.tempo = gravity.clock.Tempo();
break;
case PARAM_MAIN_RUN:
updateSelection(app.selected_sub_param, val, 3);
app.cv_run = app.selected_sub_param;
break;
case PARAM_MAIN_RESET:
updateSelection(app.selected_sub_param, val, 3);
app.cv_reset = app.selected_sub_param;
break;
case PARAM_MAIN_SOURCE: {
byte source = static_cast<int>(app.selected_source);
updateSelection(source, val, Clock::SOURCE_LAST);
@ -301,6 +331,7 @@ void editMainParameter(int val) {
}
break;
}
// These changes are applied upon encoder button press.
case PARAM_MAIN_ENCODER_DIR:
updateSelection(app.selected_sub_param, val, 2);
break;

View File

@ -25,6 +25,8 @@ struct AppState {
byte selected_channel = 0; // 0=tempo, 1-6=output channel
byte selected_swing = 0;
byte selected_save_slot = 0; // The currently active save slot.
byte cv_run = 0;
byte cv_reset = 0;
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24;
bool editing_param = false;
@ -34,7 +36,7 @@ struct AppState {
extern AppState app;
static Channel& GetSelectedChannel() {
static Channel &GetSelectedChannel() {
return app.channel[app.selected_channel - 1];
}

View File

@ -128,6 +128,8 @@ constexpr uint8_t CHANNEL_BOX_HEIGHT = 14;
enum ParamsMainPage : uint8_t {
PARAM_MAIN_TEMPO,
PARAM_MAIN_SOURCE,
PARAM_MAIN_RUN,
PARAM_MAIN_RESET,
PARAM_MAIN_PULSE,
PARAM_MAIN_ENCODER_DIR,
PARAM_MAIN_SAVE_DATA,
@ -296,6 +298,34 @@ void DisplayMainPage() {
break;
}
break;
case PARAM_MAIN_RUN:
mainText = F("RUN");
switch (app.cv_run) {
case 0:
subText = F("NONE");
break;
case 1:
subText = F("CV 1");
break;
case 2:
subText = F("CV 2");
break;
}
break;
case PARAM_MAIN_RESET:
mainText = F("RST");
switch (app.cv_reset) {
case 0:
subText = F("NONE");
break;
case 1:
subText = F("CV 1");
break;
case 2:
subText = F("CV 2");
break;
}
break;
case PARAM_MAIN_PULSE:
mainText = F("OUT");
switch (app.selected_pulse) {
@ -358,8 +388,9 @@ void DisplayMainPage() {
// 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"), F("ERASE")};
F("TEMPO"), F("SOURCE"), F("CLK RUN"),
F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"),
F("SAVE"), F("LOAD"), F("ERASE")};
drawMenuItems(menu_items, PARAM_MAIN_LAST);
}

View File

@ -18,7 +18,7 @@
// Define the constants for the current firmware.
const char StateManager::SKETCH_NAME[] = "ALT GRAVITY";
const char StateManager::SEMANTIC_VERSION[] =
"V2.0.0BETA2"; // NOTE: This should match the version in the
"V2.0.0BETA4"; // NOTE: This should match the version in the
// library.properties file.
// Number of available save slots.
@ -94,6 +94,8 @@ void StateManager::reset(AppState &app) {
app.selected_channel = default_app.selected_channel;
app.selected_source = default_app.selected_source;
app.selected_pulse = default_app.selected_pulse;
app.cv_run = default_app.cv_run;
app.cv_reset = default_app.cv_reset;
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
app.channel[i].Init();
@ -148,6 +150,8 @@ void StateManager::_saveState(const AppState &app, byte slot_index) {
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.cv_run = app.cv_run;
save_data.cv_reset = app.cv_reset;
// TODO: break this out into a separate function. Save State should be
// broken out into global / per-channel save methods. When saving via
@ -185,6 +189,8 @@ void StateManager::_loadState(AppState &app, byte slot_index) {
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.cv_run = load_data.cv_run;
app.cv_reset = load_data.cv_reset;
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
auto &ch = app.channel[i];

View File

@ -73,6 +73,8 @@ public:
byte selected_channel;
byte selected_source;
byte selected_pulse;
byte cv_run;
byte cv_reset;
ChannelState channel_data[Gravity::OUTPUT_COUNT];
};

View File

@ -18,7 +18,9 @@ const int CALIBRATED_LOW = -566;
const int CALIBRATED_HIGH = 512;
class AnalogInput {
public:
public:
static const int GATE_THRESHOLD = 0;
AnalogInput() {}
~AnalogInput() {}
@ -41,7 +43,8 @@ class AnalogInput {
int raw = analogRead(pin_);
read_ = map(raw, 0, MAX_INPUT, low_, high_);
read_ = constrain(read_ - offset_, -512, 512);
if (inverted_) read_ = -read_;
if (inverted_)
read_ = -read_;
}
// Set calibration values.
@ -50,7 +53,7 @@ class AnalogInput {
void AdjustCalibrationHigh(int amount) { high_ += amount; }
void SetOffset(float percent) { offset_ = -(percent)*512; }
void SetOffset(float percent) { offset_ = -(percent) * 512; }
void SetAttenuation(float percent) {
low_ = abs(percent) * CALIBRATED_LOW;
@ -74,7 +77,20 @@ class AnalogInput {
*/
inline float Voltage() { return ((read_ / 512.0) * 5.0); }
private:
/**
* Checks for a rising edge transition across a threshold.
*
* @param threshold The value that the input must cross.
* @return True if the value just crossed the threshold from below, false
* otherwise.
*/
inline bool IsRisingEdge(int16_t threshold) const {
bool was_high = old_read_ > threshold;
bool is_high = read_ > threshold;
return is_high && !was_high;
}
private:
uint8_t pin_;
int16_t read_;
uint16_t old_read_;

View File

@ -16,7 +16,7 @@
const byte DEFAULT_TRIGGER_DURATION_MS = 5;
class DigitalOutput {
public:
public:
/**
* Initializes an CV Output paired object.
*
@ -43,8 +43,10 @@ class DigitalOutput {
* @param state Arduino digital HIGH or LOW values.
*/
inline void Update(uint8_t state) {
if (state == HIGH) High(); // Rising
if (state == LOW) Low(); // Falling
if (state == HIGH)
High(); // Rising
if (state == LOW)
Low(); // Falling
}
// Sets the cv output HIGH to about 5v.
@ -65,7 +67,8 @@ class DigitalOutput {
* Return a bool representing the on/off state of the output.
*/
inline void Process() {
// If trigger is HIGH and the trigger duration time has elapsed, set the output low.
// If trigger is HIGH and the trigger duration time has elapsed, set the
// output low.
if (on_ && (millis() - last_triggered_) >= trigger_duration_) {
update(LOW);
}
@ -78,11 +81,10 @@ class DigitalOutput {
*/
inline bool On() { return on_; }
private:
private:
unsigned long last_triggered_;
uint8_t trigger_duration_;
uint8_t cv_pin_;
uint8_t led_pin_;
bool on_;
void update(uint8_t state) {