Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
nlms_adaptive_filter.hpp
Go to the documentation of this file.
1#pragma once
2
13
14#include <algorithm>
15#include <array>
16#include <atomic>
17#include <cmath>
18#include <cstddef>
19#include <cstring>
20#include <limits>
21#include <vector>
22
23namespace pulp::signal {
24
25template <typename SampleType = float>
27public:
28 struct Result {
29 SampleType estimate{};
30 SampleType error{};
31 };
32
36 void prepare(double sample_rate, int max_taps) {
37 max_taps_ = std::clamp(max_taps, 1, kMaxTaps);
38 sample_rate_ = std::isfinite(sample_rate) && sample_rate > 0.0 ? sample_rate : 0.0;
39 history_.assign(static_cast<std::size_t>(max_taps_), 0.0);
40 weights_.assign(static_cast<std::size_t>(max_taps_), 0.0);
41 published_.assign(static_cast<std::size_t>(kPublishBuffers * max_taps_), 0.0);
42 active_taps_ = max_taps_;
43 write_pos_ = 0;
44 publish_index_.store(0, std::memory_order_relaxed);
45 reader_index_.store(-1, std::memory_order_relaxed);
46 for (auto& count : published_taps_)
47 count.store(0, std::memory_order_relaxed);
48 reset();
49 }
50
51 bool set_active_taps(int taps) noexcept {
52 if (taps < 1 || taps > max_taps_) return false;
53 active_taps_ = taps;
54 publish_current();
55 return true;
56 }
57
60 void set_step_size(SampleType mu) noexcept {
61 const double v = finite_or(static_cast<double>(mu), 0.5);
62 mu_ = std::clamp(v, kParameterFloor, 2.0 - kParameterFloor);
63 }
64
66 void set_denominator_floor(SampleType eps) noexcept {
67 const double v = finite_or(static_cast<double>(eps), 1.0e-8);
68 denominator_floor_ = std::max(v, kParameterFloor);
69 }
70
73 void set_leakage(SampleType leakage) noexcept {
74 const double v = finite_or(static_cast<double>(leakage), 0.0);
75 leakage_ = std::clamp(v, 0.0, 1.0 - kParameterFloor);
76 }
77
78 void set_adapt_enabled(bool enabled) noexcept { adapt_enabled_ = enabled; }
79
80 void reset() noexcept {
81 std::fill(history_.begin(), history_.end(), 0.0);
82 std::fill(weights_.begin(), weights_.end(), 0.0);
83 write_pos_ = 0;
84 // Publish through the same ownership protocol as processing. Clearing
85 // every slot here would let a control reader race a reset; an old
86 // stable snapshot is preferable to a torn one and is replaced by the
87 // next writer-owned slot atomically.
88 publish_current();
89 }
90
91 Result process_sample(SampleType primary, SampleType reference) noexcept {
92 if (max_taps_ == 0 || sample_rate_ <= 0.0 ||
93 !std::isfinite(static_cast<double>(primary)) ||
94 !std::isfinite(static_cast<double>(reference)))
95 return {};
96
97 const double primary_value = flush_denormal(static_cast<double>(primary));
98 const double reference_value = flush_denormal(static_cast<double>(reference));
99 history_[static_cast<std::size_t>(write_pos_)] = reference_value;
100 write_pos_ = (write_pos_ + 1) % max_taps_;
101 double estimate = 0.0;
102 double power = 0.0;
103 for (int i = 0; i < active_taps_; ++i) {
104 const double x = history_at(i);
105 estimate += weights_[static_cast<std::size_t>(i)] * x;
106 power += x * x;
107 }
108 const double error = primary_value - estimate;
109 if (adapt_enabled_) {
110 const double step = mu_ * error / std::max(denominator_floor_ + power,
111 std::numeric_limits<double>::min());
112 const double retain = 1.0 - leakage_;
113 for (int i = 0; i < active_taps_; ++i) {
114 double& w = weights_[static_cast<std::size_t>(i)];
115 // Apply leakage to the complete normalized update. Keeping
116 // the update outside `retain` makes the legal corner
117 // mu -> 2, leakage -> 1 an unstable gain of almost -2.
118 w = retain * (w + step * history_at(i));
119 if (!std::isfinite(w) || std::abs(w) <= kDenormalFloor) w = 0.0;
120 }
121 }
122 publish_current();
123 return {static_cast<SampleType>(estimate), static_cast<SampleType>(error)};
124 }
125
126 void process_block(const SampleType* primary, const SampleType* reference,
127 Result* out, int n) noexcept {
128 if (n <= 0 || primary == nullptr || reference == nullptr || out == nullptr) return;
129 for (int i = 0; i < n; ++i) out[i] = process_sample(primary[i], reference[i]);
130 }
131
132 bool try_snapshot_coefficients(SampleType* dest, int capacity,
133 int& active_taps_out) const noexcept {
134 if (dest == nullptr || capacity < 0) return false;
135 for (int attempt = 0; attempt < kSnapshotAttempts; ++attempt) {
136 const int index = publish_index_.load(std::memory_order_seq_cst);
137 if (index < 0 || index >= kPublishBuffers) continue;
138 // The reservation and validation participate in one global order
139 // with the writer's slot selection. Acquire/release is not enough:
140 // the writer may legally miss a new reservation while the reader
141 // still validates a stale publication index, then revisit and
142 // overwrite that slot during the copy.
143 reader_index_.store(index, std::memory_order_seq_cst);
144 if (publish_index_.load(std::memory_order_seq_cst) != index) {
145 reader_index_.store(-1, std::memory_order_seq_cst);
146 continue;
147 }
148 const int count = published_taps_[static_cast<std::size_t>(index)].load(
149 std::memory_order_acquire);
150 if (count < 0 || count > capacity) {
151 reader_index_.store(-1, std::memory_order_seq_cst);
152 return false;
153 }
154 const double* source = published_.data() + static_cast<std::size_t>(index * max_taps_);
156 reader_index_.store(-1, std::memory_order_seq_cst);
157 active_taps_out = count;
158 return true;
159 }
160 return false;
161 }
162
163 int active_taps() const noexcept { return active_taps_; }
164 int max_taps() const noexcept { return max_taps_; }
165 double sample_rate() const noexcept { return sample_rate_; }
166 int latency_samples() const noexcept { return 0; }
167 std::size_t retained_bytes() const noexcept {
168 // Three publication copies plus the live history and weights. The
169 // vectors themselves are handles; this reports their actual backing
170 // storage, which is double precision regardless of SampleType.
171 return static_cast<std::size_t>(kPublishBuffers + 2) *
172 static_cast<std::size_t>(max_taps_) * sizeof(double);
173 }
174
175private:
176 static constexpr int kMaxTaps = 8192;
177 static constexpr int kPublishBuffers = 3;
178 static constexpr int kSnapshotAttempts = 4;
179 static constexpr double kParameterFloor = 1.0e-12;
180 static constexpr double kDenormalFloor = 1.0e-18;
181
182 static double finite_or(double v, double fallback) noexcept {
183 return std::isfinite(v) ? v : fallback;
184 }
185
186 static double flush_denormal(double v) noexcept {
187 return std::abs(v) <= kDenormalFloor ? 0.0 : v;
188 }
189
190 double history_at(int delay) const noexcept {
191 int index = write_pos_ - 1 - delay;
192 if (index < 0) index += max_taps_ * ((-index / max_taps_) + 1);
193 return history_[static_cast<std::size_t>(index % max_taps_)];
194 }
195
196 void publish_current() noexcept {
197 if (max_taps_ == 0) return;
198 const int current = publish_index_.load(std::memory_order_seq_cst);
199 const int reader = reader_index_.load(std::memory_order_seq_cst);
200 int target = (current + 1) % kPublishBuffers;
201 if (target == reader) target = (target + 1) % kPublishBuffers;
202 double* destination = published_.data() + static_cast<std::size_t>(target * max_taps_);
203 std::memcpy(destination, weights_.data(),
204 static_cast<std::size_t>(active_taps_) * sizeof(double));
205 if (active_taps_ < max_taps_)
206 std::fill(destination + active_taps_, destination + max_taps_, 0.0);
207 published_taps_[static_cast<std::size_t>(target)].store(
208 active_taps_, std::memory_order_release);
209 publish_index_.store(target, std::memory_order_seq_cst);
210 }
211
212 double sample_rate_ = 0.0;
213 int max_taps_ = 0;
214 int active_taps_ = 0;
215 int write_pos_ = 0;
216 double mu_ = 0.5;
217 double denominator_floor_ = 1.0e-8;
218 double leakage_ = 0.0;
219 bool adapt_enabled_ = true;
220 std::vector<double> history_;
221 std::vector<double> weights_;
222 std::vector<double> published_;
223 mutable std::atomic<int> publish_index_{0};
224 mutable std::atomic<int> reader_index_{-1};
225 mutable std::array<std::atomic<int>, kPublishBuffers> published_taps_{};
226};
227
230
231} // namespace pulp::signal
Definition nlms_adaptive_filter.hpp:26
void prepare(double sample_rate, int max_taps)
Allocate fixed storage.
Definition nlms_adaptive_filter.hpp:36
void set_step_size(SampleType mu) noexcept
Clamp finite mu into the open NLMS stability interval (0, 2).
Definition nlms_adaptive_filter.hpp:60
void process_block(const SampleType *primary, const SampleType *reference, Result *out, int n) noexcept
Definition nlms_adaptive_filter.hpp:126
int max_taps() const noexcept
Definition nlms_adaptive_filter.hpp:164
void set_adapt_enabled(bool enabled) noexcept
Definition nlms_adaptive_filter.hpp:78
bool set_active_taps(int taps) noexcept
Definition nlms_adaptive_filter.hpp:51
Result process_sample(SampleType primary, SampleType reference) noexcept
Definition nlms_adaptive_filter.hpp:91
void set_denominator_floor(SampleType eps) noexcept
Clamp the normalization floor to a finite, positive lower bound.
Definition nlms_adaptive_filter.hpp:66
bool try_snapshot_coefficients(SampleType *dest, int capacity, int &active_taps_out) const noexcept
Definition nlms_adaptive_filter.hpp:132
int active_taps() const noexcept
Definition nlms_adaptive_filter.hpp:163
void reset() noexcept
Definition nlms_adaptive_filter.hpp:80
std::size_t retained_bytes() const noexcept
Definition nlms_adaptive_filter.hpp:167
double sample_rate() const noexcept
Definition nlms_adaptive_filter.hpp:165
int latency_samples() const noexcept
Definition nlms_adaptive_filter.hpp:166
void set_leakage(SampleType leakage) noexcept
Per-sample loss in [0, 1): the complete normalized update is retained by (1 - leakage),...
Definition nlms_adaptive_filter.hpp:73
Definition additive_bank.hpp:218
@ delay
Definition envelope.hpp:57
NlmsAdaptiveFilterT< float > NlmsAdaptiveFilter
Definition nlms_adaptive_filter.hpp:228
@ reference
Platform sine; the compatibility default.
Definition fast_math.hpp:40
@ i
Definition formant_filter_bank.hpp:20
NlmsAdaptiveFilterT< double > NlmsAdaptiveFilter64
Definition nlms_adaptive_filter.hpp:229
Definition nlms_adaptive_filter.hpp:28
SampleType estimate
Definition nlms_adaptive_filter.hpp:29
SampleType error
Definition nlms_adaptive_filter.hpp:30