LCOV - code coverage report
Current view: top level - src/audio/effects/amp_cab - cabinet_sim.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 85.4 % 158 135
Test Date: 2026-06-07 15:51:50 Functions: 91.7 % 12 11

            Line data    Source code
       1              : #include "audio/effects/amp_cab/cabinet_sim.h"
       2              : 
       3              : #include <algorithm>
       4              : 
       5              : #include "audio/dsp/wav_loader.h"
       6              : #include "audio/effects/core/effect_factory.h"
       7              : 
       8              : namespace Amplitron {
       9              : 
      10            2 : static EffectRegistrar<CabinetSim> reg("Cabinet");
      11              : 
      12          152 : CabinetSim::CabinetSim() {
      13          266 :     params_ = {
      14          114 :         {"Type", 0.0f, 0.0f, 2.0f, 0.0f, "",
      15           38 :          "Speaker cabinet type. 0 = 1x12 (bright/focused), 1 = 2x12 (balanced), 2 = 4x12 (huge "
      16              :          "low-end)."},
      17           38 :         {"Bright", 0.5f, 0.0f, 1.0f, 0.5f, "",
      18           38 :          "Simulates microphone placement. Higher values add a high-frequency resonance peak for "
      19              :          "more cut."},
      20          228 :     };
      21              : 
      22              :     // Default LP at ~5kHz (speaker rolloff)
      23          114 :     lp_.b0 = 0.067455f;
      24          114 :     lp_.b1 = 0.134911f;
      25          114 :     lp_.b2 = 0.067455f;
      26          114 :     lp_.a1 = -1.14298f;
      27          114 :     lp_.a2 = 0.41280f;
      28              : 
      29              :     // Default HP at ~80Hz (low cut)
      30          114 :     hp_.b0 = 0.99262f;
      31          114 :     hp_.b1 = -1.98524f;
      32          114 :     hp_.b2 = 0.99262f;
      33          114 :     hp_.a1 = -1.98519f;
      34          114 :     hp_.a2 = 0.98529f;
      35              : 
      36              :     // Resonance peak ~2kHz
      37          114 :     peak_.b0 = 1.05f;
      38          114 :     peak_.b1 = -1.65f;
      39          114 :     peak_.b2 = 0.65f;
      40          114 :     peak_.a1 = -1.65f;
      41          114 :     peak_.a2 = 0.70f;
      42              : 
      43          152 :     bright_smooth_ = params_[1].value;
      44          114 :     const float sr = static_cast<float>(std::max(sample_rate_, 1));
      45          114 :     bright_alpha_ = 1.0f - std::exp(-1.0f / (sr * 0.01f));
      46          114 :     dry_buffer_.reserve(1024);
      47          266 : }
      48              : 
      49          152 : CabinetSim::~CabinetSim() {
      50              :     // Clean up any unconsumed pending kernel
      51          114 :     ConvolutionKernel* pending = pending_kernel_.exchange(nullptr);
      52          114 :     delete pending;
      53              : 
      54              :     // Clean up active kernel
      55          114 :     delete active_kernel_;
      56              : 
      57              :     // Clean up old garbage kernel
      58          114 :     const ConvolutionKernel* old = old_kernel_to_delete_.exchange(nullptr);
      59          114 :     delete old;
      60          152 : }
      61              : 
      62           66 : int CabinetSim::max_ir_samples() const {
      63              :     // 500ms at current sample rate
      64           66 :     return sample_rate_ / 2;
      65              : }
      66              : 
      67           66 : bool CabinetSim::load_ir(const std::string& filepath) {
      68           66 :     WavData wav = load_wav_file(filepath, sample_rate_, max_ir_samples());
      69           66 :     if (wav.samples.empty()) return false;
      70              : 
      71           51 :     raw_ir_samples_ = wav.samples;
      72           51 :     ir_path_ = filepath;
      73              : 
      74              :     // Extract filename from path
      75           51 :     size_t sep = filepath.find_last_of("/\\");
      76           83 :     ir_name_ = (sep != std::string::npos) ? filepath.substr(sep + 1) : filepath;
      77           68 :     ir_duration_ms_ =
      78           51 :         static_cast<float>(raw_ir_samples_.size()) / static_cast<float>(sample_rate_) * 1000.0f;
      79              : 
      80              :     // Build kernel with current expected block size, or a reasonable default
      81           51 :     int bs = expected_block_size_.load();
      82           51 :     if (bs <= 0) bs = 256;
      83           51 :     build_kernel(bs);
      84              : 
      85           34 :     return true;
      86           66 : }
      87              : 
      88            9 : void CabinetSim::clear_ir() {
      89            9 :     raw_ir_samples_.clear();
      90            9 :     ir_path_.clear();
      91            9 :     ir_name_.clear();
      92            9 :     ir_duration_ms_ = 0.0f;
      93              : 
      94            9 :     ConvolutionKernel* old = pending_kernel_.exchange(nullptr);
      95            9 :     delete old;
      96              : 
      97              :     // Safe clear via audio thread callback
      98            9 :     clear_pending_.store(true, std::memory_order_release);
      99            9 :     expected_block_size_.store(0, std::memory_order_release);
     100            9 :     pending_block_size_.store(0);
     101            9 : }
     102              : 
     103           99 : bool CabinetSim::has_ir() const {
     104              :     // Sweep and clean up any old kernels on the GUI thread (thread-safe, lock-free GC)
     105           66 :     const ConvolutionKernel* to_delete =
     106           99 :         old_kernel_to_delete_.exchange(nullptr, std::memory_order_acquire);
     107           99 :     delete to_delete;
     108              : 
     109           99 :     return !raw_ir_samples_.empty();
     110              : }
     111              : 
     112           51 : void CabinetSim::build_kernel(int block_size) {
     113           51 :     if (raw_ir_samples_.empty() || block_size <= 0) return;
     114              : 
     115              :     // Perform a GC sweep on the GUI thread here as well
     116           34 :     const ConvolutionKernel* to_delete =
     117           51 :         old_kernel_to_delete_.exchange(nullptr, std::memory_order_acquire);
     118           51 :     delete to_delete;
     119              : 
     120           51 :     auto* kernel = new ConvolutionKernel(raw_ir_samples_, block_size);
     121           51 :     kernel->source_path = ir_path_;
     122           51 :     kernel->source_name = ir_name_;
     123           51 :     kernel->duration_ms = ir_duration_ms_;
     124              : 
     125           51 :     expected_block_size_.store(block_size, std::memory_order_release);
     126              : 
     127           51 :     ConvolutionKernel* old = pending_kernel_.exchange(kernel);
     128           51 :     delete old;
     129           17 : }
     130              : 
     131         6039 : void CabinetSim::check_pending_kernel() {
     132              :     // 1. Process pending clear commands
     133         6039 :     if (clear_pending_.exchange(false, std::memory_order_acq_rel)) {
     134            0 :         const ConvolutionKernel* old = active_kernel_;
     135            0 :         active_kernel_ = nullptr;
     136            0 :         conv_engine_.set_kernel(nullptr);
     137            0 :         if (old) {
     138            0 :             const ConvolutionKernel* prev_old =
     139            0 :                 old_kernel_to_delete_.exchange(old, std::memory_order_release);
     140            0 :             if (prev_old) {
     141            0 :                 delete prev_old;
     142            0 :             }
     143            0 :         }
     144            0 :     }
     145              : 
     146              :     // 2. Process pending kernel updates
     147         6039 :     ConvolutionKernel* pending = pending_kernel_.exchange(nullptr, std::memory_order_acquire);
     148         6039 :     if (pending) {
     149           12 :         const ConvolutionKernel* old = active_kernel_;
     150           12 :         active_kernel_ = pending;
     151           12 :         conv_engine_.set_kernel(active_kernel_);
     152           12 :         expected_block_size_.store(pending->block_size(), std::memory_order_release);
     153           12 :         if (old) {
     154            0 :             const ConvolutionKernel* prev_old =
     155            0 :                 old_kernel_to_delete_.exchange(old, std::memory_order_release);
     156            0 :             if (prev_old) {
     157            0 :                 delete prev_old;
     158            0 :             }
     159            0 :         }
     160            4 :     }
     161              : 
     162              :     // Block size mismatch is handled via pending_kernel_ rebuild on the
     163              :     // GUI thread. The audio thread only consumes pre-built kernels.
     164         6039 :     pending_block_size_.store(0);
     165         6039 : }
     166              : 
     167           75 : void CabinetSim::set_sample_rate(int sample_rate) {
     168           75 :     Effect::set_sample_rate(sample_rate);
     169              : 
     170           75 :     const float sr = static_cast<float>(std::max(sample_rate_, 1));
     171           75 :     bright_alpha_ = 1.0f - std::exp(-1.0f / (sr * 0.01f));
     172              : 
     173              :     // Reload IR at new sample rate if one is loaded
     174           75 :     if (!ir_path_.empty()) {
     175            9 :         load_ir(ir_path_);
     176            3 :     }
     177           75 : }
     178              : 
     179         6042 : void CabinetSim::process(float* buffer, int num_samples) {
     180         6042 :     if (!enabled_) return;
     181              : 
     182         6039 :     const float mix = mix_.load(std::memory_order_relaxed);
     183              : 
     184              :     // If an IR is loaded, convolve for cabinet response.
     185         6039 :     check_pending_kernel();
     186         6039 :     if (conv_engine_.has_kernel()) {
     187              :         // Audio thread: single acquire load of the atomic block size.
     188              :         // raw_ir_samples_ is NOT read here (GUI-thread owned) — the
     189              :         // expected_block_size_ > 0 check is sufficient because clear_ir()
     190              :         // stores 0 before clearing the kernel, and no IR means size is 0.
     191         3009 :         int ebs = expected_block_size_.load(std::memory_order_acquire);
     192         3009 :         if (num_samples != ebs && num_samples > 0 && ebs > 0) {
     193            0 :             pending_block_size_.store(num_samples, std::memory_order_release);
     194            0 :         }
     195              : 
     196         3009 :         if (dry_buffer_.size() < static_cast<size_t>(num_samples)) {
     197           12 :             dry_buffer_.resize(num_samples);
     198            4 :         }
     199         3009 :         std::copy(buffer, buffer + num_samples, dry_buffer_.begin());
     200              : 
     201         3009 :         conv_engine_.process(buffer, num_samples);
     202              : 
     203         3009 :         if (mix < 1.0f) {
     204            0 :             apply_mix(dry_buffer_.data(), buffer, num_samples);
     205            0 :         }
     206         3009 :         return;
     207              :     }
     208              : 
     209         3030 :     const float bright_target = params_[1].value;
     210              : 
     211      1546734 :     for (int i = 0; i < num_samples; ++i) {
     212      1543704 :         bright_smooth_ += bright_alpha_ * (bright_target - bright_smooth_);
     213      1543704 :         float bright = bright_smooth_;
     214      1543704 :         float dry = buffer[i];
     215      1543704 :         float x = buffer[i];
     216              : 
     217      1543704 :         x = hp_.process(x);
     218      1543704 :         x = lp_.process(x);
     219              : 
     220              :         // Blend in resonance based on brightness
     221      1543704 :         float peaked = peak_.process(x);
     222      1543704 :         x = x * (1.0f - bright * 0.3f) + peaked * bright * 0.3f;
     223              : 
     224      1543704 :         buffer[i] = dry * (1.0f - mix) + x * mix;
     225       514568 :     }
     226         2014 : }
     227              : 
     228           39 : void CabinetSim::reset() {
     229           39 :     lp_.reset();
     230           39 :     hp_.reset();
     231           39 :     peak_.reset();
     232           39 :     conv_engine_.reset();
     233           39 :     bright_smooth_ = params_[1].value;
     234           39 :     if (dry_buffer_.capacity() < 1024) {
     235            0 :         dry_buffer_.reserve(1024);
     236            0 :     }
     237           39 : }
     238              : 
     239              : }  // namespace Amplitron
        

Generated by: LCOV version 2.0-1