Skip to content
Snippets Groups Projects
Commit a427c87e authored by David Huss's avatar David Huss :speech_balloon:
Browse files

UI refactor

parent b7de9736
Branches
Tags
No related merge requests found
......@@ -12,7 +12,6 @@ extern Adafruit_SH1106G display;
typedef void (*callback_function)(void);
class Button {
......@@ -20,10 +19,11 @@ class Button {
bool has_been_pressed;
unsigned long press_start;
unsigned long release_start;
callback_function onPressFunction;
callback_function onPressedFunction;
callback_function onLongPressFunction;
callback_function onVeryLongPressFunction;
std::function<void()> onPressFunction;
std::function<void()> onPressedFunction;
std::function<void()> onLongPressFunction;
std::function<void()> onVeryLongPressFunction;
std::function<void()> onReleasedFunction;
public:
Button(int pin);
......@@ -32,10 +32,13 @@ class Button {
unsigned long pressed_since();
unsigned long released_since();
void onPress(callback_function f);
void onPressed(callback_function f);
void onLongPress(callback_function f);
void onVeryLongPress(callback_function f);
void onPress(std::function<void()> f);
void onPressed(std::function<void()> f);
void onLongPress(std::function<void()> f);
void onVeryLongPress(std::function<void()> f);
void onReleased(std::function<void()> f);
void reset();
bool display_value_changes = false;
void setDisplayMode(const char *description);
......@@ -62,11 +65,9 @@ void Button::read() {
if (is_pressed && this->press_start == 0) {
this->press_start = millis();
Serial.println("Set a new start");
}
if (!is_pressed && this->has_been_pressed && this->release_start == 0) {
this->release_start = millis();
Serial.println("Set a new release");
}
unsigned long pressed_since = this->pressed_since();
......@@ -85,28 +86,25 @@ void Button::read() {
if (!this->has_been_pressed) {
if (pressed_since > 0 && pressed_since < DURATION_SHORT_PRESS) {
if (this->onPressFunction) { this->onPressFunction(); }
Serial.print("Short Press (released after ");
Serial.print(pressed_since);
Serial.print(", released since ");
Serial.print(released_since);
if (released_since < 200 ) {
Serial.print(", Doublepress? ");
}
Serial.println(")");
// Serial.print("Short Press (released after ");
// Serial.print(pressed_since);
// Serial.print(", released since ");
// Serial.print(released_since);
} else if (pressed_since > 0 && pressed_since < DURATION_VERY_LONG_PRESS) {
if (this->onLongPressFunction) { this->onLongPressFunction(); }
Serial.print("Long Press (released after ");
Serial.print(pressed_since);
Serial.println(")");
// Serial.print("Long Press (released after ");
// Serial.print(pressed_since);
// Serial.println(")");
} else if (pressed_since > 0 && pressed_since >= DURATION_VERY_LONG_PRESS) {
if (this->onVeryLongPressFunction) { this->onVeryLongPressFunction(); }
Serial.print("Very Long Press (released after ");
Serial.print(pressed_since);
Serial.println(")");
// Serial.print("Very Long Press (released after ");
// Serial.print(pressed_since);
// Serial.println(")");
}
this->press_start = 0;
this->has_been_pressed = true;
this->release_start = millis();
if (this->onReleasedFunction) { this->onReleasedFunction(); }
}
}
}
......@@ -125,26 +123,37 @@ unsigned long Button::released_since() {
return millis() - this->release_start;
}
void Button::onPress(callback_function f) {
void Button::onPress(std::function<void()> f) {
this->onPressFunction = f;
}
void Button::onPressed(callback_function f) {
void Button::onPressed(std::function<void()> f) {
this->onPressedFunction = f;
}
void Button::onLongPress(callback_function f) {
void Button::onLongPress(std::function<void()> f) {
this->onLongPressFunction = f;
}
void Button::onVeryLongPress(callback_function f) {
void Button::onVeryLongPress(std::function<void()> f) {
this->onVeryLongPressFunction = f;
}
void Button::onReleased(std::function<void()> f) {
this->onReleasedFunction = f;
}
void Button::setDisplayMode(const char *description) {
this->display_value_changes = true;
this->description = description;
}
void Button::reset() {
this->onPressFunction = NULL;
this->onPressedFunction = NULL;
this->onLongPressFunction = NULL;
this->onVeryLongPressFunction = NULL;
}
#endif
\ No newline at end of file
......@@ -18,19 +18,23 @@
#include "looper.h"
#include "env_follower.h"
#include "helpers.h"
#include "ui.h"
#define UI_FPS 60.0
#define WAVEFORM_OVERSAMPLING 2
static const size_t buffer_length = 48000 * 5;
static float DSY_SDRAM_BSS buffer[buffer_length];
static atoav::Looper looper;
atoav::Looper looper;
static PitchShifter pitch_shifter;
static atoav::EnvelopeFollower output_envelope_follower;
static atoav::EnvelopeFollower input_envelope_follower;
DelayLine<float, 24000> delayline;
DSY_SDRAM_BSS ReverbSc reverb;
static Compressor compressor;
Oscillator lfo;
static SampleHold sample_and_hold;
static WhiteNoise noise;
static Metro tick;
// Resonator res;
CpuLoadMeter load;
......@@ -56,23 +60,29 @@ Potentiometer pot_7 = Potentiometer(A6);
RGBLed rgb_led = RGBLed(A10, A9, A11);
// OLED Display
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // QT-PY / XIAO
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
uint16_t waveform_cache[SCREEN_WIDTH] = {0};
// User Interface
Ui ui;
// Daisy
DaisyHardware hw;
// Variables for the Audio-Callback
size_t num_channels;
float blocksize;
double last_render = 0.0;
float drywetmix = 0.0f;
float delaymix = 0.0f;
float delaytime = 100.0f;
float reverbmix = 0.0f;
float lfo_amount = 0.0f;
auto pitch_val = 0.5f;
// Actual audio-processing is orchestrated here
void AudioCallback(float **in, float **out, size_t size) {
float output = 0.0f;
float no_delay = 0.0f;
......@@ -80,10 +90,29 @@ void AudioCallback(float **in, float **out, size_t size) {
delayline.SetDelay(delaytime);
float out1, out2;
for (size_t i = 0; i < size; i++) {
uint8_t trig = tick.Process();
float lfo_value = lfo.Process();
float rand = sample_and_hold.Process(trig, noise.Process() * 5.0f, sample_and_hold.MODE_SAMPLE_HOLD);
// When the metro ticks, trigger the envelope to start.
float random_amount = (lfo_amount -0.5f) * 2.0;
if (trig) {
// tick.SetFreq(rand / (0.1 + random_amount*1000.0f) + 1);
// If the dial is over 50% jump instead
if (lfo_amount > 0.5f) {
looper.addToPlayhead(rand * random_amount* 48000.0f);
looper.setPlaybackSpeed(pitch_val);
}
}
if (lfo_amount > 0.5f) {
looper.setPlaybackSpeed(pitch_val);
} else {
looper.setPlaybackSpeed(pitch_val + lfo_value * lfo_amount);
}
// res.SetDamping(0.1+delaymix*0.2f);
auto looper_out = looper.Process(in[1][i]);
// FIXME:
float input_envelope = input_envelope_follower.Process(in[1][i]);
input_envelope_follower.Process(in[1][i]);
output = drywetmix * pitch_shifter.Process(looper_out) + in[1][i] * (1.0f - drywetmix);
// output = output * (1.0f - resmix) + res.Process(output*resmix);
no_delay = output;
......@@ -92,38 +121,24 @@ void AudioCallback(float **in, float **out, size_t size) {
output += wet_delay * delaymix;
compressor.Process(output);
// Process reverb
reverb.Process(output, output, &out1, &out2);
// Mix reverb with the dry signal depending on the amount dialed
output = output * (1.0f - reverbmix) + out1 * reverbmix;
out[0][i] = out[1][i] = out1;
out[0][i] = out[1][i] = output;
}
}
bool waveform_cache_dirty = false;
auto record_on = false;
auto overdub_on = false;
auto loop_start = 0.0f;
auto loop_length = 1.0f;
auto pitch_val = 0.5f;
bool rec_mode_limit_length_active = false;
bool display_rec_mode = false;
double last_displayed_rec_mode = 0.0;
void activate_rec() {
record_on = true;
waveform_cache_dirty = true;
}
void toggleRecMode() {
rec_mode_limit_length_active = looper.toggleRecMode();
last_displayed_rec_mode = millis();
display_rec_mode = true;
}
// void activate_rec() {
// ui.activateRecording();
// }
void activate_overdub() {
overdub_on = true;
record_on = true;
waveform_cache_dirty = true;
}
// void activate_overdub() {
// ui.activateOverdub();
// }
void setup() {
float sample_rate;
......@@ -134,13 +149,15 @@ void setup() {
sample_rate = DAISY.get_samplerate();
blocksize = 64.0f;
tick.Init(10, sample_rate);
looper.Init(buffer, buffer_length);
input_envelope_follower.Init(sample_rate);
input_envelope_follower.SetAttack(100.0);
input_envelope_follower.SetDecay(1000.0);
reverb.Init(sample_rate);
reverb.SetFeedback(0.85f);
reverb.SetFeedback(0.95f);
reverb.SetLpFreq(18000.0f);
compressor.SetThreshold(-64.0f);
......@@ -150,6 +167,14 @@ void setup() {
pitch_shifter.Init(sample_rate);
// set parameters for LFO oscillator object
lfo.Init(sample_rate);
lfo.SetWaveform(Oscillator::WAVE_TRI);
lfo.SetAmp(1);
lfo.SetFreq(8.0);
noise.Init();
delayline.Init();
delayline.SetDelay(48000.0f);
// res.Init(.015, 24, sample_rate);
......@@ -158,32 +183,15 @@ void setup() {
load.Init(sample_rate, blocksize);
Serial.begin(250000);
Serial.println("Serial communication started");
// Initialize Display
display.begin(0x3C, true);
delay(50);
display.setTextSize(1);
display.setTextColor(SH110X_BLACK);
// Play a fancy intro splash screen
for (int i=0; i < 91; i++) {
display.clearDisplay();
display.fillCircle(display.width()/2, display.height()/2, i, SH110X_WHITE);
display.setCursor(30, 30);
display.println("D A I S Y Y");
display.display();
delay(5);
}
display.clearDisplay();
display.setTextColor(SH110X_WHITE);
display.setCursor(30, 30);
display.println("D A I S Y Y");
display.display();
delay(500);
display.clearDisplay();
display.display();
ui.Render();
DAISY.begin(AudioCallback);
Serial.println("Started");
rgb_led.init();
......@@ -193,11 +201,11 @@ void setup() {
// Set Knob names and display functions
pot_1.name = "Start";
pot_2.name = "Length";
pot_3.setDisplayMode("Pitch", 12.0f, POT_DISPLAY_MODE_PITCH);
pot_3.setDisplayMode("Pitch", 1000.0f, POT_DISPLAY_MODE_PERCENT);
pot_4.setDisplayMode("Mix", 100.0f, POT_DISPLAY_MODE_PERCENT);
pot_5.setDisplayMode("Delay", 100.0f, POT_DISPLAY_MODE_PERCENT);
pot_6.setDisplayMode("Time", 100.0f, POT_DISPLAY_MODE_PERCENT);
// pot_7.setDisplayMode("Structure", 100.0f, POT_DISPLAY_MODE_PERCENT);
pot_6.setDisplayMode("Reverb", 100.0f, POT_DISPLAY_MODE_PERCENT);
pot_7.setDisplayMode("LFO", 100.0f, POT_DISPLAY_MODE_PERCENT);
// Set Knob Scaling Modes
pot_3.setBipolar();
......@@ -206,9 +214,13 @@ void setup() {
button_1.init();
button_2.init();
button_3.init();
button_1.onPressed(activate_rec);
button_2.onPress(toggleRecMode);
button_3.onPressed(activate_overdub);
button_4.init();
button_5.init();
button_6.init();
// button_1.onPressed(activate_rec);
// button_2.onPress(toggleRecMode);
// button_3.onPressed(activate_overdub);
// button_4.onPressed(display_fx_menu);
}
void loop() {
......@@ -222,155 +234,34 @@ void loop() {
float p7 = pot_7.read();
// Deactivate recording and overdub before reading buttons
record_on = false;
overdub_on = false;
ui.update();
// Read buttons
button_1.read();
button_2.read();
button_3.read();
button_4.read();
button_5.read();
button_6.read();
// Set loop parameters
loop_start = p1;
loop_length = p2;
looper.SetLoop(loop_start, loop_length);
// Set loop start and loop length
ui.setLoop(p1, p2);
// Value should go 1 octave up/down (12 semitones)
pitch_val = 12.0f * p3;
pitch_shifter.SetTransposition(pitch_val);
// Value should go 10 octaves up/down (10*1.0 = 1000%)
pitch_val = 10.0f * p3;
// res.SetFreq(10.0f + p3 * 1500.0f);
// Set other parameters
drywetmix = p4;
delaymix = p5;
delaytime = 100.0f + p6 * 23900.0f;
// res.SetStructure(p7);
delaytime = 100.0f + p5 * 23900.0f;
reverbmix = p6;
lfo_amount = p7;
// Toggle record
looper.SetRecording(record_on, overdub_on);
// Render UI
// load.OnBlockStart();
renderBuffer();
// load.OnBlockEnd();
ui.Render();
rgb_led.setAudioLevelIndicator(int(input_envelope_follower.getValue() * 255));
// Serial.println(load.GetMaxCpuLoad());
}
void renderBuffer() {
double now = millis();
if (now - last_render > UI_FPS) {
display.clearDisplay();
// Render the waveform
int wave_height = display.height() * 1.0f;
int step = buffer_length / (display.width() * WAVEFORM_OVERSAMPLING);
int bottom = display.height()-1;
// Render the waveform
for (int i=0; i<display.width()*WAVEFORM_OVERSAMPLING; i+=WAVEFORM_OVERSAMPLING) {
uint16_t x = int(i / WAVEFORM_OVERSAMPLING);
// Only recalculate if needed, else use cache
if (waveform_cache_dirty) {
float sig = 0.0f;
for (int s=0; s<WAVEFORM_OVERSAMPLING; s++) {
float abs_sig = buffer[step*i];
abs_sig = abs(abs_sig) * 100.0f;
sig += abs_sig;
}
sig = sig / float(WAVEFORM_OVERSAMPLING);
if (sig != 0.0f) {
sig = log10(sig)/3.6f;
}
waveform_cache[x] = int(sig * wave_height);
}
// Serial.print(waveform_cache[x]);
// Serial.print(",");
display.drawFastVLine(x, bottom, -waveform_cache[x], SH110X_WHITE);
display.drawFastHLine(0, bottom, display.width(), SH110X_WHITE);
}
waveform_cache_dirty = false;
// Draw Line for loop start
int x_start_loop = int(loop_start * display.width());
display.drawLine(x_start_loop, 0, x_start_loop, bottom, SH110X_WHITE);
display.fillTriangle(x_start_loop, 6, x_start_loop, 0, x_start_loop+3, 0, SH110X_WHITE);
// Draw Line for Loop End
int x_loop_length = int(loop_length * display.width());
int x_loop_end = (x_start_loop + x_loop_length) % display.width();
display.drawLine(x_loop_end, 0, x_loop_end, bottom, SH110X_WHITE);
display.fillTriangle(x_loop_end, 6, x_loop_end-3, 0, x_loop_end, 0, SH110X_WHITE);
// Connecting line for start and end
if (x_loop_end >= x_start_loop) {
display.drawLine(x_start_loop, 0, x_loop_end, 0, SH110X_WHITE);
} else {
display.drawLine(x_start_loop, 0, display.width(), 0, SH110X_WHITE);
display.drawLine(0, 0, x_loop_end, 0, SH110X_WHITE);
}
// Draw Playhead
int x_playhead = int(looper.GetPlayhead() * display.width()) + x_start_loop;
display.drawLine(x_playhead, 6, x_playhead, 24, SH110X_WHITE);
// Draw Recording stuff
if (record_on && ! overdub_on) {
// Draw Rec Head
int x_rec_head = int(looper.GetRecHead() * display.width());
display.drawLine(x_rec_head, 10, x_rec_head, bottom, SH110X_WHITE);
display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE);
// Record sign
display.fillRect(0, 0, 13, 13, SH110X_WHITE);
display.fillRect(2, 2, 12, 12, SH110X_WHITE);
display.fillRect(1, 1, 11, 11, SH110X_BLACK);
display.fillCircle(6, 6, 3, SH110X_WHITE);
}
// Draw Overdub stuff
if (overdub_on) {
// Overdub sign (a "plus")
display.fillRect(0, 0, 13, 13, SH110X_WHITE);
display.fillRect(2, 2, 12, 12, SH110X_WHITE);
display.fillRect(1, 1, 11, 11, SH110X_BLACK);
display.drawLine(6, 2, 6, 10, SH110X_WHITE);
display.drawLine(2, 6, 10, 6, SH110X_WHITE);
}
// Render potentiometer UIs in case a knob is changed
pot_1.renderUi();
pot_2.renderUi();
pot_3.renderUi();
pot_4.renderUi();
pot_5.renderUi();
pot_6.renderUi();
// pot_7.renderUi();
// After pressing the rec mode button display info
if (display_rec_mode) {
int x_margin = 10;
int y_margin = 13;
int x_center = display.width()/2;
int y_center = display.height()/2;
// Render a rectangle Backdrop for the text
display.fillRect(3+x_margin, 3+y_margin, display.width()-x_margin*2, display.height()-y_margin*2, SH110X_WHITE);
display.fillRect(x_margin, y_margin, display.width()-x_margin*2, display.height()-y_margin*2, SH110X_WHITE);
display.fillRect(x_margin+1, y_margin+1, display.width()-(x_margin+1)*2, display.height()-(y_margin+1)*2, SH110X_BLACK);
if (rec_mode_limit_length_active) {
centeredText("Limit recording", x_center, y_center-4, SH110X_WHITE);
centeredText("to loop length", x_center, y_center+4, SH110X_WHITE);
} else {
centeredText("Record into", x_center, y_center-4, SH110X_WHITE);
centeredText("full buffer", x_center, y_center+4, SH110X_WHITE);
}
}
if (now - last_displayed_rec_mode > 500.0) {
display_rec_mode = false;
}
// Display all that stuff and store the time of the last render
display.display();
last_render = now;
}
}
......@@ -30,6 +30,112 @@ int centeredText(const char *buf, int x, int y, int color, int lineheight=8) {
return w;
}
int centeredTextMark(const char *buf, int x, int y, int color, int underline_line=0, int lineheight=8) {
int16_t x1, y1;
uint16_t w, h;
char *line_pointer = strchr(buf, '\n');
display.setTextColor(color);
if (!line_pointer) {
display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h); //calc width of new string
int x_start = x - (w / 2);
int y_start = y - (h / 2);
display.setCursor(x_start, y_start);
if (underline_line == 1) {
display.drawFastHLine(x_start-2, y_start+lineheight, w+4, color);
}
display.print(buf);
}else {
char *tmp = strdup(buf);
char* d = strtok(tmp, "\n");
int line = 0;
while (d != NULL) {
display.getTextBounds(d, 0, 0, &x1, &y1, &w, &h); //calc width of new string
int x_start = x - (w / 2);
int y_start = y - (h / 2)-lineheight/2 + (line*lineheight);
display.setCursor(x_start, y_start);
display.print(d);
d = strtok(NULL, ",");
if (underline_line == 1) {
display.drawFastHLine(x_start-2, y_start+lineheight, w+4, color);
}
line++;
}
free(tmp);
}
return w;
}
int centeredTextMarkMulti(const char *buf, int x, int y, int color, int underline_line=0, int lineheight=8) {
int16_t x1, y1;
uint16_t w, h;
char *line_pointer = strchr(buf, '\n');
display.setTextColor(color);
if (!line_pointer) {
display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h); //calc width of new string
int x_start = x - (w / 2);
int y_start = y - (h / 2);
display.setCursor(x_start, y_start);
if (underline_line == 1) {
display.drawFastHLine(x_start-2, y_start, w+4, color);
}
display.print(buf);
}else {
char *tmp = strdup(buf);
char* d = strtok(tmp, "\n");
int line = 0;
while (d != NULL) {
display.getTextBounds(d, 0, 0, &x1, &y1, &w, &h); //calc width of new string
int x_start = x - (w / 2);
int y_start = y - (h / 2)-lineheight/2 + (line*lineheight);
display.setCursor(x_start, y_start);
display.print(d);
d = strtok(NULL, ",");
if (line == underline_line) {
display.drawFastHLine(x_start-2, y_start+lineheight-3, w+4, color);
}
line++;
}
free(tmp);
}
return w;
}
int button_multi(const char *buf, int x, int y, int color, int underline_line=0, int lines=0) {
int16_t x1, y1;
uint16_t w, h;
display.setTextColor(color);
char *tmp = strdup(buf);
int line = 0;
char* pch = NULL;
pch = strtok(tmp, "\n");
int radius = 3;
int cell_width = 128/3;
int left_x = x - (cell_width/2);
int margin = 10;
int circle_start_x = left_x + margin;
int spacing = (cell_width - margin - margin) / lines;
while (pch != NULL){
// Draw Option-Circles
display.drawCircle(circle_start_x + line*spacing, y+7, radius, SH110X_WHITE);
// Only display the active text
if (line == underline_line) {
display.getTextBounds(pch, 0, 0, &x1, &y1, &w, &h); //calc width of new string
int x_start = x - (w / 2);
int y_start = y - (h / 2)-7;
display.setCursor(x_start, y_start);
display.print(pch);
// On the active option, draw a filled circle
display.fillCircle(circle_start_x + line*spacing, y+7, radius, SH110X_WHITE);
}
line++;
pch = strtok(NULL, "\n");
}
free(tmp);
return w;
}
#endif
\ No newline at end of file
......@@ -3,6 +3,20 @@
namespace atoav {
enum RecPitchMode {
REC_PITCH_MODE_NORMAL,
REC_PITCH_MODE_PITCHED,
REC_PITCH_MODE_UNPITCHED,
REC_PITCH_MODE_LAST,
};
enum RecStartMode {
REC_START_MODE_BUFFER,
REC_START_MODE_LOOP,
REC_START_MODE_PLAYHEAD,
REC_START_MODE_LAST,
};
class Looper {
public:
void Init(float *buf, size_t length) {
......@@ -12,19 +26,34 @@ class Looper {
memset(_buffer, 0, sizeof(float) * _buffer_length);
}
void SetRecording(bool is_rec_on, bool is_overdub_on) {
this->_is_overdub_on = is_overdub_on;
RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL;
RecStartMode rec_start_mode = REC_START_MODE_BUFFER;
void SetRecording(bool is_recording, bool is_overdub) {
this->is_overdub = is_overdub;
this->is_recording = is_recording || is_overdub;
//Initialize recording head position on start
if (_rec_env_pos_inc <= 0 && is_rec_on) {
// _rec_head = (_loop_start + _play_head) % _buffer_length;
_rec_head = (_loop_start) % _buffer_length;
if (_rec_env_pos_inc <= 0 && is_recording) {
// rec_head = (_loop_start + play_head) % _buffer_length;
switch (rec_start_mode) {
case REC_START_MODE_LOOP:
rec_head = (_loop_start) % _buffer_length;
break;
case REC_START_MODE_BUFFER:
rec_head = 0.0f;
break;
case REC_START_MODE_PLAYHEAD:
rec_head = fmod(_loop_start + play_head, float(_buffer_length));
break;
}
_is_empty = false;
}
// When record switch changes state it effectively
// sets ramp to rising/falling, providing a
// fade in/out in the beginning and at the end of
// the recorded region.
_rec_env_pos_inc = is_rec_on ? 1 : -1;
_rec_env_pos_inc = is_recording ? 1 : -1;
}
void SetLoop(const float loop_start, const float loop_length) {
......@@ -56,20 +85,49 @@ class Looper {
if (_rec_env_pos > 0) {
// Calculate fade in/out
float rec_attenuation = static_cast<float>(_rec_env_pos) / static_cast<float>(kFadeLength);
if (this->_is_overdub_on) {
_buffer[_rec_head] += in * rec_attenuation;
if (this->is_overdub) {
_buffer[int(rec_head)] += in * rec_attenuation;
} else {
_buffer[_rec_head] = in * rec_attenuation + _buffer[_rec_head] * (1.f - rec_attenuation);
_buffer[int(rec_head)] = in * rec_attenuation + _buffer[int(rec_head)] * (1.f - rec_attenuation);
}
// Set recording pitch mode
switch (rec_pitch_mode) {
case REC_PITCH_MODE_NORMAL:
rec_head += 1.0f;
break;
case REC_PITCH_MODE_UNPITCHED:
rec_head += playback_increment;
break;
case REC_PITCH_MODE_PITCHED:
if (playback_increment != 0.0) {
rec_head += 1.0f/playback_increment;
}
break;
}
_rec_head ++;
// Different recording modes
if (!_limit_rec_length) {
_rec_head %= _buffer_length;
if (!stop_after_recording) {
if (!stay_within_loop) {
// record into whole buffer
rec_head = fmod(rec_head, float(_buffer_length));
} else {
// Limit rec head to stay inside the loop
_rec_head %= (_loop_start + _loop_length);
_rec_head = max(_loop_start, _rec_head);
rec_head = fmod(rec_head, float(_loop_start + _loop_length));
rec_head = max(float(_loop_start), rec_head);
}
} else {
if (!stay_within_loop) {
if (rec_head > _buffer_length) { SetRecording(false, false); }
} else {
if (rec_head > _loop_start + _loop_length) { SetRecording(false, false); }
}
}
if (rec_head > _buffer_length) {
rec_head = 0.0f;
} else if (rec_head < 0) {
rec_head = _buffer_length;
}
}
......@@ -81,48 +139,112 @@ class Looper {
// Playback from the buffer
float attenuation = 1;
float output = 0;
// Calculate fade in/out
if (_play_head < kFadeLength) {
attenuation = static_cast<float>(_play_head) / static_cast<float>(kFadeLength);
if (play_head < kFadeLength) {
attenuation = static_cast<float>(play_head) / static_cast<float>(kFadeLength);
}
else if (_play_head >= _loop_length - kFadeLength) {
attenuation = static_cast<float>(_loop_length - _play_head) / static_cast<float>(kFadeLength);
else if (play_head >= _loop_length - kFadeLength) {
attenuation = static_cast<float>(_loop_length - play_head) / static_cast<float>(kFadeLength);
}
// Read from the buffer
auto play_pos = (_loop_start + _play_head) % _buffer_length;
auto play_pos = int(_loop_start + play_head) % _buffer_length;
output = _buffer[play_pos] * attenuation;
// Advance playhead
_play_head ++;
if (_play_head >= _loop_length) {
play_head += playback_increment;
// Ensure the playhead stays within bounds
if (play_head >= _loop_length) {
_loop_start = _pending_loop_start;
_loop_length = _pending_loop_length;
play_head = 0;
} else if (play_head <= 0) {
_loop_start = _pending_loop_start;
_loop_length = _pending_loop_length;
_play_head = 0;
play_head = _loop_length;
}
return output * attenuation;
}
float GetPlayhead() {
return float(_play_head) / float(_buffer_length);
return float(play_head) / float(_buffer_length);
}
float GetLoopStart() {
return float(_loop_start) / float(_buffer_length);
float GetRecHead() {
return float(rec_head) / float(_buffer_length);
}
float GetLoopLength() {
return float(_loop_length) / float(_buffer_length);
bool toggleRecMode() {
stay_within_loop = !stay_within_loop;
return stay_within_loop;
}
float GetRecHead() {
return float(_rec_head) / float(_buffer_length);
void setRecModeFull() {
Serial.println("[Looper] Set RecMode to Full");
stay_within_loop = false;
stop_after_recording = false;
}
bool toggleRecMode() {
_limit_rec_length = !_limit_rec_length;
return _limit_rec_length;
void setRecModeLoop() {
Serial.println("[Looper] Set RecMode to Loop");
stay_within_loop = true;
stop_after_recording = false;
}
void setRecModeFullShot() {
Serial.println("[Looper] Set RecMode to Loop FullShot");
stay_within_loop = false;
stop_after_recording = true;
}
void setRecModeLoopShot() {
Serial.println("[Looper] Set RecMode to Loop Oneshot");
stay_within_loop = true;
stop_after_recording = true;
}
void setPlaybackSpeed(float increment) {
playback_increment = increment;
}
void addToPlayhead(float value) {
play_head += value;
}
float* getBuffer() {
return _buffer;
}
size_t getBufferLength() {
return _buffer_length;
}
bool isRecording() {
return is_recording;
}
bool isOverdubbing() {
return is_overdub;
}
void setRecPitchMode(RecPitchMode mode) {
Serial.print("[Looper] Set RecPitchMode from ");
Serial.print(rec_pitch_mode);
Serial.print(" to ");
Serial.println(mode);
rec_pitch_mode = mode;
}
void setRecStartMode(RecStartMode mode) {
Serial.print("[Looper] Set RecStartMode from ");
Serial.print(rec_start_mode);
Serial.print(" to ");
Serial.println(mode);
rec_start_mode = mode;
}
private:
......@@ -137,19 +259,21 @@ class Looper {
size_t _loop_start = 0;
size_t _pending_loop_start = 0;
size_t _play_head = 0;
size_t _rec_head = 0;
float play_head = 0.0f;
float rec_head = 0.0f;
float playback_increment = 1.0f;
size_t _rec_env_pos = 0;
int32_t _rec_env_pos_inc = 0;
bool _is_empty = true;
bool _is_loop_set = false;
bool _limit_rec_length = false;
bool _is_overdub_on = false;
bool stay_within_loop = false;
bool is_overdub = false;
bool is_recording = false;
bool stop_after_recording = false;
};
};
}; // namespace atoav
#endif
\ No newline at end of file
#include <stdint.h>
#include "WSerial.h"
#include "usbd_def.h"
#ifndef Ui_h
#define Ui_h
#include "Adafruit_SH110X.h"
#include "Adafruit_GFX.h"
#include "potentiometers.h"
#include "buttons.h"
#include "looper.h"
#define UI_MAX_FPS 60
#define WAVEFORM_OVERSAMPLING 2
extern Potentiometer pot_1, pot_2, pot_3, pot_4, pot_5, pot_6, pot_7;
extern Button button_1, button_2, button_3, button_4, button_5, button_6;
extern Adafruit_SH1106G display;
extern atoav::Looper looper;
enum UiMode {
UI_MODE_SPLASH,
UI_MODE_DEFAULT,
UI_MODE_REC_MENU,
UI_MODE_PLAY_MENU,
UI_MODE_TRIGGER_MENU,
UI_MODE_FX_MENU,
UI_MODE_LAST
};
enum RecMode {
REC_MODE_FULL,
REC_MODE_LOOP,
REC_MODE_FULL_SHOT,
REC_MODE_LAST = 6
};
enum PlayMode {
PLAY_MODE_DRUNK,
PLAY_MODE_WINDOW,
PLAY_MODE_LOOP,
PLAY_MODE_GRAIN,
PLAY_MODE_ONESHOT,
PLAY_MODE_LAST
};
enum RecordingState {
REC_STATE_NOT_RECORDING,
REC_STATE_RECORDING,
REC_STATE_OVERDUBBING,
REC_STATE_LAST
};
// Different types of buttons
enum ButtonType {
BUTTON_TYPE_SIMPLE, // Simple Button that can be pressed
BUTTON_TYPE_TOGGLE, // Toggles between two values (name needs to have a break "\n" insided)
BUTTON_TYPE_MULTITOGGLE, // Toggles between two or more values
BUTTON_TYPE_ENUM, // Toggles between a group of buttons
BUTTON_TYPE_LAST
};
class GridButton {
public:
const char* name;
GridButton(const char* name, Button& button, bool is_home=false, ButtonType type=BUTTON_TYPE_SIMPLE, int default_value=0)
:
name(name),
button(button),
is_home(is_home),
type(type),
active(default_value)
{
// Count the number of lines in the name
for(int i = 0; name[i] != '\0'; i++) {
if(name[i] == '\n')
++lines;
}
}
Button button;
bool is_home;
int active;
ButtonType type;
int lines = 0;
void next() {
active++;
if (active > lines) {
active = 0;
}
}
};
class ButtonGrid {
public:
ButtonGrid(const GridButton (&grid_buttons)[6])
: grid_buttons_{grid_buttons[0], grid_buttons[1], grid_buttons[2], grid_buttons[3], grid_buttons[4], grid_buttons[5]} {}
GridButton grid_buttons_[6];
void render(int button_enum) {
int width = display.width();
int height = display.height();
int box_width = int(width/3.0f);
int box_height= int(height/2.0f);
int i = 0;
// Draw boxes (2 rows, 3 columns)
for (int box_y=0; box_y<2; box_y++) {
for (int box_x=0; box_x<3; box_x++) {
const char* name = grid_buttons_[i].name;
// Prepare colors
uint16_t bg_color = SH110X_BLACK;
uint16_t text_color = SH110X_WHITE;
// Home-Buttons have a inverted color scheme
if (grid_buttons_[i].is_home) {
bg_color = SH110X_WHITE;
text_color = SH110X_BLACK;
}
// Position variables
uint16_t x = box_x * box_width;
uint16_t y = box_y * box_height;
uint16_t xc = x + box_width/2;
uint16_t yc = y + box_height/2;
// Fill Background
display.fillRect(x, y, box_width, box_height, bg_color);
// Render the different button types
if (grid_buttons_[i].type == BUTTON_TYPE_TOGGLE) {
centeredTextMarkMulti(name, xc, yc, text_color, grid_buttons_[i].active, 12);
} else if (grid_buttons_[i].type == BUTTON_TYPE_MULTITOGGLE) {
button_multi(name, xc, yc, text_color, grid_buttons_[i].active, grid_buttons_[i].lines);
} else if (grid_buttons_[i].type == BUTTON_TYPE_ENUM) {
bool active = i == button_enum;
centeredTextMark(name, xc, yc, text_color, active);
} else {
centeredText(name, xc, yc, text_color);
}
// Counter for the number of the button
i++;
}
}
// Draw divider Line
display.drawFastVLine(box_width, 0, height, SH110X_WHITE);
display.drawFastVLine(box_width*2, 0, height, SH110X_WHITE);
display.drawFastHLine(0, box_height, width, SH110X_WHITE);
}
};
class Ui {
public:
Ui() : button_grids {
ButtonGrid({
GridButton("REC\nMENU", button_1, true),
GridButton("MOM\nTOGGLE", button_2, false, BUTTON_TYPE_TOGGLE, 0),
GridButton("PRE\nPOST\nOUT\nNOISE", button_3, false, BUTTON_TYPE_MULTITOGGLE, 0),
GridButton("FULL\nLOOP\nSHOT", button_4, false, BUTTON_TYPE_MULTITOGGLE, 1),
GridButton("NORMAL\nPITCHD\nUNPTCH", button_5, false, BUTTON_TYPE_MULTITOGGLE, 0),
GridButton("START\nLOOPST\nPLAYHD", button_6, false, BUTTON_TYPE_MULTITOGGLE, 0),
}),
ButtonGrid({
GridButton("LOOP", button_1, false),
GridButton("PLAY\nMENU", button_2, true),
GridButton("WINDOW", button_3, false),
GridButton("DRUNK", button_4, false),
GridButton("GRAIN", button_5, false),
GridButton("SHOT", button_6, false),
}),
ButtonGrid({
GridButton("MIDI\nTRIG.", button_1, false),
GridButton("MIDI\nUNMUTE", button_2, false),
GridButton("TRIG.\nMENU", button_3, true),
GridButton("MANUAL\nTRIG.", button_4, false),
GridButton("MANUAL\nUNMUTE", button_5, false),
GridButton("AUTO", button_6, false),
}),
ButtonGrid({
GridButton("DELAY", button_1, false),
GridButton("REVERB", button_2, false),
GridButton("-", button_3, false),
GridButton("-", button_4, false),
GridButton("-", button_5, false),
GridButton("FX\nMENU", button_6, true),
}),
} {};
UiMode ui_mode = UI_MODE_SPLASH;
ButtonGrid button_grids[4];
// RecMode rec_mode = REC_MODE_FULL;
RecMode rec_mode = REC_MODE_LOOP;
void setMode(UiMode mode) {
if (last_ui_mode == mode) { return; }
last_ui_mode = ui_mode;
ui_mode = mode;
switch (ui_mode) {
case UI_MODE_SPLASH:
break;
case UI_MODE_DEFAULT:
break;
case UI_MODE_REC_MENU:
break;
case UI_MODE_PLAY_MENU:
break;
case UI_MODE_TRIGGER_MENU:
break;
case UI_MODE_FX_MENU:
break;
}
}
void Render() {
switch (ui_mode) {
case UI_MODE_SPLASH:
renderSplash();
break;
case UI_MODE_DEFAULT:
setupDefault();
renderDefault();
break;
case UI_MODE_REC_MENU:
setupRecMenu();
renderGrid(0, rec_mode);
break;
case UI_MODE_PLAY_MENU:
renderGrid(1);
break;
case UI_MODE_TRIGGER_MENU:
renderGrid(2);
break;
case UI_MODE_FX_MENU:
renderGrid(3);
break;
}
}
// Render button grids
void renderGrid(size_t num, int button_enum=0) {
double now = millis();
if (now - last_render > UI_MAX_FPS) {
display.clearDisplay();
button_grids[num].render(button_enum);
// Display all that stuff and store the time of the last render
display.display();
last_render = now;
}
}
// Renders a splash screen
void renderSplash() {
display.setTextSize(1);
// display.setTextColor(SH110X_BLACK);
// // Play a fancy intro splash screen
// for (int i=0; i < 91; i++) {
// display.clearDisplay();
// display.fillCircle(display.width()/2, display.height()/2, i, SH110X_WHITE);
// display.fillCircle(display.width()/2, display.height()/2, max(0, i-2), SH110X_BLACK);
// if (i < 10) {
// centeredText("I", display.width()/2, display.height()/2-4, SH110X_WHITE);
// centeredText("O", display.width()/2, display.height()/2+4, SH110X_WHITE);
// } else if (i < 20) {
// centeredText("I S", display.width()/2, display.height()/2-4, SH110X_WHITE);
// centeredText("O P", display.width()/2, display.height()/2+4, SH110X_WHITE);
// } else if (i < 40) {
// centeredText("A I S", display.width()/2, display.height()/2-4, SH110X_WHITE);
// centeredText("O O P", display.width()/2, display.height()/2+4, SH110X_WHITE);
// } else if (i < 50) {
// centeredText("A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE);
// centeredText("O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE);
// } else if (i < 60) {
// centeredText("D A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE);
// centeredText("L O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE);
// } else {
// centeredText("D A I S Y Y", display.width()/2, display.height()/2-4, SH110X_WHITE);
// centeredText("L O O P E R", display.width()/2, display.height()/2+4, SH110X_WHITE);
// }
// display.display();
// delay(1);
// }
// display.invertDisplay(true);
// display.display();
// delay(800);
// display.clearDisplay();
// display.invertDisplay(false);
// display.display();
// Splash rendering is now done, go to next UI Mode
setMode(UI_MODE_DEFAULT);
}
void resetControls() {
button_1.reset();
button_2.reset();
button_3.reset();
button_4.reset();
button_5.reset();
button_6.reset();
}
void setupRecMenu() {
// Only run once
if (ui_mode == UI_MODE_REC_MENU && last_ui_mode != UI_MODE_REC_MENU) {
Serial.println("[UI] Setup Rec Menu");
resetControls();
// Stay in this menu as long as the button is pressed, otherwise return
button_1.onPressed([this](){ this->setMode(UI_MODE_REC_MENU); });
button_1.onReleased([this](){ this->setMode(UI_MODE_DEFAULT); });
// Toggle between momentary and toggle recording modes
button_2.onPress([this](){
Serial.print("[UI] Mom/Toggle option ");
if (rec_button_momentary) {
Serial.println("momentary -> toggle");
}else{
Serial.println("toggle -> momentary");
}
rec_button_momentary = !rec_button_momentary;
button_grids[0].grid_buttons_[1].active = !rec_button_momentary;
});
// Set Recording Source (Pre/Post/Out/Noise)
button_3.onPress([this](){
// TODO: Implement Pre/Post/Out Recording
button_grids[0].grid_buttons_[2].next();
}); // FULL ONESHOT
// Switch Recording modes
button_4.onPress([this](){
button_grids[0].grid_buttons_[3].next();
// Button.active returns number according to mode:
rec_mode = (RecMode) button_grids[0].grid_buttons_[3].active;
switch (rec_mode) {
case REC_MODE_FULL: looper.setRecModeFull(); break;
case REC_MODE_LOOP: looper.setRecModeLoop(); break;
case REC_MODE_FULL_SHOT: looper.setRecModeFullShot(); break;
}
});
// Set Recording Pitch mode (Normal/Pitched/Unpitched)
button_5.onPress([this](){
button_grids[0].grid_buttons_[4].next();
looper.setRecPitchMode((atoav::RecPitchMode) button_grids[0].grid_buttons_[4].active);
});
// Set Recording Start Option (Buffer Start/Loop Start/Playhead)
button_6.onPress([this](){
button_grids[0].grid_buttons_[5].next();
looper.setRecStartMode((atoav::RecStartMode) button_grids[0].grid_buttons_[5].active);
});
last_ui_mode = ui_mode;
}
}
void setupDefault() {
// Only run once
if (ui_mode == UI_MODE_DEFAULT && last_ui_mode != UI_MODE_DEFAULT) {
Serial.println("[UI] Setup Default mode");
resetControls();
// Set up the initial recording mode
switch (rec_mode) {
case REC_MODE_FULL: looper.setRecModeFull(); break;
case REC_MODE_LOOP: looper.setRecModeLoop(); break;;
case REC_MODE_FULL_SHOT: looper.setRecModeFullShot(); break;
};
// Setup Button functions
button_1.onPressed([this](){ this->setMode(UI_MODE_REC_MENU); });
button_2.onPressed([this](){ this->setMode(UI_MODE_PLAY_MENU); });
button_3.onPressed([this](){ this->setMode(UI_MODE_TRIGGER_MENU); });
// Momentary/Toggle Recording modes
if (rec_button_momentary) {
Serial.println("[UI] Set to momentary mode");
button_4.onPressed([this](){ this->activateRecording(); });
button_5.onPressed([this](){ this->activateOverdub(); });
button_4.onReleased([this](){ this->stopRecording(); });
button_5.onReleased([this](){ this->stopRecording(); });
} else {
Serial.println("[UI] Set to toggle mode");
button_4.onReleased([this](){ this->toggleRecording(); });
button_5.onReleased([this](){ this->toggleOverdub(); });
}
button_6.onPressed([this](){ this->setMode(UI_MODE_FX_MENU); });
last_ui_mode = ui_mode;
}
}
void renderDefault() {
double now = millis();
if (now - last_render > UI_MAX_FPS) {
display.clearDisplay();
// Render the waveform
int wave_height = display.height() * 1.0f;
int step = looper.getBufferLength() / (display.width() * WAVEFORM_OVERSAMPLING);
int bottom = display.height()-1;
// Render the waveform
for (int i=0; i<display.width()*WAVEFORM_OVERSAMPLING; i+=WAVEFORM_OVERSAMPLING) {
uint16_t x = int(i / WAVEFORM_OVERSAMPLING);
// Only recalculate if needed, else use cache
if (waveform_cache_dirty) {
float sig = 0.0f;
for (int s=0; s<WAVEFORM_OVERSAMPLING; s++) {
float abs_sig = looper.getBuffer()[step*i];
abs_sig = abs(abs_sig) * 100.0f;
sig += abs_sig;
}
sig = sig / float(WAVEFORM_OVERSAMPLING);
if (sig != 0.0f) {
sig = log10(sig)/3.6f;
}
waveform_cache[x] = int(sig * wave_height);
}
// Serial.print(waveform_cache[x]);
// Serial.print(",");
display.drawFastVLine(x, bottom, -waveform_cache[x], SH110X_WHITE);
display.drawFastHLine(0, bottom, display.width(), SH110X_WHITE);
}
waveform_cache_dirty = false;
// Draw Line for loop start
int x_start_loop = int(loop_start * display.width());
display.drawLine(x_start_loop, 0, x_start_loop, bottom, SH110X_WHITE);
display.fillTriangle(x_start_loop, 6, x_start_loop, 0, x_start_loop+3, 0, SH110X_WHITE);
// Draw Line for Loop End
int x_loop_length = int(loop_length * display.width());
int x_loop_end = (x_start_loop + x_loop_length) % display.width();
display.drawLine(x_loop_end, 0, x_loop_end, bottom, SH110X_WHITE);
display.fillTriangle(x_loop_end, 6, x_loop_end-3, 0, x_loop_end, 0, SH110X_WHITE);
// Connecting line for start and end
if (x_loop_end >= x_start_loop) {
display.drawLine(x_start_loop, 0, x_loop_end, 0, SH110X_WHITE);
} else {
display.drawLine(x_start_loop, 0, display.width(), 0, SH110X_WHITE);
display.drawLine(0, 0, x_loop_end, 0, SH110X_WHITE);
}
// Draw Playhead
int x_playhead = int(looper.GetPlayhead() * display.width()) + x_start_loop;
display.drawLine(x_playhead, 6, x_playhead, 24, SH110X_WHITE);
// Draw Recording stuff
if (recording_state == REC_STATE_RECORDING) {
// Draw Rec Head
int x_rec_head = int(looper.GetRecHead() * display.width());
display.drawLine(x_rec_head, 10, x_rec_head, bottom, SH110X_WHITE);
display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE);
// Record sign
display.fillRect(0, 0, 13, 13, SH110X_WHITE);
display.fillRect(2, 2, 12, 12, SH110X_WHITE);
display.fillRect(1, 1, 11, 11, SH110X_BLACK);
display.fillCircle(6, 6, 3, SH110X_WHITE);
}
// Draw Overdub stuff
if (recording_state == REC_STATE_OVERDUBBING) {
// Overdub sign (a "plus")
display.fillRect(0, 0, 13, 13, SH110X_WHITE);
display.fillRect(2, 2, 12, 12, SH110X_WHITE);
display.fillRect(1, 1, 11, 11, SH110X_BLACK);
display.drawLine(6, 2, 6, 10, SH110X_WHITE);
display.drawLine(2, 6, 10, 6, SH110X_WHITE);
}
// Render potentiometer UIs in case a knob is changed
pot_1.renderUi();
pot_2.renderUi();
pot_3.renderUi();
pot_4.renderUi();
pot_5.renderUi();
pot_6.renderUi();
pot_7.renderUi();
display.display();
last_render = now;
}
}
void activateRecording() {
if (recording_state != REC_STATE_RECORDING) {
Serial.println("[UI] Activate Recording");
looper.SetRecording(true, false);
waveform_cache_dirty = true;
recording_state = REC_STATE_RECORDING;
}
}
void toggleRecording() {
Serial.print("[UI] Toggle Recording ");
Serial.print(recording_state);
switch (recording_state) {
case REC_STATE_NOT_RECORDING:
activateRecording();
break;
case REC_STATE_RECORDING:
stopRecording();
break;
case REC_STATE_OVERDUBBING:
activateRecording();
break;
}
Serial.print(" -> ");
Serial.println(recording_state);
}
bool isRecording() {
return looper.isRecording();
}
void activateOverdub() {
if (recording_state != REC_STATE_OVERDUBBING) {
Serial.println("[UI] Activate Overdub");
waveform_cache_dirty = true;
recording_state = REC_STATE_OVERDUBBING;
looper.SetRecording(true, true);
}
}
void stopRecording() {
if (recording_state != REC_STATE_NOT_RECORDING) {
Serial.println("[UI] Stop Recording");
recording_state = REC_STATE_NOT_RECORDING;
looper.SetRecording(false, false);
}
}
void toggleOverdub() {
Serial.print("[UI] Toggle Overdub ");
Serial.print(recording_state);
switch (recording_state) {
case REC_STATE_NOT_RECORDING:
activateOverdub();
break;
case REC_STATE_OVERDUBBING:
stopRecording();
break;
case REC_STATE_RECORDING:
activateOverdub();
break;
}
Serial.print(" -> ");
Serial.println(recording_state);
}
bool isOverdubbing() {
return looper.isOverdubbing();
}
void resetRecordingState() {
if (recording_state == REC_STATE_RECORDING || recording_state == REC_STATE_OVERDUBBING) {
waveform_cache_dirty = true;
}
}
void update() {
resetRecordingState();
}
void setLoop(float start, float length) {
loop_start = start;
loop_length = length;
looper.SetLoop(start, length);
}
private:
double last_render;
uint16_t waveform_cache[128] = {0};
bool waveform_cache_dirty = true;
RecordingState recording_state = REC_STATE_NOT_RECORDING;
UiMode last_ui_mode = UI_MODE_LAST;
bool rec_button_momentary = true;
float loop_start = 0.0f;
float loop_length = 0.0f;
};
#endif
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment