diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 0df2363..c990c99 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -6,20 +6,20 @@ * @date 2025-05-04 * * @copyright Copyright (c) 2025 - * + * * This version of Gravity firmware is a full rewrite that leverages the * libGravity hardware abstraction library. The goal of this project was to * create an open source friendly version of the firmware that makes it easy * for users/developers to modify and create their own original alt firmware - * implementations. - * - * The libGravity library represents wrappers around the + * implementations. + * + * The libGravity library represents wrappers around the * hardware peripherials to make it easy to interact with and add behavior - * to them. The library tries not to make any assumptions about what the + * to them. The library tries not to make any assumptions about what the * firmware can or should do. - * + * * The Gravity firmware is a slightly different implementation of the original - * firmware. There are a few notable changes; the internal clock operates at + * firmware. There are a few notable changes; the internal clock operates at * 96 PPQN instead of the original 24 PPQN, which allows for more granular * quantization of features like duty cycle (pulse width) or offset. * Additionally, this firmware replaces the sequencer with a Euclidean Rhythm @@ -172,34 +172,36 @@ void HandleEncoderPressed() { // Check if leaving editing mode should apply a selection. if (app.editing_param) { 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); } - else if (app.selected_param == PARAM_MAIN_SAVE_DATA) { - if (app.selected_sub_param != 0) { + if (app.selected_param == PARAM_MAIN_SAVE_DATA) { + if (app.selected_sub_param < MAX_SAVE_SLOTS) { app.selected_save_slot = app.selected_sub_param - 1; - stateManager.saveData(app, app.selected_save_slot); + stateManager.saveData(app); } } - else if (app.selected_param == PARAM_MAIN_LOAD_DATA) { - if (app.selected_sub_param != 0) { + if (app.selected_param == PARAM_MAIN_LOAD_DATA) { + if (app.selected_sub_param < MAX_SAVE_SLOTS) { app.selected_save_slot = app.selected_sub_param - 1; stateManager.loadData(app, app.selected_save_slot); InitGravity(app); } } - else if (app.selected_param == PARAM_MAIN_RESET_STATE) { + if (app.selected_param == PARAM_MAIN_RESET_STATE) { if (app.selected_sub_param == 0) { // Reset stateManager.reset(app); InitGravity(app); } } } - // Only mark dirty when leaving editing mode. + // Only mark dirty and reset selected_sub_param when leaving editing mode. stateManager.markDirty(); + app.selected_sub_param = 0; } - app.selected_sub_param = 0; + app.editing_param = !app.editing_param; app.refresh_screen = true; } @@ -236,7 +238,6 @@ void editMainParameter(int val) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); app.tempo = gravity.clock.Tempo(); break; - case PARAM_MAIN_SOURCE: { byte source = static_cast(app.selected_source); updateSelection(source, val, Clock::SOURCE_LAST); @@ -244,7 +245,7 @@ void editMainParameter(int val) { gravity.clock.SetSource(app.selected_source); break; } - case PARAM_MAIN_PULSE: + case PARAM_MAIN_PULSE: { byte pulse = static_cast(app.selected_pulse); updateSelection(pulse, val, Clock::PULSE_LAST); app.selected_pulse = static_cast(pulse); @@ -252,6 +253,7 @@ void editMainParameter(int val) { gravity.pulse.Low(); } break; + } 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 d1f119c..c52fc26 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -12,10 +12,10 @@ struct AppState { bool refresh_screen = true; bool editing_param = false; byte selected_param = 0; - byte selected_sub_param = 0; - byte selected_channel = 0; // 0=tempo, 1-6=output channel - byte selected_shuffle = 0; - byte selected_save_slot = 0; // The currently active save slot. + byte selected_sub_param = 0; // Temporary value for editing params. + byte selected_channel = 0; // 0=tempo, 1-6=output channel + byte selected_swing = 0; + 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]; diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index c898871..e07657d 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -4,6 +4,7 @@ #include #include "app_state.h" +#include "save_state.h" // // UI Display functions for drawing the UI to the OLED display. @@ -169,6 +170,15 @@ void swingDivisionMark() { } } +// Human friendly display value for save slot. +String displaySaveSlot(int slot) { + if (slot >= 0 && slot <= 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)); + } +} + // Main display functions void DisplayMainPage() { @@ -230,27 +240,25 @@ void DisplayMainPage() { subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); break; case PARAM_MAIN_SAVE_DATA: - mainText = "SAVE"; - if (app.selected_sub_param == 0) { - subText = F("BACK"); - } else { - subText = F("TO SLOT "); - subText += String(app.selected_sub_param + 1); - } - break; case PARAM_MAIN_LOAD_DATA: - mainText = "LOAD"; - if (app.selected_sub_param == 0) { - subText = F("BACK"); + if (app.selected_sub_param == MAX_SAVE_SLOTS) { + mainText = F("x"); + subText = F("BACK TO MAIN"); } else { - subText = F("FROM SLOT "); - subText += String(app.selected_sub_param + 1); + mainText = displaySaveSlot(app.selected_sub_param); + subText = (app.selected_param == PARAM_MAIN_SAVE_DATA) + ? F("SAVE TO SLOT") + : F("LOAD FROM SLOT"); } break; case PARAM_MAIN_RESET_STATE: - mainText = F("RST"); - subText = app.selected_sub_param == 0 ? F("BACK") : F("RESET ALL"); - break; + if (app.selected_sub_param == 0) { + mainText = F("RST"); + subText = F("RESET ALL"); + } else { + mainText = F("x"); + subText = F("BACK TO MAIN"); + } } drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index 84b312e..bf61730 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -18,6 +18,7 @@ bool StateManager::initialize(AppState& app) { // Load data from the last active slot. return loadData(app, app.selected_save_slot); } else { + // TODO: save default state to all save slots. reset(app); return false; } @@ -49,7 +50,7 @@ bool StateManager::loadData(AppState& app, byte slot_index) { ch.setProbability(saved_ch_state.base_probability); ch.setDutyCycle(saved_ch_state.base_duty_cycle); ch.setOffset(saved_ch_state.base_offset); - ch.setSwing(saved_ch_state.base_shuffle); + ch.setSwing(saved_ch_state.base_swing); ch.setSteps(saved_ch_state.base_euc_steps); ch.setHits(saved_ch_state.base_euc_hits); ch.setCv1Dest(static_cast(saved_ch_state.cv1_dest)); @@ -59,21 +60,22 @@ bool StateManager::loadData(AppState& app, byte slot_index) { return true; } -void StateManager::saveData(const AppState& app, byte slot_index) { - if (slot_index >= MAX_SAVE_SLOTS) return; +void StateManager::saveData(const AppState& app) { + if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; - _save(app, slot_index); + _save(app); _isDirty = false; } void StateManager::update(const AppState& app) { if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { - _save(app, app.selected_save_slot); + _save(app); _isDirty = false; } } void StateManager::reset(AppState& app) { + noInterrupts(); app.tempo = Clock::DEFAULT_TEMPO; app.encoder_reversed = false; app.selected_param = 0; @@ -87,6 +89,7 @@ void StateManager::reset(AppState& app) { } // TODO: Should this overwrite save slot 0? + interrupts(); _isDirty = false; } @@ -104,15 +107,15 @@ bool StateManager::_isDataValid() { return name_match && version_match; } -void StateManager::_save(const AppState& app, byte slot_index) { +void StateManager::_save(const AppState& app) { noInterrupts(); - _saveState(app, slot_index); - _saveMetadata(slot_index); + _saveState(app); + _saveMetadata(app.selected_save_slot); interrupts(); } -void StateManager::_saveState(const AppState& app, byte slot_index) { - if (slot_index >= MAX_SAVE_SLOTS) return; +void StateManager::_saveState(const AppState& app) { + if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; static EepromData save_data; @@ -122,6 +125,7 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { 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); + save_data.selected_save_slot = app.selected_save_slot; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { const auto& ch = app.channel[i]; @@ -130,14 +134,14 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { save_ch.base_probability = ch.getProbability(false); save_ch.base_duty_cycle = ch.getDutyCycle(false); save_ch.base_offset = ch.getOffset(false); - save_ch.base_shuffle = ch.getSwing(false); + save_ch.base_swing = ch.getSwing(false); save_ch.base_euc_steps = ch.getSteps(false); save_ch.base_euc_hits = ch.getHits(false); save_ch.cv1_dest = static_cast(ch.getCv1Dest()); save_ch.cv2_dest = static_cast(ch.getCv2Dest()); } - int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData)); + int address = EEPROM_DATA_START_ADDR + (app.selected_save_slot * sizeof(EepromData)); EEPROM.put(address, save_data); } diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index e110843..b77e6be 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -9,10 +9,10 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 6; +const byte SKETCH_VERSION = 7; // Number of available save slots. -const byte MAX_SAVE_SLOTS = 16; +const byte MAX_SAVE_SLOTS = 8; // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; @@ -28,8 +28,8 @@ class StateManager { bool initialize(AppState& app); // Load data from specified slot. bool loadData(AppState& app, byte slot_index); - // Save data to specified slot. - void saveData(const AppState& app, byte slot_index); + // Save data to slot defined by app. + void saveData(const AppState& app); // Reset AppState instance back to default values. void reset(AppState& app); // Call from main loop, check if state has changed and needs to be saved. @@ -49,7 +49,7 @@ class StateManager { byte base_probability; byte base_duty_cycle; byte base_offset; - byte base_shuffle; + byte base_swing; byte base_euc_steps; byte base_euc_hits; byte cv1_dest; // Cast the CvDestination enum as a byte for storage @@ -63,12 +63,13 @@ class StateManager { byte selected_channel; byte selected_source; byte selected_pulse; + byte selected_save_slot; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; private: bool _isDataValid(); - void _save(const AppState& app, byte slot_index); - void _saveState(const AppState& app, byte slot_index); + void _save(const AppState& app); + void _saveState(const AppState& app); void _saveMetadata(byte active_slot); bool _isDirty;