LCOV - code coverage report
Current view: top level - src/audio/effects - looper.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 97.7 % 215 210
Test Date: 2026-06-01 11:15:25 Functions: 100.0 % 20 20

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

Generated by: LCOV version 2.0-1