From b5bb5db32ad2cc35e1d9b8d4bf61ccef0d6e9f70 Mon Sep 17 00:00:00 2001 From: David Huss <dh@atoav.com> Date: Fri, 8 Dec 2023 17:28:32 +0100 Subject: [PATCH] Basic multi-playhead functionality --- code/daisy-looper/daisy-looper.ino | 2 +- code/daisy-looper/looper.h | 597 ++++++++++++++++++----------- code/daisy-looper/ui.h | 67 ++-- 3 files changed, 408 insertions(+), 258 deletions(-) diff --git a/code/daisy-looper/daisy-looper.ino b/code/daisy-looper/daisy-looper.ino index cb83c81..3d48e15 100644 --- a/code/daisy-looper/daisy-looper.ino +++ b/code/daisy-looper/daisy-looper.ino @@ -380,7 +380,7 @@ void loop() { // Delaytime is in samples if (!isnan(p5)) { lfo_amount = p5; } if (!isnan(p6)) { delaytime = 100.0f + p6 * 23900.0f; } - if (!isnan(p7)) { reverbmix = p7; Serial.print("Reverb was not NaN: "); Serial.println(reverbmix); } + if (!isnan(p7)) { reverbmix = p7; } // 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 22a8804..1914c7f 100644 --- a/code/daisy-looper/looper.h +++ b/code/daisy-looper/looper.h @@ -1,5 +1,6 @@ -#ifndef Looper_h -#define Looper_h +#pragma once + +#include "luts.h" namespace atoav { @@ -17,267 +18,395 @@ enum RecStartMode { REC_START_MODE_LAST, }; -class Looper { +enum PlaybackState { + PLAYBACK_STATE_STOPPED, + PLAYBACK_STATE_LOOP, + PLAYBACK_STATE_MULTILOOP, + PLAYBACK_STATE_MIDI, + PLAYBACK_STATE_LAST, +}; + +enum RecordingState { + REC_STATE_EMPTY, + REC_STATE_RECORDING, + REC_STATE_OVERDUBBING, + REC_STATE_ERASING, + REC_STATE_NONE, + REC_STATE_LAST, +}; + +// ================================================= +// = H E A D = +// ================================================= + +class Head { public: - void Init(float *buf, size_t length) { - buffer = buf; - buffer_length = length; - // Reset buffer contents to zero - memset(buffer, 0, sizeof(float) * buffer_length); - } + Head(); + void activate(); + void deactivate(); + bool isActive(); + void setPosition(float value); + void setIncrement(float value); + void incrementBy(float value); + void update(); + float read(); + float increment = 1.0f; + private: + bool active = true; + float position = 0.0f; +}; +Head::Head() { + +} +void Head::activate() { + this->active = true; +} +void Head::deactivate() { + this->active = false; +} +bool Head::isActive() { + return active; +} +void Head::setPosition(float value) { + this->position = value; +} +void Head::setIncrement(float value) { + this->increment = value; +} +void Head::incrementBy(float value) { + this->position += value; +} +void Head::update() { + this->position += this->increment; +} +float Head::read() { + return this->position; +} + + +// ================================================= +// = L O O P E R = +// ================================================= + +class Looper { + public: + Looper(); + void Init(float *buf, size_t buf_size); + void SetRecord(); + void SetOverdub(); + void SetErase(); + void SetStopWriting(); + bool isWriting(); + void ResetRecHead(); + void SetLoop(float loop_start_time, float loop_length_time); + void Record(float in); + float Process(); + float* getBuffer(); + size_t getBufferLength(); + void setRecPitchMode(RecPitchMode mode); + void setRecStartMode(RecStartMode mode); + float GetPlayhead(); + float* GetPlayheads(); + float GetRecHead(); + bool toggleRecMode(); + void setRecModeFull(); + void setRecModeLoop(); + void setRecModeFullShot(); + void setRecModeLoopShot(); + void setPlaybackSpeed(float increment); + void addToPlayhead(float value); + float loop_start_f = 0.0f; + float loop_length_f = 1.0f; RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL; RecStartMode rec_start_mode = REC_START_MODE_BUFFER; + PlaybackState playback_state = PLAYBACK_STATE_LOOP; + RecordingState recording_state = REC_STATE_EMPTY; - void SetRecording(bool is_recording, bool is_overdub) { - this->is_overdub = is_overdub; - this->is_recording = is_recording || is_overdub; - //Initialize recording head position on start - // if (rec_env_pos_inc <= 0 && is_recording) { - if (is_recording) { - // rec_head = (loop_start + play_head) % buffer_length; - switch (rec_start_mode) { - case REC_START_MODE_LOOP: - rec_head = (loop_start) % buffer_length; - break; - case REC_START_MODE_BUFFER: - rec_head = 0.0f; - break; - case REC_START_MODE_PLAYHEAD: - rec_head = fmod(loop_start + play_head, float(buffer_length)); - break; - } - - is_empty = false; - } - // When record switch changes state it effectively - // sets ramp to rising/falling, providing a - // fade in/out in the beginning and at the end of - // the recorded region. - rec_env_pos_inc = is_recording ? 1 : -1; - } - - void SetLoop(const float loop_start_time, const float loop_length_time) { - if (!isnan(loop_start_time)) { - loop_start_f = loop_start_time; - // Set the start of the next loop - pending_loop_start = static_cast<size_t>(loop_start_time * (buffer_length - 1)); + private: + static const size_t kFadeLength = 200; + static const size_t kMinLoopLength = 2 * kFadeLength; - // If the current loop start is not set yet, set it too - if (!is_loop_set) loop_start = pending_loop_start; - } + float* buffer; + size_t buffer_length = 0; - if (!isnan(loop_length_time)) { - loop_length_f = loop_length_time; - // Set the length of the next loop - pendingloop_length = max(kMinLoopLength, static_cast<size_t>(loop_length_time * buffer_length)); + Head playheads[9]; + Head rec_head; - // CHECK if this is truly good - // loop_length = pendingloop_length; - // loop_length = pendingloop_length; + size_t loop_start = 0; + size_t loop_length = 48000; - //If the current loop length is not set yet, set it too - if (!is_loop_set) loop_length = pendingloop_length; - } - is_loop_set = true; - } - - void Record(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 we're in the middle of the ramp - record to the buffer. - if (rec_env_pos > 0) { - // Calculate fade in/out - float rec_attenuation = static_cast<float>(rec_env_pos) / static_cast<float>(kFadeLength); - if (this->is_overdub) { - buffer[int(rec_head)] += in * rec_attenuation; - } else { - buffer[int(rec_head)] = in * rec_attenuation + buffer[int(rec_head)] * (1.f - rec_attenuation); - } + bool stop_after_recording = false; + bool stay_within_loop = true; - // Set recording pitch mode - switch (rec_pitch_mode) { - case REC_PITCH_MODE_NORMAL: - rec_head += 1.0f; - break; - case REC_PITCH_MODE_UNPITCHED: - rec_head += playback_increment; - break; - case REC_PITCH_MODE_PITCHED: - if (playback_increment != 0.0) { - rec_head += 1.0f/playback_increment; - } - break; - } +}; - // Different recording modes - if (!stop_after_recording) { - if (!stay_within_loop) { - // record into whole buffer - rec_head = fmod(rec_head, float(buffer_length)); - } else { - // Limit rec head to stay inside the loop - rec_head = fmod(rec_head, float(loop_start + loop_length)); - rec_head = max(float(loop_start), rec_head); - } - } else { - if (!stay_within_loop) { - if (rec_head > buffer_length) { SetRecording(false, false); } - } else { - if (rec_head > loop_start + loop_length) { SetRecording(false, false); } - } - } - if (rec_head > buffer_length) { - rec_head = 0.0f; - } else if (rec_head < 0) { - rec_head = buffer_length; +Looper::Looper() {} + +void Looper::Init(float *buf, size_t buf_size) { + buffer = buf; + buffer_length = buf_size; + memset(buffer, 0, sizeof(float) * buffer_length); +} + +void Looper::SetRecord() { + recording_state = REC_STATE_RECORDING; + ResetRecHead(); + rec_head.activate(); +} + +void Looper::SetOverdub() { + recording_state = REC_STATE_OVERDUBBING; + ResetRecHead(); + rec_head.activate(); +} + +void Looper::SetErase() { + recording_state = REC_STATE_ERASING; + ResetRecHead(); + rec_head.activate(); +} + +void Looper::SetStopWriting() { + recording_state = REC_STATE_NONE; + rec_head.deactivate(); +} + +bool Looper::isWriting() { + return recording_state == REC_STATE_RECORDING + || recording_state == REC_STATE_OVERDUBBING + || recording_state == REC_STATE_ERASING; +} + +void Looper::ResetRecHead() { + if (isWriting()) { + switch (rec_start_mode) { + case REC_START_MODE_LOOP: + rec_head.setPosition(loop_start % buffer_length); + break; + case REC_START_MODE_BUFFER: + rec_head.setPosition(0.0f); + break; + case REC_START_MODE_PLAYHEAD: + rec_head.setPosition(fmod(loop_start + playheads[0].read(), float(buffer_length))); + break; + } + } +} + +void Looper::SetLoop(float loop_start_time, float loop_length_time) { + if (!isnan(loop_start_time)) { + loop_start_f = loop_start_time; + loop_start = static_cast<size_t>(loop_start_time * (buffer_length - 1)); + } + if (!isnan(loop_length_time)) { + loop_length_f = loop_length_time; + loop_length = max(kMinLoopLength, static_cast<size_t>(loop_length_time * buffer_length)); + } +} + +void Looper::Record(float in) { + // Overwrite/Add/Erase the buffer depending on the mode + switch (recording_state) { + case REC_STATE_RECORDING: + buffer[int(rec_head.read())] = in; + break; + case REC_STATE_OVERDUBBING: + buffer[int(rec_head.read())] += in; + break; + case REC_STATE_ERASING: + buffer[int(rec_head.read())] = 0.0f; + break; + } + + // Advance the recording head if needed + if (isWriting()) { + // Set Recording head increment depending on the mode + switch (rec_pitch_mode) { + case REC_PITCH_MODE_NORMAL: + rec_head.setIncrement(1.0f); + break; + case REC_PITCH_MODE_UNPITCHED: + rec_head.setIncrement(playheads[0].increment); + break; + case REC_PITCH_MODE_PITCHED: + if (playheads[0].increment != 0.0) { + rec_head.setIncrement(1.0f/playheads[0].increment); } - - } + break; } - - float Process() { - // Early return if the buffer is empty - if (is_empty) { - return 0; + // Increment recording head + rec_head.update(); + + // Limit the position of the rec head depending on the active mode + if (!stop_after_recording) { + if (!stay_within_loop) { + // record into whole buffer + rec_head.setPosition(fmod(rec_head.read(), float(buffer_length))); + } else { + // Limit rec head to stay inside the loop + rec_head.setPosition(fmod(rec_head.read(), float(loop_start + loop_length))); + rec_head.setPosition(max(float(loop_start), rec_head.read())); } - - // Variables for the Playback from the Buffer - float attenuation = 1; - float output = 0; - - // Calculate fade in/out - if (play_head < kFadeLength) { - attenuation = static_cast<float>(play_head) / static_cast<float>(kFadeLength); - } - else if (play_head >= loop_length - kFadeLength) { - attenuation = static_cast<float>(loop_length - play_head) / static_cast<float>(kFadeLength); + } else { + // Stop at end (either end of buffer or end of loop) + if (!stay_within_loop) { + if (rec_head.read() > buffer_length) { SetStopWriting(); } + } else { + if (rec_head.read() > loop_start + loop_length) { SetStopWriting(); } } - - // Ensure we are actually inside the buffer - auto play_pos = int(loop_start + play_head) % buffer_length; - - // Read from the buffer - output = buffer[play_pos] * attenuation; - - // Advance playhead by the increment - play_head += playback_increment; - - // Ensure the playhead stays within bounds of the loop - 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 = pendingloop_length; - play_head = loop_length; - } - - // Return the attenuated signal - return output * attenuation; } - float GetPlayhead() { - return float(play_head) / float(buffer_length); - } - - float GetRecHead() { - return float(rec_head) / float(buffer_length); - } - - bool toggleRecMode() { - stay_within_loop = !stay_within_loop; - return stay_within_loop; - } - - void setRecModeFull() { - stay_within_loop = false; - stop_after_recording = false; - } - - void setRecModeLoop() { - stay_within_loop = true; - stop_after_recording = false; - } - - void setRecModeFullShot() { - stay_within_loop = false; - stop_after_recording = true; - } - - void setRecModeLoopShot() { - stay_within_loop = true; - stop_after_recording = true; - } - - - void setPlaybackSpeed(float increment) { - playback_increment = increment; - } - - void addToPlayhead(float value) { - play_head += value; - } - - float* getBuffer() { - return buffer; - } - - size_t getBufferLength() { - return buffer_length; + // Ensure the Rec-Head is never without bounds, even when running backwards + if (rec_head.read() > buffer_length) { + rec_head.setPosition(0.0f); + } else if (rec_head.read() < 0) { + rec_head.setPosition(buffer_length); } + } +} + +float Looper::Process() { + // Early return if buffer is empty or not playing to save performance + if (recording_state == REC_STATE_EMPTY + || playback_state == PLAYBACK_STATE_STOPPED) { + return 0; + } + + // Deactivate all playheads except first if playback state is Loop + switch (playback_state) { + case PLAYBACK_STATE_LOOP: + playheads[0].activate(); + for (size_t i=1; i<9; i++) { + playheads[i].deactivate(); + } + break; + case PLAYBACK_STATE_MULTILOOP: + for (size_t i=0; i<9; i++) { + playheads[i].activate(); + } + break; + case PLAYBACK_STATE_STOPPED: + for (size_t i=0; i<9; i++) { + playheads[i].deactivate(); + } + break; + } - bool isRecording() { - return is_recording; - } + double mix = 0.0; - bool isOverdubbing() { - return is_overdub; - } + for (size_t i=0; i<9; i++) { + // Skip inactive playheads + if (!playheads[i].isActive()) continue; - void setRecPitchMode(RecPitchMode mode) { - rec_pitch_mode = mode; - } + // Ensure we are actually inside the buffer + int play_pos = int(loop_start + playheads[i].read()) % buffer_length; - void setRecStartMode(RecStartMode mode) { - rec_start_mode = mode; - } + // Read from the buffer + mix += buffer[play_pos]; - float loop_length_f = 1.0f; - float loop_start_f = 0.0f; + // Advance the playhead + playheads[i].update(); - private: - static const size_t kFadeLength = 200; - static const size_t kMinLoopLength = 2 * kFadeLength; + // Ensure the playhead stays within bounds of the loop + if ((playheads[i].read()) >= loop_length) { + playheads[i].setPosition(0.0f); + } else if (playheads[i].read() <= 0.0f) { + playheads[i].setPosition(loop_length); + } + } + return saturate(mix); +} + +float Looper::GetPlayhead() { + return float(playheads[0].read()) / float(buffer_length); +} + +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; +} + +float Looper::GetRecHead() { + return float(rec_head.read()) / float(buffer_length); +} + +bool Looper::toggleRecMode() { + stay_within_loop = !stay_within_loop; + return stay_within_loop; +} + +void Looper::setRecModeFull() { + stay_within_loop = false; + stop_after_recording = false; +} + +void Looper::setRecModeLoop() { + stay_within_loop = true; + stop_after_recording = false; +} + +void Looper::setRecModeFullShot() { + stay_within_loop = false; + stop_after_recording = true; +} + +void Looper::setRecModeLoopShot() { + stay_within_loop = true; + stop_after_recording = true; +} + +void Looper::setPlaybackSpeed(float increment) { + switch (playback_state) { + case PLAYBACK_STATE_LOOP: + playheads[0].setIncrement(increment); + break; + case PLAYBACK_STATE_MULTILOOP: + playheads[0].setIncrement(increment); + for (size_t i=1; i<9; i++) { + playheads[i].setIncrement(increment + increment/(1+i)); + } + break; + } +} + +void Looper::addToPlayhead(float value) { + switch (playback_state) { + case PLAYBACK_STATE_LOOP: + playheads[0].incrementBy(value); + break; + case PLAYBACK_STATE_MULTILOOP: + playheads[0].incrementBy(value); + for (size_t i=1; i<9; i++) { + playheads[i].incrementBy(value + value/(1+i)); + } + break; + } +} - float* buffer; +float* Looper::getBuffer() { + return buffer; +} - size_t buffer_length = 0; - size_t loop_length = 48000; - size_t pendingloop_length = 0; - size_t loop_start = 0; - size_t pending_loop_start = 0; +size_t Looper::getBufferLength() { + return buffer_length; +} - float play_head = 0.0f; - float rec_head = 0.0f; +void Looper::setRecPitchMode(RecPitchMode mode) { + rec_pitch_mode = mode; +} - float playback_increment = 1.0f; +void Looper::setRecStartMode(RecStartMode mode) { + rec_start_mode = mode; +} - 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; - bool stop_after_recording = false; -}; -}; // namespace atoav -#endif \ No newline at end of file +}; // namespace atoav \ No newline at end of file diff --git a/code/daisy-looper/ui.h b/code/daisy-looper/ui.h index af65c5b..288812c 100644 --- a/code/daisy-looper/ui.h +++ b/code/daisy-looper/ui.h @@ -101,12 +101,12 @@ 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("LOOP", &button_1, false), + GridButton(" ", &button_1, false), GridButton("PLAY\nMENU", &button_2, true), GridButton("ACTIVE\nSUM\nRING", &button_3, false, BUTTON_TYPE_MULTITOGGLE, 0), - GridButton("DRUNK", &button_4, false), - GridButton("GRAIN", &button_5, false), - GridButton("SHOT", &button_6, false), + GridButton("STOP\nLOOP\nMULTI\nMIDI", &button_4, false, BUTTON_TYPE_MULTITOGGLE, 1), + GridButton(" ", &button_5, false), + GridButton(" ", &button_6, false), }), ButtonGrid((int) UI_MODE_TRIGGER_MENU, { GridButton("MIDI\nTRIG.", &button_1, false), @@ -415,25 +415,22 @@ 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 ; + // Setup button Grid Button* home_button = setupButtonGrid(n); - // button_1.onPress([this, n](){ - - // }); button_3.onPress([this, n](){ button_grids[n].grid_buttons_[2].next(); buffer_summing_mode = (BufferSummingMode) button_grids[n].grid_buttons_[2].active; }); - // button_4.onPress([this, n](){ - // - // }); - // button_5.onPress([this, n](){ - // - // }); - // button_6.onPress([this, n](){ - // - // }); + + 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); + }); // Store the last ui mode, for the check on top last_ui_mode = ui_mode; @@ -573,14 +570,38 @@ class Ui { } // Draw Playhead - int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop; - display.drawLine(x_playhead, 6, x_playhead, 24, SH110X_WHITE); + switch (activeLooper()->playback_state) { + case atoav::PLAYBACK_STATE_LOOP: + { + int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop; + display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE); + break; + } + case atoav::PLAYBACK_STATE_MULTILOOP: + { + float* playheads = activeLooper()->GetPlayheads(); + int x_playhead = 0; + for (size_t i=0; i<9; i++) { + x_playhead = int(playheads[i] * display.width()) + x_start_loop; + int h = 6 + i*3; + display.drawFastVLine(x_playhead, h, 3, SH110X_WHITE); + } + break; + } + case atoav::PLAYBACK_STATE_MIDI: + { + int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop; + display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE); + break; + } + } + // Draw Recording Indicator and Recording Head if (recording_state == REC_STATE_RECORDING) { // Draw Rec Head int x_rec_head = int(activeLooper()->GetRecHead() * display.width()); - display.drawLine(x_rec_head, 10, x_rec_head, bottom, SH110X_WHITE); + display.drawFastVLine(x_rec_head, 10, bottom, SH110X_WHITE); display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE); // Record sign display.fillRect(0, 0, 13, 13, SH110X_WHITE); @@ -593,7 +614,7 @@ class Ui { if (recording_state == REC_STATE_OVERDUBBING) { // Draw Rec Head int x_rec_head = int(activeLooper()->GetRecHead() * display.width()); - display.drawLine(x_rec_head, 10, x_rec_head, bottom, SH110X_WHITE); + display.drawFastVLine(x_rec_head, 10, bottom, SH110X_WHITE); display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE); // Overdub sign (a "plus") @@ -620,7 +641,7 @@ class Ui { // Activate recording and set the waveform cache to dirty void activateRecording() { if (recording_state != REC_STATE_RECORDING) { - activeLooper()->SetRecording(true, false); + activeLooper()->SetRecord(); waveform_cache_dirty = true; recording_state = REC_STATE_RECORDING; } @@ -646,7 +667,7 @@ class Ui { if (recording_state != REC_STATE_OVERDUBBING) { waveform_cache_dirty = true; recording_state = REC_STATE_OVERDUBBING; - activeLooper()->SetRecording(true, true); + activeLooper()->SetOverdub(); } } @@ -654,7 +675,7 @@ class Ui { void stopRecording() { if (recording_state != REC_STATE_NOT_RECORDING) { recording_state = REC_STATE_NOT_RECORDING; - activeLooper()->SetRecording(false, false); + activeLooper()->SetStopWriting(); } } -- GitLab