From 8543bad56290eed68fa81c999bdb6e94a01fb41c Mon Sep 17 00:00:00 2001
From: David Huss <dh@atoav.com>
Date: Fri, 16 Feb 2024 16:17:46 +0100
Subject: [PATCH] Move code here from daisyy-hardware

---
 README.md            |   8 +
 menu.ods             | Bin 0 -> 20193 bytes
 src/.~lock.menu.ods# |   1 +
 src/button_grid.h    | 187 +++++++++
 src/buttons.h        | 165 ++++++++
 src/daisy-looper.ino | 467 +++++++++++++++++++++++
 src/env_follower.h   |  91 +++++
 src/helpers.h        | 158 ++++++++
 src/leds.h           |  78 ++++
 src/lfo.h            |   9 +
 src/looper.h         | 468 +++++++++++++++++++++++
 src/luts.h           |  47 +++
 src/potentiometers.h | 257 +++++++++++++
 src/ui.h             | 884 +++++++++++++++++++++++++++++++++++++++++++
 14 files changed, 2820 insertions(+)
 create mode 100644 menu.ods
 create mode 100644 src/.~lock.menu.ods#
 create mode 100644 src/button_grid.h
 create mode 100644 src/buttons.h
 create mode 100644 src/daisy-looper.ino
 create mode 100644 src/env_follower.h
 create mode 100644 src/helpers.h
 create mode 100644 src/leds.h
 create mode 100644 src/lfo.h
 create mode 100644 src/looper.h
 create mode 100644 src/luts.h
 create mode 100644 src/potentiometers.h
 create mode 100644 src/ui.h

diff --git a/README.md b/README.md
index e69de29..bacab1a 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,8 @@
+# daisy-looper
+
+
+
+# What does the looper do?
+
+*daisy-looper* is a software that can be installed on the [daisyy hardware](../../README.md) developed in the course *Synthesizer DIY* that took place during winter semester 2023/2024 at HFBK Hamburg. The looper allows you to record audio present at the input and [overdub](https://en.wikipedia.org/wiki/Overdubbing) into up to 5 different virtual "tapes" called *buffers*. The recorded sounds stored in each buffer will be displayed as a waveform and can be played back at different speeds (including reverse speed). 
+
diff --git a/menu.ods b/menu.ods
new file mode 100644
index 0000000000000000000000000000000000000000..2b8391b9cf03447b0e7b7cf66da2c734faf8ed40
GIT binary patch
literal 20193
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
SAZ3CKf()rF3=C;rARYj!p3Eiy

literal 0
HcmV?d00001

diff --git a/src/.~lock.menu.ods# b/src/.~lock.menu.ods#
new file mode 100644
index 0000000..3e896df
--- /dev/null
+++ b/src/.~lock.menu.ods#
@@ -0,0 +1 @@
+,d0,t,02.02.2024 17:44,file:///home/d0/.config/libreoffice/4;
\ No newline at end of file
diff --git a/src/button_grid.h b/src/button_grid.h
new file mode 100644
index 0000000..96395e3
--- /dev/null
+++ b/src/button_grid.h
@@ -0,0 +1,187 @@
+#ifndef Buttongrid_h
+#define Buttongrid_h
+
+#include "Adafruit_SH110X.h"
+#include "Adafruit_GFX.h"
+#include "potentiometers.h"
+#include "buttons.h"
+#include "ui.h"
+
+
+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;
+
+// Represents the different types of GridButtons in the UI
+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")
+  BUTTON_TYPE_MULTITOGGLE,  // Toggles between two or more values (name needs to have a break "\n")
+  BUTTON_TYPE_ENUM,         // Toggles between a group of buttons
+  BUTTON_TYPE_LAST
+};
+
+// A Gridbutton is a single button within the Ui-Button Grid
+// The name can have multiple lines, delimited by "\n" which
+// will be used to represent different states for buttons of
+// the type BUTTON_TYPE_TOGGLE or BUTTON_TYPE_MULTITOGGLE
+// is_home tells us if this is the home button (and thus should
+// be rendered inverted)
+class GridButton {
+  public:
+    const char* name;
+    const char* description;
+    GridButton(const char* name, Button *button, bool is_home=false, ButtonType type=BUTTON_TYPE_SIMPLE, int default_value=0, const char* description="")
+     : 
+      name(name),
+      button(button),
+      is_home(is_home),
+      type(type),
+      active(default_value),
+      description(description)
+    {
+      // Count the number of lines in the name
+      for(int i = 0; name[i] != '\0'; i++) {
+          if(name[i] == '\n')
+              ++lines;
+      }
+    }
+    Button *button;
+    ButtonType type;
+    bool is_home;
+    bool should_render_description = false;
+    int active;
+    int lines = 0;
+    
+    // Go to the next option
+    void next() {
+      active++;
+      if (active > lines) {
+        active = 0;
+      }
+    }
+
+    void renderDescription() {
+      if (should_render_description) {
+        display.setTextWrap(true);
+        display.clearDisplay();
+        display.setCursor(0,0);
+        display.setTextColor(SH110X_WHITE);
+        display.print(description);
+        display.setTextWrap(false);
+      }
+    }
+};
+
+// The ButtonGrid is a grid of 2×3 = 6 buttons
+class ButtonGrid {
+  public:
+    ButtonGrid(int home_mode, 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]
+      },
+      ui_mode(home_mode)
+    {}
+    GridButton grid_buttons_[6];
+    int ui_mode;
+
+    void setup() {
+      for (int n=0; n<6; n++) {
+        if (!grid_buttons_[n].is_home) {
+          // Not a home button, display help on long hold and hide on release
+          // grid_buttons_[n].button->onLongHold([this, n](){
+          //   grid_buttons_[n].should_render_description = true;
+          // });
+          // grid_buttons_[n].button->onReleased([this, n](){
+          //   grid_buttons_[n].should_render_description = false;
+          // });
+        }
+      }
+    }
+
+    int homeButtonIndex() {
+      for (int i=0; i<6; i++) {
+        if (grid_buttons_[i].is_home) {
+          return i;
+        }
+      }
+      return -1;
+    }
+
+    void hideAllDescriptions() {
+      for (int i=0; i<6; i++) {
+        grid_buttons_[i].should_render_description = false;
+      }
+    }
+
+    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++) {
+          // Get the current buttons name
+          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);
+          }
+          // Increase ounter for the index of the button
+          i++;
+        }
+      }
+      
+      // Draw divider lines
+      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);
+
+      // Render Descriptions on top (hence another loop)
+      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++) {
+          grid_buttons_[i].renderDescription();
+          i++;
+        }
+      }
+    }
+};
+
+
+#endif
\ No newline at end of file
diff --git a/src/buttons.h b/src/buttons.h
new file mode 100644
index 0000000..4ab6d99
--- /dev/null
+++ b/src/buttons.h
@@ -0,0 +1,165 @@
+#include "wiring_constants.h"
+#ifndef Buttons_h
+#define Buttons_h
+
+#include "Arduino.h"
+#include "Adafruit_SH110X.h"
+#include "Adafruit_GFX.h"
+extern Adafruit_SH1106G display;
+
+#define DURATION_SHORT_PRESS 800
+#define DURATION_VERY_LONG_PRESS 2000
+
+
+
+
+
+class Button {
+  int pin;
+  bool has_been_pressed;
+  unsigned long press_start;
+  unsigned long release_start;
+  std::function<void()> onPressFunction;
+  std::function<void()> onHoldFunction;
+  std::function<void()> onLongHoldFunction;
+  std::function<void()> onVeryLongHoldFunction;
+  std::function<void()> onLongPressFunction;
+  std::function<void()> onVeryLongPressFunction;
+  std::function<void()> onReleasedFunction;
+
+  public:
+    Button(int pin);
+    void init();
+    void read();
+    unsigned long pressed_since();
+    unsigned long released_since();
+
+    void onPress(std::function<void()> f);
+    void onHold(std::function<void()> f);
+    void onLongHold(std::function<void()> f);
+    void onVeryLongHold(std::function<void()> f);
+    void onLongPress(std::function<void()> f);
+    void onVeryLongPress(std::function<void()> f);
+    void onReleased(std::function<void()> f);
+
+    void reset();
+};
+
+Button::Button(int pin) {
+  this->pin = pin;
+}
+
+void Button::init() {
+  pinMode(this->pin, INPUT_PULLUP);
+  this->has_been_pressed = false;
+  this->press_start = 0;
+  this->release_start = 0;
+}
+
+void Button::read() {
+  int is_pressed = !digitalRead(this->pin);
+  
+  if (is_pressed && this->press_start == 0) {
+    this->press_start = millis();
+  }
+  if (!is_pressed && this->has_been_pressed && this->release_start == 0) {
+    this->release_start = millis();
+  }
+
+  unsigned long pressed_since = this->pressed_since();
+  unsigned long released_since = this->released_since();
+  
+  if (is_pressed) {
+    // Fire the callback function all the time while this is being pressed
+    if (this->onHoldFunction) { this->onHoldFunction(); }
+
+    if (this->pressed_since() > 1000) {
+      if (this->onLongHoldFunction) { this->onLongHoldFunction(); }
+    }
+    if (this->pressed_since() > 5000) {
+      if (this->onVeryLongHoldFunction) { this->onVeryLongHoldFunction(); }
+    }
+    // Serial.print("Pressed since ");
+    // Serial.println(pressed_since);
+    if ( released_since > 100) {
+      this->has_been_pressed = false;
+    }
+  } else {
+    // Not pressed.
+    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);
+      } 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(")");
+      } 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(")");
+      }
+      this->press_start = 0;
+      this->has_been_pressed = true;
+      this->release_start = millis();
+      if (this->onReleasedFunction) { this->onReleasedFunction(); }
+    }
+  }
+}
+
+unsigned long Button::pressed_since() {
+  if ( this->press_start == 0) {
+    return 0;
+  }
+  return millis() - this->press_start;
+}
+
+unsigned long Button::released_since() {
+  if ( this->release_start == 0) {
+    return 0;
+  }
+  return millis() - this->release_start;
+}
+
+void Button::onPress(std::function<void()> f) {
+  this->onPressFunction = f;
+}
+
+void Button::onHold(std::function<void()> f) {
+  this->onHoldFunction = f;
+}
+
+void Button::onLongHold(std::function<void()> f) {
+  this->onLongHoldFunction = f;
+}
+
+void Button::onVeryLongHold(std::function<void()> f) {
+  this->onVeryLongHoldFunction = f;
+}
+
+void Button::onLongPress(std::function<void()> f) {
+  this->onLongPressFunction = f;
+}
+
+void Button::onVeryLongPress(std::function<void()> f) {
+  this->onVeryLongPressFunction = f;
+}
+
+void Button::onReleased(std::function<void()> f) {
+  this->onReleasedFunction = f;
+}
+
+void Button::reset() {
+  this->onPressFunction = NULL;
+  this->onHoldFunction = NULL;
+  this->onLongPressFunction = NULL;
+  this->onVeryLongPressFunction = NULL;
+}
+
+
+#endif
\ No newline at end of file
diff --git a/src/daisy-looper.ino b/src/daisy-looper.ino
new file mode 100644
index 0000000..b19ec18
--- /dev/null
+++ b/src/daisy-looper.ino
@@ -0,0 +1,467 @@
+#include "DaisyDuino.h"
+#include <Wire.h>
+#include <Adafruit_GFX.h>
+#include <Adafruit_SH110X.h>
+#include <MIDI.h>
+
+#include "leds.h"
+#include "potentiometers.h"
+#include "buttons.h"
+#include "looper.h"
+#include "env_follower.h"
+#include "helpers.h"
+#include "luts.h"
+#include "ui.h"
+#include "lfo.h"
+
+MIDI_CREATE_DEFAULT_INSTANCE();
+#define BUFFER_LENGTH_SECONDS 5
+
+#define DEBUGMODE
+
+static const size_t buffer_length = 48000 * BUFFER_LENGTH_SECONDS;
+static float DSY_SDRAM_BSS buffer[buffer_length];
+static float DSY_SDRAM_BSS buffer_b[buffer_length];
+static float DSY_SDRAM_BSS buffer_c[buffer_length];
+static float DSY_SDRAM_BSS buffer_d[buffer_length];
+static float DSY_SDRAM_BSS buffer_e[buffer_length];
+
+
+// Create instances of audio stuff
+atoav::Looper looper_a, looper_b, looper_c, looper_d, looper_e;
+static atoav::EnvelopeFollower input_envelope_follower;
+DSY_SDRAM_BSS ReverbSc reverb;
+static Compressor compressor;
+Oscillator lfo;
+static SampleHold sample_and_hold;
+static WhiteNoise noise;
+static Metro tick;
+static Easer easer;
+
+// Initialize Buttons  
+Button button_1 = Button(D7);
+Button button_2 = Button(D8);
+Button button_3 = Button(D9);
+Button button_4 = Button(D10);
+Button button_5 = Button(D13);
+Button button_6 = Button(D27);
+
+// Initialize Potentiometers
+Potentiometer pot_1 = Potentiometer(A0);
+Potentiometer pot_2 = Potentiometer(A1);
+Potentiometer pot_3 = Potentiometer(A3);
+Potentiometer pot_4 = Potentiometer(A2);
+Potentiometer pot_5 = Potentiometer(A4);
+Potentiometer pot_6 = Potentiometer(A5);
+Potentiometer pot_7 = Potentiometer(A6);
+
+// RGB LED               R    G    B
+RGBLed rgb_led = RGBLed(A10, A11, A9);
+
+
+// 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);
+
+// User Interface
+Ui ui;
+
+// Daisy
+DaisyHardware hw;
+
+// Variables for the Audio-Callback
+size_t num_channels;
+float blocksize;
+float drywetmix = 0.0f;
+float volume = 1.0f;
+float reverbmix = 0.0f;
+float lfo_amount = 0.0f;
+float pitch_val = 0.5f;
+float midi_pitch_offset = 0.0f;
+
+float reverb_tone = 15000.0f;
+float reverb_decay = 0.95f;
+float lfo_speed = 8.0f;
+int lfo_kind = 0;
+float rand_pitch_mod = 0.0f;
+
+//float pressure = 0.0f;
+
+// Actual audio-processing is orchestrated here
+void AudioCallback(float **in, float **out, size_t size) {
+  float output1, output2 = 0.0f;
+  float out1, out2;
+  reverb.SetFeedback(reverb_decay);
+  reverb.SetLpFreq(reverb_tone);
+  lfo.SetFreq(lfo_speed);
+  tick.SetFreq(lfo_speed);
+  
+  // Iterate through the samples in the buffer
+  for (size_t i = 0; i < size; i++) {
+    float lfo_value = 0.0f;
+    switch (lfo_kind) {
+      case LfoKind::LFO_KIND_TRI:
+        lfo.SetWaveform(Oscillator::WAVE_TRI);
+        lfo_value = lfo.Process();
+        break;
+      case LfoKind::LFO_KIND_SQR:
+        lfo.SetWaveform(Oscillator::WAVE_SQUARE);
+        lfo_value = lfo.Process();
+        break;
+      default:
+        break;
+    }
+    
+    tick.SetFreq(0.25f+lfo_speed*49.75f);
+    uint8_t trig = tick.Process();
+    float noise_value = noise.Process();
+
+    switch (lfo_kind) {
+      case LfoKind::LFO_KIND_RAND:
+        easer.setFactor(0.01);
+        break;
+      case LfoKind::LFO_KIND_JUMP:
+        easer.setFactor(1.0);
+        break;
+    }
+
+    float rand = easer.Process(
+      sample_and_hold.Process(
+        trig, 
+        noise_value * 5.0f, 
+        sample_and_hold.MODE_SAMPLE_HOLD
+    ));
+
+    if (ui.rec_source == REC_SOURCE_NOISE) {
+     ui.activeLooper()->Record(noise_value * 0.5f);
+    }
+    // When the metro ticks, trigger the envelope to start.
+    float random_amount = lfo_amount * 2.0;
+    
+    if (trig) {
+      // Random LFO
+      switch (lfo_kind) {
+        case LfoKind::LFO_KIND_RAND:
+          rand_pitch_mod = rand * random_amount * 5.0f;
+          break;
+        case LfoKind::LFO_KIND_JUMP:
+          // Chance
+          if (drand(0.0f, 1.0f) < lfo_amount) {
+            ui.activeLooper()->addToPlayhead(rand * random_amount * 8000.0f);
+          }
+          break;
+        default:
+          break;
+      }
+    }
+
+    // Add the LFO to the signal if it is active
+    switch (lfo_kind) {
+      case LfoKind::LFO_KIND_TRI:
+      case LfoKind::LFO_KIND_SQR:
+        ui.activeLooper()->setPlaybackSpeed(pitch_val + lfo_value * lfo_amount + midi_pitch_offset);
+        break;
+      case LfoKind::LFO_KIND_RAND:
+        ui.activeLooper()->setPlaybackSpeed(pitch_val + rand_pitch_mod + midi_pitch_offset);
+        break;
+      default:
+        ui.activeLooper()->setPlaybackSpeed(pitch_val + midi_pitch_offset);
+        break;
+    }
+    
+
+    float looper_out;
+    
+    if (ui.rec_source == REC_SOURCE_PRE) {
+     ui.activeLooper()->Record(in[1][i]);
+    }
+
+    if (ui.rec_source == REC_SOURCE_LAST_BUF) {
+      // FIXME: This might process the previous looper twice later below, add check...
+      ui.activeLooper()->Record(ui.previousLooper()->Process());
+    }
+    
+    // Process the input envelope
+    input_envelope_follower.Process(in[1][i]);
+
+    // Process the Looper
+    switch (ui.buffer_summing_mode) {
+      case BUFFER_SUM_MODE_SOLO:
+        // Only play active looper
+        looper_out = ui.activeLooper()->Process();
+        break;
+      case BUFFER_SUM_MODE_SUM:
+        // Sum all loopers
+        looper_out = looper_a.Process();
+        looper_out += looper_b.Process();
+        looper_out += looper_c.Process();
+        looper_out += looper_d.Process();
+        looper_out += looper_e.Process();
+        looper_out = looper_out;
+        break;
+      case BUFFER_SUM_MODE_RING:
+        // Sum all loopers and ringmodulate with input
+        looper_out = looper_a.Process();
+        looper_out += looper_b.Process();
+        looper_out += looper_c.Process();
+        looper_out += looper_d.Process();
+        looper_out += looper_e.Process();
+        float deadbanded_input;
+        if (in[1][i] < 0.0f) {
+          deadbanded_input = min(0.0f, in[1][i] + 0.05f);
+        } else {
+          deadbanded_input = max(0.0f, in[1][i] - 0.05f);
+        }
+        looper_out *= deadbanded_input*2.0f;
+        looper_out = looper_out;
+        break;
+    }
+
+    // looper_out = pressure * looper_out;
+    looper_out = saturate(volume * looper_out);
+
+    // Mix the dry/Wet of the looper
+    output1 = output2 = drywetmix * looper_out + in[1][i] * (1.0f - drywetmix);
+
+    // Compress the signal
+    compressor.Process(output1);
+
+    // Process reverb
+    reverb.Process(output1, output1, &out1, &out2);
+
+    // Short decays are silent, so increase level here
+    float dec_fac = 1.0f + (1.0f - reverb_decay) * 2.0f;
+    out1 = out1 * dec_fac;
+    out2 = out2 * dec_fac;
+
+    // Mix reverb with the dry signal depending on the amount dialed
+    output1 = output1 * (1.0f - reverbmix) + out1 * reverbmix;
+    output2 = output1 * (1.0f - reverbmix) + out2 * reverbmix;
+
+    // Record the output if needed
+    if (ui.rec_source == REC_SOURCE_OUT) {
+     ui.activeLooper()->Record(output1);
+    }
+    
+    out[0][i] = output1;
+    out[1][i] = output2;
+  }
+}
+
+// TODO: Add Voice Stealing/Multiple Playheads?
+// TODO: Is USB Midi possible? https://github.com/electro-smith/DaisyExamples/blob/master/seed/USB_MIDI/USB_MIDI.cpp
+
+void handleNoteOn(byte inChannel, byte inNote, byte inVelocity) {
+  #ifdef DEBUGMODE
+  Serial.print("[MIDI ON] chn<");
+  Serial.print((int) inChannel);
+  Serial.print("> note<");
+  Serial.print((int) inNote);
+  Serial.print("> velocity<");
+  Serial.print((int) inVelocity);
+  Serial.println(">");
+  #endif
+  // Note Off can come in as Note On w/ 0 Velocity
+  if (inVelocity == 0.0f) {
+    midi_pitch_offset = 0.0f;
+  } 
+  else {
+    midi_pitch_offset = (int(inNote)-36.0)/12.0f;
+  }
+}
+
+
+
+void handleNoteOff(byte inChannel, byte inNote, byte inVelocity) {
+  #ifdef DEBUGMODE
+  Serial.print("[MIDI OFF] chn<");
+  Serial.print((int) inChannel);
+  Serial.print("> note<");
+  Serial.print((int) inNote);
+  Serial.print("> velocity<");
+  Serial.print((int) inVelocity);
+  Serial.println(">");
+  #endif
+  midi_pitch_offset = 0.0f;
+}
+
+// void handleAftertouch(byte channel, byte channel_pressure) {
+//   #ifdef DEBUGMODE
+//   Serial.print("[MIDI AFTER] chn<");
+//   Serial.print((int) channel);
+//   Serial.print("> pressure<");
+//   Serial.print((int) channel_pressure);
+//   Serial.println(">");
+//   #endif
+//   pressure = float(channel_pressure)/127.0f;
+// }
+
+
+
+void setup() {
+  float sample_rate;
+  // Initialize for Daisy pod at 48kHz
+  hw = DAISY.init(DAISY_SEED, AUDIO_SR_48K);
+  DAISY.SetAudioBlockSize(64);
+  num_channels = hw.num_channels;
+  sample_rate = DAISY.get_samplerate();
+  blocksize = 64.0f;
+
+  // Create a Tick and a noise source for the Sample and Hold
+  tick.SetFreq(1.0f+lfo_amount*99.0f);
+  tick.Init(10, sample_rate);
+  noise.Init();
+  
+  // Initialize Looper with the buffer
+  looper_a.Init(buffer, buffer_length);
+  looper_b.Init(buffer_b, buffer_length);
+  looper_c.Init(buffer_c, buffer_length);
+  looper_d.Init(buffer_d, buffer_length);
+  looper_e.Init(buffer_e, buffer_length);
+
+  // Initialize Envelope Follower for the Level LED
+  input_envelope_follower.Init(sample_rate);
+  input_envelope_follower.SetAttack(100.0);
+  input_envelope_follower.SetDecay(1000.0);
+
+  // Initialize Reverb
+  reverb.Init(sample_rate);
+  reverb.SetFeedback(reverb_decay);
+  reverb.SetLpFreq(reverb_tone);
+
+  // Initialize Compressor
+  compressor.SetThreshold(-64.0f);
+  compressor.SetRatio(2.0f);
+  compressor.SetAttack(0.005f);
+  compressor.SetRelease(0.1250);
+
+  // Initialize the LFO for modulations
+  lfo.Init(sample_rate);
+  lfo.SetWaveform(Oscillator::WAVE_TRI);
+  lfo.SetAmp(1);
+  lfo.SetFreq(lfo_speed);
+
+  // Easer for the random jumps
+  easer.setFactor(0.001);
+
+  // Start serial communications
+  Serial.begin(250000);
+
+  // Initialize Display
+  display.begin(0x3C, true);
+  delay(50);
+  ui.Render();
+  
+  // Initialize the LED
+  rgb_led.init();
+
+  // Set the analog read and write resolution to 12 bits
+  analogReadResolution(12);
+
+  // Setup MIDI handlers
+  MIDI.setHandleNoteOn(handleNoteOn);
+  MIDI.setHandleNoteOff(handleNoteOff);
+  // MIDI.setHandleAfterTouchChannel(handleAftertouch);
+  
+  MIDI.begin(MIDI_CHANNEL_OMNI); // Listen to all incoming messages
+
+  // Set Knob names and display functions
+  pot_1.name = "Start";
+  pot_2.name = "Length";
+  pot_3.setDisplayMode("Speed", 1000.0f, POT_DISPLAY_MODE_PERCENT);
+  pot_4.setDisplayMode("Mix", 100.0f, POT_DISPLAY_MODE_PERCENT);
+  pot_5.setDisplayMode("LFO", 100.0f, POT_DISPLAY_MODE_PERCENT);
+  pot_6.setDisplayMode("Volume", 400.0f, POT_DISPLAY_MODE_PERCENT);
+  pot_7.setDisplayMode("Reverb", 100.0f, POT_DISPLAY_MODE_PERCENT);
+
+  // Set Knob Scaling Modes
+  // pot_3.setBipolar();
+  pot_3.setPitch();
+
+  // Initialize Buttons (callbacks are assigned in the Ui class)
+  button_1.init();
+  button_2.init();
+  button_3.init();
+  button_4.init();
+  button_5.init();
+  button_6.init();
+
+  // Start the audio Callback
+  DAISY.begin(AudioCallback);
+}
+
+void loop() {
+  // Read the values from the potentiometers
+  float p1 = pot_1.read();
+  float p2 = pot_2.read();
+  float p3 = pot_3.read();
+  float p4 = pot_4.read();
+  float p5 = pot_5.read();
+  float p6 = pot_6.read();
+  float p7 = pot_7.read();
+
+  // Update the UI
+  ui.update();
+
+  // Read the buttons
+  button_1.read();
+  button_2.read();
+  button_3.read();
+  button_4.read();
+  button_5.read();
+  button_6.read();
+
+  // Set loop-start and loop-length with the potentiometers
+  ui.setLoop(p1, p2);
+  
+  // Tune the pitch to the ten-fold of the potentiometer 3,
+  // a bipolar pot, so it returns values from -1.0 to +1.0
+  // Pitch should go 10 octaves up/down (10*1.0 = 1000%)
+  if (!isnan(p3)) {pitch_val = 10.0f * p3; }
+
+  // Set other parameters (from 0.0 to 1.0)
+  if (!isnan(p4)) { drywetmix = p4; }
+
+  switch (ui.fx_mode) {
+    case FX_MODE_ALL:
+      if (!isnan(p5)) {  lfo_amount = p5; }
+      if (!isnan(p6)) {  volume = p6 * 4.0f; }
+      if (!isnan(p7)) {  reverbmix = p7; }
+      break;
+    case FX_MODE_REVERB:
+      if (!isnan(p5)) {  reverb_tone = 50.0f + p5 * 20000.0f; }
+      // TODO: Short Reverb Decay times are too silent?
+      if (!isnan(p6)) {  reverb_decay = 0.05f + p6 * 0.94f; }
+      if (!isnan(p7)) {  reverbmix = p7; }
+      break;
+    case FX_MODE_LFO:
+      if (!isnan(p5)) {  lfo_kind = p5; }
+      if (!isnan(p6)) {  lfo_speed = (p6 * p6 *p6) * 100.0f; }
+      if (!isnan(p7)) {  lfo_amount = p7; }
+      break;
+    case FX_MODE_GRAIN:
+      if (!isnan(p5)) {  ui.activeLooper()->grain_count = 1+int(p5); }
+      if (!isnan(p6)) {  ui.activeLooper()->grain_spread = p6*10.0f; }
+      if (!isnan(p7)) {  ui.activeLooper()->grain_variation = p7; }
+      break;
+  }
+
+  // Render the UI (frame rate limited by UI_MAX_FPS in ui.h)
+  // double start = millis();
+  ui.Render();
+  // Serial.print("ui Render took ");
+  // Serial.print(millis()-start);
+  // Serial.println("ms");
+
+  // Set the Color and brightness of the RGB LED in 8 bits
+  rgb_led.setAudioLevelIndicator(input_envelope_follower.getValue());
+
+  // MIDI
+  MIDI.read();
+}
+
+
+
+
diff --git a/src/env_follower.h b/src/env_follower.h
new file mode 100644
index 0000000..b3a1239
--- /dev/null
+++ b/src/env_follower.h
@@ -0,0 +1,91 @@
+#include "wiring_constants.h"
+#ifndef Env_follower_h
+#define Env_follower_h
+#include "Arduino.h"
+
+namespace atoav {
+
+class SmoothingFilter {
+  public:
+      void Init(float smoothing_time_ms, float sample_rate) {
+        a = exp(-TWO_PI / (smoothing_time_ms * 0.001f * sample_rate));
+        b = 1.0f - a;
+        z = 0.0f;
+      }
+
+      inline float Process(float in) {
+        z = (in * b) + (z * a);
+        return z;
+      }
+
+      void setSmoothing(float smoothing_time_ms, float sample_rate) {
+        a = exp(-TWO_PI / (smoothing_time_ms * 0.001f * sample_rate));
+        b = 1.0f - a;
+      }
+
+  private:
+    float a;
+    float b;
+    float z;
+};
+
+
+class EnvelopeFollower {
+  public:
+    void Init(float sample_rate) {
+      sample_rate = sample_rate;
+      attack = 200.0f;
+      decay = 4000.0f;
+      smoothing = 50.0f;
+      value  = 0.0f;
+      smoothing_filter.Init(smoothing, sample_rate);
+    }
+
+    void SetAttack(float attack_ms) {
+      attack = pow(0.01, 1.0 / (attack_ms * sample_rate * 0.001));
+    }
+
+    void SetDecay(float decay_ms) {
+      decay = pow(0.01, 1.0 / (decay_ms * sample_rate * 0.001));
+    }
+
+    void SetSmoothing(float smoothing_ms) {
+      smoothing_filter.setSmoothing(smoothing_ms, sample_rate);
+    }
+
+    float Process(float in) {
+      abs_value = smoothing_filter.Process(abs(in));
+      if (abs_value > value) {
+        value = attack * (value - abs_value) + abs_value;
+      } else {
+        value = decay * (value - abs_value) + abs_value;
+      }
+      return value;
+    }
+
+    float value;
+
+    float getValue() {
+      if (value == 0.0f || value == -0.0f) {
+        return value;
+      }
+      return max(0.0f, (3.0f + log10(value)) / 3.0f);
+      // return value;
+    }
+
+
+  private:
+    float sample_rate;
+    float attack;
+    float decay;
+    float smoothing;
+    float abs_value = 0.0f;
+    SmoothingFilter smoothing_filter;
+};
+};
+
+
+
+
+
+#endif
\ No newline at end of file
diff --git a/src/helpers.h b/src/helpers.h
new file mode 100644
index 0000000..2f6601a
--- /dev/null
+++ b/src/helpers.h
@@ -0,0 +1,158 @@
+#ifndef Helpers_h
+#define Helpers_h
+
+#include "Adafruit_SH110X.h"
+#include "Adafruit_GFX.h"
+extern Adafruit_SH1106G display;
+
+
+
+int centeredText(const char *buf, int x, int y, int color, 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
+    display.setCursor(x - (w / 2), y - (h / 2));
+    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
+        display.setCursor(x - (w / 2), y - (h / 2)-lineheight/2 + (line*lineheight));
+        display.print(d);
+        d = strtok(NULL, ",");
+        line++;
+    }
+    free(tmp);
+  }
+  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 = 2;
+  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;
+}
+
+float saturate(float x) {
+  if (x < -3.0f) {
+    return -1.0f;
+  } else if (x > 3.0f) {
+    return 1.0f;
+  } else {
+    return x * (27.0f + x * x ) / (27.0f + 9.0f * x * x);
+  }
+}
+
+
+double drand(double minf, double maxf){
+  return minf + random(1UL << 31) * (maxf - minf) / (1UL << 31);  // use 1ULL<<63 for max double values)
+}
+
+
+#endif
\ No newline at end of file
diff --git a/src/leds.h b/src/leds.h
new file mode 100644
index 0000000..5c4a816
--- /dev/null
+++ b/src/leds.h
@@ -0,0 +1,78 @@
+#ifndef Leds_h
+#define Leds_h
+#include "Arduino.h"
+#include "luts.h"
+
+
+// Lookup Curves LED Red b
+float red_lut_x[] = {170.0, 173.5, 177.0, 180.5, 184.0, 187.5, 191.0, 194.5, 198.0, 201.5, 205.0, 208.5, 212.0, 215.5, 219.0, 222.5, 240.0, 240.75, 241.5, 242.25, 243.0, 243.75, 244.5, 245.25, 246.0, 246.75, 247.5, 248.25, 249.0, 249.75, 250.5, 251.25, 255};
+float red_lut_y[] = {0.0, 0.0037500000000000007, 0.030000000000000006, 0.10125000000000003, 0.24000000000000005, 0.46875, 0.8100000000000003, 1.2862500000000003, 1.9200000000000004, 2.7337500000000006, 3.75, 4.991250000000002, 6.480000000000002, 8.238750000000001, 10.290000000000003, 12.65625, 30.0, 31.1503125, 32.4525, 34.0584375, 36.12, 38.7890625, 42.2175, 46.5571875, 51.96, 58.5778125, 66.5625, 76.06593750000002, 87.24000000000001, 100.23656250000002, 115.20750000000002, 132.3046875, 255};
+size_t red_lut_len = 33;
+
+// Lookup Curves LED Green b
+float green_lut_x[] = {10.0, 18.5, 27.0, 35.5, 44.0, 52.5, 61.00000000000001, 69.5, 78.0, 86.5, 95.0, 103.50000000000001, 112.00000000000001, 120.5, 129.0, 137.5, 180.0, 183.5, 187.0, 190.5, 194.0, 197.5, 201.0, 204.5, 208.0, 211.5, 215.0, 218.5, 222.0, 225.5, 229.0, 232.5, 250};
+float green_lut_y[] = {0.0, 0.0075000000000000015, 0.06000000000000001, 0.20250000000000007, 0.4800000000000001, 0.9375, 1.6200000000000006, 2.5725000000000007, 3.8400000000000007, 5.467500000000001, 7.5, 9.982500000000003, 12.960000000000004, 16.477500000000003, 20.580000000000005, 25.3125, 60.0, 56.43, 52.92, 49.47, 46.08, 42.75, 39.48, 36.269999999999996, 33.120000000000005, 30.029999999999998, 27.000000000000007, 24.03, 21.119999999999997, 18.269999999999996, 15.480000000000004, 12.750000000000007, 0};
+size_t green_lut_len = 33;
+
+// Lookup Curves LED Blue b
+float blue_lut_x[] = {0.0, 2.0, 4.0, 6.000000000000001, 8.0, 10.0, 12.000000000000002, 14.000000000000002, 16.0, 18.0, 20.0, 22.0, 24.000000000000004, 26.0, 28.000000000000004, 30.0, 40.0, 46.5, 53.0, 59.5, 66.0, 72.5, 79.0, 85.5, 92.0, 98.5, 105.0, 111.5, 118.00000000000001, 124.5, 131.0, 137.5, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0};
+float blue_lut_y[] = {0.0, 0.10099999999999998, 0.20799999999999996, 0.327, 0.46399999999999997, 0.6249999999999999, 0.8160000000000001, 1.0430000000000001, 1.312, 1.629, 1.9999999999999998, 2.4310000000000005, 2.928000000000001, 3.497, 4.144000000000001, 4.875, 10.0, 9.025, 8.1, 7.225, 6.4, 5.625, 4.9, 4.225, 3.5999999999999996, 3.0250000000000004, 2.5, 2.0249999999999986, 1.5999999999999996, 1.2250000000000014, 0.9000000000000004, 0.625, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
+size_t blue_lut_len = 48;
+
+
+
+
+
+class RGBLed {
+  int pin_red;
+  int pin_green;
+  int pin_blue;
+  float level = 0.0f;
+  Easer easer;
+
+  public:
+    RGBLed(int pin_red, int pin_green, int pin_blue);
+    void init();
+    void off();
+    void setColor(int r, int g, int b);
+    void setAudioLevelIndicator(float envelope_value);
+};
+
+RGBLed::RGBLed(int pin_red, int pin_green, int pin_blue) {
+  this->pin_red = pin_red;
+  this->pin_green = pin_green;
+  this->pin_blue = pin_blue;
+  this->easer.setFactor(0.7);
+}
+
+void RGBLed::init() {
+  pinMode(this->pin_red, OUTPUT);
+  pinMode(this->pin_green, OUTPUT);
+  pinMode(this->pin_blue, OUTPUT);
+}
+
+void RGBLed::off() {
+  digitalWrite(this->pin_red, LOW);
+  digitalWrite(this->pin_green, LOW);
+  digitalWrite(this->pin_blue, LOW);
+}
+
+void RGBLed::setColor(int r, int g, int b) {
+  r = min(255, max(0, r));
+  g = min(255, max(0, g));
+  b = min(255, max(0, b));
+  analogWrite(this->pin_red, r);
+  analogWrite(this->pin_green, g);
+  analogWrite(this->pin_blue, b);
+}
+
+void RGBLed::setAudioLevelIndicator(float envelope_value) {
+  level = easer.Process(envelope_value);
+  int brightness = int(min(255.0f, max(0.0f, level * 255)));
+  int red = get_from_xy_table(red_lut_x, red_lut_y, brightness, red_lut_len);
+  int green = get_from_xy_table(green_lut_x, green_lut_y, brightness, green_lut_len);
+  int blue = get_from_xy_table(blue_lut_x, blue_lut_y, brightness, blue_lut_len);
+  this->setColor(red, green, blue);
+}
+
+#endif
\ No newline at end of file
diff --git a/src/lfo.h b/src/lfo.h
new file mode 100644
index 0000000..a91cdb6
--- /dev/null
+++ b/src/lfo.h
@@ -0,0 +1,9 @@
+#pragma once
+
+enum LfoKind {
+	LFO_KIND_TRI,
+	LFO_KIND_SQR,
+	LFO_KIND_RAND,
+	LFO_KIND_JUMP,
+	LFO_KIND_NONE,
+};
\ No newline at end of file
diff --git a/src/looper.h b/src/looper.h
new file mode 100644
index 0000000..594ae1a
--- /dev/null
+++ b/src/looper.h
@@ -0,0 +1,468 @@
+#pragma once
+
+#include "luts.h"
+
+namespace atoav {
+
+enum RecPitchMode {
+  REC_PITCH_MODE_NORMAL,
+  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,
+};
+
+enum PlaybackState {
+  PLAYBACK_STATE_STOPPED,
+  PLAYBACK_STATE_LOOP,
+  PLAYBACK_STATE_MULTILOOP,
+  PLAYBACK_STATE_MIDI,
+  PLAYBACK_STATE_LAST,
+};
+
+enum RecordingState {
+  REC_STATE_EMPTY,
+  REC_STATE_RECORDING,
+  REC_STATE_OVERDUBBING,
+  REC_STATE_ERASING,
+  REC_STATE_NONE,
+  REC_STATE_LAST,
+};
+
+// =================================================
+// =                   H E A D                     =
+// =================================================
+
+class Head {
+  public:
+    Head();
+    void activate();
+    void deactivate();
+    bool isActive();
+    void setPosition(float value);
+    void reset();
+    void setIncrement(float value);
+    void incrementBy(float value);
+    void slowDown();
+    void reverse();
+    void speedUp();
+    void update();
+    float read();
+    float increment = 1.0f;
+    float variation = 0.0f;
+    float variation_amount = 0.0f;
+    float speed_multiplier = 1.0f;
+  private:
+    bool active = true;
+    float position = 0.0f;
+};
+
+Head::Head() {
+  variation = random(-0.1f, 0.1f);
+}
+void Head::activate() {
+  this->active = true;
+}
+void Head::deactivate() {
+  this->active = false;
+}
+bool Head::isActive() {
+  return active;
+}
+void Head::reset() {
+  this->position = 0.0f;
+}
+void Head::setPosition(float value) {
+  this->position = value * speed_multiplier;
+}
+void Head::setIncrement(float value) {
+  this->increment = value;
+}
+void Head::incrementBy(float value) {
+  this->position += value;
+}
+void Head::slowDown() {
+  this->speed_multiplier = max(0.0f, this->speed_multiplier - 0.0025f);
+}
+void Head::reverse() {
+  this->speed_multiplier = -abs(this->speed_multiplier );
+}
+void Head::speedUp() {
+  this->speed_multiplier = 1.0f;
+}
+void Head::update() {
+  this->position += (this->increment + (variation * variation_amount)) * speed_multiplier;
+}
+float Head::read() {
+  return this->position;
+}
+
+
+// =================================================
+// =                 L O O P E R                   =
+// =================================================
+
+class Looper {
+	public:
+		Looper();
+    void Init(float *buf, size_t buf_size);
+    void SetRecord();
+    void SetOverdub();
+    void SetErase();
+    void SetStopWriting();
+    bool isWriting();
+    void ResetRecHead();
+    void SetLoop(float loop_start_time, float loop_length_time);
+    void Record(float in);
+    float Process();
+    float* getBuffer();
+    size_t getBufferLength();
+    void setRecPitchMode(RecPitchMode mode);
+    void setRecStartMode(RecStartMode mode);
+    float GetPlayhead();
+    float* GetPlayheads();
+    uint8_t GetPlayheadCount();
+    float GetRecHead();
+    bool toggleRecMode();
+    void setRecModeFull();
+    void setRecModeLoop();
+    void setRecModeFullShot();
+    void setRecModeLoopShot();
+    void setPlaybackSpeed(float increment);
+    void addToPlayhead(float value);
+    void slowDown();
+    void reverse();
+    void restart();
+    void speedUp();
+    float loop_start_f = 0.0f;
+    float loop_length_f = 1.0f;
+    uint8_t grain_count = 8;
+    float grain_spread = 2.0f;
+    float grain_variation = 0.0f;
+    RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL;
+    RecStartMode rec_start_mode = REC_START_MODE_BUFFER;
+    PlaybackState playback_state = PLAYBACK_STATE_LOOP;
+    RecordingState recording_state = REC_STATE_EMPTY;
+
+  private:
+    static const size_t kFadeLength = 200;
+    static const size_t kMinLoopLength = 2 * kFadeLength;
+
+    float* buffer;
+    size_t buffer_length = 0;
+
+    Head playheads[9];
+    Head rec_head;
+
+    size_t loop_start = 0;
+    size_t loop_length = 48000;
+
+    bool stop_after_recording = false;
+    bool stay_within_loop = true;
+
+};
+
+
+Looper::Looper() {}
+
+void Looper::Init(float *buf, size_t buf_size) {
+  buffer = buf;
+  buffer_length = buf_size;
+  memset(buffer, 0, sizeof(float) * buffer_length);
+}
+
+void Looper::SetRecord() {
+  recording_state = REC_STATE_RECORDING;
+  ResetRecHead();
+  rec_head.activate();
+}
+
+void Looper::SetOverdub() {
+  recording_state = REC_STATE_OVERDUBBING;
+  ResetRecHead();
+  rec_head.activate();
+}
+
+void Looper::SetErase() {
+  recording_state = REC_STATE_ERASING;
+  ResetRecHead();
+  rec_head.activate();
+}
+
+void Looper::SetStopWriting() {
+  recording_state = REC_STATE_NONE;
+  rec_head.deactivate();
+}
+
+bool Looper::isWriting() {
+  return recording_state == REC_STATE_RECORDING 
+    || recording_state == REC_STATE_OVERDUBBING 
+    || recording_state == REC_STATE_ERASING; 
+}
+
+void Looper::ResetRecHead() {
+  if (isWriting()) {
+    switch (rec_start_mode) {
+      case REC_START_MODE_LOOP:
+        rec_head.setPosition(loop_start % buffer_length);
+        break;
+      case REC_START_MODE_BUFFER:
+        rec_head.setPosition(0.0f);
+        break;
+      case REC_START_MODE_PLAYHEAD:
+        rec_head.setPosition(fmod(loop_start + playheads[0].read(), float(buffer_length)));
+        break;
+    }
+  }
+}
+
+void Looper::SetLoop(float loop_start_time, float loop_length_time) {
+  if (!isnan(loop_start_time)) {
+    loop_start_f = loop_start_time;
+    loop_start = static_cast<size_t>(loop_start_time * (buffer_length - 1));
+  }
+  if (!isnan(loop_length_time)) {
+    loop_length_f = loop_length_time;
+    loop_length = max(kMinLoopLength, static_cast<size_t>(loop_length_time * buffer_length));
+  }
+}
+
+void Looper::Record(float in) {
+  // Overwrite/Add/Erase the buffer depending on the mode
+  switch (recording_state) {
+    case REC_STATE_RECORDING:
+      buffer[int(rec_head.read())] = in;
+      break;
+    case REC_STATE_OVERDUBBING:
+      buffer[int(rec_head.read())] += in;
+      break;
+    case REC_STATE_ERASING:
+      buffer[int(rec_head.read())] = 0.0f;
+      break;
+  }
+
+  // Advance the recording head if needed
+  if (isWriting()) {
+    // Set Recording head increment depending on the mode
+    switch (rec_pitch_mode) {
+      case REC_PITCH_MODE_NORMAL: 
+        rec_head.setIncrement(1.0f); 
+        break;
+      case REC_PITCH_MODE_UNPITCHED: 
+        rec_head.setIncrement(playheads[0].increment); 
+        break;
+    }
+    // Increment recording head
+    rec_head.update();
+
+    // Limit the position of the rec head depending on the active mode
+    if (!stop_after_recording) {
+      if (!stay_within_loop) {
+        // record into whole buffer
+        rec_head.setPosition(fmod(rec_head.read(), float(buffer_length)));
+      } else {
+        // Limit rec head to stay inside the loop
+        rec_head.setPosition(fmod(rec_head.read(), float(loop_start + loop_length)));
+        rec_head.setPosition(max(float(loop_start), rec_head.read()));
+      }
+    } else {
+      // Stop at end (either end of buffer or end of loop)
+      if (!stay_within_loop) {
+        if (rec_head.read() > buffer_length) { SetStopWriting(); }
+      } else {
+        if (rec_head.read() > loop_start + loop_length) { SetStopWriting(); }
+      }
+    }
+
+    // Ensure the Rec-Head is never without bounds, even when running backwards
+    if (rec_head.read() > buffer_length) {
+      rec_head.setPosition(0.0f);
+    } else if (rec_head.read() < 0) {
+      rec_head.setPosition(buffer_length);
+    }
+  }
+}
+
+float Looper::Process() {
+  // Early return if buffer is empty or not playing to save performance
+  if (recording_state == REC_STATE_EMPTY 
+    || playback_state == PLAYBACK_STATE_STOPPED) {
+    return 0;
+  }
+
+  // Deactivate all playheads except first if playback state is Loop
+  switch (playback_state) {
+    case PLAYBACK_STATE_LOOP:
+      playheads[0].activate();
+      for (size_t i=1; i<9; i++) {
+        playheads[i].deactivate();
+      }
+      break;
+    case PLAYBACK_STATE_MULTILOOP:
+      for (size_t i=0; i<9; i++) {
+        playheads[i].activate();
+      }
+      break;
+    case PLAYBACK_STATE_STOPPED:
+      for (size_t i=0; i<9; i++) {
+        playheads[i].deactivate();
+      }
+      break;
+  }
+
+  double mix = 0.0;
+
+  for (size_t i=0; i<grain_count-1; i++) {
+    // Skip inactive playheads
+    if (!playheads[i].isActive()) continue;
+
+    // Ensure we are actually inside the buffer
+    int from_start = int(playheads[i].read()) % loop_length;
+    int play_pos = loop_start + from_start;
+
+    float vol = 1.0f;
+    if (from_start <= kFadeLength) {
+      vol = from_start / float(kFadeLength);
+    }
+    int from_end = abs(int(loop_length) - from_start);
+    if (from_end <= kFadeLength) {
+      vol = from_end / float(kFadeLength);
+    }
+
+    // Read from the buffer
+    mix += buffer[play_pos] * vol;
+
+    // Advance the playhead
+    playheads[i].update();
+
+    // Ensure the playhead stays within bounds of the loop
+    float pos = playheads[i].read();
+    if (pos >= loop_length || pos <= 0.0f) {
+      playheads[i].setPosition(fmod(pos, float(loop_length)));
+    }
+  }
+  return mix;
+}
+
+float Looper::GetPlayhead() {
+  return float(int(playheads[0].read()) % loop_length) / float(buffer_length);
+}
+
+float* Looper::GetPlayheads() {
+  static float playhead_positions[9];
+  for (size_t i=0; i<9; i++) {
+    playhead_positions[i] = float(int(playheads[i].read()) % loop_length) / float(buffer_length);
+  }
+  return playhead_positions;
+}
+
+uint8_t Looper::GetPlayheadCount() {
+  return grain_count;
+}
+
+float Looper::GetRecHead() {
+  return  float(rec_head.read()) / float(buffer_length);
+}
+
+bool Looper::toggleRecMode() {
+  stay_within_loop = !stay_within_loop;
+  return stay_within_loop;
+}
+
+void Looper::setRecModeFull() {
+  stay_within_loop = false;
+  stop_after_recording = false;
+}
+
+void Looper::setRecModeLoop() {
+  stay_within_loop = true;
+  stop_after_recording = false;
+}
+
+void Looper::setRecModeFullShot() {
+  stay_within_loop = false;
+  stop_after_recording = true;
+}
+
+void Looper::setRecModeLoopShot() {
+  stay_within_loop = true;
+  stop_after_recording = true;
+}
+
+void Looper::setPlaybackSpeed(float increment) {
+  switch (playback_state) {
+    case PLAYBACK_STATE_LOOP:
+      playheads[0].setIncrement(increment);
+      break;
+    case PLAYBACK_STATE_MULTILOOP:
+      playheads[0].setIncrement(increment);
+      for (size_t i=1; i<9; i++) {
+        playheads[i].variation_amount = grain_variation;
+        playheads[i].setIncrement(increment + increment*grain_spread);
+      }
+      break;
+  }
+}
+
+void Looper::addToPlayhead(float value) {
+  switch (playback_state) {
+    case PLAYBACK_STATE_LOOP:
+      playheads[0].incrementBy(value);
+      break;
+    case PLAYBACK_STATE_MULTILOOP:
+      playheads[0].incrementBy(value);
+      for (size_t i=1; i<9; i++) {
+        playheads[i].incrementBy(value + value/(1+i));
+      }
+      break;
+  }
+}
+
+void Looper::slowDown() {
+  for (size_t i=0; i<9; i++) {
+    playheads[i].slowDown();
+  }
+}
+
+void Looper::reverse() {
+  for (size_t i=0; i<9; i++) {
+    playheads[i].reverse();
+  }
+}
+
+void Looper::restart() {
+  for (size_t i=0; i<9; i++) {
+    playheads[i].reset();
+  }
+}
+
+void Looper::speedUp() {
+  for (size_t i=0; i<9; i++) {
+    playheads[i].speedUp();
+  }
+}
+
+float* Looper::getBuffer() {
+  return buffer;
+}
+
+size_t Looper::getBufferLength() {
+  return buffer_length;
+}
+
+void Looper::setRecPitchMode(RecPitchMode mode) {
+  rec_pitch_mode = mode;
+}
+
+void Looper::setRecStartMode(RecStartMode mode) {
+  rec_start_mode = mode;
+}
+
+
+
+}; // namespace atoav
\ No newline at end of file
diff --git a/src/luts.h b/src/luts.h
new file mode 100644
index 0000000..b332ed6
--- /dev/null
+++ b/src/luts.h
@@ -0,0 +1,47 @@
+#ifndef LUTs_h
+#define LUTs_h
+
+#include <MultiMap.h>
+
+// Lookup Table for Pitch Knob
+float pitch_knob_lookup_x[] = {0.0, 0.0025, 0.0175, 0.0225, 0.0375, 0.0425, 0.057499999999999996, 0.0625, 0.0775, 0.0825, 0.0975, 0.10250000000000001, 0.1175, 0.1225, 0.1375, 0.14250000000000002, 0.1575, 0.1625, 0.1975, 0.2025, 0.2475, 0.2525, 0.2975, 0.3025, 0.3775, 0.3825, 0.4175, 0.4225, 0.4575, 0.4625, 0.4975, 0.5025, 0.5375000000000001, 0.5425, 0.5775000000000001, 0.5825, 0.6175, 0.6224999999999999, 0.6975, 0.7024999999999999, 0.7475, 0.7525, 0.7975000000000001, 0.8025, 0.8375, 0.8424999999999999, 0.8575, 0.8624999999999999, 0.8775000000000001, 0.8825, 0.8975000000000001, 0.9025, 0.9175000000000001, 0.9225, 0.9375, 0.9424999999999999, 0.9575, 0.9624999999999999, 0.9775, 0.9824999999999999, 0.9975, 1.0};
+float pitch_knob_lookup_y[] = {-1.0, -1.0, -0.9, -0.9, -0.8, -0.8, -0.7, -0.7, -0.6, -0.6, -0.5, -0.5, -0.4, -0.4, -0.3, -0.3, -0.2, -0.2, -0.1, -0.1, -0.05, -0.05, -0.025, -0.025, -0.0125, -0.0125, -0.00625, -0.00625, -0.003125, -0.003125, 0.0, 0.0, 0.003125, 0.003125, 0.00625, 0.00625, 0.0125, 0.0125, 0.025, 0.025, 0.05, 0.05, 0.1, 0.1, 0.2, 0.2, 0.3, 0.3, 0.4, 0.4, 0.5, 0.5, 0.6, 0.6, 0.7, 0.7, 0.8, 0.8, 0.9, 0.9, 1.0, 1.0};
+size_t pitch_knob_lookup_length = 62;
+
+
+class Easer {
+  float output = 0.0f;
+  float delta = 0.0f;
+  float easing = 0.1f;
+  public:
+    Easer();
+    
+    float Process(float input) {
+      delta = input - output;
+      output += delta * easing;
+      return output;
+    }
+
+    void setFactor(float factor) {
+      easing = min(max(0.00001f, factor), 1.0f);
+    }
+};
+
+Easer::Easer() {
+
+};
+
+float lerp(float a, float b, float f) {
+  f = min(1.0f, max(0.0f, f));
+
+  if (f == 0.0) { return a; }
+  else if (f == 1.0f) { return b; }
+  else { return a * (1.0f-f) + b * f; }
+}
+
+float get_from_xy_table(float* xtable, float* ytable, float f, size_t length) {
+  return multiMap<float>(f, xtable, ytable, length);
+}
+
+
+#endif
\ No newline at end of file
diff --git a/src/potentiometers.h b/src/potentiometers.h
new file mode 100644
index 0000000..055264c
--- /dev/null
+++ b/src/potentiometers.h
@@ -0,0 +1,257 @@
+#include "WCharacter.h"
+#include "wiring_analog.h"
+#include <stdint.h>
+#include <limits>
+#ifndef Potentiometers_h
+#define Potentiometers_h
+#include "Arduino.h"
+#include "luts.h"
+#include "helpers.h"
+
+#include "Adafruit_SH110X.h"
+#include "Adafruit_GFX.h"
+extern Adafruit_SH1106G display;
+
+// The length of the moving average filter that smooths the
+// controls. Higher number is smoother, but less responsive
+// and needs more memory.
+#define POT_MOVING_AVERAGE_SIZE 2
+
+// Length of the Textbuffer for floats in the UI
+#define UI_TEXTBUFFER_LENGTH 6
+
+// Modes
+enum PotMode {
+  POT_MODE_LIN,
+  POT_MODE_BIP,
+  POT_MODE_PITCH,
+  POT_MODE_SWITCH,
+  POT_MODE_LAST
+};
+
+// Display Modes
+enum PotDisplayMode {
+  POT_DISPLAY_MODE_DEFAULT,
+  POT_DISPLAY_MODE_PITCH,
+  POT_DISPLAY_MODE_PERCENT,
+  POT_DISPLAY_MODE_SWITCH,
+  POT_DISPLAY_MODE_SWITCH_NUMBERS,
+  POT_DISPLAY_MODE_LAST
+};
+
+typedef void (*callback_function)(void);
+
+class Potentiometer {
+  int pin;
+  int readings[POT_MOVING_AVERAGE_SIZE];
+  PotMode mode = POT_MODE_LIN;
+  PotDisplayMode display_mode = POT_DISPLAY_MODE_DEFAULT;
+  float last_reading, last_normalized_reading = 0.0f;
+  float display_scale = 1.0f;
+  callback_function onChangeFunction;
+  Easer easer;
+
+  public:
+    Potentiometer(int pin);
+    void init();
+    void setLinear();
+    void setPitch();
+    void setSwitch();
+    float read();
+    void setOnChange(callback_function f);
+    void renderUi();
+    void setDisplayMode(const char *name, float display_scale, PotDisplayMode display_mode);
+    const char *name;
+    double last_displayed = 0.0;
+    bool should_display = false;
+    bool display_value_changes = false;
+    bool last_was_nan = false;
+    uint8_t switch_positions;
+    uint8_t switch_offset = 0;
+    const char* const switch_labels[4] = {"TRI", "SQR", "RAND", "JUMP"};
+};
+
+Potentiometer::Potentiometer(int pin) {
+  this->pin = pin;
+}
+
+void Potentiometer::init() {
+  analogReadResolution(12);
+  easer.setFactor(0.001);
+}
+
+void Potentiometer::setLinear() {
+  this->mode = POT_MODE_LIN;
+}
+
+void Potentiometer::setPitch() {
+  this->mode = POT_MODE_PITCH;
+}
+
+void Potentiometer::setSwitch() {
+  this->mode = POT_MODE_SWITCH;
+}
+
+float Potentiometer::read() {
+  int reading = analogRead(this->pin);
+  // Shift all readings in the buffer over by one position, deleting the oldest
+  // and adding the newest
+  for (int i=0; i<POT_MOVING_AVERAGE_SIZE; i++) {
+    int next = i+1;
+    if (next < POT_MOVING_AVERAGE_SIZE) {
+      (this->readings)[i] = (this->readings)[next];
+    }
+  }
+  (this->readings)[POT_MOVING_AVERAGE_SIZE-1] = reading;
+
+  // Get the average of the last readings
+  reading = 0;
+  for (int i=0; i<POT_MOVING_AVERAGE_SIZE; i++) {
+    reading += (this->readings)[i];
+  }
+  reading = reading / POT_MOVING_AVERAGE_SIZE;
+
+  // Convert the last reading to a float and return
+  float current_reading = easer.Process(reading / 4096.0f);
+  float normalized_reading = current_reading;
+
+  // Depending on the Mode
+  switch (this->mode) {
+    case POT_MODE_PITCH:
+      current_reading = get_from_xy_table(pitch_knob_lookup_x, pitch_knob_lookup_y, current_reading, pitch_knob_lookup_length);
+      break;
+    case POT_MODE_SWITCH:
+      current_reading = int(current_reading * switch_positions);
+      break;
+  }
+
+  bool changed = abs(normalized_reading - this->last_normalized_reading) > 0.002;
+
+  // If the difference to the last reading is big enough assume the knob has been touched
+  if (this->last_normalized_reading && changed) {
+    if (display_value_changes) {
+      last_displayed = millis();
+      should_display = true;
+    }
+    if (this->onChangeFunction) { this->onChangeFunction(); }
+  }
+
+  if (this->last_normalized_reading && !changed) {
+    // if (!last_was_nan) {
+    //   Serial.print(this->name);
+    //   Serial.println(" returned NaN");
+    // }
+    last_was_nan = true;
+    return std::numeric_limits<float>::quiet_NaN();
+  }
+  last_was_nan = false;
+  this->last_reading = current_reading;
+  this->last_normalized_reading = normalized_reading;
+  return current_reading;
+}
+
+void Potentiometer::setOnChange(callback_function f) {
+  this->onChangeFunction = f;
+}
+
+void Potentiometer::setDisplayMode(const char *name, float display_scale, PotDisplayMode display_mode) {
+  this->display_value_changes = true;
+  this->name = name;
+  this->display_scale = display_scale;
+  this->display_mode = display_mode;
+}
+
+void Potentiometer::renderUi() {
+  double now = millis();
+    if (this->should_display) {
+      int x_margin = 28;
+      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);
+      
+
+      // Render the name of the parameter (e.g. "Pitch")
+      centeredText(this->name, x_center, y_center-4, SH110X_WHITE);
+      
+      // Choose how many digits to display depending on the mode
+      int digits = 2;
+      if (this->display_mode == POT_DISPLAY_MODE_PERCENT) { digits = 0; }
+      // Allocate a buffer for the float and convert it into characters
+      char value_buffer[UI_TEXTBUFFER_LENGTH]; // Buffer big enough for 7-character float
+      dtostrf(this->last_reading*display_scale, 6, digits, value_buffer); // Leave room for too large numbers!
+      
+      // If we are on a bipolar pot display an indicator if we are in the center
+      if (this->mode == POT_MODE_BIP && this->last_reading > -0.0001 && this->last_reading < 0.0001) {
+        display.fillTriangle(x_center, y_center+10, x_center+3, y_center+15, x_center-3, y_center+15, SH110X_WHITE);
+      }
+
+      // If we are on a pitch pot display an indicator if we are in the the right steps
+      if (this->mode == POT_MODE_PITCH) {
+        float reading_mod = fmod(abs(this->last_reading), 0.05f);
+        if (reading_mod > 0.999f || reading_mod < 0.001f) {
+          display.fillTriangle(x_center, y_center+10, x_center+3, y_center+15, x_center-3, y_center+15, SH110X_WHITE);
+        }
+      }
+
+      // The float value may contain some empty whitespace characters, remove them by
+      // first figuring out which the first actual character is
+      int nonwhite = 0;
+      for (int i=0; i<UI_TEXTBUFFER_LENGTH; i++) {
+        if (value_buffer[i] == ' ') {
+          nonwhite++;
+        }
+      }
+
+      // Create a new buffer that can hold everything
+      char text_buffer[UI_TEXTBUFFER_LENGTH+6];
+
+      // Copy all non-white characters over
+      for (int i = 0; i<UI_TEXTBUFFER_LENGTH; i++) {
+        text_buffer[i] = value_buffer[i+int(nonwhite)];
+      }
+
+      // Figure out where the last character (\0) in our new buffer is
+      int last = UI_TEXTBUFFER_LENGTH+6;
+      for (int i=16; i>0; i--) {
+        if (text_buffer[i] == '\0') {
+          last = i;
+        }
+      }
+
+      // Add units depending on the display mode : )
+      if (this->display_mode == POT_DISPLAY_MODE_PERCENT) {
+        text_buffer[last] = ' ';
+        text_buffer[last+1] = '%';
+        text_buffer[last+2] = '\0';
+      } else if (this->display_mode == POT_DISPLAY_MODE_PITCH) {
+        text_buffer[last] = ' ';
+        text_buffer[last+1] = 'S';
+        text_buffer[last+2] = 'e';
+        text_buffer[last+3] = 'm';
+        text_buffer[last+4] = 'i';
+        text_buffer[last+5] = '\0';
+      }
+
+      // Render that new text
+      if (this->display_mode == POT_DISPLAY_MODE_SWITCH) {
+        centeredText(switch_labels[int(last_reading+switch_offset)], x_center, y_center+4, SH110X_WHITE);
+      } else if (this->display_mode == POT_DISPLAY_MODE_SWITCH_NUMBERS) {
+        sprintf(text_buffer, "%d", int(last_reading));  
+        centeredText(text_buffer, x_center, y_center+4, SH110X_WHITE);
+      } else {
+        centeredText(text_buffer, x_center, y_center+4, SH110X_WHITE);
+      }
+    }
+
+    // Show this for 700 ms after it has been last touched
+    if ((now - this->last_displayed) > 700.0) {
+      this->should_display = false;
+    }
+}
+
+
+#endif
\ No newline at end of file
diff --git a/src/ui.h b/src/ui.h
new file mode 100644
index 0000000..030f266
--- /dev/null
+++ b/src/ui.h
@@ -0,0 +1,884 @@
+#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"
+#include "button_grid.h"
+
+#define UI_MAX_FPS 10
+#define WAVEFORM_OVERSAMPLING 2
+#define WAVEFORM_LIN true
+
+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_a, looper_b, looper_c, looper_d, looper_e;
+
+// Should the splash-screen be shown on boot?
+bool show_splash = false;
+
+// Represents the possible states of the UI
+enum UiMode {
+  UI_MODE_SPLASH,         // A splash screen that is shown on startup
+  UI_MODE_DEFAULT,        // Default screen: Show Waveform and Parameters
+  UI_MODE_REC_MENU,       // ButtonGrid Menu: Recording Settings
+  UI_MODE_PLAY_MENU,      // ButtonGrid Menu: Playback Settings
+  UI_MODE_TRIGGER_MENU,   // ButtonGrid Menu: Trigger Settings
+  UI_MODE_FX_MENU,        // ButtonGrid Menu: FX Settings
+  UI_MODE_BUFFER_MENU,    // ButtonGrid Menu: Buffer Settings
+  UI_MODE_LAST
+};
+
+// Represents possible recording modes
+enum RecMode {
+  REC_MODE_FULL,          // Record into full buffer (looping back to start)
+  REC_MODE_LOOP,          // Limit recording to the loop
+  REC_MODE_FULL_SHOT,     // Record into full buffer, stop at the end
+  REC_MODE_LAST
+};
+
+// Represents possible recording sources
+enum RecSource {
+  REC_SOURCE_PRE,            // Record Incoming audio
+  REC_SOURCE_LAST_BUF,       // Record Last selected Buffer
+  REC_SOURCE_OUT,            // Record the buffer output
+  REC_SOURCE_NOISE,          // Record Noise
+  REC_SOURCE_LAST
+};
+
+// Represents possible playback modes
+enum PlayMode {
+  PLAY_MODE_DRUNK,        // Drunken Walk
+  PLAY_MODE_WINDOW,       // Sliding window
+  PLAY_MODE_LOOP,         // Loop
+  PLAY_MODE_GRAIN,        // Granular ?
+  PLAY_MODE_ONESHOT,      // Play it once
+  PLAY_MODE_LAST
+};
+
+// Represents possible recording states
+enum RecordingState {
+  REC_STATE_NOT_RECORDING, // Not recording
+  REC_STATE_RECORDING,     // Recording (replace what is in the buffer)
+  REC_STATE_OVERDUBBING,   // Overdubbing (mix recorded values with the existing samples)
+  REC_STATE_LAST
+};
+
+enum ActiveBuffer {
+  ACTIVE_BUFFER_A,
+  ACTIVE_BUFFER_B,
+  ACTIVE_BUFFER_C,
+  ACTIVE_BUFFER_D,
+  ACTIVE_BUFFER_E,
+  ACTIVE_BUFFER_LAST,
+};
+
+enum BufferSummingMode {
+  BUFFER_SUM_MODE_SOLO,
+  BUFFER_SUM_MODE_SUM,
+  BUFFER_SUM_MODE_RING,
+  BUFFER_SUM_MODE_LAST,
+};
+
+enum FXMode {
+  FX_MODE_ALL,
+  FX_MODE_REVERB,
+  FX_MODE_NONE,
+  FX_MODE_LFO,
+  FX_MODE_GRAIN,
+  FX_MODE_FILTER,
+  FX_MODE_LAST,
+};
+
+// The Ui is _the_ coordinating class for the whole interaction.
+// The default mode
+// Note Descriptions get a space of 21 chars and 8 lines
+class Ui {
+  public:
+    Ui() : button_grids {
+      ButtonGrid((int) UI_MODE_REC_MENU, {
+        GridButton("REC\nMENU", &button_1, true),
+        GridButton("MOM\nTOGGLE", &button_2, false, BUTTON_TYPE_TOGGLE, 0),
+        GridButton("PRE\nLAST\nOUT\nNOISE", &button_3, false, BUTTON_TYPE_MULTITOGGLE, 0),
+        GridButton("FULL\nLOOP\nSHOT", &button_4, false, BUTTON_TYPE_MULTITOGGLE, 1),
+        GridButton("NORMAL\nUNPTCH", &button_5, false, BUTTON_TYPE_MULTITOGGLE, 0),
+        GridButton("START\nLOOPST\nPLAYHD", &button_6, false, BUTTON_TYPE_MULTITOGGLE, 0),
+      }),
+      ButtonGrid((int) UI_MODE_PLAY_MENU, {
+        GridButton("STOP\nLOOP\nMULTI\nMIDI", &button_1, false, BUTTON_TYPE_MULTITOGGLE, 1),
+        GridButton("PLAY\nMENU", &button_2, true),
+        GridButton("ACTIVE\nSUM\nRING", &button_3, false, BUTTON_TYPE_MULTITOGGLE, 0),
+        GridButton("RE\nSTART", &button_4, false),
+        GridButton("SLOW\nDOWN", &button_5, false),
+        GridButton("REV\nERSE", &button_6, false),
+      }),
+      ButtonGrid((int) UI_MODE_TRIGGER_MENU, {
+        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((int) UI_MODE_FX_MENU, {
+        GridButton("ALL", &button_1, false, BUTTON_TYPE_ENUM, 1),
+        GridButton("REVERB", &button_2, false, BUTTON_TYPE_ENUM, 0),
+        GridButton("FX\nMENU", &button_3, true),
+        GridButton("LFO", &button_4, false, BUTTON_TYPE_ENUM, 0),
+        GridButton("GRAIN", &button_5, false, BUTTON_TYPE_ENUM, 0),
+        GridButton("-", &button_6, false, BUTTON_TYPE_ENUM, 0),
+      }),
+      ButtonGrid((int) UI_MODE_BUFFER_MENU, {
+        GridButton("A", &button_1, false, BUTTON_TYPE_ENUM, 1),
+        GridButton("B", &button_2, false, BUTTON_TYPE_ENUM, 0),
+        GridButton("C", &button_3, false, BUTTON_TYPE_ENUM, 0),
+        GridButton("D", &button_4, false, BUTTON_TYPE_ENUM, 0),
+        GridButton("E", &button_5, false, BUTTON_TYPE_ENUM, 0),
+        GridButton("BUFFER\nMENU", &button_6, true),
+      }),
+    } {};
+
+    // Store the Button Grids declared above (make sure the lenght matches!)
+    ButtonGrid button_grids[5];
+
+    // Stores the current Ui Mode
+    UiMode ui_mode = UI_MODE_SPLASH;
+
+    // Default Recording Mode
+    RecMode rec_mode = REC_MODE_LOOP;
+
+    // Default Recording Source
+    RecSource rec_source = REC_SOURCE_PRE;
+
+    // Default active buffer
+    ActiveBuffer active_buffer = ACTIVE_BUFFER_A;
+    ActiveBuffer previous_buffer = ACTIVE_BUFFER_A;
+
+    // Default active summing mode
+    BufferSummingMode buffer_summing_mode = BUFFER_SUM_MODE_SOLO;
+
+    FXMode fx_mode = FX_MODE_ALL;
+
+    // Render the UI
+    // Except for the splash screen this is expected to be called
+    // repeatedly in a loop
+    void Render() {
+      double now = millis();
+      // Serial.println(1000.0/UI_MAX_FPS);
+      if ((now - last_render) > (1000.0/UI_MAX_FPS)) {
+        switch (ui_mode) {
+          case UI_MODE_SPLASH:
+            renderSplash();
+            break;
+          case UI_MODE_DEFAULT:
+            renderDefault();
+            break;
+          case UI_MODE_REC_MENU:
+            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, fx_mode);
+            break;
+          case UI_MODE_BUFFER_MENU:
+            renderGrid(4, active_buffer);
+            break;
+        }
+        last_render = now;
+      }
+    }
+
+    // Helper method to render a certain button grid
+    void renderGrid(size_t num, int button_enum=0) {
+      display.clearDisplay();
+      button_grids[num].render(button_enum);
+      display.display();
+    }
+
+    // Renders a splash screen (runs once)
+    void renderSplash() {
+      display.setTextSize(1);
+
+      // Splash rendering is now done, go to next UI Mode
+      setMode(UI_MODE_DEFAULT);
+    }
+
+    // Helper method to reset the controls
+    void resetControls() {
+      button_1.reset();
+      button_2.reset();
+      button_3.reset();
+      button_4.reset();
+      button_5.reset();
+      button_6.reset();
+    }
+
+    Button* setupButtonGrid(int n) {
+      // Find the index of the home button
+      int home_button_index = button_grids[n].homeButtonIndex();
+
+      // Create a pointer to the hoime button
+      Button* home_button = button_grids[n].grid_buttons_[home_button_index].button;
+
+      // Reset the controls
+      resetControls();
+
+      // Setup the button grid
+      button_grids[n].setup();
+
+      // Return to default mode on release
+      home_button->onReleased([this, n](){
+        this->setMode(UI_MODE_DEFAULT);
+        this->button_grids[n].hideAllDescriptions();
+        activeLooper()->speedUp();
+      });
+
+      // Return pointer to the home button
+      return home_button;
+    }
+
+    // Setup the Recording Menu
+    void setupRecMenu() {
+      // Only run once when the ui_mode changed
+      if (ui_mode == UI_MODE_REC_MENU && last_ui_mode != UI_MODE_REC_MENU) {
+        int n = 0;
+
+        // Setup button Grid
+        Button* home_button = setupButtonGrid(n);
+
+        // Toggle between momentary and toggle recording modes
+        button_2.onPress([this, n](){
+          rec_button_momentary = !rec_button_momentary;
+          button_grids[n].grid_buttons_[1].active = !rec_button_momentary;
+        });
+        
+        // Set Recording Source (Pre/Post/Out/Noise)
+        button_3.onPress([this, n](){
+          button_grids[n].grid_buttons_[2].next();
+          rec_source = (RecSource) button_grids[n].grid_buttons_[2].active;
+        }); 
+
+        // Switch Recording modes (Full/Loop/Oneshot)
+        button_4.onPress([this, n](){ 
+          button_grids[n].grid_buttons_[3].next();
+          // Button.active returns number according to mode, we cast it to a RecMode enum
+          rec_mode = (RecMode) button_grids[n].grid_buttons_[3].active;
+          switch (rec_mode) {
+            case REC_MODE_FULL: 
+              looper_a.setRecModeFull(); 
+              looper_b.setRecModeFull(); 
+              looper_c.setRecModeFull(); 
+              looper_d.setRecModeFull(); 
+              looper_e.setRecModeFull(); 
+              break;
+            case REC_MODE_LOOP: 
+              looper_a.setRecModeLoop();
+              looper_b.setRecModeLoop();
+              looper_c.setRecModeLoop();
+              looper_d.setRecModeLoop();
+              looper_e.setRecModeLoop();
+              break;
+            case REC_MODE_FULL_SHOT:
+              looper_a.setRecModeFullShot();
+              looper_b.setRecModeFullShot();
+              looper_c.setRecModeFullShot();
+              looper_d.setRecModeFullShot();
+              looper_e.setRecModeFullShot();
+              break;
+          }
+        });
+
+        // Set Recording Pitch mode (Normal/Pitched/Unpitched)
+        button_5.onPress([this, n](){ 
+          button_grids[n].grid_buttons_[4].next();
+          looper_a.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
+          looper_b.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
+          looper_c.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
+          looper_d.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
+          looper_e.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
+        });
+
+        // Set Recording Start Option (Buffer Start/Loop Start/Playhead)
+        button_6.onPress([this, n](){
+          button_grids[n].grid_buttons_[5].next();
+          looper_a.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
+          looper_b.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
+          looper_c.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
+          looper_d.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
+          looper_e.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
+        });
+
+        // Store the last ui mode, for the check on top
+        last_ui_mode = ui_mode;
+      }
+    }
+
+    // Setup the Buffer Menu
+    void setupBufferMenu() {
+      // Only run once when the ui_mode changed
+      if (ui_mode == UI_MODE_BUFFER_MENU && last_ui_mode != UI_MODE_BUFFER_MENU) {
+        int n = 4;
+
+        // Setup button Grid
+        Button* home_button = setupButtonGrid(n);
+
+        button_1.onPress([this, n](){ 
+          previous_buffer = active_buffer;
+          active_buffer = ACTIVE_BUFFER_A;
+          waveform_cache_dirty = true;
+        });
+        button_2.onPress([this, n](){ 
+          previous_buffer = active_buffer;
+          active_buffer = ACTIVE_BUFFER_B;
+          waveform_cache_dirty = true;
+        });
+        button_3.onPress([this, n](){ 
+          previous_buffer = active_buffer;
+          active_buffer = ACTIVE_BUFFER_C;
+          waveform_cache_dirty = true;
+        });
+        button_4.onPress([this, n](){ 
+          previous_buffer = active_buffer;
+          active_buffer = ACTIVE_BUFFER_D;
+          waveform_cache_dirty = true;
+        });
+        button_5.onPress([this, n](){ 
+          previous_buffer = active_buffer;
+          active_buffer = ACTIVE_BUFFER_E;
+          waveform_cache_dirty = true;
+        });
+
+        // Store the last ui mode, for the check on top
+        last_ui_mode = ui_mode;
+      }
+    }
+
+    // Setup the Play Menu
+    void setupPlayMenu() {
+      // Only run once when the ui_mode changed
+      if (ui_mode == UI_MODE_PLAY_MENU && last_ui_mode != UI_MODE_PLAY_MENU) {
+        int n = 1;
+
+        // Ensure the UI showes the play mode of the active looper
+        button_grids[n].grid_buttons_[0].active = (int) activeLooper()->playback_state ;
+
+        // Setup button Grid
+        Button* home_button = setupButtonGrid(n);
+
+        // Change the way in which buffers are summed
+        button_3.onPress([this, n](){ 
+          button_grids[n].grid_buttons_[2].next();
+          buffer_summing_mode = (BufferSummingMode) button_grids[n].grid_buttons_[2].active;
+        });
+
+        // Change playback state (mode) of the current looper
+        button_1.onPress([this, n](){ 
+          button_grids[n].grid_buttons_[0].next();
+          activeLooper()->playback_state = (atoav::PlaybackState) (button_grids[n].grid_buttons_[0].active);
+        });
+
+        // Restart
+        button_4.onPress([this, n](){ 
+          activeLooper()->restart();
+        });
+
+        // DJ-style slow-down effect
+        button_5.onHold([this, n](){ 
+          activeLooper()->slowDown();
+        });
+        button_5.onReleased([this, n](){ 
+          activeLooper()->speedUp();
+        });
+
+        button_6.onHold([this, n](){ 
+          activeLooper()->reverse();
+        });
+        button_6.onReleased([this, n](){ 
+          activeLooper()->speedUp();
+        });
+
+        // Store the last ui mode, for the check on top
+        last_ui_mode = ui_mode;
+      }
+    }
+
+    // Setup the FX Menu
+    void setupFXMenu() {
+      // Only run once when the ui_mode changed
+      if (ui_mode == UI_MODE_FX_MENU && last_ui_mode != UI_MODE_FX_MENU) {
+        int n = 3;
+
+        // Ensure the UI showes the play mode of the active looper
+        // button_grids[n].grid_buttons_[0].active = (int) fx_mode;
+
+        // Setup button Grid
+        Button* home_button = setupButtonGrid(n);
+
+        // Select the active Effect (All)
+        button_1.onPress([this, n](){ 
+          fx_mode = FX_MODE_ALL;
+          pot_5.setDisplayMode("LFO", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_5.setLinear();
+          pot_6.setDisplayMode("Volume", 400.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_7.setDisplayMode("Reverb", 100.0f, POT_DISPLAY_MODE_PERCENT);
+        });
+
+        // Select the active Effect (Reverb)
+        button_2.onPress([this, n](){ 
+          fx_mode = FX_MODE_REVERB;
+          pot_5.setDisplayMode("Rev. Tone", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_5.setLinear();
+          pot_6.setDisplayMode("Rev. Decay", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_7.setDisplayMode("Reverb Mix", 100.0f, POT_DISPLAY_MODE_PERCENT);
+        });
+
+        // Select the active Effect (LFO)
+        button_4.onPress([this, n](){ 
+          fx_mode = FX_MODE_LFO;
+          pot_5.setDisplayMode("LFO Mode", 100.0f, POT_DISPLAY_MODE_SWITCH);
+          pot_5.setSwitch();
+          pot_5.switch_positions = 4;
+          pot_5.switch_offset = 0;
+          pot_6.setDisplayMode("LFO Speed", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_7.setDisplayMode("LFO Amount", 100.0f, POT_DISPLAY_MODE_PERCENT);
+        });
+
+        // Select the active Effect (GRAIN)
+        button_5.onPress([this, n](){ 
+          fx_mode = FX_MODE_GRAIN;
+          pot_5.setDisplayMode("Grain Num", 100.0f, POT_DISPLAY_MODE_SWITCH_NUMBERS);
+          pot_5.setSwitch();
+          pot_5.switch_positions = 8;
+          pot_6.setDisplayMode("Grn. Spread", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_7.setDisplayMode("Grain Var.", 100.0f, POT_DISPLAY_MODE_PERCENT);
+        });
+
+        // Select the active Effect (FILTER)
+        button_6.onPress([this, n](){ 
+          fx_mode = FX_MODE_FILTER;
+          pot_5.setDisplayMode("Lowpass", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_6.setDisplayMode("Highpass", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_7.setDisplayMode("Resonance", 100.0f, POT_DISPLAY_MODE_PERCENT);
+        });
+
+        // Store the last ui mode, for the check on top
+        last_ui_mode = ui_mode;
+      }
+    }
+
+    // Setup the default (waveform) screen
+    void setupDefault() {
+      // Only run once on mode change
+      if (ui_mode == UI_MODE_DEFAULT && last_ui_mode != UI_MODE_DEFAULT) {
+        // Reset controls
+        resetControls();
+
+        // Set up the initial recording mode
+        switch (rec_mode) {
+          case REC_MODE_FULL: 
+            looper_a.setRecModeFull(); 
+            looper_b.setRecModeFull(); 
+            looper_c.setRecModeFull(); 
+            looper_d.setRecModeFull(); 
+            looper_e.setRecModeFull(); 
+            break;
+          case REC_MODE_LOOP:
+            looper_a.setRecModeLoop(); 
+            looper_b.setRecModeLoop(); 
+            looper_c.setRecModeLoop();
+            looper_d.setRecModeLoop();
+            looper_e.setRecModeLoop(); 
+            break;
+          case REC_MODE_FULL_SHOT:
+            looper_a.setRecModeFullShot();
+            looper_b.setRecModeFullShot();
+            looper_c.setRecModeFullShot();
+            looper_d.setRecModeFullShot();
+            looper_e.setRecModeFullShot();
+            break;
+        };
+
+        // Setup Button functions (these should enter the ButtonGrid Menus)
+        button_1.onHold([this](){ this->setMode(UI_MODE_REC_MENU); });
+        button_2.onHold([this](){ this->setMode(UI_MODE_PLAY_MENU); });
+        button_3.onHold([this](){ this->setMode(UI_MODE_FX_MENU); });
+        button_6.onHold([this](){ this->setMode(UI_MODE_BUFFER_MENU); });
+
+        // Set the recording/overdub buttons to toggle or momentary
+        // depending on the value of the option
+        if (rec_button_momentary) {
+          button_4.onHold([this](){ this->activateRecording(); });
+          button_5.onHold([this](){ this->activateOverdub(); });
+          button_4.onReleased([this](){ this->stopRecording(); });
+          button_5.onReleased([this](){ this->stopRecording(); });
+        } else {
+          button_4.onReleased([this](){ this->toggleRecording(); });
+          button_5.onReleased([this](){ this->toggleOverdub(); });
+        }
+        
+        // Store the last ui mode, for the check on top
+        last_ui_mode = ui_mode;
+      }
+    }
+
+    // Render the default screen (waveform)
+    void renderDefault() {
+      // Store the current time and check how long ago the last frame was
+      // in ms
+      
+      // Clear the display
+      display.clearDisplay();
+
+      // Waveform should be maximum screen-heigh
+      int wave_height = display.height() * 1.0f;
+      // Ensure that when stepping from left to right we fit the waveform on the screen
+      int step = activeLooper()->getBufferLength() / (display.width() * WAVEFORM_OVERSAMPLING);
+      // Helper variable for the bottom of the screen
+      int bottom = display.height()-1;
+
+      // Render the waveform by iterating through the samples (oversampled by a factor
+      // defined on top of this file). Average the samples for each pixel of the 128 px
+      // wide screen and cache the resulting heights so we only have to recalculate when
+      // the waveform changes
+      for (int i=0; i<display.width()*WAVEFORM_OVERSAMPLING; i+=WAVEFORM_OVERSAMPLING) {
+        uint16_t x = int(i / WAVEFORM_OVERSAMPLING);
+        // Only recalculate if the cahce is dirty, else use cache
+        if (waveform_cache_dirty) {
+          float sig = 0.0f;
+          float scale = 1.0f;
+          if (!WAVEFORM_LIN) {
+            scale = 10.0f;
+          }
+          // Step through the buffer and sum the absolute values
+          for (int s=0; s<WAVEFORM_OVERSAMPLING; s++) {
+            float abs_sig = activeLooper()->getBuffer()[step*i];
+            abs_sig = abs(abs_sig) * scale;
+            sig += abs_sig;
+          }
+          // We oversampled so divide here
+          sig = sig / float(WAVEFORM_OVERSAMPLING);
+
+          if (!WAVEFORM_LIN) {
+            // Volume is logarithmic (hiding silent noises)
+            if (sig != 0.0f) {
+              sig = log10(sig);
+            }
+          }
+          waveform_cache[x] = int(sig * wave_height);
+        }
+
+        // Draw the vertical lines from bottom up, depending on the level of the
+        // calulcated wave on this point of the screen
+        display.drawFastVLine(x, bottom, -waveform_cache[x], SH110X_WHITE);
+        
+      }
+      // Draw one horizontal line on bottom
+      display.drawFastHLine(0, bottom, display.width(), SH110X_WHITE);
+
+      // Cache is now marked as clean
+      waveform_cache_dirty = false;
+
+      // Draw Indicator for loop start 
+      int x_start_loop = int(activeLooper()->loop_start_f * 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 Indicator for Loop End
+      int x_loop_length = int(activeLooper()->loop_length_f * 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);
+
+      // Draw 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
+      switch (activeLooper()->playback_state) {
+        case atoav::PLAYBACK_STATE_LOOP:
+          {
+            int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop;
+            display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE);
+            break;
+          }
+        case atoav::PLAYBACK_STATE_MULTILOOP:
+          {
+            float* playheads = activeLooper()->GetPlayheads();
+            uint8_t count = activeLooper()->GetPlayheadCount();
+            int x_playhead = 0;
+            for (size_t i=0; i<count-1; i++) {
+              x_playhead = int(playheads[i] * display.width()) + x_start_loop;
+              int h = 6 + i*3;
+              display.drawFastVLine(x_playhead, h, 3, SH110X_WHITE);
+            }
+            break;
+          }
+        case atoav::PLAYBACK_STATE_MIDI:
+          {
+            int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop;
+            display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE);
+            break;
+          }
+      }
+      
+
+      // Draw Recording Indicator and Recording Head
+      if (recording_state == REC_STATE_RECORDING) {
+        // Draw Rec Head
+        int x_rec_head = int(activeLooper()->GetRecHead() * display.width());
+        display.drawFastVLine(x_rec_head, 10, 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 Indicator and Recording Head
+      if (recording_state == REC_STATE_OVERDUBBING) {
+        // Draw Rec Head
+        int x_rec_head = int(activeLooper()->GetRecHead() * display.width());
+        display.drawFastVLine(x_rec_head, 10, bottom, SH110X_WHITE);
+        display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE);
+
+        // 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 all the things done above
+      display.display();
+    }
+
+    // Activate recording and set the waveform cache to dirty
+    void activateRecording() {
+      if (recording_state != REC_STATE_RECORDING) {
+        activeLooper()->SetRecord();
+        waveform_cache_dirty = true;
+        recording_state = REC_STATE_RECORDING;
+      }
+    }
+
+    // Toggle recording
+    void toggleRecording() {
+      switch (recording_state) {
+        case REC_STATE_NOT_RECORDING:
+          activateRecording();
+          break;
+        case REC_STATE_RECORDING:
+          stopRecording();
+          break;
+        case REC_STATE_OVERDUBBING:
+          activateRecording();
+          break;
+      }
+    }
+
+    // Activates overdubbing
+    void activateOverdub() {
+      if (recording_state != REC_STATE_OVERDUBBING) {
+        waveform_cache_dirty = true;
+        recording_state = REC_STATE_OVERDUBBING;
+        activeLooper()->SetOverdub();
+      }
+    }
+
+    // Stop the recording
+    void stopRecording() {
+      if (recording_state != REC_STATE_NOT_RECORDING) {
+        recording_state = REC_STATE_NOT_RECORDING;
+        activeLooper()->SetStopWriting();
+      }
+    }
+
+    // Toggle overdub off and on
+    void toggleOverdub() {
+      switch (recording_state) {
+        case REC_STATE_NOT_RECORDING:
+          activateOverdub();
+          break;
+        case REC_STATE_OVERDUBBING:
+          stopRecording();
+          break;
+        case REC_STATE_RECORDING:
+          activateOverdub();
+          break;
+      }
+    }
+
+    // Reset the recording state (mark waveform cahce dirty)
+    void resetRecordingState() {
+      if (recording_state == REC_STATE_RECORDING || recording_state == REC_STATE_OVERDUBBING) {
+        waveform_cache_dirty = true; 
+      }
+    }
+
+   // Set the mode of the UI (and thus change the screen)
+    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:
+          this->button_grids[0].hideAllDescriptions();
+          break;
+        case UI_MODE_PLAY_MENU:
+          this->button_grids[1].hideAllDescriptions();
+          break;
+        case UI_MODE_TRIGGER_MENU:
+          this->button_grids[2].hideAllDescriptions();
+          break;
+        case UI_MODE_FX_MENU:
+          this->button_grids[3].hideAllDescriptions();
+          break;
+        case UI_MODE_BUFFER_MENU:
+          this->button_grids[4].hideAllDescriptions();
+          break;
+      }
+    }
+
+
+    // Update the Ui variables (expected to run repeatedly)
+    void update() {
+      resetRecordingState();
+
+      switch (ui_mode) {
+        case UI_MODE_SPLASH:
+          break;
+        case UI_MODE_DEFAULT:
+          setupDefault();
+          break;
+        case UI_MODE_REC_MENU:
+          setupRecMenu();
+          break;
+        case UI_MODE_PLAY_MENU:
+          setupPlayMenu();
+          break;
+        case UI_MODE_TRIGGER_MENU:
+          break;
+        case UI_MODE_FX_MENU:
+          setupFXMenu();
+          break;
+        case UI_MODE_BUFFER_MENU:
+          setupBufferMenu();
+          break;
+      }
+    }
+
+    // Returns a pointer to the currently active looper
+    atoav::Looper * activeLooper() {
+      switch(active_buffer) {
+        case ACTIVE_BUFFER_A: {
+          atoav::Looper * ptr = &looper_a;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_B: {
+          atoav::Looper * ptr = &looper_b;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_C: {
+          atoav::Looper * ptr = &looper_c;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_D: {
+          atoav::Looper * ptr = &looper_d;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_E: {
+          atoav::Looper * ptr = &looper_e;
+          return ptr;
+          break;
+        }
+      }
+      // Unreachable, but makes the compiler shut up
+      atoav::Looper * ptr = &looper_a;
+      return ptr;
+    }
+
+    // Returns a pointer to the currently active looper
+    atoav::Looper * previousLooper() {
+      switch(previous_buffer) {
+        case ACTIVE_BUFFER_A: {
+          atoav::Looper * ptr = &looper_a;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_B: {
+          atoav::Looper * ptr = &looper_b;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_C: {
+          atoav::Looper * ptr = &looper_c;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_D: {
+          atoav::Looper * ptr = &looper_d;
+          return ptr;
+          break;
+        }
+        case ACTIVE_BUFFER_E: {
+          atoav::Looper * ptr = &looper_e;
+          return ptr;
+          break;
+        }
+      }
+      // Unreachable, but makes the compiler shut up
+      atoav::Looper * ptr = &looper_a;
+      return ptr;
+    }
+
+    // Set the Looper start/length to a given value
+    void setLoop(float start, float length) {
+      activeLooper()->SetLoop(start, length);
+    }
+
+    private:
+      double last_render = 0.0;
+      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;
+};
+
+
+
+#endif
\ No newline at end of file
-- 
GitLab