diff --git a/circuitsim/.jupyter/desktop-workspaces/default-37a8.jupyterlab-workspace b/circuitsim/.jupyter/desktop-workspaces/default-37a8.jupyterlab-workspace index 04bc81b3f970093890db43fb14747100038b7349..79abff054c2269d9040f61149bdb41c951134600 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 24c5b4fb713131e438a18f9a576a88e31eb4bb7d..7bec7a59810757934a0fe0484d6d42c118d7a158 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 0a086e42f16c7453e38f1d7b04efc91bbc582f42..d3401e064b9d0e652195d0b4b7ec48e0d9f6808c 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 f69de3883337a405757ffc084851f6b2502d862d..e798e3a2f4ac2a8aed7f05a017211dda53181d2d 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 9624a92b60ca87b97b56e674345b1dc54cc5e731..a71079bceea8005804484778fa0147b08af46669 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 2d27771b6adb5bccd3782dde6b8fde5534100dbc..06134d752f06a3535bbe28bbded255accc2f96fc 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 Binary files /dev/null and b/menu.ods differ