Compare commits
28 Commits
bootsplash
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| acd028846c | |||
| ed625e75fc | |||
| b60dcc0e68 | |||
| 909d589609 | |||
| 330f5e6ceb | |||
| 87dacd869b | |||
| 64f467d6ac | |||
| 84cafe2387 | |||
| 8bb89a5f4b | |||
| 499bc7a643 | |||
| 3f670fa9f7 | |||
| b5029bde88 | |||
| 4bcd618073 | |||
| 6ada2aba30 | |||
| c5965aa1f7 | |||
| 7c02628403 | |||
| 1161da38c1 | |||
| 872af30fbc | |||
| fc17afc9a1 | |||
| b6402380c0 | |||
| 19473db67e | |||
| dd7217d04e | |||
| d1c8ee16a4 | |||
| 65dde4d62e | |||
| c7a3277b5f | |||
| fb44601707 | |||
| ec34bc3a7b | |||
| 01f32407f6 |
31
README.md
31
README.md
@ -1,6 +1,18 @@
|
|||||||
# Sitka Instruments Gravity Firmware Abstraction
|
# Sitka Instruments Gravity Firmware Abstraction
|
||||||
|
|
||||||
This library helps make writing firmware easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library.
|
This library helps make writing firmware for the [Sitka Instruments Gravity](https://sitkainstruments.com/gravity/) eurorack module easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library.
|
||||||
|
|
||||||
|
The latest releases of all Sitka Instruments Gravity firmware builds can be found on the [Updater](https://sitkainstruments.com/gravity/updater/) page. You can use this page to flash the latest build directly to the Arduino Nano on the back of your module.
|
||||||
|
|
||||||
|
## Project Code Layout
|
||||||
|
|
||||||
|
* [`src/`](src/) - **libGravity**: This is the hardware abstraction library used to simplify the creation of new Gravity module firmware by providing common reusable wrappers around the module peripherials like [DigitalOutput](src/digital_output.h#L18) providing methods like [`Update(uint8_t state)`](src/digital_output.h#L45) which allow you to set that output channel voltage high or low, and common module behavior like [Clock](src/clock.h#L30) which provides handlers like [`AttachExtHandler(callback)`](src/clock.h#L69) which takes a callback function to handle external clock tick behavior when receiving clock trigger.
|
||||||
|
|
||||||
|
* [`firmware/Gravity`](firmware/Gravity/) - **Alt Gravity**: This is the implementation of the default 6-channel trigger/gate clock modulation firmware. This is a full rewrite of the original firmware designed to use `libGravity` with a focus on open source friendlines.
|
||||||
|
|
||||||
|
* `firmware/GridSeq` - **GridSeq**: Comming Soon.
|
||||||
|
|
||||||
|
* [`examples/skeleton`](examples/skeleton/skeleton.ino) - **Skeleton**: This is the bare bones scaffloding for a `libGravity` firmware app.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -17,13 +29,14 @@ Common directory locations:
|
|||||||
* [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) 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.
|
* [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation.
|
||||||
* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library.
|
* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library.
|
||||||
|
* [NeoHWSerial](https://github.com/SlashDevin/NeoHWSerial) [GPL] - Hardware serial library with attachInterrupt.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation.
|
Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include "gravity.h"
|
#include "libGravity.h"
|
||||||
|
|
||||||
byte idx = 0;
|
byte idx = 0;
|
||||||
bool reversed = false;
|
bool reversed = false;
|
||||||
@ -75,11 +88,11 @@ void HandlePlayPressed() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleRotate(Direction dir, int val) {
|
void HandleRotate(int val) {
|
||||||
if (selected_param == 0) {
|
if (selected_param == 0) {
|
||||||
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
||||||
} else if (selected_param == 1) {
|
} else if (selected_param == 1) {
|
||||||
reversed = (dir == DIRECTION_DECREMENT);
|
reversed = (val < 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,8 +124,16 @@ void UpdateDisplay() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Building New Firmware Using libGravity**
|
||||||
|
|
||||||
|
When starting a new firmware sketch you can use the [skeleton](examples/skeleton/skeleton.ino) app as a place to start.
|
||||||
|
|
||||||
|
**Building New Firmware from scratch**
|
||||||
|
|
||||||
|
If you do not want to use the libGravity hardware abstraction library and want to roll your own vanilla firmware, take a look at the [peripherials.h](src/peripherials.h) file for the pinout definitions used by the module.
|
||||||
|
|
||||||
### Build for release
|
### Build for release
|
||||||
|
|
||||||
```
|
```
|
||||||
$ arduino-cli compile -v -b arduino:avr:nano ./firmware/Gravity/Gravity.ino -e --output-dir=./build/
|
$ arduino-cli compile -v -b arduino:avr:nano ./firmware/Gravity/Gravity.ino -e --output-dir=./build/
|
||||||
```
|
```
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
* TODO: Store the calibration value in EEPROM.
|
* TODO: Store the calibration value in EEPROM.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "gravity.h"
|
#include "libGravity.h"
|
||||||
|
|
||||||
#define TEXT_FONT u8g2_font_profont11_tf
|
#define TEXT_FONT u8g2_font_profont11_tf
|
||||||
#define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t
|
#define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t
|
||||||
@ -43,7 +43,7 @@ void NextCalibrationPoint() {
|
|||||||
selected_param = (selected_param + 1) % 6;
|
selected_param = (selected_param + 1) % 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibrateCV(Direction dir, int val) {
|
void CalibrateCV(int val) {
|
||||||
AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
||||||
switch (selected_param % 3) {
|
switch (selected_param % 3) {
|
||||||
case 0:
|
case 0:
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "gravity.h"
|
#include "libGravity.h"
|
||||||
|
|
||||||
#define TEXT_FONT u8g2_font_profont11_tf
|
#define TEXT_FONT u8g2_font_profont11_tf
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ void NextCalibrationPoint() {
|
|||||||
selected_param = (selected_param + 1) % 2;
|
selected_param = (selected_param + 1) % 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibrateCV(Direction dir, int val) {
|
void CalibrateCV(int val) {
|
||||||
// AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
// AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
||||||
AnalogInput* cv = &gravity.cv1;
|
AnalogInput* cv = &gravity.cv1;
|
||||||
switch (selected_param % 2) {
|
switch (selected_param % 2) {
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "gravity.h"
|
#include <libGravity.h>
|
||||||
|
|
||||||
// Firmware state variables.
|
// Firmware state variables.
|
||||||
struct Channel {
|
struct Channel {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#include "gravity.h"
|
#include "libGravity.h"
|
||||||
|
|
||||||
byte idx = 0;
|
byte idx = 0;
|
||||||
bool reversed = false;
|
bool reversed = false;
|
||||||
@ -33,28 +33,28 @@ void IntClock(uint32_t tick) {
|
|||||||
if (tick % 12 == 0 && ! freeze) {
|
if (tick % 12 == 0 && ! freeze) {
|
||||||
gravity.outputs[idx].Low();
|
gravity.outputs[idx].Low();
|
||||||
if (reversed) {
|
if (reversed) {
|
||||||
idx = (idx == 0) ? OUTPUT_COUNT - 1 : idx - 1;
|
idx = (idx == 0) ? Gravity::OUTPUT_COUNT - 1 : idx - 1;
|
||||||
} else {
|
} else {
|
||||||
idx = (idx + 1) % OUTPUT_COUNT;
|
idx = (idx + 1) % Gravity::OUTPUT_COUNT;
|
||||||
}
|
}
|
||||||
gravity.outputs[idx].High();
|
gravity.outputs[idx].High();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandlePlayPressed() {
|
void HandlePlayPressed() {
|
||||||
gravity.clock.Pause();
|
gravity.clock.Stop();
|
||||||
if (gravity.clock.IsPaused()) {
|
if (gravity.clock.IsPaused()) {
|
||||||
for (int i = 0; i < OUTPUT_COUNT; i++) {
|
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||||
gravity.outputs[i].Low();
|
gravity.outputs[i].Low();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleRotate(Direction dir, int val) {
|
void HandleRotate(int val) {
|
||||||
if (selected_param == 0) {
|
if (selected_param == 0) {
|
||||||
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
||||||
} else if (selected_param == 1) {
|
} else if (selected_param == 1) {
|
||||||
reversed = (dir == DIRECTION_DECREMENT);
|
reversed = (val < 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ void UpdateDisplay() {
|
|||||||
gravity.display.print("Direction: ");
|
gravity.display.print("Direction: ");
|
||||||
gravity.display.print((reversed) ? "Backward" : "Forward");
|
gravity.display.print((reversed) ? "Backward" : "Forward");
|
||||||
|
|
||||||
gravity.display.drawChar(0, selected_param * 10, 0x10, 1, 0, 1);
|
gravity.display.drawStr(0, selected_param * 10, "x");
|
||||||
|
|
||||||
gravity.display.display();
|
gravity.display.display();
|
||||||
}
|
}
|
||||||
118
examples/skeleton/skeleton.ino
Normal file
118
examples/skeleton/skeleton.ino
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* @file skeleton.ino
|
||||||
|
* @author YOUR_NAME (<url>)
|
||||||
|
* @brief Skeleton app for Sitka Instruments Gravity.
|
||||||
|
* @version vX.Y.Z - MONTH YEAR YOUR_NAME
|
||||||
|
* @date YYYY-MM-DD
|
||||||
|
*
|
||||||
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
|
*
|
||||||
|
* Skeleton app for basic structure of a new firmware for Sitka Instruments
|
||||||
|
* Gravity using the libGravity library.
|
||||||
|
*
|
||||||
|
* ENCODER:
|
||||||
|
* Press: change between selecting a parameter and editing the parameter.
|
||||||
|
* Hold & Rotate: change current selected output channel.
|
||||||
|
*
|
||||||
|
* BTN1:
|
||||||
|
* Play/pause - start or stop the internal clock.
|
||||||
|
*
|
||||||
|
* BTN2:
|
||||||
|
* Shift - hold and rotate encoder to change current selected output channel.
|
||||||
|
*
|
||||||
|
* EXT:
|
||||||
|
* External clock input. When Gravity is set to INTERNAL or MIDI clock
|
||||||
|
* source, this input is used to reset clocks.
|
||||||
|
*
|
||||||
|
* CV1:
|
||||||
|
* External analog input used to provide modulation to any channel parameter.
|
||||||
|
*
|
||||||
|
* CV2:
|
||||||
|
* External analog input used to provide modulation to any channel parameter.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <libGravity.h>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Global state for settings and app behavior.
|
||||||
|
struct AppState {
|
||||||
|
int tempo = Clock::DEFAULT_TEMPO;
|
||||||
|
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
|
||||||
|
// Add app specific state variables here.
|
||||||
|
};
|
||||||
|
|
||||||
|
AppState app;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Arduino setup and loop.
|
||||||
|
//
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Start Gravity.
|
||||||
|
gravity.Init();
|
||||||
|
|
||||||
|
// Clock handlers.
|
||||||
|
gravity.clock.AttachIntHandler(HandleIntClockTick);
|
||||||
|
gravity.clock.AttachExtHandler(HandleExtClockTick);
|
||||||
|
|
||||||
|
// Encoder rotate and press handlers.
|
||||||
|
gravity.encoder.AttachPressHandler(HandleEncoderPressed);
|
||||||
|
gravity.encoder.AttachRotateHandler(HandleRotate);
|
||||||
|
gravity.encoder.AttachPressRotateHandler(HandlePressedRotate);
|
||||||
|
|
||||||
|
// Button press handlers.
|
||||||
|
gravity.play_button.AttachPressHandler(HandlePlayPressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Process change in state of inputs and outputs.
|
||||||
|
gravity.Process();
|
||||||
|
|
||||||
|
// Non-ISR loop behavior.
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Firmware handlers for clocks.
|
||||||
|
//
|
||||||
|
|
||||||
|
void HandleIntClockTick(uint32_t tick) {
|
||||||
|
bool refresh = false;
|
||||||
|
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||||
|
// Process each output tick handlers.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleExtClockTick() {
|
||||||
|
switch (app.selected_source) {
|
||||||
|
case Clock::SOURCE_INTERNAL:
|
||||||
|
case Clock::SOURCE_EXTERNAL_MIDI:
|
||||||
|
// Use EXT as Reset when not used for clock source.
|
||||||
|
gravity.clock.Reset();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Register EXT cv clock tick.
|
||||||
|
gravity.clock.Tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// UI handlers for encoder and buttons.
|
||||||
|
//
|
||||||
|
|
||||||
|
void HandlePlayPressed() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleEncoderPressed() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleRotate(int val) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandlePressedRotate(int val) {
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Application logic goes here.
|
||||||
|
//
|
||||||
@ -2,7 +2,7 @@
|
|||||||
* @file Gravity.ino
|
* @file Gravity.ino
|
||||||
* @author Adam Wonak (https://github.com/awonak/)
|
* @author Adam Wonak (https://github.com/awonak/)
|
||||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||||
* @version v2.0.1 - June 2025 awonak - Full rewrite
|
* @version v2.0.0 - August 2025 awonak - Full rewrite
|
||||||
* @version v1.0 - August 2023 Oleksiy H - Initial release
|
* @version v1.0 - August 2023 Oleksiy H - Initial release
|
||||||
* @date 2025-07-04
|
* @date 2025-07-04
|
||||||
*
|
*
|
||||||
@ -37,16 +37,18 @@
|
|||||||
* Shift - hold and rotate encoder to change current selected output channel.
|
* Shift - hold and rotate encoder to change current selected output channel.
|
||||||
*
|
*
|
||||||
* EXT:
|
* EXT:
|
||||||
* External clock input. When Gravity is set to INTERNAL clock mode, this
|
* External clock input. When Gravity is set to INTERNAL or MIDI clock
|
||||||
* input is used to reset clocks.
|
* source, this input is used to reset clocks.
|
||||||
*
|
*
|
||||||
* CV1:
|
* CV1:
|
||||||
|
* External analog input used to provide modulation to any channel parameter.
|
||||||
|
*
|
||||||
* CV2:
|
* CV2:
|
||||||
* External analog input used to provide modulation to any channel parameter.
|
* External analog input used to provide modulation to any channel parameter.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <gravity.h>
|
#include <libGravity.h>
|
||||||
|
|
||||||
#include "app_state.h"
|
#include "app_state.h"
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
@ -64,10 +66,6 @@ void setup() {
|
|||||||
// Start Gravity.
|
// Start Gravity.
|
||||||
gravity.Init();
|
gravity.Init();
|
||||||
|
|
||||||
// Show bootsplash when initializing firmware.
|
|
||||||
Bootsplash();
|
|
||||||
delay(2000);
|
|
||||||
|
|
||||||
// Initialize the state manager. This will load settings from EEPROM
|
// Initialize the state manager. This will load settings from EEPROM
|
||||||
stateManager.initialize(app);
|
stateManager.initialize(app);
|
||||||
InitGravity(app);
|
InitGravity(app);
|
||||||
@ -89,18 +87,8 @@ void loop() {
|
|||||||
// Process change in state of inputs and outputs.
|
// Process change in state of inputs and outputs.
|
||||||
gravity.Process();
|
gravity.Process();
|
||||||
|
|
||||||
// Read CVs and call the update function for each channel.
|
// Check if cv run or reset is active and read cv.
|
||||||
int cv1 = gravity.cv1.Read();
|
CheckRunReset(gravity.cv1, gravity.cv2);
|
||||||
int cv2 = gravity.cv2.Read();
|
|
||||||
|
|
||||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
|
||||||
auto& ch = app.channel[i];
|
|
||||||
// Only apply CV to the channel when the current channel has cv
|
|
||||||
// mod configured.
|
|
||||||
if (ch.isCvModActive()) {
|
|
||||||
ch.applyCvMod(cv1, cv2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for dirty state eligible to be saved.
|
// Check for dirty state eligible to be saved.
|
||||||
stateManager.update(app);
|
stateManager.update(app);
|
||||||
@ -155,17 +143,41 @@ void HandleIntClockTick(uint32_t tick) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HandleExtClockTick() {
|
void HandleExtClockTick() {
|
||||||
if (gravity.clock.InternalSource()) {
|
switch (app.selected_source) {
|
||||||
// Use EXT as Reset when internally clocked.
|
case Clock::SOURCE_INTERNAL:
|
||||||
ResetOutputs();
|
case Clock::SOURCE_EXTERNAL_MIDI:
|
||||||
gravity.clock.Reset();
|
// Use EXT as Reset when not used for clock source.
|
||||||
} else {
|
ResetOutputs();
|
||||||
// Register clock tick.
|
gravity.clock.Reset();
|
||||||
gravity.clock.Tick();
|
break;
|
||||||
|
default:
|
||||||
|
// Register EXT cv clock tick.
|
||||||
|
gravity.clock.Tick();
|
||||||
}
|
}
|
||||||
app.refresh_screen = true;
|
app.refresh_screen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CheckRunReset(AnalogInput& cv1, AnalogInput& cv2) {
|
||||||
|
// Clock Run
|
||||||
|
if (app.cv_run == 1 || app.cv_run == 2) {
|
||||||
|
const int val = (app.cv_run == 1) ? cv1.Read() : cv2.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 && cv1.IsRisingEdge(AnalogInput::GATE_THRESHOLD)) ||
|
||||||
|
(app.cv_reset == 2 && cv2.IsRisingEdge(AnalogInput::GATE_THRESHOLD))) {
|
||||||
|
gravity.clock.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// UI handlers for encoder and buttons.
|
// UI handlers for encoder and buttons.
|
||||||
//
|
//
|
||||||
@ -197,37 +209,42 @@ void HandleEncoderPressed() {
|
|||||||
// Check if leaving editing mode should apply a selection.
|
// Check if leaving editing mode should apply a selection.
|
||||||
if (app.editing_param) {
|
if (app.editing_param) {
|
||||||
if (app.selected_channel == 0) { // main page
|
if (app.selected_channel == 0) { // main page
|
||||||
// TODO: rewrite as switch
|
switch (app.selected_param) {
|
||||||
if (app.selected_param == PARAM_MAIN_ENCODER_DIR) {
|
case PARAM_MAIN_ENCODER_DIR:
|
||||||
app.encoder_reversed = app.selected_sub_param == 1;
|
app.encoder_reversed = app.selected_sub_param == 1;
|
||||||
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
||||||
}
|
break;
|
||||||
if (app.selected_param == PARAM_MAIN_SAVE_DATA) {
|
case PARAM_MAIN_ROTATE_DISP:
|
||||||
if (app.selected_sub_param < MAX_SAVE_SLOTS) {
|
app.rotate_display = app.selected_sub_param == 1;
|
||||||
app.selected_save_slot = app.selected_sub_param;
|
gravity.display.setFlipMode(app.rotate_display ? 1 : 0);
|
||||||
stateManager.saveData(app);
|
break;
|
||||||
}
|
case PARAM_MAIN_SAVE_DATA:
|
||||||
}
|
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||||
if (app.selected_param == PARAM_MAIN_LOAD_DATA) {
|
app.selected_save_slot = app.selected_sub_param;
|
||||||
if (app.selected_sub_param < MAX_SAVE_SLOTS) {
|
stateManager.saveData(app);
|
||||||
app.selected_save_slot = app.selected_sub_param;
|
}
|
||||||
stateManager.loadData(app, app.selected_save_slot);
|
break;
|
||||||
InitGravity(app);
|
case PARAM_MAIN_LOAD_DATA:
|
||||||
}
|
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||||
}
|
app.selected_save_slot = app.selected_sub_param;
|
||||||
if (app.selected_param == PARAM_MAIN_RESET_STATE) {
|
// Load pattern data into app state.
|
||||||
if (app.selected_sub_param == 0) { // Reset
|
stateManager.loadData(app, app.selected_save_slot);
|
||||||
stateManager.reset(app);
|
// Load global performance settings if they have changed.
|
||||||
InitGravity(app);
|
if (gravity.clock.Tempo() != app.tempo) {
|
||||||
}
|
gravity.clock.SetTempo(app.tempo);
|
||||||
}
|
}
|
||||||
if (app.selected_param == PARAM_MAIN_FACTORY_RESET) {
|
// Load global settings only clock is not active.
|
||||||
if (app.selected_sub_param == 0) { // Reset
|
if (gravity.clock.IsPaused()) {
|
||||||
Bootsplash();
|
InitGravity(app);
|
||||||
stateManager.factoryReset();
|
}
|
||||||
stateManager.reset(app);
|
}
|
||||||
InitGravity(app);
|
break;
|
||||||
}
|
case PARAM_MAIN_FACTORY_RESET:
|
||||||
|
if (app.selected_sub_param == 0) { // Erase
|
||||||
|
stateManager.factoryReset(app);
|
||||||
|
InitGravity(app);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only mark dirty and reset selected_sub_param when leaving editing mode.
|
// Only mark dirty and reset selected_sub_param when leaving editing mode.
|
||||||
@ -277,6 +294,14 @@ void editMainParameter(int val) {
|
|||||||
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
||||||
app.tempo = gravity.clock.Tempo();
|
app.tempo = gravity.clock.Tempo();
|
||||||
break;
|
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: {
|
case PARAM_MAIN_SOURCE: {
|
||||||
byte source = static_cast<int>(app.selected_source);
|
byte source = static_cast<int>(app.selected_source);
|
||||||
updateSelection(source, val, Clock::SOURCE_LAST);
|
updateSelection(source, val, Clock::SOURCE_LAST);
|
||||||
@ -293,15 +318,14 @@ void editMainParameter(int val) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// These changes are applied upon encoder button press.
|
||||||
case PARAM_MAIN_ENCODER_DIR:
|
case PARAM_MAIN_ENCODER_DIR:
|
||||||
|
case PARAM_MAIN_ROTATE_DISP:
|
||||||
updateSelection(app.selected_sub_param, val, 2);
|
updateSelection(app.selected_sub_param, val, 2);
|
||||||
break;
|
break;
|
||||||
case PARAM_MAIN_SAVE_DATA:
|
case PARAM_MAIN_SAVE_DATA:
|
||||||
case PARAM_MAIN_LOAD_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;
|
break;
|
||||||
case PARAM_MAIN_FACTORY_RESET:
|
case PARAM_MAIN_FACTORY_RESET:
|
||||||
updateSelection(app.selected_sub_param, val, 2);
|
updateSelection(app.selected_sub_param, val, 2);
|
||||||
@ -365,6 +389,7 @@ void InitGravity(AppState& app) {
|
|||||||
gravity.clock.SetTempo(app.tempo);
|
gravity.clock.SetTempo(app.tempo);
|
||||||
gravity.clock.SetSource(app.selected_source);
|
gravity.clock.SetSource(app.selected_source);
|
||||||
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
||||||
|
gravity.display.setFlipMode(app.rotate_display ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResetOutputs() {
|
void ResetOutputs() {
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
* @file app_state.h
|
* @file app_state.h
|
||||||
* @author Adam Wonak (https://github.com/awonak/)
|
* @author Adam Wonak (https://github.com/awonak/)
|
||||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||||
* @version 2.0.1
|
* @version 2.0.0
|
||||||
* @date 2025-07-04
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -12,24 +12,27 @@
|
|||||||
#ifndef APP_STATE_H
|
#ifndef APP_STATE_H
|
||||||
#define APP_STATE_H
|
#define APP_STATE_H
|
||||||
|
|
||||||
#include <gravity.h>
|
#include <libGravity.h>
|
||||||
|
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
|
|
||||||
// Global state for settings and app behavior.
|
// Global state for settings and app behavior.
|
||||||
struct AppState {
|
struct AppState {
|
||||||
int tempo = Clock::DEFAULT_TEMPO;
|
int tempo = Clock::DEFAULT_TEMPO;
|
||||||
bool encoder_reversed = false;
|
Channel channel[Gravity::OUTPUT_COUNT];
|
||||||
bool refresh_screen = true;
|
|
||||||
bool editing_param = false;
|
|
||||||
byte selected_param = 0;
|
byte selected_param = 0;
|
||||||
byte selected_sub_param = 0; // Temporary value for editing params.
|
byte selected_sub_param = 0; // Temporary value for editing params.
|
||||||
byte selected_channel = 0; // 0=tempo, 1-6=output channel
|
byte selected_channel = 0; // 0=tempo, 1-6=output channel
|
||||||
byte selected_swing = 0;
|
byte selected_swing = 0;
|
||||||
byte selected_save_slot = 0; // The currently active save slot.
|
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::Source selected_source = Clock::SOURCE_INTERNAL;
|
||||||
Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24;
|
Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24;
|
||||||
Channel channel[Gravity::OUTPUT_COUNT];
|
bool editing_param = false;
|
||||||
|
bool encoder_reversed = false;
|
||||||
|
bool rotate_display = false;
|
||||||
|
bool refresh_screen = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern AppState app;
|
extern AppState app;
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
* @file channel.h
|
* @file channel.h
|
||||||
* @author Adam Wonak (https://github.com/awonak/)
|
* @author Adam Wonak (https://github.com/awonak/)
|
||||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||||
* @version 2.0.1
|
* @version 2.0.0
|
||||||
* @date 2025-07-04
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -13,7 +13,7 @@
|
|||||||
#define CHANNEL_H
|
#define CHANNEL_H
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <gravity.h>
|
#include <libGravity.h>
|
||||||
|
|
||||||
#include "euclidean.h"
|
#include "euclidean.h"
|
||||||
|
|
||||||
@ -70,14 +70,6 @@ class Channel {
|
|||||||
base_duty_cycle = 50;
|
base_duty_cycle = 50;
|
||||||
base_offset = 0;
|
base_offset = 0;
|
||||||
base_swing = 50;
|
base_swing = 50;
|
||||||
base_euc_steps = 1;
|
|
||||||
base_euc_hits = 1;
|
|
||||||
|
|
||||||
cvmod_clock_mod_index = base_clock_mod_index;
|
|
||||||
cvmod_probability = base_probability;
|
|
||||||
cvmod_duty_cycle = base_duty_cycle;
|
|
||||||
cvmod_offset = base_offset;
|
|
||||||
cvmod_swing = base_swing;
|
|
||||||
|
|
||||||
cv1_dest = CV_DEST_NONE;
|
cv1_dest = CV_DEST_NONE;
|
||||||
cv2_dest = CV_DEST_NONE;
|
cv2_dest = CV_DEST_NONE;
|
||||||
@ -88,78 +80,104 @@ class Channel {
|
|||||||
_recalculatePulses();
|
_recalculatePulses();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; }
|
||||||
|
|
||||||
// Setters (Set the BASE value)
|
// Setters (Set the BASE value)
|
||||||
|
|
||||||
void setClockMod(int index) {
|
void setClockMod(int index) {
|
||||||
base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1);
|
base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1);
|
||||||
if (!isCvModActive()) {
|
_recalculatePulses();
|
||||||
cvmod_clock_mod_index = base_clock_mod_index;
|
|
||||||
_recalculatePulses();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setProbability(int prob) {
|
void setProbability(int prob) {
|
||||||
base_probability = constrain(prob, 0, 100);
|
base_probability = constrain(prob, 0, 100);
|
||||||
if (!isCvModActive()) {
|
|
||||||
cvmod_probability = base_probability;
|
|
||||||
_recalculatePulses();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setDutyCycle(int duty) {
|
void setDutyCycle(int duty) {
|
||||||
base_duty_cycle = constrain(duty, 1, 99);
|
base_duty_cycle = constrain(duty, 1, 99);
|
||||||
if (!isCvModActive()) {
|
_recalculatePulses();
|
||||||
cvmod_duty_cycle = base_duty_cycle;
|
|
||||||
_recalculatePulses();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setOffset(int off) {
|
void setOffset(int off) {
|
||||||
base_offset = constrain(off, 0, 99);
|
base_offset = constrain(off, 0, 99);
|
||||||
if (!isCvModActive()) {
|
_recalculatePulses();
|
||||||
cvmod_offset = base_offset;
|
|
||||||
_recalculatePulses();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void setSwing(int val) {
|
void setSwing(int val) {
|
||||||
base_swing = constrain(val, 50, 95);
|
base_swing = constrain(val, 50, 95);
|
||||||
if (!isCvModActive()) {
|
_recalculatePulses();
|
||||||
cvmod_swing = base_swing;
|
|
||||||
_recalculatePulses();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Euclidean
|
// Euclidean
|
||||||
void setSteps(int val) {
|
void setSteps(int val) {
|
||||||
base_euc_steps = constrain(val, 1, MAX_PATTERN_LEN);
|
pattern.SetSteps(val);
|
||||||
if (cv1_dest != CV_DEST_EUC_STEPS && cv2_dest != CV_DEST_EUC_STEPS) {
|
|
||||||
pattern.SetSteps(val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void setHits(int val) {
|
void setHits(int val) {
|
||||||
base_euc_hits = constrain(val, 1, base_euc_steps);
|
pattern.SetHits(val);
|
||||||
if (cv1_dest != CV_DEST_EUC_HITS && cv2_dest != CV_DEST_EUC_HITS) {
|
|
||||||
pattern.SetHits(val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCv1Dest(CvDestination dest) { cv1_dest = dest; }
|
void setCv1Dest(CvDestination dest) {
|
||||||
void setCv2Dest(CvDestination dest) { cv2_dest = dest; }
|
cv1_dest = dest;
|
||||||
|
_recalculatePulses();
|
||||||
|
}
|
||||||
|
void setCv2Dest(CvDestination dest) {
|
||||||
|
cv2_dest = dest;
|
||||||
|
_recalculatePulses();
|
||||||
|
}
|
||||||
CvDestination getCv1Dest() const { return cv1_dest; }
|
CvDestination getCv1Dest() const { return cv1_dest; }
|
||||||
CvDestination getCv2Dest() const { return cv2_dest; }
|
CvDestination getCv2Dest() const { return cv2_dest; }
|
||||||
|
|
||||||
// Getters (Get the BASE value for editing or cv modded value for display)
|
// Getters (Get the BASE value for editing or cv modded value for display)
|
||||||
|
int getProbability() const { return base_probability; }
|
||||||
|
int getDutyCycle() const { return base_duty_cycle; }
|
||||||
|
int getOffset() const { return base_offset; }
|
||||||
|
int getSwing() const { return base_swing; }
|
||||||
|
int getClockMod() const { return pgm_read_word_near(&CLOCK_MOD[getClockModIndex()]); }
|
||||||
|
int getClockModIndex() const { return base_clock_mod_index; }
|
||||||
|
byte getSteps() const { return pattern.GetSteps(); }
|
||||||
|
byte getHits() const { return pattern.GetHits(); }
|
||||||
|
|
||||||
int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; }
|
// Getters that calculate the value with CV modulation applied.
|
||||||
int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; }
|
int getClockModIndexWithMod(int cv1_val, int cv2_val) {
|
||||||
int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; }
|
int clock_mod_index = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2);
|
||||||
int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; }
|
return constrain(base_clock_mod_index + clock_mod_index, 0, MOD_CHOICE_SIZE - 1);
|
||||||
int getClockMod(bool withCvMod = false) const { return pgm_read_word_near(&CLOCK_MOD[getClockModIndex(withCvMod)]); }
|
}
|
||||||
int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; }
|
|
||||||
bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; }
|
|
||||||
|
|
||||||
byte getSteps(bool withCvMod = false) const { return withCvMod ? pattern.GetSteps() : base_euc_steps; }
|
int getClockModWithMod(int cv1_val, int cv2_val) {
|
||||||
byte getHits(bool withCvMod = false) const { return withCvMod ? pattern.GetHits() : base_euc_hits; }
|
int clock_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2);
|
||||||
|
return pgm_read_word_near(&CLOCK_MOD[getClockModIndexWithMod(cv1_val, cv2_val)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getProbabilityWithMod(int cv1_val, int cv2_val) {
|
||||||
|
int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50);
|
||||||
|
return constrain(base_probability + prob_mod, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getDutyCycleWithMod(int cv1_val, int cv2_val) {
|
||||||
|
int duty_mod = _calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50);
|
||||||
|
return constrain(base_duty_cycle + duty_mod, 1, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getOffsetWithMod(int cv1_val, int cv2_val) {
|
||||||
|
int offset_mod = _calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50);
|
||||||
|
return constrain(base_offset + offset_mod, 0, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getSwingWithMod(int cv1_val, int cv2_val) {
|
||||||
|
int swing_mod = _calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25);
|
||||||
|
return constrain(base_swing + swing_mod, 50, 95);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte getStepsWithMod(int cv1_val, int cv2_val) {
|
||||||
|
int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN);
|
||||||
|
return constrain(pattern.GetSteps() + step_mod, 1, MAX_PATTERN_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte getHitsWithMod(int cv1_val, int cv2_val) {
|
||||||
|
// The number of hits is dependent on the modulated number of steps.
|
||||||
|
byte modulated_steps = getStepsWithMod(cv1_val, cv2_val);
|
||||||
|
int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, modulated_steps);
|
||||||
|
return constrain(pattern.GetHits() + hit_mod, 1, modulated_steps);
|
||||||
|
}
|
||||||
|
|
||||||
void toggleMute() { mute = !mute; }
|
void toggleMute() { mute = !mute; }
|
||||||
|
|
||||||
@ -176,6 +194,13 @@ class Channel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCvModActive()) _recalculatePulses();
|
||||||
|
|
||||||
|
int cv1 = gravity.cv1.Read();
|
||||||
|
int cv2 = gravity.cv2.Read();
|
||||||
|
int cvmod_clock_mod_index = getClockModIndexWithMod(cv1, cv2);
|
||||||
|
int cvmod_probability = getProbabilityWithMod(cv1, cv2);
|
||||||
|
|
||||||
const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]);
|
const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]);
|
||||||
|
|
||||||
// Conditionally apply swing on down beats.
|
// Conditionally apply swing on down beats.
|
||||||
@ -211,56 +236,6 @@ class Channel {
|
|||||||
output.Low();
|
output.Low();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @brief Calculate and store cv modded values using bipolar mapping.
|
|
||||||
* Default to base value if not the current CV destination.
|
|
||||||
*
|
|
||||||
* @param cv1_val analog input reading for cv1
|
|
||||||
* @param cv2_val analog input reading for cv2
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
void applyCvMod(int cv1_val, int cv2_val) {
|
|
||||||
// Note: This is optimized for cpu performance. This method is called
|
|
||||||
// from the main loop and stores the cv mod values. This reduces CPU
|
|
||||||
// cycles inside the internal clock interrupt, which is preferrable.
|
|
||||||
// However, if RAM usage grows too much, we have an opportunity to
|
|
||||||
// refactor this to store just the CV read values, and calculate the
|
|
||||||
// cv mod value per channel inside the getter methods by passing cv
|
|
||||||
// values. This would reduce RAM usage, but would introduce a
|
|
||||||
// significant CPU cost, which may have undesirable performance issues.
|
|
||||||
if (!isCvModActive()) {
|
|
||||||
cvmod_clock_mod_index = base_clock_mod_index;
|
|
||||||
cvmod_probability = base_clock_mod_index;
|
|
||||||
cvmod_duty_cycle = base_clock_mod_index;
|
|
||||||
cvmod_offset = base_clock_mod_index;
|
|
||||||
cvmod_swing = base_clock_mod_index;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2);
|
|
||||||
cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, 100);
|
|
||||||
|
|
||||||
int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50);
|
|
||||||
cvmod_probability = constrain(base_probability + prob_mod, 0, 100);
|
|
||||||
|
|
||||||
int duty_mod = _calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50);
|
|
||||||
cvmod_duty_cycle = constrain(base_duty_cycle + duty_mod, 1, 99);
|
|
||||||
|
|
||||||
int offset_mod = _calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50);
|
|
||||||
cvmod_offset = constrain(base_offset + offset_mod, 0, 99);
|
|
||||||
|
|
||||||
int swing_mod = _calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25);
|
|
||||||
cvmod_swing = constrain(base_swing + swing_mod, 50, 95);
|
|
||||||
|
|
||||||
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);
|
|
||||||
pattern.SetHits(base_euc_hits + hit_mod);
|
|
||||||
|
|
||||||
// After all cvmod values are updated, recalculate clock pulse modifiers.
|
|
||||||
_recalculatePulses();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int _calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) {
|
int _calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) {
|
||||||
@ -270,13 +245,19 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _recalculatePulses() {
|
void _recalculatePulses() {
|
||||||
const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]);
|
int cv1 = gravity.cv1.Read();
|
||||||
_duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L);
|
int cv2 = gravity.cv2.Read();
|
||||||
_offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L);
|
int clock_mod_index = getClockModIndexWithMod(cv1, cv2);
|
||||||
|
int duty_cycle = getDutyCycleWithMod(cv1, cv2);
|
||||||
|
int offset = getOffsetWithMod(cv1, cv2);
|
||||||
|
int swing = getSwingWithMod(cv1, cv2);
|
||||||
|
const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[clock_mod_index]);
|
||||||
|
_duty_pulses = max((long)((mod_pulses * (100L - duty_cycle)) / 100L), 1L);
|
||||||
|
_offset_pulses = (long)((mod_pulses * (100L - offset)) / 100L);
|
||||||
|
|
||||||
// Calculate the down beat swing amount.
|
// Calculate the down beat swing amount.
|
||||||
if (cvmod_swing > 50) {
|
if (swing > 50) {
|
||||||
int shifted_swing = cvmod_swing - 50;
|
int shifted_swing = swing - 50;
|
||||||
_swing_pulse_amount = (long)((mod_pulses * (100L - shifted_swing)) / 100L);
|
_swing_pulse_amount = (long)((mod_pulses * (100L - shifted_swing)) / 100L);
|
||||||
} else {
|
} else {
|
||||||
_swing_pulse_amount = 0;
|
_swing_pulse_amount = 0;
|
||||||
@ -289,15 +270,6 @@ class Channel {
|
|||||||
byte base_duty_cycle;
|
byte base_duty_cycle;
|
||||||
byte base_offset;
|
byte base_offset;
|
||||||
byte base_swing;
|
byte base_swing;
|
||||||
byte base_euc_steps;
|
|
||||||
byte base_euc_hits;
|
|
||||||
|
|
||||||
// Base value with cv mod applied.
|
|
||||||
byte cvmod_clock_mod_index;
|
|
||||||
byte cvmod_probability;
|
|
||||||
byte cvmod_duty_cycle;
|
|
||||||
byte cvmod_offset;
|
|
||||||
byte cvmod_swing;
|
|
||||||
|
|
||||||
// CV mod configuration
|
// CV mod configuration
|
||||||
CvDestination cv1_dest;
|
CvDestination cv1_dest;
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
* @file display.h
|
* @file display.h
|
||||||
* @author Adam Wonak (https://github.com/awonak/)
|
* @author Adam Wonak (https://github.com/awonak/)
|
||||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||||
* @version 2.0.1
|
* @version 2.0.0
|
||||||
* @date 2025-07-04
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -47,7 +47,7 @@ const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM =
|
|||||||
* https://stncrn.github.io/u8g2-unifont-helper/
|
* https://stncrn.github.io/u8g2-unifont-helper/
|
||||||
* "%/0123456789ABCDEFILNORSTUVXx"
|
* "%/0123456789ABCDEFILNORSTUVXx"
|
||||||
*/
|
*/
|
||||||
const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") =
|
const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") PROGMEM =
|
||||||
"\35\0\4\4\4\5\3\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\341%'\17;\226\261\245FL"
|
"\35\0\4\4\4\5\3\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\341%'\17;\226\261\245FL"
|
||||||
"\64B\214\30\22\223\220)Bj\10Q\232\214\42R\206\310\210\21d\304\30\32a\254\304\270!\0/\14"
|
"\64B\214\30\22\223\220)Bj\10Q\232\214\42R\206\310\210\21d\304\30\32a\254\304\270!\0/\14"
|
||||||
"\272\272\275\311H\321g\343\306\1\60\37|\373\35CJT\20:fW\207\320\210\60\42\304\204\30D\247"
|
"\272\272\275\311H\321g\343\306\1\60\37|\373\35CJT\20:fW\207\320\210\60\42\304\204\30D\247"
|
||||||
@ -100,11 +100,13 @@ constexpr uint8_t CHANNEL_BOX_HEIGHT = 14;
|
|||||||
enum ParamsMainPage : uint8_t {
|
enum ParamsMainPage : uint8_t {
|
||||||
PARAM_MAIN_TEMPO,
|
PARAM_MAIN_TEMPO,
|
||||||
PARAM_MAIN_SOURCE,
|
PARAM_MAIN_SOURCE,
|
||||||
|
PARAM_MAIN_RUN,
|
||||||
|
PARAM_MAIN_RESET,
|
||||||
PARAM_MAIN_PULSE,
|
PARAM_MAIN_PULSE,
|
||||||
PARAM_MAIN_ENCODER_DIR,
|
PARAM_MAIN_ENCODER_DIR,
|
||||||
|
PARAM_MAIN_ROTATE_DISP,
|
||||||
PARAM_MAIN_SAVE_DATA,
|
PARAM_MAIN_SAVE_DATA,
|
||||||
PARAM_MAIN_LOAD_DATA,
|
PARAM_MAIN_LOAD_DATA,
|
||||||
PARAM_MAIN_RESET_STATE,
|
|
||||||
PARAM_MAIN_FACTORY_RESET,
|
PARAM_MAIN_FACTORY_RESET,
|
||||||
PARAM_MAIN_LAST,
|
PARAM_MAIN_LAST,
|
||||||
};
|
};
|
||||||
@ -214,10 +216,10 @@ void swingDivisionMark() {
|
|||||||
|
|
||||||
// Human friendly display value for save slot.
|
// Human friendly display value for save slot.
|
||||||
String displaySaveSlot(int 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);
|
return String("A") + String(slot + 1);
|
||||||
} else if (slot >= MAX_SAVE_SLOTS / 2 && slot <= MAX_SAVE_SLOTS) {
|
} else if (slot >= StateManager::MAX_SAVE_SLOTS / 2 && slot <= StateManager::MAX_SAVE_SLOTS) {
|
||||||
return String("B") + String(slot - (MAX_SAVE_SLOTS / 2) + 1);
|
return String("B") + String(slot - (StateManager::MAX_SAVE_SLOTS / 2) + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,11 +257,42 @@ void DisplayMainPage() {
|
|||||||
case Clock::SOURCE_EXTERNAL_PPQN_4:
|
case Clock::SOURCE_EXTERNAL_PPQN_4:
|
||||||
subText = F("4 PPQN");
|
subText = F("4 PPQN");
|
||||||
break;
|
break;
|
||||||
|
case Clock::SOURCE_EXTERNAL_PPQN_1:
|
||||||
|
subText = F("1 PPQN");
|
||||||
|
break;
|
||||||
case Clock::SOURCE_EXTERNAL_MIDI:
|
case Clock::SOURCE_EXTERNAL_MIDI:
|
||||||
subText = F("MIDI");
|
subText = F("MIDI");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
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:
|
case PARAM_MAIN_PULSE:
|
||||||
mainText = F("OUT");
|
mainText = F("OUT");
|
||||||
switch (app.selected_pulse) {
|
switch (app.selected_pulse) {
|
||||||
@ -281,9 +314,13 @@ void DisplayMainPage() {
|
|||||||
mainText = F("DIR");
|
mainText = F("DIR");
|
||||||
subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED");
|
subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED");
|
||||||
break;
|
break;
|
||||||
|
case PARAM_MAIN_ROTATE_DISP:
|
||||||
|
mainText = F("ROT");
|
||||||
|
subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("FLIPPED");
|
||||||
|
break;
|
||||||
case PARAM_MAIN_SAVE_DATA:
|
case PARAM_MAIN_SAVE_DATA:
|
||||||
case PARAM_MAIN_LOAD_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");
|
mainText = F("x");
|
||||||
subText = F("BACK TO MAIN");
|
subText = F("BACK TO MAIN");
|
||||||
} else {
|
} else {
|
||||||
@ -297,15 +334,6 @@ void DisplayMainPage() {
|
|||||||
: F("LOAD FROM SLOT");
|
: F("LOAD FROM SLOT");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PARAM_MAIN_RESET_STATE:
|
|
||||||
if (app.selected_sub_param == 0) {
|
|
||||||
mainText = F("RST");
|
|
||||||
subText = F("RESET ALL");
|
|
||||||
} else {
|
|
||||||
mainText = F("x");
|
|
||||||
subText = F("BACK TO MAIN");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case PARAM_MAIN_FACTORY_RESET:
|
case PARAM_MAIN_FACTORY_RESET:
|
||||||
if (app.selected_sub_param == 0) {
|
if (app.selected_sub_param == 0) {
|
||||||
mainText = F("DEL");
|
mainText = F("DEL");
|
||||||
@ -321,7 +349,7 @@ void DisplayMainPage() {
|
|||||||
drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT);
|
drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT);
|
||||||
|
|
||||||
// Draw Main Page menu items
|
// 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")};
|
String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("ROTATE DISP"), F("SAVE"), F("LOAD"), F("ERASE")};
|
||||||
drawMenuItems(menu_items, PARAM_MAIN_LAST);
|
drawMenuItems(menu_items, PARAM_MAIN_LAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,10 +366,12 @@ void DisplayChannelPage() {
|
|||||||
// When editing a param, just show the base value. When not editing show
|
// When editing a param, just show the base value. When not editing show
|
||||||
// the value with cv mod.
|
// the value with cv mod.
|
||||||
bool withCvMod = !app.editing_param;
|
bool withCvMod = !app.editing_param;
|
||||||
|
int cv1 = gravity.cv1.Read();
|
||||||
|
int cv2 = gravity.cv2.Read();
|
||||||
|
|
||||||
switch (app.selected_param) {
|
switch (app.selected_param) {
|
||||||
case PARAM_CH_MOD: {
|
case PARAM_CH_MOD: {
|
||||||
int mod_value = ch.getClockMod(withCvMod);
|
int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2) : ch.getClockMod();
|
||||||
if (mod_value > 1) {
|
if (mod_value > 1) {
|
||||||
mainText = F("/");
|
mainText = F("/");
|
||||||
mainText += String(mod_value);
|
mainText += String(mod_value);
|
||||||
@ -354,30 +384,30 @@ void DisplayChannelPage() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PARAM_CH_PROB:
|
case PARAM_CH_PROB:
|
||||||
mainText = String(ch.getProbability(withCvMod)) + F("%");
|
mainText = String(withCvMod ? ch.getProbabilityWithMod(cv1, cv2) : ch.getProbability()) + F("%");
|
||||||
subText = F("HIT CHANCE");
|
subText = F("HIT CHANCE");
|
||||||
break;
|
break;
|
||||||
case PARAM_CH_DUTY:
|
case PARAM_CH_DUTY:
|
||||||
mainText = String(ch.getDutyCycle(withCvMod)) + F("%");
|
mainText = String(withCvMod ? ch.getDutyCycleWithMod(cv1, cv2) : ch.getDutyCycle()) + F("%");
|
||||||
subText = F("PULSE WIDTH");
|
subText = F("PULSE WIDTH");
|
||||||
break;
|
break;
|
||||||
case PARAM_CH_OFFSET:
|
case PARAM_CH_OFFSET:
|
||||||
mainText = String(ch.getOffset(withCvMod)) + F("%");
|
mainText = String(withCvMod ? ch.getOffsetWithMod(cv1, cv2) : ch.getOffset()) + F("%");
|
||||||
subText = F("SHIFT HIT");
|
subText = F("SHIFT HIT");
|
||||||
break;
|
break;
|
||||||
case PARAM_CH_SWING:
|
case PARAM_CH_SWING:
|
||||||
ch.getSwing() == 50
|
ch.getSwing() == 50
|
||||||
? mainText = F("OFF")
|
? mainText = F("OFF")
|
||||||
: mainText = String(ch.getSwing(withCvMod)) + F("%");
|
: mainText = String(withCvMod ? ch.getSwingWithMod(cv1, cv2) : ch.getSwing()) + F("%");
|
||||||
subText = "DOWN BEAT";
|
subText = "DOWN BEAT";
|
||||||
swingDivisionMark();
|
swingDivisionMark();
|
||||||
break;
|
break;
|
||||||
case PARAM_CH_EUC_STEPS:
|
case PARAM_CH_EUC_STEPS:
|
||||||
mainText = String(ch.getSteps(withCvMod));
|
mainText = String(withCvMod ? ch.getStepsWithMod(cv1, cv2) : ch.getSteps());
|
||||||
subText = "EUCLID STEPS";
|
subText = "EUCLID STEPS";
|
||||||
break;
|
break;
|
||||||
case PARAM_CH_EUC_HITS:
|
case PARAM_CH_EUC_HITS:
|
||||||
mainText = String(ch.getHits(withCvMod));
|
mainText = String(withCvMod ? ch.getHitsWithMod(cv1, cv2) : ch.getHits());
|
||||||
subText = "EUCLID HITS";
|
subText = "EUCLID HITS";
|
||||||
break;
|
break;
|
||||||
case PARAM_CH_CV1_DEST:
|
case PARAM_CH_CV1_DEST:
|
||||||
@ -469,21 +499,4 @@ void UpdateDisplay() {
|
|||||||
} while (gravity.display.nextPage());
|
} while (gravity.display.nextPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bootsplash() {
|
|
||||||
gravity.display.firstPage();
|
|
||||||
do {
|
|
||||||
int textWidth;
|
|
||||||
gravity.display.setFont(TEXT_FONT);
|
|
||||||
|
|
||||||
textWidth = gravity.display.getStrWidth(SKETCH_NAME);
|
|
||||||
gravity.display.drawStr(24 + (textWidth / 2), 24, SKETCH_NAME);
|
|
||||||
|
|
||||||
textWidth = gravity.display.getStrWidth(SEMANTIC_VERSION);
|
|
||||||
gravity.display.drawStr(24 + (textWidth / 2), 36, SEMANTIC_VERSION);
|
|
||||||
|
|
||||||
textWidth = gravity.display.getStrWidth("LOADING....");
|
|
||||||
gravity.display.drawStr(34 + (textWidth / 2), 48, "LOADING....");
|
|
||||||
} while (gravity.display.nextPage());
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // DISPLAY_H
|
#endif // DISPLAY_H
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
* @file euclidean.h
|
* @file euclidean.h
|
||||||
* @author Adam Wonak (https://github.com/awonak/)
|
* @author Adam Wonak (https://github.com/awonak/)
|
||||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||||
* @version 2.0.1
|
* @version 2.0.0
|
||||||
* @date 2025-07-04
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
* @file save_state.cpp
|
* @file save_state.cpp
|
||||||
* @author Adam Wonak (https://github.com/awonak/)
|
* @author Adam Wonak (https://github.com/awonak/)
|
||||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||||
* @version 2.0.1
|
* @version 2.0.0
|
||||||
* @date 2025-07-04
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -15,67 +15,92 @@
|
|||||||
|
|
||||||
#include "app_state.h"
|
#include "app_state.h"
|
||||||
|
|
||||||
|
// Define the constants for the current firmware.
|
||||||
|
const char StateManager::SKETCH_NAME[] = "ALT GRAVITY";
|
||||||
|
const char StateManager::SEMANTIC_VERSION[] = "2.0.0"; // 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.
|
// Calculate the starting address for EepromData, leaving space for metadata.
|
||||||
static const int METADATA_START_ADDR = 0;
|
const int StateManager::METADATA_START_ADDR = 0;
|
||||||
static const int EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata);
|
const int StateManager::EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata);
|
||||||
|
|
||||||
StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {}
|
StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {}
|
||||||
|
|
||||||
bool StateManager::initialize(AppState& app) {
|
bool StateManager::initialize(AppState& app) {
|
||||||
|
noInterrupts();
|
||||||
|
bool success = false;
|
||||||
if (_isDataValid()) {
|
if (_isDataValid()) {
|
||||||
// Load data from the transient slot.
|
// Load global settings.
|
||||||
return loadData(app, TRANSIENT_SLOT);
|
_loadMetadata(app);
|
||||||
|
// Load app data from the transient slot.
|
||||||
|
_loadState(app, TRANSIENT_SLOT);
|
||||||
|
success = true;
|
||||||
}
|
}
|
||||||
// EEPROM does not contain save data for this firmware & version.
|
// EEPROM does not contain save data for this firmware & version.
|
||||||
else {
|
else {
|
||||||
// Erase EEPROM and initialize state. Save default pattern to all save slots.
|
// Erase EEPROM and initialize state. Save default pattern to all save slots.
|
||||||
factoryReset();
|
factoryReset(app);
|
||||||
// Initialize eeprom and save default patter to all save slots.
|
|
||||||
_saveMetadata(app);
|
|
||||||
reset(app);
|
|
||||||
for (int i = 0; i < MAX_SAVE_SLOTS; i++) {
|
|
||||||
_saveState(app, i);
|
|
||||||
}
|
|
||||||
_saveState(app, TRANSIENT_SLOT);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
interrupts();
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StateManager::loadData(AppState& app, byte slot_index) {
|
bool StateManager::loadData(AppState& app, byte slot_index) {
|
||||||
// Check if slot_index is within max range + 1 for transient.
|
// Check if slot_index is within max range + 1 for transient.
|
||||||
if (slot_index >= MAX_SAVE_SLOTS + 1) return false;
|
if (slot_index >= MAX_SAVE_SLOTS + 1) return false;
|
||||||
|
|
||||||
_loadState(app, slot_index);
|
noInterrupts();
|
||||||
_loadMetadata(app);
|
|
||||||
|
|
||||||
|
// 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 on next update.
|
||||||
|
_isDirty = true;
|
||||||
|
|
||||||
|
interrupts();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save app state to user specified save slot.
|
// Save app state to user specified save slot.
|
||||||
void StateManager::saveData(const AppState& app) {
|
void StateManager::saveData(const AppState& app) {
|
||||||
|
noInterrupts();
|
||||||
// Check if slot_index is within max range + 1 for transient.
|
// Check if slot_index is within max range + 1 for transient.
|
||||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
||||||
|
|
||||||
_saveState(app, app.selected_save_slot);
|
_saveState(app, app.selected_save_slot);
|
||||||
|
_saveMetadata(app);
|
||||||
_isDirty = false;
|
_isDirty = false;
|
||||||
|
interrupts();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save transient state if it has changed and enough time has passed since last save.
|
// Save transient state if it has changed and enough time has passed since last save.
|
||||||
void StateManager::update(const AppState& app) {
|
void StateManager::update(const AppState& app) {
|
||||||
if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) {
|
if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) {
|
||||||
|
noInterrupts();
|
||||||
_saveState(app, TRANSIENT_SLOT);
|
_saveState(app, TRANSIENT_SLOT);
|
||||||
_saveMetadata(app);
|
_saveMetadata(app);
|
||||||
_isDirty = false;
|
_isDirty = false;
|
||||||
|
interrupts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StateManager::reset(AppState& app) {
|
void StateManager::reset(AppState& app) {
|
||||||
app.tempo = Clock::DEFAULT_TEMPO;
|
noInterrupts();
|
||||||
app.selected_param = 0;
|
|
||||||
app.selected_channel = 0;
|
AppState default_app;
|
||||||
app.selected_source = Clock::SOURCE_INTERNAL;
|
app.tempo = default_app.tempo;
|
||||||
app.selected_pulse = Clock::PULSE_PPQN_24;
|
app.selected_param = default_app.selected_param;
|
||||||
app.selected_save_slot = 0;
|
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++) {
|
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||||
app.channel[i].Init();
|
app.channel[i].Init();
|
||||||
@ -85,6 +110,7 @@ void StateManager::reset(AppState& app) {
|
|||||||
_loadMetadata(app);
|
_loadMetadata(app);
|
||||||
|
|
||||||
_isDirty = false;
|
_isDirty = false;
|
||||||
|
interrupts();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StateManager::markDirty() {
|
void StateManager::markDirty() {
|
||||||
@ -93,19 +119,27 @@ void StateManager::markDirty() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Erases all data in the EEPROM by writing 0 to every address.
|
// Erases all data in the EEPROM by writing 0 to every address.
|
||||||
void StateManager::factoryReset() {
|
void StateManager::factoryReset(AppState& app) {
|
||||||
noInterrupts();
|
noInterrupts();
|
||||||
for (unsigned int i = 0; i < EEPROM.length(); i++) {
|
for (unsigned int i = 0; i < EEPROM.length(); i++) {
|
||||||
EEPROM.write(i, 0);
|
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();
|
interrupts();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StateManager::_isDataValid() {
|
bool StateManager::_isDataValid() {
|
||||||
Metadata load_meta;
|
Metadata metadata;
|
||||||
EEPROM.get(METADATA_START_ADDR, load_meta);
|
EEPROM.get(METADATA_START_ADDR, metadata);
|
||||||
bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0);
|
bool name_match = (strcmp(metadata.sketch_name, SKETCH_NAME) == 0);
|
||||||
bool version_match = (strcmp(load_meta.version, SEMANTIC_VERSION) == 0);
|
bool version_match = (strcmp(metadata.version, SEMANTIC_VERSION) == 0);
|
||||||
return name_match && version_match;
|
return name_match && version_match;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,15 +147,15 @@ void StateManager::_saveState(const AppState& app, byte slot_index) {
|
|||||||
// Check if slot_index is within max range + 1 for transient.
|
// Check if slot_index is within max range + 1 for transient.
|
||||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
||||||
|
|
||||||
noInterrupts();
|
|
||||||
static EepromData save_data;
|
static EepromData save_data;
|
||||||
|
|
||||||
save_data.tempo = app.tempo;
|
save_data.tempo = app.tempo;
|
||||||
save_data.encoder_reversed = app.encoder_reversed;
|
|
||||||
save_data.selected_param = app.selected_param;
|
save_data.selected_param = app.selected_param;
|
||||||
save_data.selected_channel = app.selected_channel;
|
save_data.selected_channel = app.selected_channel;
|
||||||
save_data.selected_source = static_cast<byte>(app.selected_source);
|
save_data.selected_source = static_cast<byte>(app.selected_source);
|
||||||
save_data.selected_pulse = static_cast<byte>(app.selected_pulse);
|
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
|
// TODO: break this out into a separate function. Save State should be
|
||||||
// broken out into global / per-channel save methods. When saving via
|
// broken out into global / per-channel save methods. When saving via
|
||||||
@ -130,27 +164,25 @@ void StateManager::_saveState(const AppState& app, byte slot_index) {
|
|||||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||||
const auto& ch = app.channel[i];
|
const auto& ch = app.channel[i];
|
||||||
auto& save_ch = save_data.channel_data[i];
|
auto& save_ch = save_data.channel_data[i];
|
||||||
save_ch.base_clock_mod_index = ch.getClockModIndex(false);
|
save_ch.base_clock_mod_index = ch.getClockModIndex();
|
||||||
save_ch.base_probability = ch.getProbability(false);
|
save_ch.base_probability = ch.getProbability();
|
||||||
save_ch.base_duty_cycle = ch.getDutyCycle(false);
|
save_ch.base_duty_cycle = ch.getDutyCycle();
|
||||||
save_ch.base_offset = ch.getOffset(false);
|
save_ch.base_offset = ch.getOffset();
|
||||||
save_ch.base_swing = ch.getSwing(false);
|
save_ch.base_swing = ch.getSwing();
|
||||||
save_ch.base_euc_steps = ch.getSteps(false);
|
save_ch.base_euc_steps = ch.getSteps();
|
||||||
save_ch.base_euc_hits = ch.getHits(false);
|
save_ch.base_euc_hits = ch.getHits();
|
||||||
save_ch.cv1_dest = static_cast<byte>(ch.getCv1Dest());
|
save_ch.cv1_dest = static_cast<byte>(ch.getCv1Dest());
|
||||||
save_ch.cv2_dest = static_cast<byte>(ch.getCv2Dest());
|
save_ch.cv2_dest = static_cast<byte>(ch.getCv2Dest());
|
||||||
}
|
}
|
||||||
|
|
||||||
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
|
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
|
||||||
EEPROM.put(address, save_data);
|
EEPROM.put(address, save_data);
|
||||||
interrupts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StateManager::_loadState(AppState& app, byte slot_index) {
|
void StateManager::_loadState(AppState& app, byte slot_index) {
|
||||||
// Check if slot_index is within max range + 1 for transient.
|
// Check if slot_index is within max range + 1 for transient.
|
||||||
if (slot_index >= MAX_SAVE_SLOTS + 1) return;
|
if (slot_index >= MAX_SAVE_SLOTS + 1) return;
|
||||||
|
|
||||||
noInterrupts();
|
|
||||||
static EepromData load_data;
|
static EepromData load_data;
|
||||||
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
|
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
|
||||||
EEPROM.get(address, load_data);
|
EEPROM.get(address, load_data);
|
||||||
@ -161,7 +193,8 @@ void StateManager::_loadState(AppState& app, byte slot_index) {
|
|||||||
app.selected_channel = load_data.selected_channel;
|
app.selected_channel = load_data.selected_channel;
|
||||||
app.selected_source = static_cast<Clock::Source>(load_data.selected_source);
|
app.selected_source = static_cast<Clock::Source>(load_data.selected_source);
|
||||||
app.selected_pulse = static_cast<Clock::Pulse>(load_data.selected_pulse);
|
app.selected_pulse = static_cast<Clock::Pulse>(load_data.selected_pulse);
|
||||||
app.selected_save_slot = slot_index;
|
app.cv_run = load_data.cv_run;
|
||||||
|
app.cv_reset = load_data.cv_reset;
|
||||||
|
|
||||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||||
auto& ch = app.channel[i];
|
auto& ch = app.channel[i];
|
||||||
@ -177,11 +210,9 @@ void StateManager::_loadState(AppState& app, byte slot_index) {
|
|||||||
ch.setCv1Dest(static_cast<CvDestination>(saved_ch_state.cv1_dest));
|
ch.setCv1Dest(static_cast<CvDestination>(saved_ch_state.cv1_dest));
|
||||||
ch.setCv2Dest(static_cast<CvDestination>(saved_ch_state.cv2_dest));
|
ch.setCv2Dest(static_cast<CvDestination>(saved_ch_state.cv2_dest));
|
||||||
}
|
}
|
||||||
interrupts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StateManager::_saveMetadata(const AppState& app) {
|
void StateManager::_saveMetadata(const AppState& app) {
|
||||||
noInterrupts();
|
|
||||||
Metadata current_meta;
|
Metadata current_meta;
|
||||||
strcpy(current_meta.sketch_name, SKETCH_NAME);
|
strcpy(current_meta.sketch_name, SKETCH_NAME);
|
||||||
strcpy(current_meta.version, SEMANTIC_VERSION);
|
strcpy(current_meta.version, SEMANTIC_VERSION);
|
||||||
@ -189,16 +220,15 @@ void StateManager::_saveMetadata(const AppState& app) {
|
|||||||
// Global user settings
|
// Global user settings
|
||||||
current_meta.selected_save_slot = app.selected_save_slot;
|
current_meta.selected_save_slot = app.selected_save_slot;
|
||||||
current_meta.encoder_reversed = app.encoder_reversed;
|
current_meta.encoder_reversed = app.encoder_reversed;
|
||||||
|
current_meta.rotate_display = app.rotate_display;
|
||||||
|
|
||||||
EEPROM.put(METADATA_START_ADDR, current_meta);
|
EEPROM.put(METADATA_START_ADDR, current_meta);
|
||||||
interrupts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StateManager::_loadMetadata(AppState& app) {
|
void StateManager::_loadMetadata(AppState& app) {
|
||||||
noInterrupts();
|
|
||||||
Metadata metadata;
|
Metadata metadata;
|
||||||
EEPROM.get(METADATA_START_ADDR, metadata);
|
EEPROM.get(METADATA_START_ADDR, metadata);
|
||||||
app.selected_save_slot = metadata.selected_save_slot;
|
app.selected_save_slot = metadata.selected_save_slot;
|
||||||
app.encoder_reversed = metadata.encoder_reversed;
|
app.encoder_reversed = metadata.encoder_reversed;
|
||||||
interrupts();
|
app.rotate_display = metadata.rotate_display;
|
||||||
}
|
}
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* @file save_state.h
|
* @file save_state.h
|
||||||
* @author Adam Wonak (https://github.com/awonak/)
|
* @author Adam Wonak (https://github.com/awonak/)
|
||||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||||
* @version 2.0.1
|
* @version 2.0.0
|
||||||
* @date 2025-07-04
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -13,22 +13,11 @@
|
|||||||
#define SAVE_STATE_H
|
#define SAVE_STATE_H
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <gravity.h>
|
#include <libGravity.h>
|
||||||
|
|
||||||
// Forward-declare AppState to avoid circular dependencies.
|
// Forward-declare AppState to avoid circular dependencies.
|
||||||
struct AppState;
|
struct AppState;
|
||||||
|
|
||||||
// Define the constants for the current firmware.
|
|
||||||
const char SKETCH_NAME[] = "ALT GRAVITY";
|
|
||||||
const char SEMANTIC_VERSION[] = "V2.0.0BETA1";
|
|
||||||
|
|
||||||
// Number of available save slots.
|
|
||||||
const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets.
|
|
||||||
const byte TRANSIENT_SLOT = 10; // Transient slot index to persist state when powered off.
|
|
||||||
|
|
||||||
// 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.
|
* @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
|
* The number of user slots is defined by MAX_SAVE_SLOTS, and one additional slot
|
||||||
@ -39,6 +28,11 @@ static const unsigned long SAVE_DELAY_MS = 2000;
|
|||||||
*/
|
*/
|
||||||
class StateManager {
|
class StateManager {
|
||||||
public:
|
public:
|
||||||
|
static const char SKETCH_NAME[];
|
||||||
|
static const char SEMANTIC_VERSION[];
|
||||||
|
static const byte MAX_SAVE_SLOTS;
|
||||||
|
static const byte TRANSIENT_SLOT;
|
||||||
|
|
||||||
StateManager();
|
StateManager();
|
||||||
|
|
||||||
// Populate the AppState instance with values from EEPROM if they exist.
|
// Populate the AppState instance with values from EEPROM if they exist.
|
||||||
@ -54,7 +48,7 @@ class StateManager {
|
|||||||
// Indicate that state has changed and we should save.
|
// Indicate that state has changed and we should save.
|
||||||
void markDirty();
|
void markDirty();
|
||||||
// Erase all data stored in the EEPROM.
|
// Erase all data stored in the EEPROM.
|
||||||
void factoryReset();
|
void factoryReset(AppState& app);
|
||||||
|
|
||||||
// This struct holds the data that identifies the firmware version.
|
// This struct holds the data that identifies the firmware version.
|
||||||
struct Metadata {
|
struct Metadata {
|
||||||
@ -63,6 +57,7 @@ class StateManager {
|
|||||||
// Additional global/hardware settings
|
// Additional global/hardware settings
|
||||||
byte selected_save_slot;
|
byte selected_save_slot;
|
||||||
bool encoder_reversed;
|
bool encoder_reversed;
|
||||||
|
bool rotate_display;
|
||||||
};
|
};
|
||||||
struct ChannelState {
|
struct ChannelState {
|
||||||
byte base_clock_mod_index;
|
byte base_clock_mod_index;
|
||||||
@ -78,11 +73,12 @@ class StateManager {
|
|||||||
// This struct holds all the parameters we want to save.
|
// This struct holds all the parameters we want to save.
|
||||||
struct EepromData {
|
struct EepromData {
|
||||||
int tempo;
|
int tempo;
|
||||||
bool encoder_reversed;
|
|
||||||
byte selected_param;
|
byte selected_param;
|
||||||
byte selected_channel;
|
byte selected_channel;
|
||||||
byte selected_source;
|
byte selected_source;
|
||||||
byte selected_pulse;
|
byte selected_pulse;
|
||||||
|
byte cv_run;
|
||||||
|
byte cv_reset;
|
||||||
ChannelState channel_data[Gravity::OUTPUT_COUNT];
|
ChannelState channel_data[Gravity::OUTPUT_COUNT];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,6 +89,10 @@ class StateManager {
|
|||||||
void _saveState(const AppState& app, byte slot_index);
|
void _saveState(const AppState& app, byte slot_index);
|
||||||
void _loadState(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;
|
bool _isDirty;
|
||||||
unsigned long _lastChangeTime;
|
unsigned long _lastChangeTime;
|
||||||
};
|
};
|
||||||
|
|||||||
10
library.properties
Normal file
10
library.properties
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
name=libGravity
|
||||||
|
version=2.0.1
|
||||||
|
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,NeoHWSerial
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* @file analog_input.h
|
* @file analog_input.h
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Class for interacting with analog inputs.
|
* @brief Class for interacting with analog inputs.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-05-23
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -19,6 +19,8 @@ const int CALIBRATED_HIGH = 512;
|
|||||||
|
|
||||||
class AnalogInput {
|
class AnalogInput {
|
||||||
public:
|
public:
|
||||||
|
static const int GATE_THRESHOLD = 0;
|
||||||
|
|
||||||
AnalogInput() {}
|
AnalogInput() {}
|
||||||
~AnalogInput() {}
|
~AnalogInput() {}
|
||||||
|
|
||||||
@ -74,6 +76,18 @@ class AnalogInput {
|
|||||||
*/
|
*/
|
||||||
inline float Voltage() { return ((read_ / 512.0) * 5.0); }
|
inline float Voltage() { return ((read_ / 512.0) * 5.0); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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:
|
private:
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
int16_t read_;
|
int16_t read_;
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* @file button.h
|
* @file button.h
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Wrapper class for interacting with trigger / gate inputs.
|
* @brief Wrapper class for interacting with trigger / gate inputs.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-04-20
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* @file clock.h
|
* @file clock.h
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Wrapper Class for clock timing functions.
|
* @brief Wrapper Class for clock timing functions.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-05-04
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -15,7 +15,7 @@
|
|||||||
#include <NeoHWSerial.h>
|
#include <NeoHWSerial.h>
|
||||||
|
|
||||||
#include "peripherials.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.
|
// MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards.
|
||||||
#define MIDI_CLOCK 0xF8
|
#define MIDI_CLOCK 0xF8
|
||||||
@ -35,15 +35,16 @@ class Clock {
|
|||||||
SOURCE_INTERNAL,
|
SOURCE_INTERNAL,
|
||||||
SOURCE_EXTERNAL_PPQN_24,
|
SOURCE_EXTERNAL_PPQN_24,
|
||||||
SOURCE_EXTERNAL_PPQN_4,
|
SOURCE_EXTERNAL_PPQN_4,
|
||||||
|
SOURCE_EXTERNAL_PPQN_1,
|
||||||
SOURCE_EXTERNAL_MIDI,
|
SOURCE_EXTERNAL_MIDI,
|
||||||
SOURCE_LAST,
|
SOURCE_LAST,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum Pulse {
|
enum Pulse {
|
||||||
PULSE_NONE,
|
PULSE_NONE,
|
||||||
PULSE_PPQN_1,
|
|
||||||
PULSE_PPQN_4,
|
|
||||||
PULSE_PPQN_24,
|
PULSE_PPQN_24,
|
||||||
|
PULSE_PPQN_4,
|
||||||
|
PULSE_PPQN_1,
|
||||||
PULSE_LAST,
|
PULSE_LAST,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,6 +97,10 @@ class Clock {
|
|||||||
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
|
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
|
||||||
uClock.setInputPPQN(uClock.PPQN_4);
|
uClock.setInputPPQN(uClock.PPQN_4);
|
||||||
break;
|
break;
|
||||||
|
case SOURCE_EXTERNAL_PPQN_1:
|
||||||
|
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
|
||||||
|
uClock.setInputPPQN(uClock.PPQN_1);
|
||||||
|
break;
|
||||||
case SOURCE_EXTERNAL_MIDI:
|
case SOURCE_EXTERNAL_MIDI:
|
||||||
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
|
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
|
||||||
uClock.setInputPPQN(uClock.PPQN_24);
|
uClock.setInputPPQN(uClock.PPQN_24);
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* @file digital_output.h
|
* @file digital_output.h
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Class for interacting with trigger / gate outputs.
|
* @brief Class for interacting with trigger / gate outputs.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-04-17
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -82,7 +82,6 @@ class DigitalOutput {
|
|||||||
unsigned long last_triggered_;
|
unsigned long last_triggered_;
|
||||||
uint8_t trigger_duration_;
|
uint8_t trigger_duration_;
|
||||||
uint8_t cv_pin_;
|
uint8_t cv_pin_;
|
||||||
uint8_t led_pin_;
|
|
||||||
bool on_;
|
bool on_;
|
||||||
|
|
||||||
void update(uint8_t state) {
|
void update(uint8_t state) {
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* @file encoder_dir.h
|
* @file encoder_dir.h
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Class for interacting with encoders.
|
* @brief Class for interacting with encoders.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-04-19
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -1,15 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* @file gravity.cpp
|
* @file libGravity.cpp
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-04-19
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "gravity.h"
|
#include "libGravity.h"
|
||||||
|
|
||||||
// Initialize the static pointer for the EncoderDir class to null. We want to
|
// 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.
|
||||||
@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* @file gravity.h
|
* @file libGravity.h
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-04-19
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -2,8 +2,8 @@
|
|||||||
* @file peripherials.h
|
* @file peripherials.h
|
||||||
* @author Adam Wonak (https://github.com/awonak)
|
* @author Adam Wonak (https://github.com/awonak)
|
||||||
* @brief Arduino pin definitions for the Sitka Instruments Gravity module.
|
* @brief Arduino pin definitions for the Sitka Instruments Gravity module.
|
||||||
* @version 0.1
|
* @version 2.0.0
|
||||||
* @date 2025-04-19
|
* @date 2025-08-17
|
||||||
*
|
*
|
||||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||||
*
|
*
|
||||||
@ -32,7 +32,7 @@
|
|||||||
* DEALINGS IN THE SOFTWARE.
|
* DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
#include "uClock.h"
|
#include "uClock.h"
|
||||||
#include "uClock/platforms/avr.h"
|
#include "platforms/avr.h"
|
||||||
|
|
||||||
//
|
//
|
||||||
// Platform specific timer setup/control
|
// 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