Select Git revision
RP2040-VCO-bom.xlsx
looper.h 11.04 KiB
#pragma once
#include "luts.h"
namespace atoav {
enum RecPitchMode {
REC_PITCH_MODE_NORMAL,
REC_PITCH_MODE_UNPITCHED,
REC_PITCH_MODE_LAST,
};
enum RecStartMode {
REC_START_MODE_BUFFER,
REC_START_MODE_LOOP,
REC_START_MODE_PLAYHEAD,
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 reset();
void setIncrement(float value);
void incrementBy(float value);
void slowDown();
void reverse();
void speedUp();
void update();
float read();
float increment = 1.0f;
float variation = 0.0f;
float variation_amount = 0.0f;
float speed_multiplier = 1.0f;
private:
bool active = true;
float position = 0.0f;
};
Head::Head() {
variation = random(-0.1f, 0.1f);
}
void Head::activate() {
this->active = true;
}
void Head::deactivate() {
this->active = false;
}
bool Head::isActive() {
return active;
}
void Head::reset() {
this->position = 0.0f;
}
void Head::setPosition(float value) {
this->position = value * speed_multiplier;
}
void Head::setIncrement(float value) {
this->increment = value;
}
void Head::incrementBy(float value) {
this->position += value;
}
void Head::slowDown() {
this->speed_multiplier = max(0.0f, this->speed_multiplier - 0.0025f);
}
void Head::reverse() {
this->speed_multiplier = -abs(this->speed_multiplier );
}
void Head::speedUp() {
this->speed_multiplier = 1.0f;
}
void Head::update() {
this->position += (this->increment + (variation * variation_amount)) * speed_multiplier;
}
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();
uint8_t GetPlayheadCount();
float GetRecHead();
bool toggleRecMode();
void setRecModeFull();
void setRecModeLoop();
void setRecModeFullShot();
void setRecModeLoopShot();
void setPlaybackSpeed(float increment);
void addToPlayhead(float value);
void slowDown();
void reverse();
void restart();
void speedUp();
float loop_start_f = 0.0f;
float loop_length_f = 1.0f;
uint8_t grain_count = 8;
float grain_spread = 2.0f;
float grain_variation = 0.0f;
RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL;
RecStartMode rec_start_mode = REC_START_MODE_BUFFER;
PlaybackState playback_state = PLAYBACK_STATE_LOOP;
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 = 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;
}
// 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()));
}
} 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 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;
}
double mix = 0.0;
for (size_t i=0; i<grain_count-1; i++) {
// Skip inactive playheads
if (!playheads[i].isActive()) continue;
// Ensure we are actually inside the buffer
int from_start = int(playheads[i].read()) % loop_length;
int play_pos = loop_start + from_start;
float vol = 1.0f;
if (from_start <= kFadeLength) {
vol = from_start / float(kFadeLength);
}
int from_end = abs(int(loop_length) - from_start);
if (from_end <= kFadeLength) {
vol = from_end / float(kFadeLength);
}
// Read from the buffer
mix += buffer[play_pos] * vol;
// Advance the playhead
playheads[i].update();
// Ensure the playhead stays within bounds of the loop
float pos = playheads[i].read();
if (pos >= loop_length || pos <= 0.0f) {
playheads[i].setPosition(fmod(pos, float(loop_length)));
}
}
return mix;
}
float Looper::GetPlayhead() {
return float(int(playheads[0].read()) % loop_length) / float(buffer_length);
}
float* Looper::GetPlayheads() {
static float playhead_positions[9];
for (size_t i=0; i<9; i++) {
playhead_positions[i] = float(int(playheads[i].read()) % loop_length) / float(buffer_length);
}
return playhead_positions;
}
uint8_t Looper::GetPlayheadCount() {
return grain_count;
}
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].variation_amount = grain_variation;
playheads[i].setIncrement(increment + increment*grain_spread);
}
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;
}
}
void Looper::slowDown() {
for (size_t i=0; i<9; i++) {
playheads[i].slowDown();
}
}
void Looper::reverse() {
for (size_t i=0; i<9; i++) {
playheads[i].reverse();
}
}
void Looper::restart() {
for (size_t i=0; i<9; i++) {
playheads[i].reset();
}
}
void Looper::speedUp() {
for (size_t i=0; i<9; i++) {
playheads[i].speedUp();
}
}
float* Looper::getBuffer() {
return buffer;
}
size_t Looper::getBufferLength() {
return buffer_length;
}
void Looper::setRecPitchMode(RecPitchMode mode) {
rec_pitch_mode = mode;
}
void Looper::setRecStartMode(RecStartMode mode) {
rec_start_mode = mode;
}
}; // namespace atoav