Skip to content
Snippets Groups Projects
Commit 7ffaf3a8 authored by David Huss's avatar David Huss :speech_balloon:
Browse files

Merge branch 'looper2'

parents 2290274a b5bb5db3
No related branches found
No related tags found
No related merge requests found
......@@ -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();
......
#ifndef Looper_h
#define Looper_h
#pragma once
#include "luts.h"
namespace atoav {
......@@ -17,267 +18,395 @@ enum RecStartMode {
REC_START_MODE_LAST,
};
enum PlaybackState {
PLAYBACK_STATE_STOPPED,
PLAYBACK_STATE_LOOP,
PLAYBACK_STATE_MULTILOOP,
PLAYBACK_STATE_MIDI,
PLAYBACK_STATE_LAST,
};
enum RecordingState {
REC_STATE_EMPTY,
REC_STATE_RECORDING,
REC_STATE_OVERDUBBING,
REC_STATE_ERASING,
REC_STATE_NONE,
REC_STATE_LAST,
};
// =================================================
// = H E A D =
// =================================================
class Head {
public:
Head();
void activate();
void deactivate();
bool isActive();
void setPosition(float value);
void 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:
void Init(float *buf, size_t length) {
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;
private:
static const size_t kFadeLength = 200;
static const size_t kMinLoopLength = 2 * kFadeLength;
float* buffer;
size_t buffer_length = 0;
Head playheads[9];
Head rec_head;
size_t loop_start = 0;
size_t loop_length = 48000;
bool stop_after_recording = false;
bool stay_within_loop = true;
};
Looper::Looper() {}
void Looper::Init(float *buf, size_t buf_size) {
buffer = buf;
buffer_length = length;
// Reset buffer contents to zero
buffer_length = buf_size;
memset(buffer, 0, sizeof(float) * buffer_length);
}
RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL;
RecStartMode rec_start_mode = REC_START_MODE_BUFFER;
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();
}
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;
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 = (loop_start) % buffer_length;
rec_head.setPosition(loop_start % buffer_length);
break;
case REC_START_MODE_BUFFER:
rec_head = 0.0f;
rec_head.setPosition(0.0f);
break;
case REC_START_MODE_PLAYHEAD:
rec_head = fmod(loop_start + play_head, float(buffer_length));
rec_head.setPosition(fmod(loop_start + playheads[0].read(), 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) {
void Looper::SetLoop(float loop_start_time, 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));
// If the current loop start is not set yet, set it too
if (!is_loop_set) loop_start = pending_loop_start;
loop_start = static_cast<size_t>(loop_start_time * (buffer_length - 1));
}
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));
// CHECK if this is truly good
// 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 = pendingloop_length;
loop_length = max(kMinLoopLength, static_cast<size_t>(loop_length_time * buffer_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);
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;
}
// Set recording pitch mode
// 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 += 1.0f;
rec_head.setIncrement(1.0f);
break;
case REC_PITCH_MODE_UNPITCHED:
rec_head += playback_increment;
rec_head.setIncrement(playheads[0].increment);
break;
case REC_PITCH_MODE_PITCHED:
if (playback_increment != 0.0) {
rec_head += 1.0f/playback_increment;
if (playheads[0].increment != 0.0) {
rec_head.setIncrement(1.0f/playheads[0].increment);
}
break;
}
// Increment recording head
rec_head.update();
// Different recording modes
// 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 = fmod(rec_head, float(buffer_length));
rec_head.setPosition(fmod(rec_head.read(), 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.setPosition(fmod(rec_head.read(), float(loop_start + loop_length)));
rec_head.setPosition(max(float(loop_start), rec_head.read()));
}
} else {
// Stop at end (either end of buffer or end of loop)
if (!stay_within_loop) {
if (rec_head > buffer_length) { SetRecording(false, false); }
if (rec_head.read() > buffer_length) { SetStopWriting(); }
} else {
if (rec_head > loop_start + loop_length) { SetRecording(false, false); }
if (rec_head.read() > loop_start + loop_length) { SetStopWriting(); }
}
}
if (rec_head > buffer_length) {
rec_head = 0.0f;
} else if (rec_head < 0) {
rec_head = 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 Process() {
// Early return if the buffer is empty
if (is_empty) {
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;
}
// 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);
// 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();
}
else if (play_head >= loop_length - kFadeLength) {
attenuation = static_cast<float>(loop_length - play_head) / static_cast<float>(kFadeLength);
break;
case PLAYBACK_STATE_MULTILOOP:
for (size_t i=0; i<9; i++) {
playheads[i].activate();
}
break;
case PLAYBACK_STATE_STOPPED:
for (size_t i=0; i<9; i++) {
playheads[i].deactivate();
}
break;
}
double mix = 0.0;
for (size_t i=0; i<9; i++) {
// Skip inactive playheads
if (!playheads[i].isActive()) continue;
// Ensure we are actually inside the buffer
auto play_pos = int(loop_start + play_head) % buffer_length;
int play_pos = int(loop_start + playheads[i].read()) % buffer_length;
// Read from the buffer
output = buffer[play_pos] * attenuation;
mix += buffer[play_pos];
// Advance playhead by the increment
play_head += playback_increment;
// Advance the playhead
playheads[i].update();
// 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;
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);
}
// Return the attenuated signal
return output * attenuation;
float Looper::GetPlayhead() {
return float(playheads[0].read()) / float(buffer_length);
}
float GetPlayhead() {
return float(play_head) / 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 GetRecHead() {
return float(rec_head) / float(buffer_length);
float Looper::GetRecHead() {
return float(rec_head.read()) / float(buffer_length);
}
bool toggleRecMode() {
bool Looper::toggleRecMode() {
stay_within_loop = !stay_within_loop;
return stay_within_loop;
}
void setRecModeFull() {
void Looper::setRecModeFull() {
stay_within_loop = false;
stop_after_recording = false;
}
void setRecModeLoop() {
void Looper::setRecModeLoop() {
stay_within_loop = true;
stop_after_recording = false;
}
void setRecModeFullShot() {
void Looper::setRecModeFullShot() {
stay_within_loop = false;
stop_after_recording = true;
}
void setRecModeLoopShot() {
void Looper::setRecModeLoopShot() {
stay_within_loop = true;
stop_after_recording = true;
}
void setPlaybackSpeed(float increment) {
playback_increment = increment;
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));
}
void addToPlayhead(float value) {
play_head += value;
break;
}
float* getBuffer() {
return buffer;
}
size_t getBufferLength() {
return buffer_length;
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;
}
}
bool isRecording() {
return is_recording;
float* Looper::getBuffer() {
return buffer;
}
bool isOverdubbing() {
return is_overdub;
size_t Looper::getBufferLength() {
return buffer_length;
}
void setRecPitchMode(RecPitchMode mode) {
void Looper::setRecPitchMode(RecPitchMode mode) {
rec_pitch_mode = mode;
}
void setRecStartMode(RecStartMode mode) {
void Looper::setRecStartMode(RecStartMode mode) {
rec_start_mode = mode;
}
float loop_length_f = 1.0f;
float loop_start_f = 0.0f;
private:
static const size_t kFadeLength = 200;
static const size_t kMinLoopLength = 2 * kFadeLength;
float* 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;
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;
bool stay_within_loop = false;
bool is_overdub = false;
bool is_recording = false;
bool stop_after_recording = false;
};
}; // namespace atoav
\ No newline at end of file
#endif
\ No newline at end of file
......@@ -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
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.drawLine(x_playhead, 6, x_playhead, 24, SH110X_WHITE);
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();
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment