Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
stn_decomposer.hpp
Go to the documentation of this file.
1#pragma once
2
28
30
31#include <algorithm>
32#include <cassert>
33#include <cmath>
34#include <cstddef>
35#include <vector>
36
37namespace pulp::signal {
38
39struct StnConfig {
40 int num_bins = 1025;
41 int time_median = 9;
42 int freq_median = 9;
46 float beta = 2.0f;
59 bool causal = false;
60};
61
63template <typename SampleType = float>
64struct StnMasksT {
65 std::vector<SampleType> sines;
66 std::vector<SampleType> transients;
67 std::vector<SampleType> noise;
68};
69
72
73template <typename SampleType = float>
75public:
76 static bool checked_retained_bytes(const StnConfig& config,
77 std::uint64_t target_max_bytes,
78 std::uint64_t& bytes) noexcept {
79 const auto bins = static_cast<std::uint64_t>(config.num_bins);
80 std::uint64_t history = 0;
81 if (!checked_capacity_product(static_cast<std::uint64_t>(config.time_median), bins,
82 UINT64_MAX, history))
83 return false;
84 CheckedRetainedByteCharge charge(target_max_bytes);
85 if (!charge.add<SampleType>(history)
86 || !charge.add<SampleType>(bins) || !charge.add<SampleType>(bins)
87 || !charge.add<SampleType>(bins) || !charge.add<SampleType>(bins)
88 || !charge.add<SampleType>(bins)
89 || !charge.add<SampleType>(static_cast<std::uint64_t>(config.time_median))
90 || !charge.add<SampleType>(static_cast<std::uint64_t>(config.freq_median)))
91 return false;
92 bytes = charge.total();
93 return true;
94 }
95
100 void prepare(const StnConfig& config) {
101 assert(config.num_bins > 0);
102 assert(config.time_median >= 1 && (config.time_median % 2) == 1);
103 assert(config.freq_median >= 1 && (config.freq_median % 2) == 1);
104 config_ = config;
105 const auto bins = static_cast<size_t>(config.num_bins);
106
107 history_.assign(static_cast<size_t>(config.time_median) * bins, SampleType{0});
108 history_pos_ = 0;
109 history_filled_ = 0;
110
111 masks_.sines.assign(bins, SampleType{0});
112 masks_.transients.assign(bins, SampleType{0});
113 masks_.noise.assign(bins, SampleType{1});
114
115 harmonic_.assign(bins, SampleType{0});
116 percussive_.assign(bins, SampleType{0});
117 time_window_.assign(static_cast<size_t>(config.time_median), SampleType{0});
118 freq_window_.assign(static_cast<size_t>(config.freq_median), SampleType{0});
119 }
120
121 int num_bins() const { return config_.num_bins; }
122
130 const StnMasksT<SampleType>& process(const SampleType* mag) {
131 const int n = config_.num_bins;
132 const int th = config_.time_median, fh = config_.freq_median;
133
134 // Store the newest magnitude frame in the ring.
135 SampleType* slot = history_.data() + static_cast<size_t>(history_pos_) * n;
136 std::copy(mag, mag + n, slot);
137 history_pos_ = (history_pos_ + 1) % th;
138 if (history_filled_ < th) ++history_filled_;
139
140 // Horizontal (time) median → harmonic estimate. The time-median is
141 // order-independent over the filled ring (trailing window), so its
142 // value is the same either way; what differs is which frame the masks
143 // describe: the ring center (lower bias, lags the input) or the newest
144 // frame (causal, aligned to a caller that just pushed it).
145 const int center = config_.causal
146 ? (history_pos_ - 1 + th) % th
147 : (history_pos_ + th / 2) % th;
148 const SampleType* center_mag = history_.data() + static_cast<size_t>(center) * n;
149 for (int k = 0; k < n; ++k) {
150 int count = 0;
151 for (int t = 0; t < history_filled_; ++t)
152 time_window_[static_cast<size_t>(count++)] =
153 history_[static_cast<size_t>(t) * n + k];
154 harmonic_[static_cast<size_t>(k)] = median(time_window_.data(), count);
155 }
156
157 // Vertical (frequency) median on the center frame → percussive.
158 for (int k = 0; k < n; ++k) {
159 int count = 0;
160 for (int j = k - fh / 2; j <= k + fh / 2; ++j) {
161 const int kk = std::clamp(j, 0, n - 1);
162 freq_window_[static_cast<size_t>(count++)] = center_mag[kk];
163 }
164 percussive_[static_cast<size_t>(k)] = median(freq_window_.data(), count);
165 }
166
167 // Soft masks from the two estimates (Fitzgerald saturating split).
168 const SampleType beta = static_cast<SampleType>(config_.beta);
169 for (int k = 0; k < n; ++k) {
170 const SampleType h = harmonic_[static_cast<size_t>(k)];
171 const SampleType pc = percussive_[static_cast<size_t>(k)];
172 const SampleType total = h + pc + static_cast<SampleType>(1e-12);
173 // Sine confidence rises when the time-median dominates; transient
174 // when the freq-median dominates; the residue is noise.
175 const SampleType s_raw = saturate(h, beta * pc);
176 const SampleType t_raw = saturate(pc, beta * h);
177 const SampleType s = s_raw;
178 const SampleType t = t_raw * (SampleType{1} - s);
179 const SampleType nse = std::max(SampleType{0}, SampleType{1} - s - t);
180 (void)total;
181 masks_.sines[static_cast<size_t>(k)] = s;
182 masks_.transients[static_cast<size_t>(k)] = t;
183 masks_.noise[static_cast<size_t>(k)] = nse;
184 }
185 return masks_;
186 }
187
190 const SampleType* center_magnitude() const {
191 const int th = config_.time_median;
192 const int center = config_.causal
193 ? (history_pos_ - 1 + th) % th
194 : (history_pos_ + th / 2) % th;
195 return history_.data() + static_cast<size_t>(center) * config_.num_bins;
196 }
197
198 int latency_frames() const {
199 return config_.causal ? 0 : (config_.time_median - 1) / 2;
200 }
201
202 void reset() {
203 std::fill(history_.begin(), history_.end(), SampleType{0});
204 history_pos_ = 0;
205 history_filled_ = 0;
206 }
207
208private:
209 // 1 when a >= b, 0 when a <= b/beta-equivalent, smooth in between —
210 // Fitzgerald's sin^2 ramp on the ratio gives a soft Wiener-like mask.
211 static SampleType saturate(SampleType a, SampleType b) {
212 const SampleType denom = a + b + static_cast<SampleType>(1e-12);
213 const SampleType r = a / denom; // 0.5 at the crossover
214 // Map r in [0,1] through a smoothstep centered at 0.5.
215 const SampleType x = std::clamp((r - static_cast<SampleType>(0.25))
216 / static_cast<SampleType>(0.5),
217 SampleType{0}, SampleType{1});
218 return x * x * (static_cast<SampleType>(3) - static_cast<SampleType>(2) * x);
219 }
220
221 static SampleType median(SampleType* v, int count) {
222 const int mid = count / 2;
223 std::nth_element(v, v + mid, v + count);
224 return v[mid];
225 }
226
227 StnConfig config_;
228 std::vector<SampleType> history_; // time_median * num_bins ring
229 int history_pos_ = 0;
230 int history_filled_ = 0;
231
232 StnMasksT<SampleType> masks_;
233 std::vector<SampleType> harmonic_;
234 std::vector<SampleType> percussive_;
235 std::vector<SampleType> time_window_;
236 std::vector<SampleType> freq_window_;
237};
238
241
242} // namespace pulp::signal
Definition checked_allocation.hpp:57
bool add(std::uint64_t elements) noexcept
Definition checked_allocation.hpp:63
std::uint64_t total() const noexcept
Definition checked_allocation.hpp:83
Definition stn_decomposer.hpp:74
int latency_frames() const
Definition stn_decomposer.hpp:198
const SampleType * center_magnitude() const
The center-frame magnitude the current masks apply to (delayed by (time_median-1)/2 frames vs the new...
Definition stn_decomposer.hpp:190
int num_bins() const
Definition stn_decomposer.hpp:121
const StnMasksT< SampleType > & process(const SampleType *mag)
Push one magnitude frame and compute its STN masks.
Definition stn_decomposer.hpp:130
void reset()
Definition stn_decomposer.hpp:202
void prepare(const StnConfig &config)
RT contract: prepare() allocates history, scratch, and mask storage and is not audio-thread safe.
Definition stn_decomposer.hpp:100
static bool checked_retained_bytes(const StnConfig &config, std::uint64_t target_max_bytes, std::uint64_t &bytes) noexcept
Definition stn_decomposer.hpp:76
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
Definition additive_bank.hpp:218
@ center
Definition multi_channel_meter.hpp:68
StnDecomposerT< double > StnDecomposer64
Definition stn_decomposer.hpp:240
bool checked_capacity_product(std::uint64_t lhs, std::uint64_t rhs, std::uint64_t maximum, std::uint64_t &product) noexcept
Definition checked_allocation.hpp:12
@ a
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
StnDecomposerT< float > StnDecomposer
Definition stn_decomposer.hpp:239
StnMasksT< double > StnMasks64
Definition stn_decomposer.hpp:71
StnMasksT< float > StnMasks
Definition stn_decomposer.hpp:70
@ v
Definition input_events.hpp:41
Definition stn_decomposer.hpp:39
float beta
Soft-mask separation factor (Fitzgerald β): the bin is assigned to a class when that class's filtered...
Definition stn_decomposer.hpp:46
int freq_median
odd; bins for the vertical (transient) median
Definition stn_decomposer.hpp:42
bool causal
Align the masks to the NEWEST pushed frame instead of the ring's center frame.
Definition stn_decomposer.hpp:59
int num_bins
fft_size/2 + 1
Definition stn_decomposer.hpp:40
int time_median
odd; frames for the horizontal (tonal) median
Definition stn_decomposer.hpp:41
Per-frame soft masks. Each spans num_bins and sums to ~1 per bin.
Definition stn_decomposer.hpp:64
std::vector< float > noise
Definition stn_decomposer.hpp:67
std::vector< float > transients
Definition stn_decomposer.hpp:66
std::vector< float > sines
Definition stn_decomposer.hpp:65