From e82cdcbf696be6c415ac5f0ae91b38aeb403f415 Mon Sep 17 00:00:00 2001
From: David Huss <dh@atoav.com>
Date: Fri, 8 Dec 2023 23:20:20 +0100
Subject: [PATCH] Implements FX Menu, better pitch curve

---
 circuitsim/lookup-tables.ipynb     | 139 ++++++++++++++--------
 code/daisy-looper/daisy-looper.ino |  85 +++++++------
 code/daisy-looper/looper.h         |  22 ++--
 code/daisy-looper/luts.h           |   7 +-
 code/daisy-looper/potentiometers.h |  38 ++++--
 code/daisy-looper/ui.h             | 185 +++++++++++++++++++----------
 6 files changed, 308 insertions(+), 168 deletions(-)

diff --git a/circuitsim/lookup-tables.ipynb b/circuitsim/lookup-tables.ipynb
index 5844a1e..61b523f 100644
--- a/circuitsim/lookup-tables.ipynb
+++ b/circuitsim/lookup-tables.ipynb
@@ -480,45 +480,19 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 15,
    "id": "e51416f3-f34d-4513-9f0c-fa52c468274e",
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "-0.9\n",
-      "-0.8\n",
-      "-0.7\n",
-      "-0.6\n",
-      "-0.5\n",
-      "-0.4\n",
-      "-0.3\n",
-      "-0.2\n",
-      "-0.1\n",
-      "0.0\n",
-      "0.1\n",
-      "0.2\n",
-      "0.3\n",
-      "0.4\n",
-      "0.5\n",
-      "0.6\n",
-      "0.7\n",
-      "0.8\n",
-      "0.9\n",
-      "1.0\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "336acd5c2a274f49b7edab41a67e6469",
+       "model_id": "098c400f3b684499968bef484bd18f3d",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "interactive(children=(FloatSlider(value=0.2, description='f', max=1.005, step=0.001), Output()), _dom_classes=…"
+       "interactive(children=(FloatSlider(value=0.2, description='f', max=1.0, step=0.001), Output()), _dom_classes=('…"
       ]
      },
      "metadata": {},
@@ -527,7 +501,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "47a764c793a04e188a376cf6ba4f4853",
+       "model_id": "59ad9d541c5340b2980b7531a9a1a285",
        "version_major": 2,
        "version_minor": 0
       },
@@ -607,27 +581,94 @@
     "        return lerp(ya, yb, new_f)\n",
     "        \n",
     "    \n",
-    "lines_orig = [\n",
-    "    [0.0, 0.0, -0.5],\n",
-    "    [0.45, 0.5, 0.0],\n",
-    "    [0.55, 0.5, 1.0],\n",
-    "    [1.0, 1.0, 0.0],\n",
-    "]\n",
-    "\n",
-    "half_deadband = 0.005\n",
+    "# lines_orig = [\n",
+    "#     [0.0, 0.0, -0.5],\n",
+    "#     [0.45, 0.5, 0.0],\n",
+    "#     [0.55, 0.5, 1.0],\n",
+    "#     [1.0, 1.0, 0.0],\n",
+    "# ]\n",
+    "\n",
+    "# half_deadband = 0.005\n",
+    "\n",
+    "# lines_orig = [\n",
+    "#     [0.0, -1.0, 0.0],\n",
+    "#     [0.0+half_deadband, -1.0, 0.0],\n",
+    "# ] \n",
+    "\n",
+    "# steps = list(range(-9, 11))\n",
+    "# for i in steps:\n",
+    "#     f = float(i)\n",
+    "#     print(f/10)\n",
+    "#     lines_orig.append([0.5+f/20-half_deadband, f/10, 0.0])\n",
+    "#     lines_orig.append([0.5+f/20+half_deadband, f/10, 0.0])\n",
+    "\n",
+    "db = 0.005\n",
+    "hb = db/2.0\n",
     "\n",
     "lines_orig = [\n",
-    "    [0.0, -1.0, 0.0],\n",
-    "    [0.0+half_deadband, -1.0, 0.0],\n",
-    "] \n",
-    "\n",
-    "steps = list(range(-9, 11))\n",
-    "for i in steps:\n",
-    "    f = float(i)\n",
-    "    print(f/10)\n",
-    "    lines_orig.append([0.5+f/20-half_deadband, f/10, 0.0])\n",
-    "    lines_orig.append([0.5+f/20+half_deadband, f/10, 0.0])\n",
-    "\n",
+    "    [0.0, -1.0, 0.0],    # -1000%\n",
+    "    [0.0+hb, -1.0, 0.0], # -1000%\n",
+    "    [0.02-hb, -0.9, 0.0], # -900%\n",
+    "    [0.02+hb, -0.9, 0.0], # -900%\n",
+    "    [0.04-hb, -0.8, 0.0], # -800%\n",
+    "    [0.04+hb, -0.8, 0.0], # -800%\n",
+    "    [0.06-hb, -0.7, 0.0], # -700%\n",
+    "    [0.06+hb, -0.7, 0.0], # -700%\n",
+    "    [0.08-hb, -0.6, 0.0], # -600%\n",
+    "    [0.08+hb, -0.6, 0.0], # -600%\n",
+    "    [0.1-hb, -0.5, 0.0], # -500%\n",
+    "    [0.1+hb, -0.5, 0.0], # -500%\n",
+    "    [0.12-hb, -0.4, 0.0], # -400%\n",
+    "    [0.12+hb, -0.4, 0.0], # -400%\n",
+    "    [0.14-hb, -0.3, 0.0], # -300%\n",
+    "    [0.14+hb, -0.3, 0.0], # -300%\n",
+    "    [0.16-hb, -0.2, 0.0], # -200%\n",
+    "    [0.16+hb, -0.2, 0.0], # -200%\n",
+    "    [0.2-hb, -0.1, 0.0], # -100%\n",
+    "    [0.2+hb, -0.1, 0.0], # -100%\n",
+    "    [0.25-hb, -0.05, 0.0], # -50%\n",
+    "    [0.25+hb, -0.05, 0.0], # -50%\n",
+    "    [0.3-hb, -0.025, 0.0], # -25%\n",
+    "    [0.3+hb, -0.025, 0.0], # -25%\n",
+    "    [0.38-hb, -0.0125, 0.0], # -12.5%\n",
+    "    [0.38+hb, -0.0125, 0.0], # -12.5%\n",
+    "    [0.42-hb, -0.00625, 0.0], # -6.25%\n",
+    "    [0.42+hb, -0.00625, 0.0], # -6.25%\n",
+    "    [0.46-hb, -0.003125, 0.0], # -3.125%\n",
+    "    [0.46+hb, -0.003125, 0.0], # -3.125%\n",
+    "    [0.5-hb, 0.0, 0.0], # 0%\n",
+    "    [0.5+hb, 0.0, 0.0], # 0%\n",
+    "    [1.0-0.46-hb, 0.003125, 0.0], # 3.125%\n",
+    "    [1.0-0.46+hb, 0.003125, 0.0], # 3.125%\n",
+    "    [1.0-0.42-hb, 0.00625, 0.0], # 6.25%\n",
+    "    [1.0-0.42+hb, 0.00625, 0.0], # 6.25%\n",
+    "    [1.0-0.38-hb, 0.0125, 0.0], # 12.5%\n",
+    "    [1.0-0.38+hb, 0.0125, 0.0], # 12.5%\n",
+    "    [1.0-0.3-hb, 0.025, 0.0], # 25%\n",
+    "    [1.0-0.3+hb, 0.025, 0.0], # 25%\n",
+    "    [1.0-0.25-hb, 0.05, 0.0], # 50%\n",
+    "    [1.0-0.25+hb, 0.05, 0.0], # 50%\n",
+    "    [1.0-0.2-hb, 0.1, 0.0], # 100%\n",
+    "    [1.0-0.2+hb, 0.1, 0.0], # 100%\n",
+    "    [1.0-0.16-hb, 0.2, 0.0], # 200%\n",
+    "    [1.0-0.16+hb, 0.2, 0.0], # 200%\n",
+    "    [1.0-0.14-hb, 0.3, 0.0], # 300%\n",
+    "    [1.0-0.14+hb, 0.3, 0.0], # 300%\n",
+    "    [1.0-0.12-hb, 0.4, 0.0], # 400%\n",
+    "    [1.0-0.12+hb, 0.4, 0.0], # 400%\n",
+    "    [1.0-0.1-hb, 0.5, 0.0], # 500%\n",
+    "    [1.0-0.1+hb, 0.5, 0.0], # 500%\n",
+    "    [1.0-0.08-hb, 0.6, 0.0], # 600%\n",
+    "    [1.0-0.08+hb, 0.6, 0.0], # 600%\n",
+    "    [1.0-0.06-hb, 0.7, 0.0], # 700%\n",
+    "    [1.0-0.06+hb, 0.7, 0.0], # 700%\n",
+    "    [1.0-0.04-hb, 0.8, 0.0], # 800%\n",
+    "    [1.0-0.04+hb, 0.8, 0.0], # 800%\n",
+    "    [1.0-0.02-hb, 0.9, 0.0], # 900%\n",
+    "    [1.0-0.02+hb, 0.9, 0.0], # 900%\n",
+    "    [1.0-0.0-hb, 1.0, 0.0], # 1000%\n",
+    "    [1.0-0.0, 1.0, 0.0],    # 1000%\n",
+    "]\n",
     "\n",
     "# Calculate curves for points of curvature\n",
     "def make_lines(lines_orig, resolution=20):\n",
diff --git a/code/daisy-looper/daisy-looper.ino b/code/daisy-looper/daisy-looper.ino
index 3d48e15..ab07cfa 100644
--- a/code/daisy-looper/daisy-looper.ino
+++ b/code/daisy-looper/daisy-looper.ino
@@ -29,7 +29,6 @@ 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;
-DelayLine<float, 24000> delayline;
 DSY_SDRAM_BSS ReverbSc reverb;
 static Compressor compressor;
 Oscillator lfo;
@@ -75,29 +74,34 @@ DaisyHardware hw;
 size_t num_channels;
 float blocksize;
 float drywetmix = 0.0f;
-float delaymix = 0.0f;
-float delaytime = 100.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 pressure = 0.0f;
 
 // Actual audio-processing is orchestrated here
 void AudioCallback(float **in, float **out, size_t size) {
   float output = 0.0f;
-  float no_delay = 0.0f;
-  float wet_delay;
   float out1, out2;
-  // set the delay
-  delayline.SetDelay(delaytime);
+  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++) {
     uint8_t trig = tick.Process();
     float lfo_value = lfo.Process();
     float noise_value = noise.Process();
+
     float rand = easer.Process(
       sample_and_hold.Process(
         trig, 
@@ -110,25 +114,23 @@ void AudioCallback(float **in, float **out, size_t size) {
     // When the metro ticks, trigger the envelope to start.
     float random_amount = (lfo_amount -0.5f) * 2.0;
     if (trig) {
-      // tick.SetFreq(rand / (0.1 + random_amount*1000.0f) + 1);
-          // If the dial is over 50% jump instead
-      if (lfo_amount > 0.5f) {
-        ui.activeLooper()->addToPlayhead(rand * random_amount* 48000.0f);
+      // Random LFO
+      if (lfo_kind == 1) {
+        ui.activeLooper()->addToPlayhead(rand * random_amount * 48000.0f);
         ui.activeLooper()->setPlaybackSpeed(pitch_val + midi_pitch_offset);
       }
     }
 
-    // Add the LFO to the 
-    if (lfo_amount > 0.5f) {
-      ui.activeLooper()->setPlaybackSpeed(pitch_val + midi_pitch_offset);
-    } else {
+    // Add the LFO to the signal if it is active
+    if (lfo_kind == 0) {
       ui.activeLooper()->setPlaybackSpeed(pitch_val + lfo_value * lfo_amount + midi_pitch_offset);
+    } else {
+      ui.activeLooper()->setPlaybackSpeed(pitch_val + midi_pitch_offset);
     }
+    
 
     float looper_out;
     
-    // res.SetDamping(0.1+delaymix*0.2f);
-    // Record into the buffer
     if (ui.rec_source == REC_SOURCE_PRE) {
      ui.activeLooper()->Record(in[1][i]);
     }
@@ -170,17 +172,10 @@ void AudioCallback(float **in, float **out, size_t size) {
     }
 
     // looper_out = pressure * looper_out;
+    looper_out = volume * looper_out;
 
     // Mix the dry/Wet of the looper
     output = drywetmix * looper_out + in[1][i] * (1.0f - drywetmix);
-    
-    // output = output * (1.0f - resmix) + res.Process(output*resmix);
-    no_delay = output;
-    wet_delay = delayline.Read();
-    delayline.Write((wet_delay * delaymix * 0.95f)/2.0f + (no_delay*delaymix)/2.0f);
-
-    // Add the delay
-    output += wet_delay * delaymix;
 
     // Compress the signal
     compressor.Process(output);
@@ -277,8 +272,8 @@ void setup() {
 
   // Initialize Reverb
   reverb.Init(sample_rate);
-  reverb.SetFeedback(0.95f);
-  reverb.SetLpFreq(18000.0f);
+  reverb.SetFeedback(reverb_decay);
+  reverb.SetLpFreq(reverb_tone);
 
   // Initialize Compressor
   compressor.SetThreshold(-64.0f);
@@ -290,11 +285,7 @@ void setup() {
   lfo.Init(sample_rate);
   lfo.SetWaveform(Oscillator::WAVE_TRI);
   lfo.SetAmp(1);
-  lfo.SetFreq(8.0);
-
-  // Initialize the Delay at a length of 1 second
-  delayline.Init();
-  delayline.SetDelay(sample_rate);
+  lfo.SetFreq(lfo_speed);
 
   // Easer for the random jumps
   easer.setFactor(0.001);
@@ -326,7 +317,7 @@ void setup() {
   pot_3.setDisplayMode("Pitch", 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("Delay", 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
@@ -376,11 +367,29 @@ void loop() {
 
   // Set other parameters (from 0.0 to 1.0)
   if (!isnan(p4)) { drywetmix = p4; }
-  if (!isnan(p5)) { delaymix = p5; }
-  // Delaytime is in samples
-  if (!isnan(p5)) {  lfo_amount = p5; }
-  if (!isnan(p6)) {  delaytime = 100.0f + p6 * 23900.0f; }
-  if (!isnan(p7)) {  reverbmix = p7; }
+
+  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; }
+      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();
diff --git a/code/daisy-looper/looper.h b/code/daisy-looper/looper.h
index 1914c7f..9524360 100644
--- a/code/daisy-looper/looper.h
+++ b/code/daisy-looper/looper.h
@@ -51,13 +51,15 @@ class Head {
     void update();
     float read();
     float increment = 1.0f;
+    float variation = 0.0f;
+    float variation_amount = 0.0f;
   private:
     bool active = true;
     float position = 0.0f;
 };
 
 Head::Head() {
-
+  variation = random();
 }
 void Head::activate() {
   this->active = true;
@@ -78,7 +80,7 @@ void Head::incrementBy(float value) {
   this->position += value;
 }
 void Head::update() {
-  this->position += this->increment;
+  this->position += this->increment + (variation * variation_amount);
 }
 float Head::read() {
   return this->position;
@@ -108,6 +110,7 @@ class Looper {
     void setRecStartMode(RecStartMode mode);
     float GetPlayhead();
     float* GetPlayheads();
+    uint8_t GetPlayheadCount();
     float GetRecHead();
     bool toggleRecMode();
     void setRecModeFull();
@@ -118,6 +121,9 @@ class Looper {
     void addToPlayhead(float value);
     float loop_start_f = 0.0f;
     float loop_length_f = 1.0f;
+    uint8_t grain_count = 9;
+    float grain_spread = 1.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;
@@ -296,7 +302,7 @@ float Looper::Process() {
 
   double mix = 0.0;
 
-  for (size_t i=0; i<9; i++) {
+  for (size_t i=0; i<grain_count-1; i++) {
     // Skip inactive playheads
     if (!playheads[i].isActive()) continue;
 
@@ -327,13 +333,14 @@ float* Looper::GetPlayheads() {
   static float playhead_positions[9];
   for (size_t i=0; i<9; i++) {
     playhead_positions[i] = float(playheads[i].read()) / float(buffer_length);
-    Serial.print(playhead_positions[i]);
-    Serial.print(" ");
   }
-  Serial.println("");
   return playhead_positions;
 }
 
+uint8_t Looper::GetPlayheadCount() {
+  return grain_count;
+}
+
 float Looper::GetRecHead() {
   return  float(rec_head.read()) / float(buffer_length);
 }
@@ -371,7 +378,8 @@ void Looper::setPlaybackSpeed(float increment) {
     case PLAYBACK_STATE_MULTILOOP:
       playheads[0].setIncrement(increment);
       for (size_t i=1; i<9; i++) {
-        playheads[i].setIncrement(increment + increment/(1+i));
+        playheads[i].variation_amount = grain_variation;
+        playheads[i].setIncrement(increment + increment*grain_spread);
       }
       break;
   }
diff --git a/code/daisy-looper/luts.h b/code/daisy-looper/luts.h
index a549f51..a1d2684 100644
--- a/code/daisy-looper/luts.h
+++ b/code/daisy-looper/luts.h
@@ -8,9 +8,10 @@ float bip_lookup[] = {0.0, 0.08060869565217388, 0.1597391304347826, 0.2388695652
 size_t bip_lookup_length = 16;
 
 // Lookup Table for Pitch Knob
-float pitch_knob_lookup_x[] = {0.0, 0.005, 0.04499999999999999, 0.054999999999999986, 0.09499999999999997, 0.10499999999999998, 0.14500000000000002, 0.15500000000000003, 0.195, 0.20500000000000002, 0.245, 0.255, 0.295, 0.305, 0.345, 0.355, 0.395, 0.405, 0.445, 0.455, 0.495, 0.505, 0.545, 0.555, 0.595, 0.605, 0.645, 0.655, 0.695, 0.705, 0.745, 0.755, 0.795, 0.805, 0.845, 0.855, 0.895, 0.905, 0.945, 0.955, 0.995, 1.005};
-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.0, 0.0, 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 = 42;
+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;
+
 
 // Lookup Curves for Saturation b
 float saturation_lookup_x[] = {-1.5, -1.46, -1.42, -1.38, -1.3399999999999999, -1.3, -1.26, -1.22, -0.7, 0.0, 0.7, 0.74, 0.78, 0.82, 0.86, 0.8999999999999999, 0.94, 0.98, 1.5};
diff --git a/code/daisy-looper/potentiometers.h b/code/daisy-looper/potentiometers.h
index 2ad20ca..0af8ca4 100644
--- a/code/daisy-looper/potentiometers.h
+++ b/code/daisy-looper/potentiometers.h
@@ -15,16 +15,17 @@ 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 4
+#define POT_MOVING_AVERAGE_SIZE 2
 
 // Length of the Textbuffer for floats in the UI
-#define UI_TEXTBUFFER_LENGTH 8
+#define UI_TEXTBUFFER_LENGTH 6
 
 // Modes
 enum PotMode {
   POT_MODE_LIN,
   POT_MODE_BIP,
   POT_MODE_PITCH,
+  POT_MODE_SWITCH,
   POT_MODE_LAST
 };
 
@@ -33,6 +34,8 @@ 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
 };
 
@@ -54,6 +57,7 @@ class Potentiometer {
     void setLinear();
     void setBipolar();
     void setPitch();
+    void setSwitch();
     float read();
     void setOnChange(callback_function f);
     void renderUi();
@@ -62,6 +66,10 @@ class Potentiometer {
     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[2] = {"LFO", "RANDOM"};
 };
 
 Potentiometer::Potentiometer(int pin) {
@@ -81,11 +89,13 @@ void Potentiometer::setBipolar() {
   this->mode = POT_MODE_BIP;
 }
 
-
 void Potentiometer::setPitch() {
   this->mode = POT_MODE_PITCH;
 }
 
+void Potentiometer::setSwitch() {
+  this->mode = POT_MODE_SWITCH;
+}
 
 float Potentiometer::read() {
   int reading = analogRead(this->pin);
@@ -119,9 +129,12 @@ float Potentiometer::read() {
     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.001;
+  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) {
@@ -133,10 +146,14 @@ float Potentiometer::read() {
   }
 
   if (this->last_normalized_reading && !changed) {
-    // Serial.print(this->name);
-    // Serial.println(" returned NaN");
+    // 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;
@@ -229,7 +246,14 @@ void Potentiometer::renderUi() {
       }
 
       // Render that new text
-      centeredText(text_buffer, x_center, y_center+4, SH110X_WHITE);
+      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
diff --git a/code/daisy-looper/ui.h b/code/daisy-looper/ui.h
index 288812c..b389749 100644
--- a/code/daisy-looper/ui.h
+++ b/code/daisy-looper/ui.h
@@ -21,7 +21,7 @@ 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 = true;
+bool show_splash = false;
 
 // Represents the possible states of the UI
 enum UiMode {
@@ -86,6 +86,16 @@ enum BufferSummingMode {
   BUFFER_SUM_MODE_LAST,
 };
 
+enum FXMode {
+  FX_MODE_ALL,
+  FX_MODE_REVERB,
+  FX_MODE_NONE,
+  FX_MODE_LFO,
+  FX_MODE_GRAIN,
+  FX_MODE_EMPTY,
+  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
@@ -101,10 +111,10 @@ class Ui {
         GridButton("START\nLOOPST\nPLAYHD", &button_6, false, BUTTON_TYPE_MULTITOGGLE, 0, "START RECORDING AT\n\nSTART--->Start of the\n         Buffer\nLOOP---->Start of the\n         Loop\nPLAYH--->Position of\n         the Playhead"),
       }),
       ButtonGrid((int) UI_MODE_PLAY_MENU, {
-        GridButton(" ", &button_1, false),
+        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("STOP\nLOOP\nMULTI\nMIDI", &button_4, false, BUTTON_TYPE_MULTITOGGLE, 1),
+        GridButton(" ", &button_4, false),
         GridButton(" ", &button_5, false),
         GridButton(" ", &button_6, false),
       }),
@@ -117,12 +127,12 @@ class Ui {
         GridButton("AUTO", &button_6, false),
       }),
       ButtonGrid((int) UI_MODE_FX_MENU, {
-        GridButton("DELAY", &button_1, false),
-        GridButton("REVERB", &button_2, false),
-        GridButton("-", &button_3, false),
-        GridButton("-", &button_4, false),
-        GridButton("-", &button_5, false),
-        GridButton("FX\nMENU", &button_6, true),
+        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),
@@ -146,37 +156,13 @@ class Ui {
     // Default Recording Source
     RecSource rec_source = REC_SOURCE_PRE;
 
+    // Default active buffer
     ActiveBuffer active_buffer = ACTIVE_BUFFER_A;
 
+    // Default active summing mode
     BufferSummingMode buffer_summing_mode = BUFFER_SUM_MODE_SOLO;
 
-    // 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;
-      }
-    }
+    FXMode fx_mode = FX_MODE_ALL;
 
     // Render the UI
     // Except for the splash screen this is expected to be called
@@ -202,7 +188,7 @@ class Ui {
             renderGrid(2);
             break;
           case UI_MODE_FX_MENU:
-            renderGrid(3);
+            renderGrid(3, fx_mode);
             break;
           case UI_MODE_BUFFER_MENU:
             renderGrid(4, active_buffer);
@@ -216,7 +202,6 @@ class Ui {
     void renderGrid(size_t num, int button_enum=0) {
       display.clearDisplay();
       button_grids[num].render(button_enum);
-      // Display all that stuff and store the time of the last render
       display.display();
     }
 
@@ -230,24 +215,9 @@ class Ui {
           display.clearDisplay();
           display.fillCircle(display.width()/2, display.height()/2, i, SH110X_WHITE);
           display.fillCircle(display.width()/2, display.height()/2, max(0, i-2), SH110X_BLACK);
-          if (i < 10) {
-            centeredText("I", display.width()/2, display.height()/2-4, SH110X_WHITE);
-            centeredText("O", display.width()/2, display.height()/2+4, SH110X_WHITE);
-          } else if (i < 20) {
-            centeredText("I S", display.width()/2, display.height()/2-4, SH110X_WHITE);
-            centeredText("O P", display.width()/2, display.height()/2+4, SH110X_WHITE);
-          } else if (i < 40) {
-            centeredText("A I S", display.width()/2, display.height()/2-4, SH110X_WHITE);
-            centeredText("O O P", display.width()/2, display.height()/2+4, SH110X_WHITE);
-          } else if (i < 50) {
-            centeredText("A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE);
-            centeredText("O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE);
-          } else if (i < 60) {
-            centeredText("D A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE);
-            centeredText("L O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE);
-          } else {
-            centeredText("D A I S Y Y", display.width()/2, display.height()/2-4, SH110X_WHITE);
-            centeredText("L O O P E R", display.width()/2, display.height()/2+4, SH110X_WHITE);
+          if (i < 50) {
+            centeredText("DAISYY", display.width()/2, display.height()/2-4, SH110X_WHITE);
+            centeredText("LOOPER", display.width()/2, display.height()/2+4, SH110X_WHITE);
           }
           
           display.display();
@@ -415,21 +385,78 @@ class Ui {
       if (ui_mode == UI_MODE_PLAY_MENU && last_ui_mode != UI_MODE_PLAY_MENU) {
         int n = 1;
 
-        // Show the setting of the current buffer
-        button_grids[n].grid_buttons_[3].active = (int) activeLooper()->playback_state ;
+        // 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);
 
+        // Chang 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);
+        });
+
+        // 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("Delay", 100.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](){ 
-          button_grids[n].grid_buttons_[3].next();
-          // 0 is stop so we add one, check looper.h for definition of enum
-          activeLooper()->playback_state = (atoav::PlaybackState) (button_grids[n].grid_buttons_[3].active);
+          fx_mode = FX_MODE_LFO;
+          pot_5.setDisplayMode("LFO Mode", 100.0f, POT_DISPLAY_MODE_SWITCH);
+          pot_5.setSwitch();
+          pot_5.switch_positions = 2;
+          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("Grain Spread", 100.0f, POT_DISPLAY_MODE_PERCENT);
+          pot_7.setDisplayMode("Grain Var.", 100.0f, POT_DISPLAY_MODE_PERCENT);
         });
 
         // Store the last ui mode, for the check on top
@@ -472,8 +499,7 @@ class Ui {
         // 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_TRIGGER_MENU); });
-        // button_6.onHold([this](){ this->setMode(UI_MODE_FX_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
@@ -580,8 +606,9 @@ class Ui {
         case atoav::PLAYBACK_STATE_MULTILOOP:
           {
             float* playheads = activeLooper()->GetPlayheads();
+            uint8_t count = activeLooper()->GetPlayheadCount();
             int x_playhead = 0;
-            for (size_t i=0; i<9; i++) {
+            for (size_t i=0; i<count; i++) {
               x_playhead = int(playheads[i] * display.width()) + x_start_loop;
               int h = 6 + i*3;
               display.drawFastVLine(x_playhead, h, 3, SH110X_WHITE);
@@ -701,6 +728,35 @@ class Ui {
       }
     }
 
+   // 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();
@@ -720,6 +776,7 @@ class Ui {
         case UI_MODE_TRIGGER_MENU:
           break;
         case UI_MODE_FX_MENU:
+          setupFXMenu();
           break;
         case UI_MODE_BUFFER_MENU:
           setupBufferMenu();
-- 
GitLab