LCOV - code coverage report
Current view: top level - src/audio/effects/utility - looper.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 97.7 % 216 211
Test Date: 2026-06-07 15:51:50 Functions: 100.0 % 20 20

            Line data    Source code
       1              : #include "audio/effects/utility/looper.h"
       2              : 
       3              : #include <algorithm>
       4              : #include <cmath>
       5              : #include <ostream>
       6              : 
       7              : #include "audio/effects/core/effect_factory.h"
       8              : 
       9              : namespace Amplitron {
      10              : 
      11            2 : static EffectRegistrar<Looper> reg("Looper");
      12              : 
      13          128 : Looper::Looper() {
      14          224 :     params_ = {
      15           96 :         {"Loop Level", 0.80f, 0.0f, 1.0f, 0.80f, "",
      16           32 :          "Playback volume of the recorded loop mixed with live input."},
      17           32 :         {"Crossfade", 5.0f, 0.0f, 20.0f, 5.0f, "ms",
      18           32 :          "Crossfade length at the loop boundary to reduce clicks/pops."},
      19          192 :     };
      20           96 :     ensure_capacity();
      21           96 :     const float sr = static_cast<float>(std::max(sample_rate_, 1));
      22           96 :     loop_level_alpha_ = 1.0f - std::exp(-1.0f / (sr * kLoopLevelSmoothingSeconds));
      23           96 :     crossfade_alpha_ = 1.0f - std::exp(-1.0f / (sr * kLoopLevelSmoothingSeconds));
      24           96 :     reset();
      25          224 : }
      26              : 
      27           57 : void Looper::set_sample_rate(int sample_rate) {
      28           57 :     Effect::set_sample_rate(sample_rate);
      29           57 :     const float sr = static_cast<float>(std::max(sample_rate_, 1));
      30           57 :     loop_level_alpha_ = 1.0f - std::exp(-1.0f / (sr * kLoopLevelSmoothingSeconds));
      31           57 :     crossfade_alpha_ = 1.0f - std::exp(-1.0f / (sr * kLoopLevelSmoothingSeconds));
      32           57 :     ensure_capacity();
      33           57 :     reset();
      34           57 : }
      35              : 
      36          153 : void Looper::ensure_capacity() {
      37          153 :     const int sr = std::max(sample_rate_, 1);
      38          153 :     const int cap = std::max(sr * kMaxSeconds, 1);
      39          153 :     if (cap == max_samples_) return;
      40          111 :     max_samples_ = cap;
      41          111 :     buffer_l_.assign(static_cast<size_t>(max_samples_), 0.0f);
      42          111 :     buffer_r_.assign(static_cast<size_t>(max_samples_), 0.0f);
      43           51 : }
      44              : 
      45          210 : void Looper::reset() {
      46          210 :     state_rt_ = State::Empty;
      47          210 :     has_loop_rt_ = false;
      48          210 :     record_pos_ = 0;
      49          210 :     playhead_ = 0;
      50          210 :     loop_length_ = 0;
      51          210 :     loop_level_smoothed_ = clamp(params_[0].value, 0.0f, 1.0f);
      52          210 :     crossfade_ms_smoothed_ = clamp(params_[1].value, 0.0f, 20.0f);
      53          210 :     pending_commands_.store(0, std::memory_order_relaxed);
      54          210 :     publish_ui_snapshot();
      55          210 : }
      56              : 
      57           87 : void Looper::request_record_toggle() {
      58           87 :     pending_commands_.fetch_or(CmdRecordToggle, std::memory_order_relaxed);
      59           87 : }
      60              : 
      61           18 : void Looper::request_play_toggle() {
      62           18 :     pending_commands_.fetch_or(CmdPlayToggle, std::memory_order_relaxed);
      63           18 : }
      64              : 
      65           21 : void Looper::request_overdub_toggle() {
      66           21 :     pending_commands_.fetch_or(CmdOverdubToggle, std::memory_order_relaxed);
      67           21 : }
      68              : 
      69            6 : void Looper::request_clear() { pending_commands_.fetch_or(CmdClear, std::memory_order_relaxed); }
      70              : 
      71          285 : int Looper::crossfade_samples_rt(float ms) const {
      72          285 :     const int xf = static_cast<int>(std::round((ms / 1000.0f) * static_cast<float>(sample_rate_)));
      73          285 :     return std::clamp(xf, 0, std::max(loop_length_ / 2, 0));
      74              : }
      75              : 
      76          564 : void Looper::publish_ui_snapshot() {
      77          564 :     ui_state_.store(static_cast<uint32_t>(state_rt_), std::memory_order_relaxed);
      78          564 :     ui_has_loop_.store(has_loop_rt_ ? 1 : 0, std::memory_order_relaxed);
      79          564 :     ui_loop_length_samples_.store(loop_length_, std::memory_order_relaxed);
      80          564 :     ui_playhead_samples_.store(playhead_, std::memory_order_relaxed);
      81          564 : }
      82              : 
      83            9 : void Looper::clear_loop_rt() {
      84            9 :     has_loop_rt_ = false;
      85            9 :     loop_length_ = 0;
      86            9 :     record_pos_ = 0;
      87            9 :     playhead_ = 0;
      88            9 :     state_rt_ = State::Empty;
      89            8 : }
      90              : 
      91           48 : void Looper::start_recording_rt() {
      92           48 :     has_loop_rt_ = false;
      93           48 :     loop_length_ = 0;
      94           48 :     record_pos_ = 0;
      95           48 :     playhead_ = 0;
      96           48 :     state_rt_ = State::Recording;
      97           48 : }
      98              : 
      99           45 : void Looper::stop_recording_rt_and_play_rt() {
     100           45 :     loop_length_ = std::clamp(record_pos_, 0, max_samples_);
     101           30 :     const int min_len =
     102           45 :         static_cast<int>(std::round(kMinLoopSeconds * static_cast<float>(sample_rate_)));
     103           45 :     if (loop_length_ < min_len) {
     104            3 :         clear_loop_rt();
     105            3 :         return;
     106              :     }
     107           42 :     has_loop_rt_ = true;
     108           42 :     playhead_ = 0;
     109           42 :     state_rt_ = State::Playing;
     110           15 : }
     111              : 
     112           15 : void Looper::toggle_play_rt() {
     113           15 :     if (!has_loop_rt_) return;
     114           15 :     if (state_rt_ == State::Playing || state_rt_ == State::Overdubbing) {
     115            9 :         state_rt_ = State::Idle;
     116            9 :     } else if (state_rt_ == State::Idle || state_rt_ == State::Empty) {
     117            6 :         state_rt_ = State::Playing;
     118            2 :     }
     119            5 : }
     120              : 
     121           18 : void Looper::toggle_overdub_rt() {
     122           18 :     if (!has_loop_rt_) return;
     123           18 :     if (state_rt_ == State::Overdubbing) {
     124            6 :         state_rt_ = State::Playing;
     125           14 :     } else if (state_rt_ == State::Playing) {
     126            9 :         state_rt_ = State::Overdubbing;
     127            6 :     } else if (state_rt_ == State::Idle) {
     128            3 :         state_rt_ = State::Overdubbing;
     129            1 :     }
     130            6 : }
     131              : 
     132          354 : void Looper::apply_pending_commands() {
     133          354 :     const uint32_t cmds = pending_commands_.exchange(0, std::memory_order_relaxed);
     134          354 :     if (cmds == 0) return;
     135              : 
     136          132 :     if (cmds & CmdClear) {
     137            6 :         clear_loop_rt();
     138            2 :     }
     139              : 
     140          132 :     if (cmds & CmdRecordToggle) {
     141           87 :         if (state_rt_ == State::Recording) {
     142           39 :             stop_recording_rt_and_play_rt();
     143           13 :         } else {
     144           48 :             start_recording_rt();
     145              :         }
     146           29 :     }
     147              : 
     148          132 :     if (cmds & CmdPlayToggle) {
     149           18 :         if (state_rt_ == State::Recording) {
     150            3 :             stop_recording_rt_and_play_rt();
     151           16 :         } else if (state_rt_ == State::Empty || state_rt_ == State::Idle ||
     152            6 :                    state_rt_ == State::Playing || state_rt_ == State::Overdubbing) {
     153           15 :             toggle_play_rt();
     154            5 :         }
     155            6 :     }
     156              : 
     157          132 :     if (cmds & CmdOverdubToggle) {
     158           21 :         if (state_rt_ != State::Recording) {
     159           18 :             toggle_overdub_rt();
     160            6 :         }
     161            7 :     }
     162          118 : }
     163              : 
     164          264 : void Looper::process(float* buffer, int num_samples) {
     165          264 :     if (!enabled_) {
     166            3 :         apply_pending_commands();
     167            3 :         publish_ui_snapshot();
     168            3 :         return;
     169              :     }
     170          261 :     process_core(buffer, nullptr, num_samples, false);
     171           88 : }
     172              : 
     173           90 : void Looper::process_stereo(float* left, float* right, int num_samples) {
     174           90 :     if (!enabled_) {
     175            0 :         apply_pending_commands();
     176            0 :         publish_ui_snapshot();
     177            0 :         return;
     178              :     }
     179           90 :     process_core(left, right, num_samples, true);
     180           30 : }
     181              : 
     182          351 : void Looper::process_core(float* left, float* right, int num_samples, bool stereo) {
     183          351 :     apply_pending_commands();
     184              : 
     185          351 :     const float loop_level_target = clamp(params_[0].value, 0.0f, 1.0f);
     186          351 :     const float crossfade_target_ms = clamp(params_[1].value, 0.0f, 20.0f);
     187          351 :     crossfade_ms_smoothed_ += crossfade_alpha_ * (crossfade_target_ms - crossfade_ms_smoothed_);
     188          351 :     const int cap = max_samples_;
     189          351 :     if (cap <= 0) {
     190            0 :         publish_ui_snapshot();
     191            0 :         return;
     192              :     }
     193              : 
     194          351 :     if (state_rt_ == State::Recording) {
     195       282903 :         for (int i = 0; i < num_samples; ++i) {
     196       282855 :             if (record_pos_ >= cap) {
     197            3 :                 stop_recording_rt_and_play_rt();
     198            3 :                 break;
     199              :             }
     200       282852 :             buffer_l_[record_pos_] = left[i];
     201       282852 :             if (stereo && right) buffer_r_[record_pos_] = right[i];
     202       282852 :             ++record_pos_;
     203        94284 :         }
     204           17 :     }
     205              : 
     206          386 :     if (has_loop_rt_ && loop_length_ > 0 &&
     207          294 :         (state_rt_ == State::Playing || state_rt_ == State::Overdubbing)) {
     208          285 :         const int xf = crossfade_samples_rt(crossfade_ms_smoothed_);
     209        90417 :         for (int i = 0; i < num_samples; ++i) {
     210        90132 :             loop_level_smoothed_ += loop_level_alpha_ * (loop_level_target - loop_level_smoothed_);
     211        90132 :             const float loop_level = loop_level_smoothed_;
     212        90132 :             const int pos = playhead_;
     213        90132 :             float loop_l = buffer_l_[pos];
     214        90132 :             float loop_r = (stereo && right) ? buffer_r_[pos] : loop_l;
     215              : 
     216        90132 :             if (xf > 0 && pos >= loop_length_ - xf) {
     217         2163 :                 const int t = pos - (loop_length_ - xf);  // 0..xf-1
     218         2163 :                 const float w_end = static_cast<float>(xf - t) / static_cast<float>(xf);
     219         2163 :                 const float w_start = 1.0f - w_end;
     220         2163 :                 const int start_pos = t;
     221         2163 :                 loop_l = buffer_l_[pos] * w_end + buffer_l_[start_pos] * w_start;
     222         2163 :                 if (stereo && right) {
     223          720 :                     loop_r = buffer_r_[pos] * w_end + buffer_r_[start_pos] * w_start;
     224          240 :                 } else {
     225          962 :                     loop_r = loop_l;
     226              :                 }
     227          721 :             }
     228              : 
     229        90132 :             const float in_l = left[i];
     230        90132 :             const float in_r = (stereo && right) ? right[i] : in_l;
     231              : 
     232        90132 :             float out_l = in_l + loop_l * loop_level;
     233        90132 :             float out_r = in_r + loop_r * loop_level;
     234              : 
     235        90132 :             if (state_rt_ == State::Overdubbing) {
     236        24576 :                 buffer_l_[pos] = soft_clip(buffer_l_[pos] + in_l);
     237        24576 :                 if (stereo && right) {
     238        15360 :                     buffer_r_[pos] = soft_clip(buffer_r_[pos] + in_r);
     239         5120 :                 }
     240         8192 :             }
     241              : 
     242        90132 :             left[i] = soft_clip(out_l);
     243        90132 :             if (stereo && right) right[i] = soft_clip(out_r);
     244              : 
     245        90132 :             ++playhead_;
     246        90132 :             if (playhead_ >= loop_length_) playhead_ = 0;
     247        30044 :         }
     248          190 :     } else {
     249              :         // Keep smoothing responsive even when not actively mixing the loop.
     250           66 :         loop_level_smoothed_ += loop_level_alpha_ * (loop_level_target - loop_level_smoothed_);
     251              :     }
     252              : 
     253          351 :     publish_ui_snapshot();
     254          117 : }
     255              : 
     256           18 : std::ostream& operator<<(std::ostream& os, Looper::State s) {
     257           18 :     switch (s) {
     258            2 :         case Looper::State::Empty:
     259            3 :             return os << "Empty";
     260              : 
     261            2 :         case Looper::State::Idle:
     262            3 :             return os << "Idle";
     263              : 
     264            2 :         case Looper::State::Recording:
     265            3 :             return os << "Recording";
     266              : 
     267            2 :         case Looper::State::Playing:
     268            3 :             return os << "Playing";
     269              : 
     270            2 :         case Looper::State::Overdubbing:
     271            3 :             return os << "Overdubbing";
     272              : 
     273            2 :         default:
     274            3 :             return os << "Unknown";
     275              :     }
     276            6 : }
     277              : 
     278              : }  // namespace Amplitron
        

Generated by: LCOV version 2.0-1