From bf9d86e9dac2e783c2529888814ec89b23d22832 Mon Sep 17 00:00:00 2001 From: David Huss <dh@atoav.com> Date: Sat, 11 Nov 2023 15:05:09 +0100 Subject: [PATCH] Refactor --- .../default-37a8.jupyterlab-workspace | 2 +- circuitsim/envelope.ipynb | 35 ++- circuitsim/lookup-tables.ipynb | 161 ++++++++++- code/daisy-looper/daisy-looper.ino | 83 +++--- code/daisy-looper/looper.h | 112 ++++---- code/daisy-looper/ui.h | 255 ++++++++++++------ menu.ods | Bin 0 -> 18938 bytes 7 files changed, 459 insertions(+), 189 deletions(-) create mode 100644 menu.ods diff --git a/circuitsim/.jupyter/desktop-workspaces/default-37a8.jupyterlab-workspace b/circuitsim/.jupyter/desktop-workspaces/default-37a8.jupyterlab-workspace index 04bc81b..79abff0 100644 --- a/circuitsim/.jupyter/desktop-workspaces/default-37a8.jupyterlab-workspace +++ b/circuitsim/.jupyter/desktop-workspaces/default-37a8.jupyterlab-workspace @@ -1 +1 @@ -{"data":{"layout-restorer:data":{"main":{"dock":{"type":"tab-area","currentIndex":2,"widgets":["notebook:circuit_sim.ipynb","notebook:lookup-tables.ipynb","notebook:envelope.ipynb"]},"current":"notebook:envelope.ipynb"},"down":{"size":0,"widgets":[]},"left":{"collapsed":false,"current":"filebrowser","widgets":["filebrowser","running-sessions","@jupyterlab/toc:plugin","extensionmanager.main-view"]},"right":{"collapsed":true,"widgets":["jp-property-inspector","debugger-sidebar"]},"relativeSizes":[0.26227795193312436,0.7377220480668757,0]},"notebook:circuit_sim.ipynb":{"data":{"path":"circuit_sim.ipynb","factory":"Notebook"}},"notebook:lookup-tables.ipynb":{"data":{"path":"lookup-tables.ipynb","factory":"Notebook"}},"notebook:envelope.ipynb":{"data":{"path":"envelope.ipynb","factory":"Notebook"}}},"metadata":{"id":"default"}} \ No newline at end of file +{"data":{"layout-restorer:data":{"main":{"dock":{"type":"tab-area","currentIndex":1,"widgets":["notebook:circuit_sim.ipynb","notebook:lookup-tables.ipynb","notebook:envelope.ipynb"]},"current":"notebook:lookup-tables.ipynb"},"down":{"size":0,"widgets":[]},"left":{"collapsed":false,"current":"filebrowser","widgets":["filebrowser","running-sessions","@jupyterlab/toc:plugin","extensionmanager.main-view"]},"right":{"collapsed":true,"widgets":["jp-property-inspector","debugger-sidebar"]},"relativeSizes":[0.13545601726929304,0.864543982730707,0]},"notebook:circuit_sim.ipynb":{"data":{"path":"circuit_sim.ipynb","factory":"Notebook"}},"notebook:lookup-tables.ipynb":{"data":{"path":"lookup-tables.ipynb","factory":"Notebook"}},"notebook:envelope.ipynb":{"data":{"path":"envelope.ipynb","factory":"Notebook"}}},"metadata":{"id":"default"}} \ No newline at end of file diff --git a/circuitsim/envelope.ipynb b/circuitsim/envelope.ipynb index 24c5b4f..7bec7a5 100644 --- a/circuitsim/envelope.ipynb +++ b/circuitsim/envelope.ipynb @@ -258,9 +258,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 121, "id": "2e2c2c7f-4bc6-4a1f-b6bd-bf5558b8f03f", "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "947364e8d6aa4d1ba675e14ddf866148", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.2, description='f', max=1.0, step=0.001), Output()), _dom_classes=('…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "<function __main__.draw(f)>" + ] + }, + "execution_count": 121, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6112aebb-dc20-45e3-9ca6-280c76523469", + "metadata": {}, "outputs": [], "source": [] } diff --git a/circuitsim/lookup-tables.ipynb b/circuitsim/lookup-tables.ipynb index 0a086e4..d3401e0 100644 --- a/circuitsim/lookup-tables.ipynb +++ b/circuitsim/lookup-tables.ipynb @@ -480,9 +480,168 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "id": "e51416f3-f34d-4513-9f0c-fa52c468274e", "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "880451c044ba49d5a1686f3db7453888", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.2, description='f', max=10.1, min=-10.0, step=0.001), Output()), _do…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "<function __main__.draw(f)>" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from matplotlib import pyplot as plt\n", + "%matplotlib inline\n", + "from ipywidgets import interact, FloatSlider\n", + "import matplotlib.transforms as transforms\n", + "import math\n", + "\n", + "def draw(f):\n", + " fig = plt.figure(figsize=(8, 4))\n", + " ax = fig.add_axes([0, 0, 1, 1])\n", + " b = scan2d(x, y, f)\n", + " ax.axhline(y=b, color='red', linestyle='--')\n", + " ax.axvline(x=f, color='red', linestyle='--')\n", + " trans = transforms.blended_transform_factory(\n", + " ax.get_yticklabels()[0].get_transform(), ax.transData)\n", + " ax.text(0.95, b, \"{:.02f}\".format(b), color=\"red\", transform=trans, ha=\"right\", va=\"bottom\")\n", + " ax.grid()\n", + " ax.plot(x, y)\n", + "\n", + "def lerp(a, b, f=0.5) -> float:\n", + " f = min(1.0, max(0.0, f))\n", + " if f == 0.0:\n", + " return a\n", + " elif f == 1.0:\n", + " return b\n", + " else:\n", + " return a * (1.0-f) + b * f\n", + "\n", + "def lerp2d(x1, y1, x2, y2, f=0.5):\n", + " if f == 0.0:\n", + " return [x1, x2]\n", + " elif f == 1.0:\n", + " return [x1, x2]\n", + " else:\n", + " x = lerp(x1, x2, f)\n", + " y = lerp(y1, y2, f)\n", + " return [x, y]\n", + "\n", + "def scan2d(x, y, f):\n", + " # f = min(1.0, max(0.0, f))\n", + " assert len(x) == len(y)\n", + " # Find ax and bx for given factor\n", + " xa = None\n", + " last_value = None\n", + " idx = None\n", + " for i, v in enumerate(x):\n", + " # this = abs(f-v)\n", + " this = f-v\n", + " if xa is None or this > 0:\n", + " xa = this\n", + " idx = i\n", + " idx2 = min(idx+1, len(x)-1)\n", + " if idx == idx2:\n", + " return y[idx]\n", + " xa = x[idx]\n", + " xb = x[idx2]\n", + " ya = y[idx]\n", + " yb = y[idx2]\n", + " xspan = xb-xa\n", + " xscaler = 1/xspan\n", + " new_f = (f-xa)*xscaler\n", + " # print(f\"xa {xa} [{idx}]\")\n", + " # print(f\"xb {xb} [{idx2}]\")\n", + " # print(f\"ya {ya} [{idx}]\")\n", + " # print(f\"yb {yb} [{idx2}]\")\n", + " # print(f\"xspan {xspan} [{xb} - {xa}]\")\n", + " # print(f\"xscaler {xscaler} [1/{xspan}]\")\n", + " # print(f\"new_f {new_f} [({f}-{xa})/{xscaler}]\")\n", + " 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.1\n", + "\n", + "lines_orig = [\n", + " [-10.0, -10.0, 0.0],\n", + " [-10.0+half_deadband, -10.0, 0.0],\n", + "] \n", + "\n", + "steps = list(range(-9, 11))\n", + "for i in steps:\n", + " f = float(i)\n", + " lines_orig.append([f-half_deadband, f, 0.0])\n", + " lines_orig.append([f+half_deadband, f, 0.0])\n", + "\n", + "\n", + "lines = []\n", + "for i, l in enumerate(lines_orig):\n", + " i2 = min(len(lines_orig)-1, i+1)\n", + " if l[2] == 0.0:\n", + " lines.append(l)\n", + " else:\n", + " xa = lines_orig[i][0]\n", + " xb = lines_orig[i2][0]\n", + " ya = lines_orig[i][1]\n", + " yb = lines_orig[i2][1]\n", + " x_span = xb-xa\n", + " y_span = yb-ya\n", + " x_step = 1/20\n", + " y_step = 1/20\n", + " for j in range(20):\n", + " x = x_step * j\n", + " y = y_step * j\n", + " y_curve = 0\n", + " if l[2] > 0.0:\n", + " y_curve = y*y*y\n", + " else:\n", + " y_curve = y*y\n", + " y = (1.0-l[2]) * y + l[2] * y_curve\n", + " lines.append([xa+x*x_span, ya+y*y_span, 0.0])\n", + "\n", + "\n", + "x = [a[0] for a in lines]\n", + "y = [a[1] for a in lines]\n", + "c = [a[2] for a in lines]\n", + "# draw(x, y, 0.45/2)\n", + "\n", + "interact(draw, f=FloatSlider(min=min(x), max=max(x), step=0.001, value=0.2))\n", + "\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f35f1609-3a10-4dce-b7dd-201d79f2c39c", + "metadata": {}, "outputs": [], "source": [] } diff --git a/code/daisy-looper/daisy-looper.ino b/code/daisy-looper/daisy-looper.ino index f69de38..e798e3a 100644 --- a/code/daisy-looper/daisy-looper.ino +++ b/code/daisy-looper/daisy-looper.ino @@ -20,13 +20,11 @@ #include "helpers.h" #include "ui.h" -#define UI_FPS 60.0 - static const size_t buffer_length = 48000 * 5; static float DSY_SDRAM_BSS buffer[buffer_length]; +// Create instances of audio stuff atoav::Looper looper; -static PitchShifter pitch_shifter; static atoav::EnvelopeFollower input_envelope_follower; DelayLine<float, 24000> delayline; DSY_SDRAM_BSS ReverbSc reverb; @@ -35,11 +33,8 @@ Oscillator lfo; static SampleHold sample_and_hold; static WhiteNoise noise; static Metro tick; -// Resonator res; - -CpuLoadMeter load; -// Buttons +// Initialize Buttons Button button_1 = Button(D7); Button button_2 = Button(D8); Button button_3 = Button(D9); @@ -47,7 +42,7 @@ Button button_4 = Button(D10); Button button_5 = Button(D13); Button button_6 = Button(D14); -// Make Potentiometers +// Initialize Potentiometers Potentiometer pot_1 = Potentiometer(A0); Potentiometer pot_2 = Potentiometer(A1); Potentiometer pot_3 = Potentiometer(A3); @@ -56,7 +51,7 @@ Potentiometer pot_5 = Potentiometer(A4); Potentiometer pot_6 = Potentiometer(A5); Potentiometer pot_7 = Potentiometer(A6); -// LED R G B +// RGB LED R G B RGBLed rgb_led = RGBLed(A10, A9, A11); @@ -73,22 +68,25 @@ Ui ui; DaisyHardware hw; // Variables for the Audio-Callback -size_t num_channels; +size_t num_channels +; float blocksize; float drywetmix = 0.0f; float delaymix = 0.0f; float delaytime = 100.0f; float reverbmix = 0.0f; float lfo_amount = 0.0f; -auto pitch_val = 0.5f; +float pitch_val = 0.5f; // Actual audio-processing is orchestrated here void AudioCallback(float **in, float **out, size_t size) { float output = 0.0f; float no_delay = 0.0f; float wet_delay; - delayline.SetDelay(delaytime); float out1, out2; + // set the delay + delayline.SetDelay(delaytime); + for (size_t i = 0; i < size; i++) { uint8_t trig = tick.Process(); float lfo_value = lfo.Process(); @@ -111,9 +109,10 @@ void AudioCallback(float **in, float **out, size_t size) { // res.SetDamping(0.1+delaymix*0.2f); auto looper_out = looper.Process(in[1][i]); - // FIXME: + + // input_envelope_follower.Process(in[1][i]); - output = drywetmix * pitch_shifter.Process(looper_out) + in[1][i] * (1.0f - drywetmix); + 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(); @@ -132,13 +131,7 @@ void AudioCallback(float **in, float **out, size_t size) { } -// void activate_rec() { -// ui.activateRecording(); -// } -// void activate_overdub() { -// ui.activateOverdub(); -// } void setup() { float sample_rate; @@ -149,36 +142,38 @@ void setup() { sample_rate = DAISY.get_samplerate(); blocksize = 64.0f; + // Create a Tick and a noise source for the Sample and Hold tick.Init(10, sample_rate); + noise.Init(); + // Initialize Looper with the buffer looper.Init(buffer, 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(0.95f); reverb.SetLpFreq(18000.0f); + // Initialize Compressor compressor.SetThreshold(-64.0f); compressor.SetRatio(2.0f); compressor.SetAttack(0.005f); compressor.SetRelease(0.1250); - pitch_shifter.Init(sample_rate); - - // set parameters for LFO oscillator object + // Initialize the LFO for modulations lfo.Init(sample_rate); lfo.SetWaveform(Oscillator::WAVE_TRI); lfo.SetAmp(1); lfo.SetFreq(8.0); - noise.Init(); - + // Initialize the Delay at a length of 1 second delayline.Init(); - delayline.SetDelay(48000.0f); - // res.Init(.015, 24, sample_rate); - // res.SetStructure(-7.f); + delayline.SetDelay(samplerate); load.Init(sample_rate, blocksize); @@ -189,10 +184,8 @@ void setup() { display.begin(0x3C, true); delay(50); ui.Render(); - - DAISY.begin(AudioCallback); - + // Initialize the LED rgb_led.init(); // Set the analog read and write resolution to 12 bits @@ -210,21 +203,20 @@ void setup() { // Set Knob Scaling Modes pot_3.setBipolar(); - // Initialize Buttons + // 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(); - // button_1.onPressed(activate_rec); - // button_2.onPress(toggleRecMode); - // button_3.onPressed(activate_overdub); - // button_4.onPressed(display_fx_menu); + + // Start the audio Callback + DAISY.begin(AudioCallback); } void loop() { - // hw.ProcessAllControls(); + // Read the values from the potentiometers float p1 = pot_1.read(); float p2 = pot_2.read(); float p3 = pot_3.read(); @@ -233,10 +225,10 @@ void loop() { float p6 = pot_6.read(); float p7 = pot_7.read(); - // Deactivate recording and overdub before reading buttons + // Update the UI ui.update(); - // Read buttons + // Read the buttons button_1.read(); button_2.read(); button_3.read(); @@ -244,21 +236,26 @@ void loop() { button_5.read(); button_6.read(); - // Set loop start and loop length + // Set loop-start and loop-length with the potentiometers ui.setLoop(p1, p2); - // Value should go 10 octaves up/down (10*1.0 = 1000%) + // 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%) pitch_val = 10.0f * p3; - // res.SetFreq(10.0f + p3 * 1500.0f); - // Set other parameters + // Set other parameters (from 0.0 to 1.0) drywetmix = p4; delaymix = p5; + // Delaytime is in samples delaytime = 100.0f + p5 * 23900.0f; reverbmix = p6; lfo_amount = p7; + // Render the UI (frame rate limited by UI_MAX_FPS in ui.h) ui.Render(); + + // Set the Color and brightness of the RGB LED in 8 bits rgb_led.setAudioLevelIndicator(int(input_envelope_follower.getValue() * 255)); } diff --git a/code/daisy-looper/looper.h b/code/daisy-looper/looper.h index 9624a92..a71079b 100644 --- a/code/daisy-looper/looper.h +++ b/code/daisy-looper/looper.h @@ -20,10 +20,10 @@ enum RecStartMode { class Looper { public: void Init(float *buf, size_t length) { - _buffer = buf; - _buffer_length = length; + buffer = buf; + buffer_length = length; // Reset buffer contents to zero - memset(_buffer, 0, sizeof(float) * _buffer_length); + memset(buffer, 0, sizeof(float) * buffer_length); } RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL; @@ -33,62 +33,62 @@ class Looper { this->is_overdub = is_overdub; this->is_recording = is_recording || is_overdub; //Initialize recording head position on start - if (_rec_env_pos_inc <= 0 && is_recording) { - // rec_head = (_loop_start + play_head) % _buffer_length; + if (rec_env_pos_inc <= 0 && is_recording) { + // rec_head = (loop_start + play_head) % buffer_length; switch (rec_start_mode) { case REC_START_MODE_LOOP: - rec_head = (_loop_start) % _buffer_length; + rec_head = (loop_start) % buffer_length; break; case REC_START_MODE_BUFFER: rec_head = 0.0f; break; case REC_START_MODE_PLAYHEAD: - rec_head = fmod(_loop_start + play_head, float(_buffer_length)); + rec_head = fmod(loop_start + play_head, float(buffer_length)); break; } - _is_empty = false; + is_empty = false; } // When record switch changes state it effectively // sets ramp to rising/falling, providing a // fade in/out in the beginning and at the end of // the recorded region. - _rec_env_pos_inc = is_recording ? 1 : -1; + rec_env_pos_inc = is_recording ? 1 : -1; } void SetLoop(const float loop_start, const float loop_length) { // Set the start of the next loop - _pending_loop_start = static_cast<size_t>(loop_start * (_buffer_length - 1)); + pending_loop_start = static_cast<size_t>(loop_start * (buffer_length - 1)); // If the current loop start is not set yet, set it too - if (!_is_loop_set) _loop_start = _pending_loop_start; + if (!is_loop_set) loop_start = pending_loop_start; // Set the length of the next loop - _pending_loop_length = max(kMinLoopLength, static_cast<size_t>(loop_length * _buffer_length)); + pendingloop_length = max(kMinLoopLength, static_cast<size_t>(loop_length * buffer_length)); // CHECK if this is truly good - // _loop_length = _pending_loop_length; - // _loop_length = _pending_loop_length; + // loop_length = pendingloop_length; + // loop_length = pendingloop_length; //If the current loop length is not set yet, set it too - if (!_is_loop_set) _loop_length = _pending_loop_length; - _is_loop_set = true; + if (!is_loop_set) loop_length = pendingloop_length; + is_loop_set = true; } float Process(float in) { // Calculate iterator position on the record level ramp. - if (_rec_env_pos_inc > 0 && _rec_env_pos < kFadeLength - || _rec_env_pos_inc < 0 && _rec_env_pos > 0) { - _rec_env_pos += _rec_env_pos_inc; + if (rec_env_pos_inc > 0 && rec_env_pos < kFadeLength + || rec_env_pos_inc < 0 && rec_env_pos > 0) { + rec_env_pos += rec_env_pos_inc; } // If we're in the middle of the ramp - record to the buffer. - if (_rec_env_pos > 0) { + if (rec_env_pos > 0) { // Calculate fade in/out - float rec_attenuation = static_cast<float>(_rec_env_pos) / static_cast<float>(kFadeLength); + float rec_attenuation = static_cast<float>(rec_env_pos) / static_cast<float>(kFadeLength); if (this->is_overdub) { - _buffer[int(rec_head)] += in * rec_attenuation; + buffer[int(rec_head)] += in * rec_attenuation; } else { - _buffer[int(rec_head)] = in * rec_attenuation + _buffer[int(rec_head)] * (1.f - rec_attenuation); + buffer[int(rec_head)] = in * rec_attenuation + buffer[int(rec_head)] * (1.f - rec_attenuation); } // Set recording pitch mode @@ -110,29 +110,29 @@ class Looper { if (!stop_after_recording) { if (!stay_within_loop) { // record into whole buffer - rec_head = fmod(rec_head, float(_buffer_length)); + rec_head = fmod(rec_head, float(buffer_length)); } else { // Limit rec head to stay inside the loop - rec_head = fmod(rec_head, float(_loop_start + _loop_length)); - rec_head = max(float(_loop_start), rec_head); + rec_head = fmod(rec_head, float(loop_start + loop_length)); + rec_head = max(float(loop_start), rec_head); } } else { if (!stay_within_loop) { - if (rec_head > _buffer_length) { SetRecording(false, false); } + if (rec_head > buffer_length) { SetRecording(false, false); } } else { - if (rec_head > _loop_start + _loop_length) { SetRecording(false, false); } + if (rec_head > loop_start + loop_length) { SetRecording(false, false); } } } - if (rec_head > _buffer_length) { + if (rec_head > buffer_length) { rec_head = 0.0f; } else if (rec_head < 0) { - rec_head = _buffer_length; + rec_head = buffer_length; } } - if (_is_empty) { + if (is_empty) { return 0; } @@ -144,25 +144,25 @@ class Looper { if (play_head < kFadeLength) { attenuation = static_cast<float>(play_head) / static_cast<float>(kFadeLength); } - else if (play_head >= _loop_length - kFadeLength) { - attenuation = static_cast<float>(_loop_length - play_head) / static_cast<float>(kFadeLength); + else if (play_head >= loop_length - kFadeLength) { + attenuation = static_cast<float>(loop_length - play_head) / static_cast<float>(kFadeLength); } // Read from the buffer - auto play_pos = int(_loop_start + play_head) % _buffer_length; - output = _buffer[play_pos] * attenuation; + auto play_pos = int(loop_start + play_head) % buffer_length; + output = buffer[play_pos] * attenuation; // Advance playhead play_head += playback_increment; // Ensure the playhead stays within bounds - if (play_head >= _loop_length) { - _loop_start = _pending_loop_start; - _loop_length = _pending_loop_length; + if (play_head >= loop_length) { + loop_start = pending_loop_start; + loop_length = pendingloop_length; play_head = 0; } else if (play_head <= 0) { - _loop_start = _pending_loop_start; - _loop_length = _pending_loop_length; - play_head = _loop_length; + loop_start = pending_loop_start; + loop_length = pendingloop_length; + play_head = loop_length; } @@ -170,11 +170,11 @@ class Looper { } float GetPlayhead() { - return float(play_head) / float(_buffer_length); + return float(play_head) / float(buffer_length); } float GetRecHead() { - return float(rec_head) / float(_buffer_length); + return float(rec_head) / float(buffer_length); } bool toggleRecMode() { @@ -216,11 +216,11 @@ class Looper { } float* getBuffer() { - return _buffer; + return buffer; } size_t getBufferLength() { - return _buffer_length; + return buffer_length; } bool isRecording() { @@ -248,26 +248,26 @@ class Looper { } private: - static const size_t kFadeLength = 200; //orig: 600 + static const size_t kFadeLength = 200; static const size_t kMinLoopLength = 2 * kFadeLength; - float* _buffer; + float* buffer; - size_t _buffer_length = 0; - size_t _loop_length = 0; - size_t _pending_loop_length = 0; - size_t _loop_start = 0; - size_t _pending_loop_start = 0; + size_t buffer_length = 0; + size_t loop_length = 0; + size_t pendingloop_length = 0; + size_t loop_start = 0; + size_t pending_loop_start = 0; float play_head = 0.0f; float rec_head = 0.0f; float playback_increment = 1.0f; - size_t _rec_env_pos = 0; - int32_t _rec_env_pos_inc = 0; - bool _is_empty = true; - bool _is_loop_set = false; + size_t rec_env_pos = 0; + int32_t rec_env_pos_inc = 0; + bool is_empty = true; + bool is_loop_set = false; bool stay_within_loop = false; bool is_overdub = false; bool is_recording = false; diff --git a/code/daisy-looper/ui.h b/code/daisy-looper/ui.h index 2d27771..06134d7 100644 --- a/code/daisy-looper/ui.h +++ b/code/daisy-looper/ui.h @@ -18,52 +18,62 @@ extern Button button_1, button_2, button_3, button_4, button_5, button_6; extern Adafruit_SH1106G display; extern atoav::Looper looper; +// Should the splash-screen be shown on boot? +bool show_splash = true; - +// Represents the possible states of the UI enum UiMode { - UI_MODE_SPLASH, - UI_MODE_DEFAULT, - UI_MODE_REC_MENU, - UI_MODE_PLAY_MENU, - UI_MODE_TRIGGER_MENU, - UI_MODE_FX_MENU, + UI_MODE_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_LAST }; +// Represents possible recording modes enum RecMode { - REC_MODE_FULL, - REC_MODE_LOOP, - REC_MODE_FULL_SHOT, - REC_MODE_LAST = 6 + 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 playback modes enum PlayMode { - PLAY_MODE_DRUNK, - PLAY_MODE_WINDOW, - PLAY_MODE_LOOP, - PLAY_MODE_GRAIN, - PLAY_MODE_ONESHOT, + 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, - REC_STATE_RECORDING, - REC_STATE_OVERDUBBING, + 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 }; -// Different types of buttons +// 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" insided) - BUTTON_TYPE_MULTITOGGLE, // Toggles between two or more values + 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; GridButton(const char* name, Button& button, bool is_home=false, ButtonType type=BUTTON_TYPE_SIMPLE, int default_value=0) @@ -81,11 +91,12 @@ class GridButton { } } Button button; + ButtonType type; bool is_home; int active; - ButtonType type; int lines = 0; + // Go to the next option void next() { active++; if (active > lines) { @@ -94,6 +105,7 @@ class GridButton { } }; +// The ButtonGrid is a grid of 2×3 = 6 buttons class ButtonGrid { public: ButtonGrid(const GridButton (&grid_buttons)[6]) @@ -109,10 +121,13 @@ class ButtonGrid { // 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; @@ -140,11 +155,11 @@ class ButtonGrid { centeredText(name, xc, yc, text_color); } - // Counter for the number of the button + // Increase ounter for the index of the button i++; } } - // Draw divider Line + // 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); @@ -152,6 +167,8 @@ class ButtonGrid { }; +// The Ui is _the_ coordinating class for the whole interaction. +// The default mode class Ui { public: Ui() : button_grids { @@ -188,11 +205,17 @@ class Ui { GridButton("FX\nMENU", button_6, true), }), } {}; - UiMode ui_mode = UI_MODE_SPLASH; + + // Store the Button Grids declared above (make sure the lenght matches!) ButtonGrid button_grids[4]; + + // Stores the current Ui Mode + UiMode ui_mode = UI_MODE_SPLASH; + // RecMode rec_mode = REC_MODE_FULL; RecMode rec_mode = REC_MODE_LOOP; + // 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; @@ -213,6 +236,9 @@ class Ui { } } + // Render the UI + // Except for the splash screen this is expected to be called + // repeatedly in a loop void Render() { switch (ui_mode) { case UI_MODE_SPLASH: @@ -238,10 +264,10 @@ class Ui { } } - // Render button grids + // Helper method to render a certain button grid void renderGrid(size_t num, int button_enum=0) { double now = millis(); - if (now - last_render > UI_MAX_FPS) { + if (now - last_render > (1000.0/UI_MAX_FPS)) { display.clearDisplay(); button_grids[num].render(button_enum); // Display all that stuff and store the time of the last render @@ -250,49 +276,52 @@ class Ui { } } - // Renders a splash screen + // Renders a splash screen (runs once) void renderSplash() { display.setTextSize(1); - // display.setTextColor(SH110X_BLACK); - // // Play a fancy intro splash screen - // for (int i=0; i < 91; i++) { - // display.clearDisplay(); - // display.fillCircle(display.width()/2, display.height()/2, i, SH110X_WHITE); - // display.fillCircle(display.width()/2, display.height()/2, max(0, i-2), SH110X_BLACK); - // if (i < 10) { - // centeredText("I", display.width()/2, display.height()/2-4, SH110X_WHITE); - // centeredText("O", display.width()/2, display.height()/2+4, SH110X_WHITE); - // } else if (i < 20) { - // centeredText("I S", display.width()/2, display.height()/2-4, SH110X_WHITE); - // centeredText("O P", display.width()/2, display.height()/2+4, SH110X_WHITE); - // } else if (i < 40) { - // centeredText("A I S", display.width()/2, display.height()/2-4, SH110X_WHITE); - // centeredText("O O P", display.width()/2, display.height()/2+4, SH110X_WHITE); - // } else if (i < 50) { - // centeredText("A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE); - // centeredText("O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE); - // } else if (i < 60) { - // centeredText("D A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE); - // centeredText("L O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE); - // } else { - // centeredText("D A I S Y Y", display.width()/2, display.height()/2-4, SH110X_WHITE); - // centeredText("L O O P E R", display.width()/2, display.height()/2+4, SH110X_WHITE); - // } - - // display.display(); - // delay(1); - // } - // display.invertDisplay(true); - // display.display(); - // delay(800); - // display.clearDisplay(); - // display.invertDisplay(false); - // display.display(); + if (show_splash) { + display.setTextColor(SH110X_BLACK); + // Play a fancy intro splash screen + for (int i=0; i < 91; i++) { + display.clearDisplay(); + display.fillCircle(display.width()/2, display.height()/2, i, SH110X_WHITE); + display.fillCircle(display.width()/2, display.height()/2, max(0, i-2), SH110X_BLACK); + if (i < 10) { + centeredText("I", display.width()/2, display.height()/2-4, SH110X_WHITE); + centeredText("O", display.width()/2, display.height()/2+4, SH110X_WHITE); + } else if (i < 20) { + centeredText("I S", display.width()/2, display.height()/2-4, SH110X_WHITE); + centeredText("O P", display.width()/2, display.height()/2+4, SH110X_WHITE); + } else if (i < 40) { + centeredText("A I S", display.width()/2, display.height()/2-4, SH110X_WHITE); + centeredText("O O P", display.width()/2, display.height()/2+4, SH110X_WHITE); + } else if (i < 50) { + centeredText("A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE); + centeredText("O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE); + } else if (i < 60) { + centeredText("D A I S Y", display.width()/2, display.height()/2-4, SH110X_WHITE); + centeredText("L O O P E", display.width()/2, display.height()/2+4, SH110X_WHITE); + } else { + centeredText("D A I S Y Y", display.width()/2, display.height()/2-4, SH110X_WHITE); + centeredText("L O O P E R", display.width()/2, display.height()/2+4, SH110X_WHITE); + } + + display.display(); + delay(1); + } + display.invertDisplay(true); + display.display(); + delay(800); + display.clearDisplay(); + display.invertDisplay(false); + display.display(); + } // Splash rendering is now done, go to next UI Mode setMode(UI_MODE_DEFAULT); } + // Helper method to reset the controls void resetControls() { button_1.reset(); button_2.reset(); @@ -302,18 +331,21 @@ class Ui { button_6.reset(); } + // Setup the Recording Menu void setupRecMenu() { - // Only run once + // Only run once when the ui_mode changed if (ui_mode == UI_MODE_REC_MENU && last_ui_mode != UI_MODE_REC_MENU) { Serial.println("[UI] Setup Rec Menu"); + + // Reset controls resetControls(); + // Stay in this menu as long as the button is pressed, otherwise return button_1.onPressed([this](){ this->setMode(UI_MODE_REC_MENU); }); button_1.onReleased([this](){ this->setMode(UI_MODE_DEFAULT); }); // Toggle between momentary and toggle recording modes - button_2.onPress([this](){ - + button_2.onPress([this](){ Serial.print("[UI] Mom/Toggle option "); if (rec_button_momentary) { Serial.println("momentary -> toggle"); @@ -330,10 +362,10 @@ class Ui { button_grids[0].grid_buttons_[2].next(); }); // FULL ONESHOT - // Switch Recording modes + // Switch Recording modes (Full/Loop/Oneshot) button_4.onPress([this](){ button_grids[0].grid_buttons_[3].next(); - // Button.active returns number according to mode: + // Button.active returns number according to mode, we cast it to a RecMode enum rec_mode = (RecMode) button_grids[0].grid_buttons_[3].active; switch (rec_mode) { case REC_MODE_FULL: looper.setRecModeFull(); break; @@ -353,26 +385,36 @@ class Ui { button_grids[0].grid_buttons_[5].next(); looper.setRecStartMode((atoav::RecStartMode) button_grids[0].grid_buttons_[5].active); }); + + // 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 + // Only run once on mode change if (ui_mode == UI_MODE_DEFAULT && last_ui_mode != UI_MODE_DEFAULT) { Serial.println("[UI] Setup Default mode"); + + // Reset controls resetControls(); + // Set up the initial recording mode switch (rec_mode) { case REC_MODE_FULL: looper.setRecModeFull(); break; case REC_MODE_LOOP: looper.setRecModeLoop(); break;; case REC_MODE_FULL_SHOT: looper.setRecModeFullShot(); break; }; - // Setup Button functions + + // Setup Button functions (these should enter the ButtonGrid Menus) button_1.onPressed([this](){ this->setMode(UI_MODE_REC_MENU); }); button_2.onPressed([this](){ this->setMode(UI_MODE_PLAY_MENU); }); button_3.onPressed([this](){ this->setMode(UI_MODE_TRIGGER_MENU); }); - // Momentary/Toggle Recording modes + button_6.onPressed([this](){ this->setMode(UI_MODE_FX_MENU); }); + + // Set the recording/overdub buttons to toggle or momentary + // depending on the value of the option if (rec_button_momentary) { Serial.println("[UI] Set to momentary mode"); button_4.onPressed([this](){ this->activateRecording(); }); @@ -384,55 +426,77 @@ class Ui { button_4.onReleased([this](){ this->toggleRecording(); }); button_5.onReleased([this](){ this->toggleOverdub(); }); } - button_6.onPressed([this](){ this->setMode(UI_MODE_FX_MENU); }); + + // 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 double now = millis(); - if (now - last_render > UI_MAX_FPS) { + if (now - last_render > (1000.0/UI_MAX_FPS)) { + + // Clear the display display.clearDisplay(); - // Render the waveform + + // 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 = looper.getBufferLength() / (display.width() * WAVEFORM_OVERSAMPLING); + // Helper variable for the bottom of the screen int bottom = display.height()-1; - // Render the waveform + + // 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 needed, else use cache + // Only recalculate if the cahce is dirty, else use cache if (waveform_cache_dirty) { float sig = 0.0f; + // Step through the buffer and sum the absolute values for (int s=0; s<WAVEFORM_OVERSAMPLING; s++) { float abs_sig = looper.getBuffer()[step*i]; abs_sig = abs(abs_sig) * 100.0f; sig += abs_sig; } + // We oversampled so divide here sig = sig / float(WAVEFORM_OVERSAMPLING); + + // Volume is logarithmic, we want to see silent noises as well if (sig != 0.0f) { sig = log10(sig)/3.6f; } waveform_cache[x] = int(sig * wave_height); } - // Serial.print(waveform_cache[x]); - // Serial.print(","); + + // 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); - display.drawFastHLine(0, bottom, display.width(), 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 Line for loop start + // Draw Indicator for loop start int x_start_loop = int(loop_start * display.width()); display.drawLine(x_start_loop, 0, x_start_loop, bottom, SH110X_WHITE); display.fillTriangle(x_start_loop, 6, x_start_loop, 0, x_start_loop+3, 0, SH110X_WHITE); - // Draw Line for Loop End + // Draw Indicator for Loop End int x_loop_length = int(loop_length * display.width()); int x_loop_end = (x_start_loop + x_loop_length) % display.width(); display.drawLine(x_loop_end, 0, x_loop_end, bottom, SH110X_WHITE); display.fillTriangle(x_loop_end, 6, x_loop_end-3, 0, x_loop_end, 0, SH110X_WHITE); - // Connecting line for start and end + // 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 { @@ -444,7 +508,7 @@ class Ui { int x_playhead = int(looper.GetPlayhead() * display.width()) + x_start_loop; display.drawLine(x_playhead, 6, x_playhead, 24, SH110X_WHITE); - // Draw Recording stuff + // Draw Recording Indicator and Recording Head if (recording_state == REC_STATE_RECORDING) { // Draw Rec Head int x_rec_head = int(looper.GetRecHead() * display.width()); @@ -457,8 +521,13 @@ class Ui { display.fillCircle(6, 6, 3, SH110X_WHITE); } - // Draw Overdub stuff + // Draw Overdub Indicator and Recording Head if (recording_state == REC_STATE_OVERDUBBING) { + // Draw Rec Head + int x_rec_head = int(looper.GetRecHead() * display.width()); + display.drawLine(x_rec_head, 10, x_rec_head, bottom, SH110X_WHITE); + display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE); + // Overdub sign (a "plus") display.fillRect(0, 0, 13, 13, SH110X_WHITE); display.fillRect(2, 2, 12, 12, SH110X_WHITE); @@ -476,11 +545,15 @@ class Ui { pot_6.renderUi(); pot_7.renderUi(); + // Display all the things done above display.display(); + + // Store the time of when we started rendering last_render = now; } } + // Activate recording and set the waveform cache to dirty void activateRecording() { if (recording_state != REC_STATE_RECORDING) { Serial.println("[UI] Activate Recording"); @@ -490,6 +563,7 @@ class Ui { } } + // Toggle recording void toggleRecording() { Serial.print("[UI] Toggle Recording "); Serial.print(recording_state); @@ -508,10 +582,12 @@ class Ui { Serial.println(recording_state); } + // Returns true if we are recording bool isRecording() { return looper.isRecording(); } + // Activates overdubbing void activateOverdub() { if (recording_state != REC_STATE_OVERDUBBING) { Serial.println("[UI] Activate Overdub"); @@ -521,6 +597,7 @@ class Ui { } } + // Stop the recording void stopRecording() { if (recording_state != REC_STATE_NOT_RECORDING) { Serial.println("[UI] Stop Recording"); @@ -529,6 +606,7 @@ class Ui { } } + // Toggle overdub off and on void toggleOverdub() { Serial.print("[UI] Toggle Overdub "); Serial.print(recording_state); @@ -547,21 +625,24 @@ class Ui { Serial.println(recording_state); } - + // Return true if overdub is running bool isOverdubbing() { return looper.isOverdubbing(); } + // 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; } } + // Update the Ui variables (expected to run repeatedly) void update() { resetRecordingState(); } + // Set the Looper start/length to a given value void setLoop(float start, float length) { loop_start = start; loop_length = length; diff --git a/menu.ods b/menu.ods new file mode 100644 index 0000000000000000000000000000000000000000..1b5d3f6346eea013ef11936ddfbd7c26749045e6 GIT binary patch literal 18938 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%0D4HGQZ_RiT|S=lJ!bd3AJZDF@dPC1R@ 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 literal 0 HcmV?d00001 -- GitLab