DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
StudioVocoder.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
76#include "../../Core/DspMath.h"
77#include "../../Core/FFT.h"
78
79#include <algorithm>
80#include <array>
81#include <atomic>
82#include <cmath>
83#include <cstdint>
84#include <memory>
85#include <numbers>
86#include <vector>
87
88namespace dspark {
89namespace detail {
90
97template <FloatType T>
99{
100public:
101 static_assert(std::atomic<double>::is_always_lock_free,
102 "StudioVocoder requires lock-free std::atomic<double>");
103 static_assert(std::atomic<unsigned>::is_always_lock_free,
104 "StudioVocoder requires lock-free std::atomic<unsigned>");
105 static_assert(std::atomic<bool>::is_always_lock_free,
106 "StudioVocoder requires lock-free std::atomic<bool>");
107
109 struct Params
110 {
111 double targetSemitones = 0.0;
112 bool transientPreserve = true;
113 bool formantPreserve = false;
114 };
115
116 // -- Lifecycle (setup thread) ---------------------------------------------
117
129 bool prepare(double sampleRate, int numChannels, int fftSize, bool resampleCompensation)
130 {
131 if (!(sampleRate > 0.0) || !std::isfinite(sampleRate) || numChannels < 1
132 || (fftSize & (fftSize - 1)) != 0 || fftSize < 256 || fftSize > (1 << 20))
133 return false;
134
135 prepared_ = false;
136 sampleRate_ = sampleRate;
137 numChannels_ = numChannels;
138 N_ = fftSize;
139 bins_ = N_ / 2 + 1;
140 Rs_ = N_ / 4;
141 resample_ = resampleCompensation;
142
143 // Onset detector: a ~21 ms frame and an eighth-frame hop at every rate.
144 No_ = 256;
145 while (No_ < 8192 && static_cast<double>(No_) * 1.5 < 0.0213 * sampleRate_) No_ <<= 1;
146 hopO_ = No_ / 8;
147 // The detector confirms an onset one hop after its frame and locates
148 // it at most No + 3 hops behind the newest input; the analysis stays
149 // this far behind so that no frame has seen a strike before it is
150 // planned for.
151 D_ = No_ + No_ / 2;
152 delta_ = std::min(16, std::max(1, N_ / 64));
153
154 int ring = 1;
155 while (ring < 2 * N_ + 2 * D_ + No_) ring <<= 1;
156 ringSize_ = ring;
157 ringMask_ = ring - 1;
158 accumSize_ = 4 * N_;
159 accumMask_ = accumSize_ - 1;
160
161 fft_ = std::make_unique<FFTReal<T>>(static_cast<size_t>(N_));
162 fftO_ = std::make_unique<FFTReal<T>>(static_cast<size_t>(No_));
163
164 window_.resize(static_cast<size_t>(N_));
165 for (int k = 0; k < N_; ++k)
166 window_[static_cast<size_t>(k)] = static_cast<T>(std::sqrt(
167 0.5 - 0.5 * std::cos(2.0 * std::numbers::pi * k / N_)));
168 windowO_.resize(static_cast<size_t>(No_));
169 for (int k = 0; k < No_; ++k)
170 windowO_[static_cast<size_t>(k)] = static_cast<T>(
171 0.5 - 0.5 * std::cos(2.0 * std::numbers::pi * k / No_));
172
173 const auto nb = static_cast<size_t>(bins_);
174 const auto spec = static_cast<size_t>(N_ + 2);
175 ring_.assign(static_cast<size_t>(numChannels_), std::vector<T>(static_cast<size_t>(ringSize_), T(0)));
176 accum_.assign(static_cast<size_t>(numChannels_), std::vector<T>(static_cast<size_t>(accumSize_), T(0)));
177 spec_.assign(static_cast<size_t>(numChannels_), std::vector<T>(spec, T(0)));
178 prevSpec_.assign(static_cast<size_t>(numChannels_), std::vector<T>(spec, T(0)));
179 specD_.resize(spec);
180 frame_.resize(static_cast<size_t>(N_));
181 mag_.resize(nb);
182 prevMag_.resize(nb);
183 crossRe_.resize(nb); crossIm_.resize(nb);
184 dRe_.resize(nb); dIm_.resize(nb);
185 theta_.resize(nb);
186 tinc_.resize(nb);
187 omPrev_.resize(nb);
188 done_.resize(nb);
189 rotRe_.resize(nb); rotIm_.resize(nb);
190 gain_.resize(nb);
191 heapKey_.resize(2 * nb + 2);
192 heapCode_.resize(2 * nb + 2);
193 cepsTime_.resize(static_cast<size_t>(N_));
194 cepsSpec_.resize(spec);
195 envLog_.resize(nb);
196
197 // Onset filterbank: quarter-tone triangles, 27.5 Hz to 16 kHz.
198 buildOnsetBank();
199 frameO_.resize(static_cast<size_t>(No_));
200 specO_.resize(static_cast<size_t>(No_ + 2));
201 powO_.resize(static_cast<size_t>(No_ / 2 + 1));
202 bandCur_.assign(static_cast<size_t>(numBands_), T(0));
203 bandPrevMax_.assign(static_cast<size_t>(numBands_), T(0));
204 bandPrev_.assign(static_cast<size_t>(numBands_), T(0));
205
206 prepared_ = true;
207 return true;
208 }
209
212 void reset() noexcept
213 {
214 if (!prepared_) return;
215 for (auto& r : ring_) std::fill(r.begin(), r.end(), T(0));
216 for (auto& a : accum_) std::fill(a.begin(), a.end(), T(0));
217 for (auto& s : prevSpec_) std::fill(s.begin(), s.end(), T(0));
218 std::fill(prevMag_.begin(), prevMag_.end(), T(0));
219 std::fill(theta_.begin(), theta_.end(), 0.0);
220 for (int k = 0; k < bins_; ++k)
221 omPrev_[static_cast<size_t>(k)] = kTwoPi * k / N_;
222
223 adoptParamsIfDirty();
224 stActive_ = std::clamp(targetSemitones_, -12.0, 12.0);
225 ratio_ = std::exp2(stActive_ / 12.0);
226
227 inCount_ = 0;
228 frameStart_ = static_cast<int64_t>(accumSize_);
229 firstFrame_ = true;
230 // Frame 0 covers input [Rs - N, Rs): it becomes ready after Rs samples
231 // plus the lookahead, like the first hop of a classic vocoder.
232 aNext_ = static_cast<double>(Rs_) - 0.5 * N_;
233 aNom_ = aNext_;
234 aCur_ = aNext_;
235 cCur_ = static_cast<double>(frameStart_) + 0.5 * N_ - Rs_;
236 origin_ = aNext_;
237 originStream_ = static_cast<double>(frameStart_) + 0.5 * N_;
238 originRatio_ = ratio_;
239 prevAnalysisStart_ = 0;
240 steer_ = false;
241
242 lockHead_ = lockCount_ = 0;
243 rejoinE_ = 0.0; rejoinC_ = 0.0; rejoinJ_ = 1.0;
244 lastLockS_ = -1e18; lastLockT_ = -1e18;
245
246 // Detector.
247 nextFrameO_ = 0;
248 std::fill(bandPrev_.begin(), bandPrev_.end(), T(0));
249 std::fill(bandPrevMax_.begin(), bandPrevMax_.end(), T(0));
250 fluxFill_ = 0; fluxPos_ = 0;
251 fluxPrev_ = 0.0; fluxPrev2_ = 0.0; candPrev_ = false; candFrame_ = -1;
252 lastOnsetFrame_ = -1000;
253 onsetRing_.fill(-1e18);
254 onsetPos_ = 0;
255 }
256
257 // -- Parameters (control thread) ------------------------------------------
258
260 void publishParams(const Params& p) noexcept
261 {
262 seq_.fetch_add(1, std::memory_order_acq_rel);
263 std::atomic_thread_fence(std::memory_order_release);
264 stgSemitones_.store(p.targetSemitones, std::memory_order_relaxed);
265 stgTransient_.store(p.transientPreserve, std::memory_order_relaxed);
266 stgFormant_.store(p.formantPreserve, std::memory_order_relaxed);
267 seq_.fetch_add(1, std::memory_order_release);
268 dirty_.store(true, std::memory_order_release);
269 }
270
271 // -- Streaming (stream owner) -----------------------------------------------
272
275 [[nodiscard]] int samplesToNextHop() const noexcept
276 {
277 const int64_t need = analysisStart() + N_ + D_ - inCount_;
278 return static_cast<int>(std::max<int64_t>(0, need));
279 }
280
283 void pushInput(int ch, const T* src, int count) noexcept
284 {
285 auto& r = ring_[static_cast<size_t>(ch)];
286 int64_t p = inCount_;
287 for (int k = 0; k < count; ++k, ++p)
288 r[static_cast<size_t>(p & ringMask_)] = src[k];
289 }
290
297 void commitInput(int count, int numActiveChannels) noexcept
298 {
299 inCount_ += count;
300 runDetector(numActiveChannels);
301 if (inCount_ >= analysisStart() + N_ + D_)
302 processFrame(numActiveChannels);
303 }
304
307 void steerTimeline(double nominalCentre) noexcept
308 {
309 steerValue_ = nominalCentre;
310 steer_ = true;
311 }
312
323 void setAnchorLeadLimit(double samples) noexcept { leadLimit_ = std::max(0.0, samples); }
324
326 [[nodiscard]] double activeRatio() const noexcept { return ratio_; }
327
329 [[nodiscard]] double nextNominalCentre() const noexcept { return aNom_; }
330
332 [[nodiscard]] double nextStreamCentre() const noexcept
333 {
334 return static_cast<double>(frameStart_) + 0.5 * N_;
335 }
336
338 [[nodiscard]] double streamPositionOf(double x) const noexcept
339 {
340 return originStream_ + originRatio_ * (x - origin_);
341 }
342
343 [[nodiscard]] const T* olaData(int ch) const noexcept { return accum_[static_cast<size_t>(ch)].data(); }
344 [[nodiscard]] int64_t olaMask() const noexcept { return static_cast<int64_t>(accumMask_); }
345 [[nodiscard]] int olaSize() const noexcept { return accumSize_; }
347 [[nodiscard]] int64_t writeHead() const noexcept { return frameStart_; }
348 [[nodiscard]] int fftSize() const noexcept { return prepared_ ? N_ : 0; }
349 [[nodiscard]] int synthHop() const noexcept { return Rs_; }
351 [[nodiscard]] int lookahead() const noexcept { return D_; }
353 [[nodiscard]] int64_t inputCount() const noexcept { return inCount_; }
354
355private:
356 static constexpr double kTwoPi = 2.0 * std::numbers::pi;
357 static constexpr int kSeqlockMaxAttempts = 3;
358 static constexpr int kMaxLocks = 32;
361 static constexpr double kHeapFloorDb = 100.0;
364 static constexpr T kRiseRatio = static_cast<T>(1.4142135623730951);
366 static constexpr double kResetCover = 0.375;
368 static constexpr double kFluxDelta = 0.025;
369 static constexpr int kFluxWindow = 12;
370
371 struct Lock { double s, target, h; };
372
373 [[nodiscard]] static double princArg(double x) noexcept
374 {
375 return x - kTwoPi * std::round(x / kTwoPi);
376 }
377
378 [[nodiscard]] int64_t analysisStart() const noexcept
379 {
380 return static_cast<int64_t>(std::llround(aNext_)) - N_ / 2;
381 }
382
387 void adoptParamsIfDirty() noexcept
388 {
389 if (!dirty_.exchange(false, std::memory_order_acquire)) return;
390 for (int attempt = 0; attempt < kSeqlockMaxAttempts; ++attempt)
391 {
392 const unsigned s0 = seq_.load(std::memory_order_acquire);
393 if ((s0 & 1u) != 0u) continue; // writer mid-publish: do not copy
394 const double st = stgSemitones_.load(std::memory_order_relaxed);
395 const bool tr = stgTransient_.load(std::memory_order_relaxed);
396 const bool fo = stgFormant_.load(std::memory_order_relaxed);
397 // Orders the copy above before the re-read below and pairs with
398 // the writer's release fence ([atomics.fences]/2).
399 std::atomic_thread_fence(std::memory_order_acquire);
400 if (s0 == seq_.load(std::memory_order_relaxed))
401 {
402 targetSemitones_ = st; transientOn_ = tr; formantOn_ = fo;
403 return;
404 }
405 }
406 dirty_.store(true, std::memory_order_release);
407 }
408
409 // -- Frame ------------------------------------------------------------------
410
411 void analyse(int ch, int64_t start, T* out) noexcept
412 {
413 const auto& r = ring_[static_cast<size_t>(ch)];
414 for (int k = 0; k < N_; ++k)
415 frame_[static_cast<size_t>(k)] = r[static_cast<size_t>((start + k) & ringMask_)]
416 * window_[static_cast<size_t>(k)];
417 fft_->forward(frame_.data(), out);
418 }
419
420 void processFrame(int nCh) noexcept
421 {
422 nCh = std::clamp(nCh, 1, numChannels_);
423 const int64_t A = analysisStart();
424 const double Ra = firstFrame_ ? static_cast<double>(Rs_)
425 : static_cast<double>(A - prevAnalysisStart_);
426 const double binW = kTwoPi / N_;
427 const double Rs = static_cast<double>(Rs_);
428
429 // --- analysis: magnitudes, time and delta cross-spectra ----------------
430 std::fill(mag_.begin(), mag_.end(), T(0));
431 std::fill(crossRe_.begin(), crossRe_.end(), 0.0);
432 std::fill(crossIm_.begin(), crossIm_.end(), 0.0);
433 std::fill(dRe_.begin(), dRe_.end(), 0.0);
434 std::fill(dIm_.begin(), dIm_.end(), 0.0);
435 for (int ch = 0; ch < nCh; ++ch)
436 {
437 T* X = spec_[static_cast<size_t>(ch)].data();
438 const T* P = prevSpec_[static_cast<size_t>(ch)].data();
439 analyse(ch, A, X);
440 analyse(ch, A + delta_, specD_.data());
441 for (int k = 0; k < bins_; ++k)
442 {
443 const double xr = X[2 * k], xi = X[2 * k + 1];
444 const double pr = P[2 * k], pi = P[2 * k + 1];
445 const double dr = specD_[static_cast<size_t>(2 * k)];
446 const double di = specD_[static_cast<size_t>(2 * k + 1)];
447 mag_[static_cast<size_t>(k)] += static_cast<T>(xr * xr + xi * xi);
448 crossRe_[static_cast<size_t>(k)] += xr * pr + xi * pi;
449 crossIm_[static_cast<size_t>(k)] += xi * pr - xr * pi;
450 dRe_[static_cast<size_t>(k)] += dr * xr + di * xi;
451 dIm_[static_cast<size_t>(k)] += di * xr - dr * xi;
452 }
453 }
454 T maxMag = T(0);
455 for (int k = 0; k < bins_; ++k)
456 {
457 auto& m = mag_[static_cast<size_t>(k)];
458 m = std::sqrt(m);
459 maxMag = std::max(maxMag, m);
460 }
461
462 // --- time-direction candidates -------------------------------------------
463 const double dl = static_cast<double>(delta_);
464 for (int k = 0; k < bins_; ++k)
465 {
466 const double wk = binW * k;
467 const double omNow = wk + princArg(std::atan2(dIm_[static_cast<size_t>(k)],
468 dRe_[static_cast<size_t>(k)]) - wk * dl) / dl;
469 double inc = 0.0;
470 if (!firstFrame_)
471 {
472 const double dphi = std::atan2(crossIm_[static_cast<size_t>(k)],
473 crossRe_[static_cast<size_t>(k)]);
474 const double prior = 0.5 * (omNow + omPrev_[static_cast<size_t>(k)]);
475 const double om = Ra >= 1.0 ? prior + princArg(dphi - prior * Ra) / Ra : prior;
476 inc = princArg(theta_[static_cast<size_t>(k)] + Rs * om - dphi);
477 }
478 tinc_[static_cast<size_t>(k)] = inc;
479 omPrev_[static_cast<size_t>(k)] = omNow;
480 }
481
482 // --- heap-ordered propagation ----------------------------------------------
483 if (firstFrame_)
484 std::fill(theta_.begin(), theta_.end(), 0.0);
485 else
486 propagate(maxMag);
487
488 // Inside a strike's lock the map has slope 1, so the frame is placed
489 // exactly where a plain delayed copy of the input would put it. The
490 // bins the strike is rising into take no rotation at all: the heap
491 // hands them one rotation, rigid but not zero, and a constant phase
492 // turn of an impulse is cos(phi) times the impulse plus sin(phi)
493 // times its Hilbert transform, whose 1/t tail reaches before the
494 // strike - measured as pre-echo 30 dB down and up to 15 ms long at
495 // ratio 1.25. Zero rotation copies the strike itself; the bins that
496 // are not rising keep their propagated phases, so a partial that
497 // sustains through the strike stays continuous. Two cases are left
498 // to the heap. A lock narrower than the window leaves frames outside
499 // it that still see the strike, and a zero-rotated copy makes their
500 // misplaced copies coherent too (at 960 strikes per minute, 4096
501 // frame and ratio 0.8 the median strike moved from 0.08 to 7.9 ms).
502 // And a strike with another one within a frame of it would share
503 // frames with it, and the copy would carry that one at this lock's
504 // offset rather than at its own place.
505 if (transientOn_ && !firstFrame_ && lockCount_ > 0)
506 {
507 const Lock& L = locks_[static_cast<size_t>(lockHead_)];
508 const double c = static_cast<double>(frameStart_) + 0.5 * N_;
509 bool alone = L.h >= kResetCover * N_;
510 for (const double o : onsetRing_)
511 if (std::abs(o - L.s) > 1.0 && std::abs(o - L.s) < static_cast<double>(N_))
512 alone = false;
513 if (alone && c >= L.target - L.h && c <= L.target + L.h)
514 for (int k = 0; k < bins_; ++k)
515 if (mag_[static_cast<size_t>(k)] > kRiseRatio * prevMag_[static_cast<size_t>(k)])
516 theta_[static_cast<size_t>(k)] = 0.0;
517 }
518 theta_[0] = 0.0;
519 theta_[static_cast<size_t>(bins_ - 1)] = 0.0;
520 for (int k = 0; k < bins_; ++k)
521 {
522 rotRe_[static_cast<size_t>(k)] = static_cast<T>(std::cos(theta_[static_cast<size_t>(k)]));
523 rotIm_[static_cast<size_t>(k)] = static_cast<T>(std::sin(theta_[static_cast<size_t>(k)]));
524 }
525
526 // --- magnitude post-stages for resampling owners --------------------------
527 const bool formant = resample_ && formantOn_ && std::abs(ratio_ - 1.0) > 1e-6;
528 const bool taper = resample_ && ratio_ > 1.0;
529 if (formant) computeFormantGains();
530 else std::fill(gain_.begin(), gain_.end(), T(1));
531 if (taper)
532 {
533 const int cut = static_cast<int>(static_cast<double>(N_ / 2) / ratio_);
534 const int taperStart = std::max(1, cut - 4);
535 for (int k = taperStart; k < bins_; ++k)
536 gain_[static_cast<size_t>(k)] *= (k <= cut)
537 ? static_cast<T>(cut - k + 1) / static_cast<T>(cut - taperStart + 1) : T(0);
538 }
539
540 // --- synthesis ---------------------------------------------------------------
541 const T norm = static_cast<T>(Rs / (0.5 * N_));
542 for (int ch = 0; ch < nCh; ++ch)
543 {
544 T* X = spec_[static_cast<size_t>(ch)].data();
545 std::copy(X, X + N_ + 2, prevSpec_[static_cast<size_t>(ch)].data());
546 for (int k = 0; k < bins_; ++k)
547 {
548 const T re = X[2 * k], im = X[2 * k + 1];
549 const T rr = rotRe_[static_cast<size_t>(k)] * gain_[static_cast<size_t>(k)];
550 const T ri = rotIm_[static_cast<size_t>(k)] * gain_[static_cast<size_t>(k)];
551 X[2 * k] = re * rr - im * ri;
552 X[2 * k + 1] = re * ri + im * rr;
553 }
554 X[1] = T(0);
555 X[N_ + 1] = T(0);
556 fft_->inverse(X, frame_.data());
557 auto& acc = accum_[static_cast<size_t>(ch)];
558 for (int k = N_ - Rs_; k < N_; ++k)
559 acc[static_cast<size_t>((frameStart_ + k) & accumMask_)] = T(0);
560 for (int k = 0; k < N_; ++k)
561 acc[static_cast<size_t>((frameStart_ + k) & accumMask_)]
562 += frame_[static_cast<size_t>(k)] * window_[static_cast<size_t>(k)] * norm;
563 }
564 for (int ch = nCh; ch < numChannels_; ++ch)
565 std::fill(prevSpec_[static_cast<size_t>(ch)].begin(),
566 prevSpec_[static_cast<size_t>(ch)].end(), T(0));
567 std::copy(mag_.begin(), mag_.end(), prevMag_.begin());
568
569 prevAnalysisStart_ = A;
570 firstFrame_ = false;
571 aCur_ = aNext_;
572 cCur_ = static_cast<double>(frameStart_) + 0.5 * N_;
573 frameStart_ += Rs_;
574 planNext();
575 }
576
582 void propagate(T maxMag) noexcept
583 {
584 const T tol = maxMag * static_cast<T>(std::pow(10.0, -kHeapFloorDb / 20.0));
585 int todo = 0;
586 for (int k = 0; k < bins_; ++k)
587 {
588 const bool quiet = mag_[static_cast<size_t>(k)] < tol;
589 done_[static_cast<size_t>(k)] = quiet ? 1 : 0;
590 theta_[static_cast<size_t>(k)] = tinc_[static_cast<size_t>(k)];
591 if (!quiet) ++todo;
592 }
593 heapSize_ = 0;
594 for (int k = 0; k < bins_; ++k)
595 if (prevMag_[static_cast<size_t>(k)] > tol)
596 heapPush(prevMag_[static_cast<size_t>(k)], 2 * k);
597
598 while (todo > 0)
599 {
600 if (heapSize_ == 0)
601 {
602 int best = -1; T bm = T(-1);
603 for (int k = 0; k < bins_; ++k)
604 if (!done_[static_cast<size_t>(k)] && mag_[static_cast<size_t>(k)] > bm)
605 { bm = mag_[static_cast<size_t>(k)]; best = k; }
606 done_[static_cast<size_t>(best)] = 1; --todo;
607 heapPush(mag_[static_cast<size_t>(best)], 2 * best + 1);
608 continue;
609 }
610 const int code = heapPop();
611 const int k = code >> 1;
612 if ((code & 1) == 0)
613 {
614 if (!done_[static_cast<size_t>(k)])
615 {
616 done_[static_cast<size_t>(k)] = 1; --todo; // theta = tinc already
617 heapPush(mag_[static_cast<size_t>(k)], 2 * k + 1);
618 }
619 }
620 else
621 {
622 const double th = theta_[static_cast<size_t>(k)];
623 if (k + 1 < bins_ && !done_[static_cast<size_t>(k + 1)])
624 {
625 done_[static_cast<size_t>(k + 1)] = 1; --todo;
626 theta_[static_cast<size_t>(k + 1)] = th;
627 heapPush(mag_[static_cast<size_t>(k + 1)], 2 * (k + 1) + 1);
628 }
629 if (k > 0 && !done_[static_cast<size_t>(k - 1)])
630 {
631 done_[static_cast<size_t>(k - 1)] = 1; --todo;
632 theta_[static_cast<size_t>(k - 1)] = th;
633 heapPush(mag_[static_cast<size_t>(k - 1)], 2 * (k - 1) + 1);
634 }
635 }
636 }
637 }
638
639 void heapPush(T key, int code) noexcept
640 {
641 int i = heapSize_++;
642 while (i > 0)
643 {
644 const int p = (i - 1) >> 1;
645 if (heapKey_[static_cast<size_t>(p)] >= key) break;
646 heapKey_[static_cast<size_t>(i)] = heapKey_[static_cast<size_t>(p)];
647 heapCode_[static_cast<size_t>(i)] = heapCode_[static_cast<size_t>(p)];
648 i = p;
649 }
650 heapKey_[static_cast<size_t>(i)] = key;
651 heapCode_[static_cast<size_t>(i)] = code;
652 }
653
654 int heapPop() noexcept
655 {
656 const int top = heapCode_[0];
657 const T key = heapKey_[static_cast<size_t>(heapSize_ - 1)];
658 const int code = heapCode_[static_cast<size_t>(heapSize_ - 1)];
659 --heapSize_;
660 int i = 0;
661 for (;;)
662 {
663 int c = 2 * i + 1;
664 if (c >= heapSize_) break;
665 if (c + 1 < heapSize_ && heapKey_[static_cast<size_t>(c + 1)] > heapKey_[static_cast<size_t>(c)]) ++c;
666 if (heapKey_[static_cast<size_t>(c)] <= key) break;
667 heapKey_[static_cast<size_t>(i)] = heapKey_[static_cast<size_t>(c)];
668 heapCode_[static_cast<size_t>(i)] = heapCode_[static_cast<size_t>(c)];
669 i = c;
670 }
671 if (heapSize_ > 0)
672 {
673 heapKey_[static_cast<size_t>(i)] = key;
674 heapCode_[static_cast<size_t>(i)] = code;
675 }
676 return top;
677 }
678
681 void computeFormantGains() noexcept
682 {
683 for (int k = 0; k < bins_; ++k)
684 cepsTime_[static_cast<size_t>(k)] = std::log(mag_[static_cast<size_t>(k)] + T(1e-9));
685 for (int k = bins_; k < N_; ++k)
686 cepsTime_[static_cast<size_t>(k)] = cepsTime_[static_cast<size_t>(N_ - k)];
687 fft_->forward(cepsTime_.data(), cepsSpec_.data());
688 const int keep = std::min(std::max(8, static_cast<int>(0.001 * sampleRate_)), bins_ - 1);
689 for (int k = keep + 1; k < bins_; ++k)
690 {
691 cepsSpec_[static_cast<size_t>(2 * k)] = T(0);
692 cepsSpec_[static_cast<size_t>(2 * k + 1)] = T(0);
693 }
694 fft_->inverse(cepsSpec_.data(), cepsTime_.data());
695 for (int k = 0; k < bins_; ++k)
696 envLog_[static_cast<size_t>(k)] = cepsTime_[static_cast<size_t>(k)];
697 for (int k = 0; k < bins_; ++k)
698 {
699 const double pos = std::min(static_cast<double>(k) * ratio_, static_cast<double>(bins_ - 1));
700 const auto i0 = static_cast<int>(pos);
701 const auto fr = static_cast<T>(pos - i0);
702 const int i1 = std::min(i0 + 1, bins_ - 1);
703 const T target = envLog_[static_cast<size_t>(i0)]
704 + (envLog_[static_cast<size_t>(i1)] - envLog_[static_cast<size_t>(i0)]) * fr;
705 gain_[static_cast<size_t>(k)] = std::exp(std::clamp(
706 target - envLog_[static_cast<size_t>(k)], T(-4.6), T(4.6)));
707 }
708 }
709
710 // -- Time map -------------------------------------------------------------
711
713 void planNext() noexcept
714 {
715 adoptParamsIfDirty();
716 const double stTarget = std::clamp(targetSemitones_, -12.0, 12.0);
717 stActive_ += std::clamp(stTarget - stActive_, -0.5, 0.5);
718 ratio_ = std::exp2(stActive_ / 12.0);
719 const double r = ratio_;
720
721 const double c = static_cast<double>(frameStart_) + 0.5 * N_;
722 if (steer_) { aNom_ = steerValue_; steer_ = false; }
723 else aNom_ += static_cast<double>(Rs_) / r;
724
725 // Retire locks the stream has passed; the map then rejoins the
726 // nominal line from where the lock left it.
727 while (lockCount_ > 0)
728 {
729 const Lock& L = locks_[static_cast<size_t>(lockHead_)];
730 if (c < L.target + L.h) break;
731 rejoinC_ = L.target + L.h;
732 rejoinE_ = (L.s + L.h) - (aNom_ + (rejoinC_ - c) / r);
733 rejoinJ_ = std::max(static_cast<double>(N_), 2.0 * L.h * std::abs(r - 1.0));
734 lockHead_ = (lockHead_ + 1) % kMaxLocks;
735 --lockCount_;
736 }
737
738 double a;
739 if (lockCount_ > 0)
740 {
741 const Lock& L = locks_[static_cast<size_t>(lockHead_)];
742 if (c >= L.target - L.h)
743 a = L.s + (c - L.target);
744 else
745 {
746 const double span = (L.target - L.h) - cCur_;
747 const double f = span > 0.0 ? (c - cCur_) / span : 1.0;
748 a = aCur_ + ((L.s - L.h) - aCur_) * std::clamp(f, 0.0, 1.0);
749 }
750 }
751 else
752 {
753 const double f = std::clamp(1.0 - (c - rejoinC_) / rejoinJ_, 0.0, 1.0);
754 a = aNom_ + rejoinE_ * f;
755 }
756 aNext_ = std::max(a, aCur_);
757 }
758
760 void addOnset(double s) noexcept
761 {
762 onsetRing_[static_cast<size_t>(onsetPos_)] = s;
763 onsetPos_ = (onsetPos_ + 1) % kOnsetRing;
764 if (!transientOn_ || lockCount_ >= kMaxLocks) return;
765 const double r = ratio_;
766 const double cNext = static_cast<double>(frameStart_) + 0.5 * N_;
767 const double target = cNext + (s - aNom_) * r;
768 double h = 0.5 * N_;
769 h = std::min(h, s - aCur_ - 1.0); // the map never runs backwards
770 h = std::min(h, target - cNext); // the lock starts at or after the next frame
771 if (r < 1.0) h = std::min(h, leadLimit_ / (1.0 / r - 1.0));
772 else if (r > 1.0) h = std::min(h, leadLimit_ / (1.0 - 1.0 / r));
773 double prevS = lastLockS_, prevT = lastLockT_, prevH = 0.0;
774 if (lockCount_ > 0)
775 {
776 const Lock& P = locks_[static_cast<size_t>((lockHead_ + lockCount_ - 1) % kMaxLocks)];
777 prevS = P.s; prevT = P.target; prevH = P.h;
778 }
779 h = std::min(h, (s - prevS) - prevH - static_cast<double>(Rs_));
780 h = std::min(h, (target - prevT) - prevH - static_cast<double>(Rs_));
781 if (!(h >= 0.5 * Rs_)) return;
782 locks_[static_cast<size_t>((lockHead_ + lockCount_) % kMaxLocks)] = Lock { s, target, h };
783 ++lockCount_;
784 lastLockS_ = s; lastLockT_ = target;
785 // Re-plan the next frame now that it may be on the approach.
786 const double keepNom = aNom_;
787 replanNextFrame(keepNom);
788 }
789
790 void replanNextFrame(double nom) noexcept
791 {
792 const double c = static_cast<double>(frameStart_) + 0.5 * N_;
793 const Lock& L = locks_[static_cast<size_t>(lockHead_)];
794 double a;
795 if (c >= L.target - L.h)
796 a = L.s + (c - L.target);
797 else
798 {
799 const double span = (L.target - L.h) - cCur_;
800 const double f = span > 0.0 ? (c - cCur_) / span : 1.0;
801 a = aCur_ + ((L.s - L.h) - aCur_) * std::clamp(f, 0.0, 1.0);
802 }
803 (void) nom;
804 // Never ask for input the ring no longer holds or the frame already
805 // passed; never run backwards.
806 aNext_ = std::max(a, aCur_);
807 }
808
809 // -- Onset detector -----------------------------------------------------------
810
811 void buildOnsetBank()
812 {
813 fbStart_.clear(); fbCount_.clear(); fbOffset_.clear(); fbW_.clear();
814 const double binHz = sampleRate_ / No_;
815 const int nb = No_ / 2 + 1;
816 const double fMax = std::min(16000.0, 0.5 * sampleRate_ * 0.999);
817 std::vector<int> centres;
818 for (int i = 0; ; ++i)
819 {
820 const double f = 27.5 * std::pow(2.0, i / 24.0);
821 if (f > fMax) break;
822 const int b = std::clamp(static_cast<int>(std::lround(f / binHz)), 0, nb - 1);
823 if (centres.empty() || b > centres.back()) centres.push_back(b);
824 }
825 for (size_t j = 1; j + 1 < centres.size(); ++j)
826 {
827 const int lo = centres[j - 1], ce = centres[j], hi = centres[j + 1];
828 fbStart_.push_back(lo);
829 fbOffset_.push_back(static_cast<int>(fbW_.size()));
830 for (int k = lo; k <= hi; ++k)
831 fbW_.push_back(static_cast<T>(k <= ce ? double(k - lo) / (ce - lo)
832 : double(hi - k) / (hi - ce)));
833 fbCount_.push_back(hi - lo + 1);
834 }
835 numBands_ = static_cast<int>(fbStart_.size());
836 odfScale_ = static_cast<T>(2048.0 / No_);
837 }
838
840 void runDetector(int nCh) noexcept
841 {
842 nCh = std::clamp(nCh, 1, numChannels_);
843 while (nextFrameO_ * hopO_ + No_ <= inCount_)
844 {
845 const int64_t start = nextFrameO_ * hopO_;
846 std::fill(powO_.begin(), powO_.end(), T(0));
847 for (int ch = 0; ch < nCh; ++ch)
848 {
849 const auto& r = ring_[static_cast<size_t>(ch)];
850 for (int k = 0; k < No_; ++k)
851 frameO_[static_cast<size_t>(k)] = r[static_cast<size_t>((start + k) & ringMask_)]
852 * windowO_[static_cast<size_t>(k)];
853 fftO_->forward(frameO_.data(), specO_.data());
854 for (int k = 0; k <= No_ / 2; ++k)
855 {
856 const T re = specO_[static_cast<size_t>(2 * k)], im = specO_[static_cast<size_t>(2 * k + 1)];
857 powO_[static_cast<size_t>(k)] += re * re + im * im;
858 }
859 }
860 const T inv = T(1) / static_cast<T>(nCh);
861 double flux = 0.0;
862 for (int b = 0; b < numBands_; ++b)
863 {
864 T acc = T(0);
865 const int s0 = fbStart_[static_cast<size_t>(b)], off = fbOffset_[static_cast<size_t>(b)];
866 for (int i = 0; i < fbCount_[static_cast<size_t>(b)]; ++i)
867 acc += std::sqrt(powO_[static_cast<size_t>(s0 + i)] * inv) * fbW_[static_cast<size_t>(off + i)];
868 const T v = std::log10(acc * odfScale_ + T(1));
869 bandCur_[static_cast<size_t>(b)] = v;
870 const double d = static_cast<double>(v - bandPrevMax_[static_cast<size_t>(b)]);
871 if (d > 0.0) flux += d;
872 }
873 flux /= std::max(1, numBands_);
874 for (int b = 0; b < numBands_; ++b)
875 {
876 T mx = bandCur_[static_cast<size_t>(b)];
877 if (b > 0) mx = std::max(mx, bandCur_[static_cast<size_t>(b - 1)]);
878 if (b + 1 < numBands_) mx = std::max(mx, bandCur_[static_cast<size_t>(b + 1)]);
879 bandPrevMax_[static_cast<size_t>(b)] = mx;
880 }
881 if (nextFrameO_ == 0) flux = 0.0; // no previous frame to rise from
882
883 // The previous frame is an onset if it rose past the median of the
884 // frames before it and is a local maximum of the flux.
885 if (candPrev_ && fluxPrev_ >= flux && candFrame_ - lastOnsetFrame_ > 4)
886 {
887 lastOnsetFrame_ = candFrame_;
888 const double s = locateOnset(candFrame_, nCh);
889 if (s >= 0.0) addOnset(s);
890 }
891
892 double med = 0.0;
893 if (fluxFill_ > 3)
894 {
895 const int n = std::min(fluxFill_, kFluxWindow);
896 for (int i = 0; i < n; ++i) medScratch_[static_cast<size_t>(i)] = fluxHist_[static_cast<size_t>(i)];
897 std::nth_element(medScratch_.begin(), medScratch_.begin() + n / 2, medScratch_.begin() + n);
898 med = medScratch_[static_cast<size_t>(n / 2)];
899 if ((n & 1) == 0)
900 {
901 const double lo = *std::max_element(medScratch_.begin(), medScratch_.begin() + n / 2);
902 med = 0.5 * (med + lo);
903 }
904 }
905 candPrev_ = (flux - med > kFluxDelta) && flux >= fluxPrev_;
906 candFrame_ = nextFrameO_;
907 fluxHist_[static_cast<size_t>(fluxPos_)] = flux;
908 fluxPos_ = (fluxPos_ + 1) % kFluxWindow;
909 ++fluxFill_;
910 fluxPrev_ = flux;
911 ++nextFrameO_;
912 }
913 }
914
917 [[nodiscard]] double locateOnset(int64_t i, int nCh) const noexcept
918 {
919 const int64_t lo = std::max<int64_t>({ 16, i * hopO_ - 2 * hopO_, inCount_ - ringSize_ + 64 });
920 const int64_t hi = std::min<int64_t>(i * hopO_ + (3 * No_) / 4 + hopO_, inCount_ - 16);
921 if (hi - lo < 8) return -1.0;
922 auto pw = [&](int64_t n) {
923 double e = 0.0;
924 for (int ch = 0; ch < nCh; ++ch)
925 {
926 const double v = ring_[static_cast<size_t>(ch)][static_cast<size_t>(n & ringMask_)];
927 e += v * v;
928 }
929 return e;
930 };
931 // Running 32-sample window centred on n: [n - 16, n + 16).
932 double run = 0.0;
933 for (int64_t n = lo - 16; n < lo + 16; ++n) run += pw(n);
934 double peak = -1.0; int64_t pk = lo;
935 // First pass: peak.
936 {
937 double rr = run;
938 for (int64_t n = lo; n < hi; ++n)
939 {
940 if (rr > peak) { peak = rr; pk = n; }
941 rr += pw(n + 16) - pw(n - 16);
942 }
943 }
944 if (peak <= 0.0) return -1.0;
945 // Walk back from the peak to the first sample at or below a quarter
946 // of it in amplitude (1/16 in power).
947 double rr = 0.0;
948 for (int64_t n = pk - 16; n < pk + 16; ++n) rr += pw(n);
949 int64_t j = pk;
950 while (j > lo && rr > peak / 16.0)
951 {
952 rr += pw(j - 17) - pw(j + 15);
953 --j;
954 }
955 return static_cast<double>(j);
956 }
957
958 // -- Members ------------------------------------------------------------------
959 double sampleRate_ = 48000.0;
960 int numChannels_ = 0;
961 bool prepared_ = false;
962 bool resample_ = false;
963 int N_ = 4096, bins_ = 2049, Rs_ = 1024;
964 int D_ = 1536, delta_ = 16;
965 int ringSize_ = 0, ringMask_ = 0, accumSize_ = 0, accumMask_ = 0;
966
967 std::unique_ptr<FFTReal<T>> fft_, fftO_;
968 std::vector<T> window_, windowO_;
969 std::vector<std::vector<T>> ring_, accum_, spec_, prevSpec_;
970 std::vector<T> specD_, frame_, mag_, prevMag_, rotRe_, rotIm_, gain_;
971 std::vector<double> crossRe_, crossIm_, dRe_, dIm_, theta_, tinc_, omPrev_;
972 std::vector<unsigned char> done_;
973 std::vector<T> heapKey_;
974 std::vector<int> heapCode_;
975 int heapSize_ = 0;
976 std::vector<T> cepsTime_, cepsSpec_, envLog_;
977
978 // Stream state.
979 int64_t inCount_ = 0;
980 int64_t frameStart_ = 0;
981 int64_t prevAnalysisStart_ = 0;
982 bool firstFrame_ = true;
983 double stActive_ = 0.0, ratio_ = 1.0;
984 double aNext_ = 0.0, aNom_ = 0.0, aCur_ = 0.0, cCur_ = 0.0;
985 double origin_ = 0.0, originStream_ = 0.0, originRatio_ = 1.0;
986 bool steer_ = false;
987 double steerValue_ = 0.0;
988 double leadLimit_ = 1e18;
989
990 // Time-map locks.
991 std::array<Lock, kMaxLocks> locks_ {};
992 int lockHead_ = 0, lockCount_ = 0;
993 double rejoinE_ = 0.0, rejoinC_ = 0.0, rejoinJ_ = 1.0;
994 double lastLockS_ = -1e18, lastLockT_ = -1e18;
995
996 // Onset detector.
997 int No_ = 1024, hopO_ = 128, numBands_ = 0;
998 T odfScale_ = T(2);
999 std::vector<int> fbStart_, fbCount_, fbOffset_;
1000 std::vector<T> fbW_, frameO_, specO_, powO_, bandCur_, bandPrev_, bandPrevMax_;
1001 std::array<double, kFluxWindow> fluxHist_ {};
1002 std::array<double, kFluxWindow> medScratch_ {};
1003 int fluxFill_ = 0, fluxPos_ = 0;
1004 int64_t nextFrameO_ = 0, candFrame_ = -1, lastOnsetFrame_ = -1000;
1005 static constexpr int kOnsetRing = 8;
1006 std::array<double, kOnsetRing> onsetRing_ {};
1007 int onsetPos_ = 0;
1008 double fluxPrev_ = 0.0, fluxPrev2_ = 0.0;
1009 bool candPrev_ = false;
1010
1011 // Parameters.
1012 double targetSemitones_ = 0.0;
1013 bool transientOn_ = true, formantOn_ = false;
1014 std::atomic<double> stgSemitones_ { 0.0 };
1015 std::atomic<bool> stgTransient_ { true };
1016 std::atomic<bool> stgFormant_ { false };
1017 std::atomic<unsigned> seq_ { 0 };
1018 std::atomic<bool> dirty_ { false };
1019};
1020
1021} // namespace detail
1022} // namespace dspark
Streaming phase-gradient vocoder with a strike-anchored time map.
int olaSize() const noexcept
double streamPositionOf(double x) const noexcept
void steerTimeline(double nominalCentre) noexcept
Owner-supplied nominal analysis centre for the NEXT frame (a resampling owner keeps its reader and th...
double activeRatio() const noexcept
const T * olaData(int ch) const noexcept
int lookahead() const noexcept
void pushInput(int ch, const T *src, int count) noexcept
Writes count (<= samplesToNextHop(), or any count when it is 0... see commitInput) samples of channel...
void reset() noexcept
Clears all signal state and adopts the latest parameters (jump, no glide). Stream owner only.
bool prepare(double sampleRate, int numChannels, int fftSize, bool resampleCompensation)
Allocates every buffer the stream will use.
double nextNominalCentre() const noexcept
int synthHop() const noexcept
void commitInput(int count, int numActiveChannels) noexcept
Advances the input head by count and runs at most one frame.
void publishParams(const Params &p) noexcept
Publishes a whole parameter set (one control thread only).
int fftSize() const noexcept
int64_t writeHead() const noexcept
void setAnchorLeadLimit(double samples) noexcept
Caps how far a strike anchor may move the analysis ahead of the nominal timeline, in input samples (o...
int64_t inputCount() const noexcept
int64_t olaMask() const noexcept
double nextStreamCentre() const noexcept
int samplesToNextHop() const noexcept
Main namespace for the DSPark framework.
constexpr T pi
Pi (3.14159...) for the given floating-point type.
Definition DspMath.h:45
The control-published parameter set (adopted as ONE unit).
bool transientPreserve
Anchor the time map on detected strikes.
bool formantPreserve
Cepstral pre-warp (resample owners).
double targetSemitones
Stretch ratio as semitones, clamped +-12.