LCOV - code coverage report
Current view: top level - src/audio/effects - cabinet_sim.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 85.2 % 142 121
Test Date: 2026-06-03 09:13:19 Functions: 91.7 % 12 11

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

Generated by: LCOV version 2.0-1