diff --git a/code/daisy-looper/buttons.h b/code/daisy-looper/buttons.h index 04d27bf144d83d57c6f6262fc352f7030aff1e05..3868069f205d3dd82263db4b04dd67a65e15708c 100644 --- a/code/daisy-looper/buttons.h +++ b/code/daisy-looper/buttons.h @@ -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 diff --git a/code/daisy-looper/daisy-looper.ino b/code/daisy-looper/daisy-looper.ino index 02e4ed872d519469fb7928e35616cf7eb77dd074..f69de3883337a405757ffc084851f6b2502d862d 100644 --- a/code/daisy-looper/daisy-looper.ino +++ b/code/daisy-looper/daisy-looper.ino @@ -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(); - - // Set loop parameters - loop_start = p1; - loop_length = p2; - looper.SetLoop(loop_start, loop_length); - - // Value should go 1 octave up/down (12 semitones) - pitch_val = 12.0f * p3; - pitch_shifter.SetTransposition(pitch_val); + button_4.read(); + button_5.read(); + button_6.read(); + + // Set loop start and loop length + ui.setLoop(p1, p2); + + // 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); - - // Toggle record - looper.SetRecording(record_on, overdub_on); + delaytime = 100.0f + p5 * 23900.0f; + reverbmix = p6; + lfo_amount = p7; - // 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; - } -} diff --git a/code/daisy-looper/helpers.h b/code/daisy-looper/helpers.h index ef66325e25d825533c04d8aab64c12846f08ef0e..3a9178fa1bc68a24b7ef84c8b1efbd6e787f5ca4 100644 --- a/code/daisy-looper/helpers.h +++ b/code/daisy-looper/helpers.h @@ -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 diff --git a/code/daisy-looper/looper.h b/code/daisy-looper/looper.h index d75f8f9137aa777857d17e4b9a3610937f4b129b..9624a92b60ca87b97b56e674345b1dc54cc5e731 100644 --- a/code/daisy-looper/looper.h +++ b/code/daisy-looper/looper.h @@ -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; - //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; - _is_empty = false; + 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_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; } - // 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; + + _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_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 = fmod(rec_head, float(_loop_start + _loop_length)); + rec_head = max(float(_loop_start), rec_head); + } } else { - // Limit rec head to stay inside the loop - _rec_head %= (_loop_start + _loop_length); - _rec_head = max(_loop_start, _rec_head); + 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); + + // Calculate fade in/out + 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; + play_head = 0; + } else if (play_head <= 0) { + _loop_start = _pending_loop_start; + _loop_length = _pending_loop_length; + 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 diff --git a/code/daisy-looper/ui.h b/code/daisy-looper/ui.h new file mode 100644 index 0000000000000000000000000000000000000000..2d27771b6adb5bccd3782dde6b8fde5534100dbc --- /dev/null +++ b/code/daisy-looper/ui.h @@ -0,0 +1,584 @@ +#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