2026-04-24 14:46:05 +02:00

315 lines
9.3 KiB
C++

/*
Copyright (c) 2020 Electrosmith, Corp
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
#pragma once
#include <algorithm>
#include "dsp.h"
namespace daisysp
{
/** Multimode audio looper
*
* Modes are:
* - Normal
* - Onetime Dub
* - Replace
* - Frippertronics
*
* Read more about the looper modes in the mode enum documentation.
*/
class Looper
{
public:
Looper() {}
~Looper() {}
/**
** Normal Mode: Input is added to the existing loop infinitely while recording
**
** Onetime Dub Mode: Recording starts at the first sample of the buffer and is added
** to the existing buffer contents. Recording automatically stops after one full loop.
**
** Replace Mode: Audio in the buffer is replaced while recording is on.
**
** Frippertronics Mode: infinite looping recording with fixed decay on each loop. The module acts like tape-delay set up.
*/
enum class Mode
{
NORMAL,
ONETIME_DUB,
REPLACE,
FRIPPERTRONICS,
};
void Init(float *mem, size_t size)
{
buffer_size_ = size;
buff_ = mem;
InitBuff();
state_ = State::EMPTY;
mode_ = Mode::NORMAL;
half_speed_ = false;
reverse_ = false;
rec_queue_ = false;
win_idx_ = 0;
increment_size = 1.0;
}
/** Handles reading/writing to the Buffer depending on the mode. */
float Process(const float input)
{
float sig = 0.f;
float inc;
bool hitloop = false;
// Record forward at normal speed during the first loop no matter what.
inc = state_ == State::EMPTY || state_ == State::REC_FIRST
? 1.f
: GetIncrementSize();
win_ = WindowVal(win_idx_ * kWindowFactor);
switch(state_)
{
case State::EMPTY:
sig = 0.0f;
pos_ = 0;
recsize_ = 0;
break;
case State::REC_FIRST:
sig = 0.f;
Write(pos_, input * win_);
if(win_idx_ < kWindowSamps - 1)
win_idx_ += 1;
recsize_ = pos_;
pos_ += inc;
if(pos_ > buffer_size_ - 1)
{
state_ = State::PLAYING;
recsize_ = pos_ - 1;
pos_ = 0;
}
break;
case State::PLAYING:
sig = Read(pos_);
/** This is a way of 'seamless looping'
** The first N samps after recording is done are recorded with the input faded out.
*/
if(win_idx_ < kWindowSamps - 1)
{
Write(pos_, sig + input * (1.f - win_));
win_idx_ += 1;
}
pos_ += inc;
if(pos_ > recsize_ - 1)
{
pos_ = 0;
hitloop = true;
}
else if(pos_ < 0)
{
pos_ = recsize_ - 1;
hitloop = true;
}
if(hitloop)
{
if(rec_queue_ && mode_ == Mode::ONETIME_DUB)
{
rec_queue_ = false;
state_ = State::REC_DUB;
win_idx_ = 0;
}
}
break;
case State::REC_DUB:
sig = Read(pos_);
switch(mode_)
{
case Mode::REPLACE: Write(pos_, input * win_); break;
case Mode::FRIPPERTRONICS:
Write(pos_, (input * win_) + (sig * kFripDecayVal));
break;
case Mode::NORMAL:
case Mode::ONETIME_DUB:
default: Write(pos_, (input * win_) + sig); break;
}
if(win_idx_ < kWindowSamps - 1)
win_idx_ += 1;
pos_ += inc;
if(pos_ > recsize_ - 1)
{
pos_ = 0;
hitloop = true;
}
else if(pos_ < 0)
{
pos_ = recsize_ - 1;
hitloop = true;
}
if(hitloop && mode_ == Mode::ONETIME_DUB)
{
state_ = State::PLAYING;
win_idx_ = 0;
}
break;
default: break;
}
near_beginning_ = state_ != State::EMPTY && !Recording() && pos_ < 4800
? true
: false;
return sig;
}
/** Effectively erases the buffer
** Note: This does not actually change what is in the buffer */
inline void Clear() { state_ = State::EMPTY; }
/** Engages/Disengages the recording, depending on Mode.
** In all modes, the first time this is triggered a new loop will be started.
** The second trigger will set the loop size, and begin playback of the loop.
*/
inline void TrigRecord()
{
switch(state_)
{
case State::EMPTY:
pos_ = 0;
recsize_ = 0;
state_ = State::REC_FIRST;
half_speed_ = false;
reverse_ = false;
break;
case State::REC_FIRST:
case State::REC_DUB: state_ = State::PLAYING; break;
case State::PLAYING:
if(mode_ == Mode::ONETIME_DUB)
rec_queue_ = true;
else
state_ = State::REC_DUB;
break;
default: state_ = State::EMPTY; break;
}
if(!rec_queue_)
win_idx_ = 0;
}
/** Returns true if the looper is currently being written to. */
inline const bool Recording() const
{
return state_ == State::REC_DUB || state_ == State::REC_FIRST;
}
inline const bool RecordingQueued() const { return rec_queue_; }
/** Increments the Mode by one step useful for buttons, etc. that need to step through the Looper modes. */
inline void IncrementMode()
{
int m = static_cast<int>(mode_);
m = m + 1;
if(m > kNumModes - 1)
m = 0;
mode_ = static_cast<Mode>(m);
}
/** Sets the recording mode to the specified Mode. */
inline void SetMode(Mode mode) { mode_ = mode; }
/** Returns the specific recording mode that is currently set. */
inline const Mode GetMode() const { return mode_; }
inline void ToggleReverse() { reverse_ = !reverse_; }
inline void SetReverse(bool state) { reverse_ = state; }
inline bool GetReverse() const { return reverse_; }
inline void ToggleHalfSpeed() { half_speed_ = !half_speed_; }
inline void SetHalfSpeed(bool state) { half_speed_ = state; }
inline bool GetHalfSpeed() const { return half_speed_; }
inline bool IsNearBeginning() const { return near_beginning_; }
inline float GetIncrementSize() const
{
float inc = increment_size;
if(half_speed_)
inc *= 0.5f;
return reverse_ ? -inc : inc;
}
void SetIncrementSize(float increment) { increment_size = increment; }
inline float GetPos() const { return pos_; }
inline size_t GetRecSize() const { return recsize_; }
private:
/** Constants */
/** Decay value for frippertronics mode is sin(PI / 4) */
static constexpr float kFripDecayVal = 0.7071067811865476f;
static constexpr int kNumModes = 4;
static constexpr int kNumPlaybackSpeeds = 3;
static constexpr int kWindowSamps = 1200;
static constexpr float kWindowFactor = (1.f / kWindowSamps);
/** Private Member Functions */
/** Initialize the buffer */
void InitBuff() { std::fill(&buff_[0], &buff_[buffer_size_ - 1], 0); }
/** Get a floating point sample from the buffer */
inline const float Read(size_t pos) const { return buff_[pos]; }
/** Reads from a specified point in the delay line using linear interpolation */
float ReadF(float pos)
{
float a, b, frac;
uint32_t i_idx = static_cast<uint32_t>(pos);
frac = pos - i_idx;
a = buff_[i_idx];
b = buff_[(i_idx + 1) % buffer_size_];
return a + (b - a) * frac;
}
/** Write to a known location in the buffer */
inline void Write(size_t pos, float val) { buff_[pos] = val; }
/** Linear to Constpower approximation for windowing*/
float WindowVal(float in) { return sin(HALFPI_F * in); }
// Private Enums
/** Internal looper state */
enum class State
{
EMPTY,
REC_FIRST,
PLAYING,
REC_DUB,
};
/** Private Member Variables */
Mode mode_;
State state_;
float *buff_;
size_t buffer_size_;
float pos_, win_;
size_t win_idx_;
bool half_speed_;
bool reverse_;
size_t recsize_;
bool rec_queue_;
bool near_beginning_;
float increment_size;
};
} // namespace daisysp