/* 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 #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(mode_); m = m + 1; if(m > kNumModes - 1) m = 0; mode_ = static_cast(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(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