DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
PitchShifter.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
56#include "../Core/AudioBuffer.h"
57#include "../Core/AudioSpec.h"
58#include "../Core/DenormalGuard.h"
59#include "../Core/DspMath.h"
60#include "../Core/FFT.h"
61#include "../Core/StateBlob.h"
62#include "../Core/WindowFunctions.h"
63
64#include <algorithm>
65#include <atomic>
66#include <cmath>
67#include <cstddef>
68#include <cstdint>
69#include <memory>
70#include <numbers>
71#include <vector>
72
73namespace dspark {
74
81template <FloatType T>
83{
84public:
85 // -- Lifecycle -------------------------------------------------------------
86
100 void prepare(const AudioSpec& spec, int fftSize = 2048)
101 {
102 if (!spec.isValid() || (fftSize & (fftSize - 1)) != 0
103 || fftSize < 256 || fftSize > (1 << 20))
104 return;
105
106 prepared_.store(false, std::memory_order_relaxed);
107
108 sampleRate_ = spec.sampleRate;
109 numChannels_ = std::max(1, spec.numChannels);
110 fftSize_ = fftSize;
111 numBins_ = fftSize_ / 2 + 1;
112 synthHop_ = fftSize_ / 4;
113 ringMask_ = fftSize_ - 1;
114
115 accumSize_ = fftSize_ * 4; // power of two: frame + read margin
116 accumMask_ = accumSize_ - 1;
117
118 // The reader trails the write head by readOffset_; the window/OLA
119 // chain adds another fftSize - synthHop_, so the measured wet latency
120 // is readOffset_ + fftSize_ - synthHop_ = 2 * fftSize (exact at unity
121 // ratio). The dry path must delay by the SAME value or partial mixes
122 // comb-filter.
123 readOffset_ = fftSize_ + synthHop_;
124 latency_ = readOffset_ + fftSize_ - synthHop_;
125 drySize_ = 1;
126 while (drySize_ < latency_ + 1) drySize_ <<= 1;
127 dryMask_ = drySize_ - 1;
128
129 fft_ = std::make_unique<FFTReal<T>>(static_cast<size_t>(fftSize_));
130
131 window_.resize(static_cast<size_t>(fftSize_));
132 WindowFunctions<T>::hann(window_.data(), fftSize_, true);
133 for (auto& w : window_) w = std::sqrt(w); // sqrt-Hann analysis+synthesis
134
135 const int nCh = numChannels_;
136 inputRing_.assign(static_cast<size_t>(nCh), {});
137 dryRing_.assign(static_cast<size_t>(nCh), {});
138 accum_.assign(static_cast<size_t>(nCh), {});
139 for (int ch = 0; ch < nCh; ++ch)
140 {
141 inputRing_[static_cast<size_t>(ch)].assign(static_cast<size_t>(fftSize_), T(0));
142 dryRing_[static_cast<size_t>(ch)].assign(static_cast<size_t>(drySize_), T(0));
143 accum_[static_cast<size_t>(ch)].assign(static_cast<size_t>(accumSize_), T(0));
144 }
145
146 fftIn_.resize(static_cast<size_t>(fftSize_));
147 spec_.resize(static_cast<size_t>(fftSize_ + 2));
148 fftResult_.resize(static_cast<size_t>(fftSize_));
149 prevAnalysis_.resize(static_cast<size_t>(fftSize_ + 2));
150 prevSynth_.resize(static_cast<size_t>(fftSize_ + 2));
151
152 // Formant preservation scratch (cepstral envelope).
153 cepsTime_.resize(static_cast<size_t>(fftSize_));
154 cepsSpec_.resize(static_cast<size_t>(fftSize_ + 2));
155 envLog_.resize(static_cast<size_t>(numBins_));
156 formantGain_.resize(static_cast<size_t>(numBins_));
157 mag_.resize(static_cast<size_t>(numBins_));
158 prevMag_.resize(static_cast<size_t>(numBins_));
159 rotRe_.resize(static_cast<size_t>(numBins_));
160 rotIm_.resize(static_cast<size_t>(numBins_));
161 peakBin_.resize(static_cast<size_t>(numBins_ / 2 + 2));
162
163 prepared_.store(true, std::memory_order_relaxed);
164 reset();
165 }
166
168 void reset() noexcept
169 {
170 if (!prepared_.load(std::memory_order_relaxed)) return;
171 for (auto& r : inputRing_) std::fill(r.begin(), r.end(), T(0));
172 for (auto& r : dryRing_) std::fill(r.begin(), r.end(), T(0));
173 for (auto& a : accum_) std::fill(a.begin(), a.end(), T(0));
174 std::fill(prevAnalysis_.begin(), prevAnalysis_.end(), T(0));
175 std::fill(prevSynth_.begin(), prevSynth_.end(), T(0));
176 std::fill(prevMag_.begin(), prevMag_.end(), T(0));
177
178 inputPos_ = 0;
179 dryPos_ = 0;
180 writeHead_ = static_cast<int64_t>(accumSize_); // keep indices positive
181 readPosInt_ = writeHead_ - readOffset_;
182 readPosFrac_ = 0.0;
183
184 stActive_ = std::clamp(static_cast<double>(semitones_.load(std::memory_order_relaxed)),
185 -12.0, 12.0);
186 ratioActive_ = std::exp2(stActive_ / 12.0);
187 currentMix_ = mix_.load(std::memory_order_relaxed);
188 hopCarry_ = 0.0;
189 inputSinceHop_ = 0;
190 analysisHop_ = computeAnalysisHop();
191 onsetEnv_ = 0.0;
192 firstFrame_ = true;
193 }
194
195 // -- Parameters (thread-safe) -----------------------------------------------
196
204 void setSemitones(T st) noexcept
205 {
206 if (!std::isfinite(st)) return;
207 semitones_.store(std::clamp(st, T(-12), T(12)), std::memory_order_relaxed);
208 }
209
212 void setPitchRatio(T ratio) noexcept
213 {
214 if (!std::isfinite(ratio)) return;
215 ratio = std::clamp(ratio, T(0.5), T(2));
216 semitones_.store(static_cast<T>(12.0 * std::log2(static_cast<double>(ratio))),
217 std::memory_order_relaxed);
218 }
219
224 void setMix(T mix) noexcept
225 {
226 if (!std::isfinite(mix)) return;
227 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
228 }
229
231 void setTransientPreserve(bool enabled) noexcept
232 {
233 transientPreserve_.store(enabled, std::memory_order_relaxed);
234 }
235
245 void setFormantPreserve(bool enabled) noexcept
246 {
247 formantPreserve_.store(enabled, std::memory_order_relaxed);
248 }
249
251 [[nodiscard]] T getSemitones() const noexcept
252 {
253 return semitones_.load(std::memory_order_relaxed);
254 }
255
257 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
258
260 [[nodiscard]] bool getTransientPreserve() const noexcept
261 {
262 return transientPreserve_.load(std::memory_order_relaxed);
263 }
264
266 [[nodiscard]] bool getFormantPreserve() const noexcept
267 {
268 return formantPreserve_.load(std::memory_order_relaxed);
269 }
270
273 [[nodiscard]] int getLatency() const noexcept { return latency_; }
274
276 [[nodiscard]] std::vector<uint8_t> getState() const
277 {
278 StateWriter w(stateId("PSHF"), 1);
279 w.write("semitones", static_cast<float>(semitones_.load(std::memory_order_relaxed)));
280 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
281 w.write("transient", transientPreserve_.load(std::memory_order_relaxed));
282 w.write("formant", formantPreserve_.load(std::memory_order_relaxed));
283 return w.blob();
284 }
285
287 bool setState(const uint8_t* data, size_t size)
288 {
289 StateReader r(data, size);
290 if (!r.isValid() || r.processorId() != stateId("PSHF")) return false;
291 setSemitones(static_cast<T>(r.read("semitones", 0.0f)));
292 setMix(static_cast<T>(r.read("mix", 1.0f)));
293 setTransientPreserve(r.read("transient", true));
294 setFormantPreserve(r.read("formant", false));
295 return true;
296 }
297
298 // -- Processing --------------------------------------------------------------
299
308 void processBlock(AudioBufferView<T> buffer) noexcept
309 {
310 if (!prepared_.load(std::memory_order_relaxed)) return;
311 DenormalGuard guard;
312
313 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
314 const int nS = buffer.getNumSamples();
315
316 // Linear per-block mix ramp with exact landing (settled: step == 0 and
317 // the per-sample value reduces to the constant, bit-identically).
318 const T mixTarget = mix_.load(std::memory_order_relaxed);
319 const T mixStart = currentMix_;
320 const T mixStep = (nS > 0) ? (mixTarget - mixStart) / static_cast<T>(nS) : T(0);
321
322 int i = 0;
323 while (i < nS)
324 {
325 const int chunk = std::min(nS - i, analysisHop_ - inputSinceHop_);
326
327 // 1. Push input into the analysis ring and the dry-compensation ring.
328 for (int ch = 0; ch < nCh; ++ch)
329 {
330 const T* in = buffer.getChannel(ch) + i;
331 auto& ring = inputRing_[static_cast<size_t>(ch)];
332 auto& dry = dryRing_[static_cast<size_t>(ch)];
333 int wp = inputPos_;
334 int dp = dryPos_;
335 for (int k = 0; k < chunk; ++k)
336 {
337 ring[static_cast<size_t>(wp)] = in[k];
338 dry[static_cast<size_t>(dp)] = in[k];
339 wp = (wp + 1) & ringMask_;
340 dp = (dp + 1) & dryMask_;
341 }
342 }
343
344 // 2. Produce output: fractional read of the synthesis stream + mix.
345 for (int ch = 0; ch < nCh; ++ch)
346 {
347 T* out = buffer.getChannel(ch) + i;
348 const auto& acc = accum_[static_cast<size_t>(ch)];
349 const auto& dry = dryRing_[static_cast<size_t>(ch)];
350
351 int64_t rp = readPosInt_;
352 double rf = readPosFrac_;
353 int dp = dryPos_;
354
355 for (int k = 0; k < chunk; ++k)
356 {
357 const T wet = readCatmullRom(acc, rp, rf);
358 const int dryIdx = (dp - latency_) & dryMask_;
359 const T drySample = dry[static_cast<size_t>(dryIdx)];
360 const T mixVal = mixStart + mixStep * static_cast<T>(i + k);
361 // Two-product blend: exact at both ends (mix 1 emits the
362 // wet stream bit-exactly, mix 0 the delayed dry).
363 out[k] = drySample * (T(1) - mixVal) + wet * mixVal;
364
365 rf += ratioActive_;
366 const auto adv = static_cast<int64_t>(rf);
367 rp += adv;
368 rf -= static_cast<double>(adv);
369 dp = (dp + 1) & dryMask_;
370 }
371 }
372
373 // Commit shared positions once per chunk.
374 {
375 double rf = readPosFrac_ + ratioActive_ * chunk;
376 const auto adv = static_cast<int64_t>(rf);
377 readPosInt_ += adv;
378 readPosFrac_ = rf - static_cast<double>(adv);
379 inputPos_ = (inputPos_ + chunk) & ringMask_;
380 dryPos_ = (dryPos_ + chunk) & dryMask_;
381 }
382
383 // 3. Run an STFT hop at the analysis boundary.
384 inputSinceHop_ += chunk;
385 if (inputSinceHop_ >= analysisHop_)
386 {
387 inputSinceHop_ = 0;
388 processHop(nCh);
389 }
390
391 i += chunk;
392 }
393
394 currentMix_ = mixTarget; // exact landing
395 }
396
397private:
398 static constexpr double kTwoPi = 2.0 * std::numbers::pi;
399
401 [[nodiscard]] static double princArg(double x) noexcept
402 {
403 return x - kTwoPi * std::round(x / kTwoPi);
404 }
405
407 [[nodiscard]] int computeAnalysisHop() noexcept
408 {
409 const double ideal = static_cast<double>(synthHop_) / ratioActive_ + hopCarry_;
410 int hop = static_cast<int>(ideal);
411 hop = std::clamp(hop, 1, fftSize_);
412 hopCarry_ = ideal - static_cast<double>(hop);
413 return hop;
414 }
415
417 [[nodiscard]] T readCatmullRom(const std::vector<T>& acc, int64_t ip, double frac) const noexcept
418 {
419 const auto m = static_cast<int64_t>(accumMask_);
420 const T x0 = acc[static_cast<size_t>((ip - 1) & m)];
421 const T x1 = acc[static_cast<size_t>(ip & m)];
422 const T x2 = acc[static_cast<size_t>((ip + 1) & m)];
423 const T x3 = acc[static_cast<size_t>((ip + 2) & m)];
424 const T f = static_cast<T>(frac);
425 return x1 + T(0.5) * f * (x2 - x0
426 + f * (T(2) * x0 - T(5) * x1 + T(4) * x2 - x3
427 + f * (T(3) * (x1 - x2) + x3 - x0)));
428 }
429
431 void processHop(int nCh) noexcept
432 {
433 // --- glide the active ratio toward the target (max 0.5 st per hop) ----
434 const double stTarget = std::clamp(
435 static_cast<double>(semitones_.load(std::memory_order_relaxed)), -12.0, 12.0);
436 stActive_ += std::clamp(stTarget - stActive_, -0.5, 0.5);
437 ratioActive_ = std::exp2(stActive_ / 12.0);
438
439 // --- reference-channel analysis ---------------------------------------
440 analyzeChannel(0);
441
442 const bool transient = detectTransient();
443 buildRotations(transient);
444
445 // Snapshot the reference analysis spectrum for the next frame's
446 // heterodyned phase increments (after decisions, before modification).
447 std::copy(spec_.begin(), spec_.end(), prevAnalysis_.begin());
448 std::copy(mag_.begin(), mag_.end(), prevMag_.begin());
449
450 // --- synthesis: reference channel first, then the rest -----------------
451 synthesizeChannel(0, true);
452 for (int ch = 1; ch < nCh; ++ch)
453 {
454 analyzeChannel(ch);
455 synthesizeChannel(ch, false);
456 }
457
458 analysisHop_ = computeAnalysisHop();
459 writeHead_ += synthHop_;
460 firstFrame_ = false;
461 }
462
464 void analyzeChannel(int ch) noexcept
465 {
466 const auto& ring = inputRing_[static_cast<size_t>(ch)];
467 const int readPos = inputPos_; // oldest sample (ring size == fftSize)
468 for (int k = 0; k < fftSize_; ++k)
469 {
470 const int idx = (readPos + k) & ringMask_;
471 fftIn_[static_cast<size_t>(k)] = ring[static_cast<size_t>(idx)]
472 * window_[static_cast<size_t>(k)];
473 }
474 fft_->forward(fftIn_.data(), spec_.data());
475
476 if (ch == 0)
477 {
478 for (int k = 0; k < numBins_; ++k)
479 {
480 const T re = spec_[static_cast<size_t>(2 * k)];
481 const T im = spec_[static_cast<size_t>(2 * k + 1)];
482 mag_[static_cast<size_t>(k)] = std::sqrt(re * re + im * im);
483 }
484 }
485 }
486
488 [[nodiscard]] bool detectTransient() noexcept
489 {
490 double energy = 0.0;
491 for (int k = 0; k < numBins_; ++k)
492 {
493 const double m = static_cast<double>(mag_[static_cast<size_t>(k)]);
494 energy += m * m;
495 }
496 const double prevEnv = onsetEnv_;
497 onsetEnv_ = std::max(energy, onsetEnv_ * 0.7);
498 if (!transientPreserve_.load(std::memory_order_relaxed))
499 return firstFrame_;
500 return firstFrame_ || (energy > 4.0 * prevEnv && energy > 1e-12);
501 }
502
510 void buildRotations(bool transient) noexcept
511 {
512 if (transient)
513 {
514 std::fill(rotRe_.begin(), rotRe_.end(), T(1));
515 std::fill(rotIm_.begin(), rotIm_.end(), T(0));
516 return;
517 }
518
519 // --- find spectral peaks (local maxima over +-2 bins, above floor) -----
520 T maxMag = T(0);
521 for (int k = 0; k < numBins_; ++k)
522 maxMag = std::max(maxMag, mag_[static_cast<size_t>(k)]);
523
524 numPeaks_ = 0;
525 if (maxMag > T(1e-9))
526 {
527 const T floorMag = maxMag * T(1e-4); // -80 dB relative floor
528 const int last = numBins_ - 2;
529 for (int k = 2; k <= last; ++k)
530 {
531 const T m = mag_[static_cast<size_t>(k)];
532 if (m < floorMag) continue;
533 if (m > mag_[static_cast<size_t>(k - 1)] && m >= mag_[static_cast<size_t>(k + 1)]
534 && m > mag_[static_cast<size_t>(k - 2)] && m >= mag_[static_cast<size_t>(k + 2)])
535 {
536 peakBin_[static_cast<size_t>(numPeaks_++)] = k;
537 k += 2; // a neighbour cannot also be a peak
538 }
539 }
540 }
541
542 if (numPeaks_ == 0)
543 {
544 std::fill(rotRe_.begin(), rotRe_.end(), T(1));
545 std::fill(rotIm_.begin(), rotIm_.end(), T(0));
546 return;
547 }
548
549 // --- per-peak heterodyned phase propagation ----------------------------
550 // analysisHop_ still holds the hop just consumed (it is recomputed
551 // after this call), which is exactly the Ra of this frame interval.
552 const double Ra = static_cast<double>(analysisHop_);
553 const double Rs = static_cast<double>(synthHop_);
554 const double binW = kTwoPi / static_cast<double>(fftSize_);
555
556 int regionStart = 0;
557 for (int p = 0; p < numPeaks_; ++p)
558 {
559 const int bin = peakBin_[static_cast<size_t>(p)];
560 const double re = static_cast<double>(spec_[static_cast<size_t>(2 * bin)]);
561 const double im = static_cast<double>(spec_[static_cast<size_t>(2 * bin + 1)]);
562 const double pre = static_cast<double>(prevAnalysis_[static_cast<size_t>(2 * bin)]);
563 const double pim = static_cast<double>(prevAnalysis_[static_cast<size_t>(2 * bin + 1)]);
564
565 double rotR = 1.0, rotI = 0.0;
566 // A peak with no spectral history is a fresh partial: keep its
567 // analysis phase instead of propagating from noise.
568 if (prevMag_[static_cast<size_t>(bin)] > T(0.1) * mag_[static_cast<size_t>(bin)])
569 {
570 // Measured inter-frame phase advance, inherently wrapped.
571 const double deltaPhi = std::atan2(im * pre - re * pim, re * pre + im * pim);
572 const double omegaK = binW * static_cast<double>(bin);
573 const double omegaInst = omegaK + princArg(deltaPhi - omegaK * Ra) / Ra;
574
575 const double psiPrev = std::atan2(
576 static_cast<double>(prevSynth_[static_cast<size_t>(2 * bin + 1)]),
577 static_cast<double>(prevSynth_[static_cast<size_t>(2 * bin)]));
578 const double phi = std::atan2(im, re);
579 const double theta = psiPrev + Rs * omegaInst - phi;
580 rotR = std::cos(theta);
581 rotI = std::sin(theta);
582 }
583
584 // Region of influence: up to the magnitude valley before the next peak.
585 int regionEnd = numBins_; // exclusive
586 if (p + 1 < numPeaks_)
587 {
588 const int nextBin = peakBin_[static_cast<size_t>(p + 1)];
589 int valley = bin + 1;
590 T valleyMag = mag_[static_cast<size_t>(valley)];
591 for (int k = bin + 2; k < nextBin; ++k)
592 {
593 if (mag_[static_cast<size_t>(k)] < valleyMag)
594 {
595 valleyMag = mag_[static_cast<size_t>(k)];
596 valley = k;
597 }
598 }
599 regionEnd = valley + 1;
600 }
601
602 for (int k = regionStart; k < regionEnd; ++k)
603 {
604 rotRe_[static_cast<size_t>(k)] = static_cast<T>(rotR);
605 rotIm_[static_cast<size_t>(k)] = static_cast<T>(rotI);
606 }
607 regionStart = regionEnd;
608 }
609
610 // DC and Nyquist are real-valued in the packed spectrum: never rotate.
611 rotRe_[0] = T(1); rotIm_[0] = T(0);
612 rotRe_[static_cast<size_t>(numBins_ - 1)] = T(1); rotIm_[static_cast<size_t>(numBins_ - 1)] = T(0);
613 }
614
616 void synthesizeChannel(int ch, bool isReference) noexcept
617 {
618 // Rigid per-region rotation preserves intra-region (and inter-channel)
619 // relative phases exactly.
620 for (int k = 1; k < numBins_ - 1; ++k)
621 {
622 const T re = spec_[static_cast<size_t>(2 * k)];
623 const T im = spec_[static_cast<size_t>(2 * k + 1)];
624 const T rr = rotRe_[static_cast<size_t>(k)];
625 const T ri = rotIm_[static_cast<size_t>(k)];
626 spec_[static_cast<size_t>(2 * k)] = re * rr - im * ri;
627 spec_[static_cast<size_t>(2 * k + 1)] = re * ri + im * rr;
628 }
629
630 // Formant preservation: pre-warp synthesis magnitudes by
631 // env(k*ratio)/env(k) so the output resampler puts the spectral
632 // envelope back where the input had it. The gain table is computed
633 // once on the reference channel and shared, keeping the stereo
634 // image coherent.
635 if (formantPreserve_.load(std::memory_order_relaxed)
636 && std::abs(ratioActive_ - 1.0) > 1e-6)
637 {
638 if (isReference)
639 computeFormantGains();
640 for (int k = 1; k < numBins_ - 1; ++k)
641 {
642 const T g = formantGain_[static_cast<size_t>(k)];
643 spec_[static_cast<size_t>(2 * k)] *= g;
644 spec_[static_cast<size_t>(2 * k + 1)] *= g;
645 }
646 }
647
648 // Anti-alias for upward shifts: the resampler multiplies frequencies
649 // by `ratio`, so taper bins that would land above Nyquist.
650 if (ratioActive_ > 1.0)
651 {
652 const int cut = static_cast<int>(static_cast<double>(fftSize_ / 2) / ratioActive_);
653 const int taperStart = std::max(1, cut - 4);
654 for (int k = taperStart; k < numBins_; ++k)
655 {
656 T g = T(0);
657 if (k <= cut)
658 g = static_cast<T>(cut - k + 1) / static_cast<T>(cut - taperStart + 1);
659 spec_[static_cast<size_t>(2 * k)] *= g;
660 spec_[static_cast<size_t>(2 * k + 1)] *= g;
661 }
662 }
663
664 if (isReference)
665 std::copy(spec_.begin(), spec_.end(), prevSynth_.begin());
666
667 fft_->inverse(spec_.data(), fftResult_.data());
668
669 synthesizeTail(ch);
670 }
671
680 void computeFormantGains() noexcept
681 {
682 // Even-symmetric log magnitude.
683 for (int k = 0; k < numBins_; ++k)
684 {
685 const T re = spec_[static_cast<size_t>(2 * k)];
686 const T im = spec_[static_cast<size_t>(2 * k + 1)];
687 cepsTime_[static_cast<size_t>(k)] =
688 std::log(std::sqrt(re * re + im * im) + T(1e-9));
689 }
690 for (int k = numBins_; k < fftSize_; ++k)
691 cepsTime_[static_cast<size_t>(k)] =
692 cepsTime_[static_cast<size_t>(fftSize_ - k)];
693
694 fft_->forward(cepsTime_.data(), cepsSpec_.data());
695
696 // Lifter: keep quefrencies below ~1 ms (the smooth envelope), zero
697 // the rest (the harmonic comb). Cepstral index is quefrency in
698 // samples: 1 ms = 0.001 * fs.
699 const int qCut = std::max(8, static_cast<int>(0.001 * sampleRate_));
700 const int keep = std::min(qCut, numBins_ - 1);
701 for (int k = keep + 1; k < numBins_; ++k)
702 {
703 cepsSpec_[static_cast<size_t>(2 * k)] = T(0);
704 cepsSpec_[static_cast<size_t>(2 * k + 1)] = T(0);
705 }
706
707 fft_->inverse(cepsSpec_.data(), cepsTime_.data());
708 for (int k = 0; k < numBins_; ++k)
709 envLog_[static_cast<size_t>(k)] = cepsTime_[static_cast<size_t>(k)];
710
711 // Gains with linear interpolation at k*ratio, clamped to +-40 dB.
712 for (int k = 0; k < numBins_; ++k)
713 {
714 const double pos = std::min(static_cast<double>(k) * ratioActive_,
715 static_cast<double>(numBins_ - 1));
716 const auto i0 = static_cast<int>(pos);
717 const auto frac = static_cast<T>(pos - i0);
718 const T target = envLog_[static_cast<size_t>(i0)]
719 + (envLog_[static_cast<size_t>(std::min(i0 + 1, numBins_ - 1))]
720 - envLog_[static_cast<size_t>(i0)]) * frac;
721 const T delta = std::clamp(target - envLog_[static_cast<size_t>(k)],
722 T(-4.6), T(4.6));
723 formantGain_[static_cast<size_t>(k)] = std::exp(delta);
724 }
725 }
726
728 void synthesizeTail(int ch) noexcept
729 {
730
731 // Overlap-add at the fixed synthesis hop. sqrt-Hann analysis+synthesis
732 // with Rs = N/4 overlap-adds to a constant 2.0, hence the 0.5 norm.
733 auto& acc = accum_[static_cast<size_t>(ch)];
734 const auto m = static_cast<int64_t>(accumMask_);
735
736 // The tail region [w + N - Rs, w + N) is entered for the first time by
737 // this frame: clear the stale ring content before accumulating.
738 for (int k = fftSize_ - synthHop_; k < fftSize_; ++k)
739 acc[static_cast<size_t>((writeHead_ + k) & m)] = T(0);
740
741 constexpr T kNorm = T(0.5);
742 for (int k = 0; k < fftSize_; ++k)
743 {
744 const auto idx = static_cast<size_t>((writeHead_ + k) & m);
745 acc[idx] += fftResult_[static_cast<size_t>(k)]
746 * window_[static_cast<size_t>(k)] * kNorm;
747 }
748 }
749
750 // -- Members -----------------------------------------------------------------
751 double sampleRate_ = 48000.0;
752 int numChannels_ = 0;
753 std::atomic<bool> prepared_ { false };
754
755 int fftSize_ = 2048;
756 int numBins_ = 1025;
757 int synthHop_ = 512;
758 int ringMask_ = 2047;
759 int accumSize_ = 8192;
760 int accumMask_ = 8191;
761 int readOffset_ = 2560;
762 int latency_ = 4096;
763 int drySize_ = 8192;
764 int dryMask_ = 8191;
765
766 std::unique_ptr<FFTReal<T>> fft_;
767 std::vector<T> window_;
768
769 std::vector<std::vector<T>> inputRing_;
770 std::vector<std::vector<T>> dryRing_;
771 std::vector<std::vector<T>> accum_;
772
773 std::vector<T> fftIn_, spec_, fftResult_;
774 std::vector<T> prevAnalysis_, prevSynth_;
775 std::vector<T> mag_, prevMag_;
776 std::vector<T> cepsTime_, cepsSpec_;
777 std::vector<T> envLog_, formantGain_;
778 std::vector<T> rotRe_, rotIm_;
779 std::vector<int> peakBin_;
780 int numPeaks_ = 0;
781
782 int inputPos_ = 0;
783 int dryPos_ = 0;
784 int64_t writeHead_ = 0;
785 int64_t readPosInt_ = 0;
786 double readPosFrac_ = 0.0;
787
788 double stActive_ = 0.0;
789 double ratioActive_ = 1.0;
790 double hopCarry_ = 0.0;
791 int analysisHop_ = 512;
792 int inputSinceHop_ = 0;
793 double onsetEnv_ = 0.0;
794 bool firstFrame_ = true;
795 T currentMix_ = T(1);
796
797 std::atomic<T> semitones_ { T(0) };
798 std::atomic<T> mix_ { T(1) };
799 std::atomic<bool> transientPreserve_ { true };
800 std::atomic<bool> formantPreserve_ { false };
801};
802
803} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Real-time phase-vocoder pitch shifter (+-12 semitones, stereo-linked).
bool getFormantPreserve() const noexcept
void setTransientPreserve(bool enabled) noexcept
Enables phase reset on detected transients (default on).
T getSemitones() const noexcept
void prepare(const AudioSpec &spec, int fftSize=2048)
Allocates all rings and spectral state.
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
void reset() noexcept
Clears all signal state (keeps parameters). Safe on the audio thread.
void setMix(T mix) noexcept
Dry/wet mix, [0, 1]. The dry path is latency-compensated and the mix is smoothed linearly over one bl...
void setSemitones(T st) noexcept
Sets the pitch shift in semitones, clamped to +-12.
void setFormantPreserve(bool enabled) noexcept
Keeps formants (vocal timbre) in place while pitch moves.
int getLatency() const noexcept
Reports total latency in samples (2 * fftSize, measured exact at unity ratio; ~85 ms at the default 2...
void setPitchRatio(T ratio) noexcept
Sets the pitch shift as a frequency ratio, clamped to [0.5, 2]. Non-finite values are ignored.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio in-place.
bool getTransientPreserve() const noexcept
T getMix() const noexcept
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
static void hann(T *output, int size, bool periodic=true) noexcept
Hann (raised cosine) window.