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
|