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
|