MayaFlux 0.3.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Spectral.cpp
Go to the documentation of this file.
1#include "Spectral.hpp"
2
3#include <Eigen/Dense>
4#include <unsupported/Eigen/FFT>
5
7
8namespace {
9
10 constexpr double k_pi = std::numbers::pi;
11 constexpr double k_tau = 2.0 * std::numbers::pi;
12
13 /**
14 * @brief Pre-computed Hann window, not normalised
15 * The WOLA loop divides by sum-of-squares so normalisation
16 * is applied analytically at reconstruction time.
17 */
18 [[nodiscard]] std::vector<double> make_hann(uint32_t n)
19 {
20 std::vector<double> w(n);
21 const double inv = 1.0 / static_cast<double>(n - 1);
22 for (uint32_t i = 0; i < n; ++i)
23 w[i] = 0.5 * (1.0 - std::cos(k_tau * i * inv));
24 return w;
25 }
26
27 /**
28 * @brief Wrap phase to (-pi, pi]
29 */
30 [[nodiscard]] inline double wrap_phase(double p) noexcept
31 {
32 p -= k_tau * std::floor((p + k_pi) / k_tau);
33 return p;
34 }
35
36 /**
37 * @brief Build a full N-point conjugate-symmetric spectrum from the
38 * one-sided representation (bins 0..bins-1, bins = N/2+1)
39 */
40 [[nodiscard]] Eigen::VectorXcd to_full_spectrum(
41 const std::vector<std::complex<double>>& onesided,
42 uint32_t N)
43 {
44 const uint32_t bins = N / 2 + 1;
45 Eigen::VectorXcd full(static_cast<Eigen::Index>(N));
46 for (uint32_t b = 0; b < bins; ++b)
47 full(static_cast<Eigen::Index>(b)) = onesided[b];
48 for (uint32_t b = bins; b < N; ++b)
49 full(static_cast<Eigen::Index>(b)) = std::conj(onesided[N - b]);
50 return full;
51 }
52
53} // namespace
54
55// ============================================================================
56// Core STFT engine
57// ============================================================================
58
59std::vector<double> apply_spectral(
60 std::span<const double> src,
61 uint32_t window_size,
62 uint32_t hop_size,
63 const SpectrumProcessor& processor)
64{
65 if (src.empty())
66 return {};
67
68 const uint32_t N = window_size;
69 const uint32_t bins = N / 2 + 1;
70 const std::vector<double> win = make_hann(N);
71
72 const size_t n_frames = (src.size() >= N)
73 ? (src.size() - N) / hop_size + 1
74 : 1;
75
76 std::vector<double> output(src.size(), 0.0);
77 std::vector<double> norm(src.size(), 0.0);
78
79 Eigen::FFT<double> fft;
80 Eigen::VectorXd frame(N);
81 Eigen::VectorXcd spectrum;
82 Eigen::VectorXd ifft_out;
83
84 for (size_t f = 0; f < n_frames; ++f) {
85 const size_t pos = f * hop_size;
86
87 for (uint32_t k = 0; k < N; ++k) {
88 const size_t idx = pos + k;
89 frame(static_cast<Eigen::Index>(k)) = (idx < src.size()) ? src[idx] * win[k] : 0.0;
90 }
91
92 fft.fwd(spectrum, frame);
93
94 std::vector<std::complex<double>> onesided(bins);
95 for (uint32_t b = 0; b < bins; ++b)
96 onesided[b] = spectrum(static_cast<Eigen::Index>(b));
97
98 processor(onesided, f);
99
100 Eigen::VectorXcd full = to_full_spectrum(onesided, N);
101 fft.inv(ifft_out, full);
102
103 const double inv_N = 1.0 / static_cast<double>(N);
104 for (uint32_t k = 0; k < N && pos + k < src.size(); ++k) {
105 const double w = win[k];
106 output[pos + k] += ifft_out(static_cast<Eigen::Index>(k)) * inv_N * w;
107 norm[pos + k] += w * w;
108 }
109 }
110
111 for (size_t i = 0; i < src.size(); ++i) {
112 if (norm[i] > 1e-10)
113 output[i] /= norm[i];
114 }
115
116 return output;
117}
118
119// ============================================================================
120// Spectral transforms
121// ============================================================================
122
123std::vector<double> spectral_filter(
124 std::span<const double> src,
125 double lo_hz,
126 double hi_hz,
127 double sample_rate,
128 uint32_t window_size,
129 uint32_t hop_size)
130{
131 const double bin_hz = sample_rate / static_cast<double>(window_size);
132
133 return apply_spectral(src, window_size, hop_size,
134 [lo_hz, hi_hz, bin_hz](std::vector<std::complex<double>>& spec, size_t) {
135 for (size_t b = 0; b < spec.size(); ++b) {
136 const double f = static_cast<double>(b) * bin_hz;
137 if (f < lo_hz || f > hi_hz)
138 spec[b] = {};
139 }
140 });
141}
142
143std::vector<double> spectral_invert(
144 std::span<const double> src,
145 uint32_t window_size,
146 uint32_t hop_size)
147{
148 return apply_spectral(src, window_size, hop_size,
149 [](std::vector<std::complex<double>>& spec, size_t) {
150 for (auto& bin : spec)
151 bin = std::conj(bin);
152 });
153}
154
155std::vector<double> harmonic_enhance(
156 std::span<const double> src,
157 double enhancement_factor,
158 uint32_t window_size,
159 uint32_t hop_size)
160{
161 const auto nyquist_bin = static_cast<double>((double)window_size / 2);
162
163 return apply_spectral(src, window_size, hop_size,
164 [enhancement_factor, nyquist_bin](std::vector<std::complex<double>>& spec, size_t) {
165 for (size_t b = 1; b < spec.size(); ++b) {
166 const double t = static_cast<double>(b) / nyquist_bin;
167 spec[b] *= 1.0 + (enhancement_factor - 1.0) * t;
168 }
169 });
170}
171
172std::vector<double> spectral_gate(
173 std::span<const double> src,
174 double threshold_db,
175 uint32_t window_size,
176 uint32_t hop_size)
177{
178 const double lin = std::pow(10.0, threshold_db / 20.0);
179
180 return apply_spectral(src, window_size, hop_size,
181 [lin](std::vector<std::complex<double>>& spec, size_t) {
182 for (auto& bin : spec) {
183 if (std::abs(bin) < lin)
184 bin = {};
185 }
186 });
187}
188
189// ============================================================================
190// Phase vocoder operations
191// ============================================================================
192
193std::vector<double> phase_vocoder_stretch(
194 std::span<const double> src,
195 double stretch_factor,
196 uint32_t window_size,
197 uint32_t analysis_hop)
198{
199 if (src.empty() || stretch_factor <= 0.0)
200 return {};
201
202 if (stretch_factor == 1.0)
203 return { src.begin(), src.end() };
204
205 const uint32_t N = window_size;
206 const uint32_t Ha = analysis_hop;
207 const auto Hs = static_cast<uint32_t>(std::round(Ha * stretch_factor));
208 const uint32_t bins = N / 2 + 1;
209
210 const std::vector<double> win = make_hann(N);
211
212 const double omega_factor = k_tau * static_cast<double>(Ha) / static_cast<double>(N);
213
214 const size_t n_frames = (src.size() >= N)
215 ? (src.size() - N) / Ha + 1
216 : 1;
217
218 const size_t out_len = static_cast<size_t>(static_cast<double>(src.size()) * stretch_factor) + N;
219
220 std::vector<double> output(out_len, 0.0);
221 std::vector<double> norm(out_len, 0.0);
222
223 std::vector<double> phase_accum(bins, 0.0);
224 std::vector<double> prev_phase(bins, 0.0);
225
226 Eigen::FFT<double> fft;
227 Eigen::VectorXd frame(N);
228 Eigen::VectorXcd spectrum;
229 Eigen::VectorXd ifft_out;
230
231 for (size_t f = 0; f < n_frames; ++f) {
232 const size_t src_pos = f * Ha;
233
234 for (uint32_t k = 0; k < N; ++k) {
235 const size_t idx = src_pos + k;
236 frame(static_cast<Eigen::Index>(k)) = (idx < src.size()) ? src[idx] * win[k] : 0.0;
237 }
238
239 fft.fwd(spectrum, frame);
240
241 std::vector<std::complex<double>> synth(bins);
242 for (uint32_t b = 0; b < bins; ++b) {
243 const std::complex<double> bin = spectrum(static_cast<Eigen::Index>(b));
244 const double mag = std::abs(bin);
245 const double phase = std::arg(bin);
246
247 const double delta = phase - prev_phase[b]
248 - omega_factor * static_cast<double>(b);
249 const double true_f = omega_factor * static_cast<double>(b)
250 + wrap_phase(delta);
251
252 phase_accum[b] += static_cast<double>(Hs) * true_f
253 / static_cast<double>(Ha);
254 prev_phase[b] = phase;
255
256 synth[b] = std::polar(mag, phase_accum[b]);
257 }
258
259 Eigen::VectorXcd full = to_full_spectrum(synth, N);
260 fft.inv(ifft_out, full);
261
262 const size_t out_pos = f * Hs;
263 const double inv_N = 1.0 / static_cast<double>(N);
264 for (uint32_t k = 0; k < N && out_pos + k < out_len; ++k) {
265 const double w = win[k];
266 output[out_pos + k] += ifft_out(static_cast<Eigen::Index>(k)) * inv_N * w;
267 norm[out_pos + k] += w * w;
268 }
269 }
270
271 for (size_t i = 0; i < out_len; ++i) {
272 if (norm[i] > 1e-10)
273 output[i] /= norm[i];
274 }
275
276 output.resize(
277 static_cast<size_t>(static_cast<double>(src.size()) * stretch_factor));
278
279 return output;
280}
281
282std::vector<double> pitch_shift(
283 std::span<const double> src,
284 double semitones,
285 uint32_t window_size,
286 uint32_t analysis_hop)
287{
288 if (src.empty() || semitones == 0.0)
289 return { src.begin(), src.end() };
290
291 const double pitch_ratio = std::pow(2.0, semitones / 12.0);
292 const double stretch_ratio = 1.0 / pitch_ratio;
293
294 std::vector<double> stretched = phase_vocoder_stretch(src, stretch_ratio, window_size, analysis_hop);
295
296 if (stretched.empty())
297 return {};
298
299 const size_t out_n = src.size();
300 std::vector<double> out(out_n);
301 const size_t in_n = stretched.size();
302 const double step = static_cast<double>(in_n - 1) / static_cast<double>(out_n - 1);
303
304 for (size_t i = 0; i < out_n; ++i) {
305 const double pos = static_cast<double>(i) * step;
306 const auto idx = static_cast<size_t>(pos);
307 const size_t idx1 = std::min(idx + 1, in_n - 1);
308 const double frac = pos - static_cast<double>(idx);
309 out[i] = stretched[idx] + frac * (stretched[idx1] - stretched[idx]);
310 }
311
312 return out;
313}
314
315} // namespace MayaFlux::Kinesis::Discrete
#define N(method_name, full_type_name)
Definition Creator.hpp:183
size_t b
Short-time Fourier domain primitives for MayaFlux::Kinesis.
std::vector< double > spectral_gate(std::span< const double > src, double threshold_db, uint32_t window_size, uint32_t hop_size)
Hard spectral gate: zero bins whose magnitude is below the threshold.
Definition Spectral.cpp:172
std::vector< double > apply_spectral(std::span< const double > src, uint32_t window_size, uint32_t hop_size, const SpectrumProcessor &processor)
Apply a per-frame spectrum processor via WOLA analysis-synthesis.
Definition Spectral.cpp:59
std::vector< double > spectral_filter(std::span< const double > src, double lo_hz, double hi_hz, double sample_rate, uint32_t window_size, uint32_t hop_size)
Hard bandpass filter: zero all bins outside [lo_hz, hi_hz].
Definition Spectral.cpp:123
std::vector< double > phase_vocoder_stretch(std::span< const double > src, double stretch_factor, uint32_t window_size, uint32_t analysis_hop)
Time-stretch via phase vocoder analysis-synthesis.
Definition Spectral.cpp:193
std::function< void(std::vector< std::complex< double > > &, size_t)> SpectrumProcessor
Callable type for per-frame spectrum modification Receives the one-sided complex spectrum (bins 0....
Definition Spectral.hpp:26
std::vector< double > harmonic_enhance(std::span< const double > src, double enhancement_factor, uint32_t window_size, uint32_t hop_size)
Linear spectral tilt: scale each bin by a factor that rises linearly from 1 at bin 0 to enhancement_f...
Definition Spectral.cpp:155
std::vector< double > spectral_invert(std::span< const double > src, uint32_t window_size, uint32_t hop_size)
Spectral phase inversion (conjugate all bins)
Definition Spectral.cpp:143
std::vector< double > pitch_shift(std::span< const double > src, double semitones, uint32_t window_size, uint32_t analysis_hop)
Pitch-shift by resampling around a phase vocoder stretch.
Definition Spectral.cpp:282