102 if (!spec.
isValid() || (fftSize & (fftSize - 1)) != 0
103 || fftSize < 256 || fftSize > (1 << 20))
106 prepared_.store(
false, std::memory_order_relaxed);
111 numBins_ = fftSize_ / 2 + 1;
112 synthHop_ = fftSize_ / 4;
113 ringMask_ = fftSize_ - 1;
115 accumSize_ = fftSize_ * 4;
116 accumMask_ = accumSize_ - 1;
123 readOffset_ = fftSize_ + synthHop_;
124 latency_ = readOffset_ + fftSize_ - synthHop_;
126 while (drySize_ < latency_ + 1) drySize_ <<= 1;
127 dryMask_ = drySize_ - 1;
129 fft_ = std::make_unique<FFTReal<T>>(
static_cast<size_t>(fftSize_));
131 window_.resize(
static_cast<size_t>(fftSize_));
133 for (
auto& w : window_) w = std::sqrt(w);
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)
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));
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));
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));
163 prepared_.store(
true, std::memory_order_relaxed);
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));
180 writeHead_ =
static_cast<int64_t
>(accumSize_);
181 readPosInt_ = writeHead_ - readOffset_;
184 stActive_ = std::clamp(
static_cast<double>(semitones_.load(std::memory_order_relaxed)),
186 ratioActive_ = std::exp2(stActive_ / 12.0);
187 currentMix_ = mix_.load(std::memory_order_relaxed);
190 analysisHop_ = computeAnalysisHop();
206 if (!std::isfinite(st))
return;
207 semitones_.store(std::clamp(st, T(-12), T(12)), std::memory_order_relaxed);
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);
226 if (!std::isfinite(mix))
return;
227 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
233 transientPreserve_.store(enabled, std::memory_order_relaxed);
247 formantPreserve_.store(enabled, std::memory_order_relaxed);
253 return semitones_.load(std::memory_order_relaxed);
257 [[nodiscard]] T
getMix() const noexcept {
return mix_.load(std::memory_order_relaxed); }
262 return transientPreserve_.load(std::memory_order_relaxed);
268 return formantPreserve_.load(std::memory_order_relaxed);
273 [[nodiscard]]
int getLatency() const noexcept {
return latency_; }
276 [[nodiscard]] std::vector<uint8_t>
getState()
const
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));
310 if (!prepared_.load(std::memory_order_relaxed))
return;
313 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
314 const int nS = buffer.getNumSamples();
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);
325 const int chunk = std::min(nS - i, analysisHop_ - inputSinceHop_);
328 for (
int ch = 0; ch < nCh; ++ch)
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)];
335 for (
int k = 0; k < chunk; ++k)
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_;
345 for (
int ch = 0; ch < nCh; ++ch)
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)];
351 int64_t rp = readPosInt_;
352 double rf = readPosFrac_;
355 for (
int k = 0; k < chunk; ++k)
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);
363 out[k] = drySample * (T(1) - mixVal) + wet * mixVal;
366 const auto adv =
static_cast<int64_t
>(rf);
368 rf -=
static_cast<double>(adv);
369 dp = (dp + 1) & dryMask_;
375 double rf = readPosFrac_ + ratioActive_ * chunk;
376 const auto adv =
static_cast<int64_t
>(rf);
378 readPosFrac_ = rf -
static_cast<double>(adv);
379 inputPos_ = (inputPos_ + chunk) & ringMask_;
380 dryPos_ = (dryPos_ + chunk) & dryMask_;
384 inputSinceHop_ += chunk;
385 if (inputSinceHop_ >= analysisHop_)
394 currentMix_ = mixTarget;
398 static constexpr double kTwoPi = 2.0 * std::numbers::pi;
401 [[nodiscard]]
static double princArg(
double x)
noexcept
403 return x - kTwoPi * std::round(x / kTwoPi);
407 [[nodiscard]]
int computeAnalysisHop() noexcept
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);
417 [[nodiscard]] T readCatmullRom(
const std::vector<T>& acc, int64_t ip,
double frac)
const noexcept
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)));
431 void processHop(
int nCh)
noexcept
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);
442 const bool transient = detectTransient();
443 buildRotations(transient);
447 std::copy(spec_.begin(), spec_.end(), prevAnalysis_.begin());
448 std::copy(mag_.begin(), mag_.end(), prevMag_.begin());
451 synthesizeChannel(0,
true);
452 for (
int ch = 1; ch < nCh; ++ch)
455 synthesizeChannel(ch,
false);
458 analysisHop_ = computeAnalysisHop();
459 writeHead_ += synthHop_;
464 void analyzeChannel(
int ch)
noexcept
466 const auto& ring = inputRing_[
static_cast<size_t>(ch)];
467 const int readPos = inputPos_;
468 for (
int k = 0; k < fftSize_; ++k)
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)];
474 fft_->forward(fftIn_.data(), spec_.data());
478 for (
int k = 0; k < numBins_; ++k)
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);
488 [[nodiscard]]
bool detectTransient() noexcept
491 for (
int k = 0; k < numBins_; ++k)
493 const double m =
static_cast<double>(mag_[
static_cast<size_t>(k)]);
496 const double prevEnv = onsetEnv_;
497 onsetEnv_ = std::max(energy, onsetEnv_ * 0.7);
498 if (!transientPreserve_.load(std::memory_order_relaxed))
500 return firstFrame_ || (energy > 4.0 * prevEnv && energy > 1e-12);
510 void buildRotations(
bool transient)
noexcept
514 std::fill(rotRe_.begin(), rotRe_.end(), T(1));
515 std::fill(rotIm_.begin(), rotIm_.end(), T(0));
521 for (
int k = 0; k < numBins_; ++k)
522 maxMag = std::max(maxMag, mag_[
static_cast<size_t>(k)]);
525 if (maxMag > T(1e-9))
527 const T floorMag = maxMag * T(1e-4);
528 const int last = numBins_ - 2;
529 for (
int k = 2; k <= last; ++k)
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)])
536 peakBin_[
static_cast<size_t>(numPeaks_++)] = k;
544 std::fill(rotRe_.begin(), rotRe_.end(), T(1));
545 std::fill(rotIm_.begin(), rotIm_.end(), T(0));
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_);
557 for (
int p = 0; p < numPeaks_; ++p)
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)]);
565 double rotR = 1.0, rotI = 0.0;
568 if (prevMag_[
static_cast<size_t>(bin)] > T(0.1) * mag_[
static_cast<size_t>(bin)])
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;
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);
585 int regionEnd = numBins_;
586 if (p + 1 < numPeaks_)
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)
593 if (mag_[
static_cast<size_t>(k)] < valleyMag)
595 valleyMag = mag_[
static_cast<size_t>(k)];
599 regionEnd = valley + 1;
602 for (
int k = regionStart; k < regionEnd; ++k)
604 rotRe_[
static_cast<size_t>(k)] =
static_cast<T
>(rotR);
605 rotIm_[
static_cast<size_t>(k)] =
static_cast<T
>(rotI);
607 regionStart = regionEnd;
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);
616 void synthesizeChannel(
int ch,
bool isReference)
noexcept
620 for (
int k = 1; k < numBins_ - 1; ++k)
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;
635 if (formantPreserve_.load(std::memory_order_relaxed)
636 && std::abs(ratioActive_ - 1.0) > 1e-6)
639 computeFormantGains();
640 for (
int k = 1; k < numBins_ - 1; ++k)
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;
650 if (ratioActive_ > 1.0)
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)
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;
665 std::copy(spec_.begin(), spec_.end(), prevSynth_.begin());
667 fft_->inverse(spec_.data(), fftResult_.data());
680 void computeFormantGains() noexcept
683 for (
int k = 0; k < numBins_; ++k)
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));
690 for (
int k = numBins_; k < fftSize_; ++k)
691 cepsTime_[
static_cast<size_t>(k)] =
692 cepsTime_[
static_cast<size_t>(fftSize_ - k)];
694 fft_->forward(cepsTime_.data(), cepsSpec_.data());
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)
703 cepsSpec_[
static_cast<size_t>(2 * k)] = T(0);
704 cepsSpec_[
static_cast<size_t>(2 * k + 1)] = T(0);
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)];
712 for (
int k = 0; k < numBins_; ++k)
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)],
723 formantGain_[
static_cast<size_t>(k)] = std::exp(delta);
728 void synthesizeTail(
int ch)
noexcept
733 auto& acc = accum_[
static_cast<size_t>(ch)];
734 const auto m =
static_cast<int64_t
>(accumMask_);
738 for (
int k = fftSize_ - synthHop_; k < fftSize_; ++k)
739 acc[
static_cast<size_t>((writeHead_ + k) & m)] = T(0);
741 constexpr T kNorm = T(0.5);
742 for (
int k = 0; k < fftSize_; ++k)
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;
751 double sampleRate_ = 48000.0;
752 int numChannels_ = 0;
753 std::atomic<bool> prepared_ {
false };
758 int ringMask_ = 2047;
759 int accumSize_ = 8192;
760 int accumMask_ = 8191;
761 int readOffset_ = 2560;
766 std::unique_ptr<FFTReal<T>> fft_;
767 std::vector<T> window_;
769 std::vector<std::vector<T>> inputRing_;
770 std::vector<std::vector<T>> dryRing_;
771 std::vector<std::vector<T>> accum_;
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_;
784 int64_t writeHead_ = 0;
785 int64_t readPosInt_ = 0;
786 double readPosFrac_ = 0.0;
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);
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 };