128 if (fftSize != 0 && ((fftSize & (fftSize - 1)) != 0
129 || fftSize < 256 || fftSize > (1 << 20)))
132 prepared_.store(
false, std::memory_order_relaxed);
138 const int fftStudio = fftSize != 0 ? fftSize : studioDefaultFrame(spec.
sampleRate);
139 if (fftSize == 0) fftSize = 2048;
146 const int synthHop = fftSize / 4;
147 readOffset_ = fftSize + synthHop;
148 latency_ = readOffset_ + fftSize - synthHop;
159 studio_.prepare(spec.
sampleRate, numChannels_, fftStudio,
true);
160 studio_.setAnchorLeadLimit(
static_cast<double>(fftStudio / 4));
161 latencyStudio_ = fftStudio + fftStudio / 2 + studio_.lookahead() + fftStudio / 4 + 64;
162 latencyLegacy_ = latency_;
164 while (drySize_ < std::max(latencyLegacy_, latencyStudio_) + 1) drySize_ <<= 1;
165 dryMask_ = drySize_ - 1;
185 engine_.prepare(spec.
sampleRate, numChannels_, fftSize,
true,
false,
false,
false);
186 mixMaxStep_ =
static_cast<T
>(1.0 / std::max(1.0, spec.
sampleRate * 0.02));
187 accumMask_ = engine_.olaMask();
189 dryRing_.assign(
static_cast<size_t>(numChannels_), {});
190 for (
int ch = 0; ch < numChannels_; ++ch)
191 dryRing_[
static_cast<size_t>(ch)].assign(
static_cast<size_t>(drySize_), T(0));
193 publishEngineParams();
194 prepared_.store(
true, std::memory_order_relaxed);
201 if (!prepared_.load(std::memory_order_relaxed))
return;
202 studioActive_ = quality_.load(std::memory_order_relaxed) ==
Quality::Studio;
203 latency_ = studioActive_ ? latencyStudio_ : latencyLegacy_;
206 for (
auto& r : dryRing_) std::fill(r.begin(), r.end(), T(0));
211 const double start = studio_.streamPositionOf(-
static_cast<double>(latencyStudio_));
212 studioReadInt_ =
static_cast<int64_t
>(std::floor(start));
213 studioReadFrac_ = start -
static_cast<double>(studioReadInt_);
217 readPosInt_ = engine_.writeHead() - readOffset_;
219 currentMix_ = mix_.load(std::memory_order_relaxed);
243 quality =
static_cast<Quality>(std::clamp(
static_cast<int>(quality), 0,
245 quality_.store(quality, std::memory_order_relaxed);
251 return quality_.load(std::memory_order_relaxed);
263 if (!std::isfinite(st))
return;
264 semitones_.store(std::clamp(st, T(-12), T(12)), std::memory_order_relaxed);
265 publishEngineParams();
272 if (!std::isfinite(ratio))
return;
273 ratio = std::clamp(ratio, T(0.5), T(2));
274 semitones_.store(
static_cast<T
>(12.0 * std::log2(
static_cast<double>(ratio))),
275 std::memory_order_relaxed);
276 publishEngineParams();
285 if (!std::isfinite(mix))
return;
286 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
292 transientPreserve_.store(enabled, std::memory_order_relaxed);
293 publishEngineParams();
307 formantPreserve_.store(enabled, std::memory_order_relaxed);
308 publishEngineParams();
314 return semitones_.load(std::memory_order_relaxed);
318 [[nodiscard]] T
getMix() const noexcept {
return mix_.load(std::memory_order_relaxed); }
323 return transientPreserve_.load(std::memory_order_relaxed);
329 return formantPreserve_.load(std::memory_order_relaxed);
336 [[nodiscard]]
int getLatency() const noexcept {
return latency_; }
339 [[nodiscard]] std::vector<uint8_t>
getState()
const
342 w.
write(
"semitones",
static_cast<float>(semitones_.load(std::memory_order_relaxed)));
343 w.
write(
"mix",
static_cast<float>(mix_.load(std::memory_order_relaxed)));
344 w.
write(
"transient", transientPreserve_.load(std::memory_order_relaxed));
345 w.
write(
"formant", formantPreserve_.load(std::memory_order_relaxed));
346 w.
write(
"quality",
static_cast<int32_t
>(quality_.load(std::memory_order_relaxed)));
375 if (!prepared_.load(std::memory_order_relaxed))
return;
378 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
379 const int nS = buffer.getNumSamples();
382 if ((quality_.load(std::memory_order_relaxed) ==
Quality::Studio) != studioActive_)
386 processStudio(buffer, nCh, nS);
393 const T mixTarget = mix_.load(std::memory_order_relaxed);
394 const T mixStart = currentMix_;
398 const bool highQuality = quality_.load(std::memory_order_relaxed) ==
Quality::High;
399 if (highQuality != highReader_)
401 highReader_ = highQuality;
402 readerFadeLeft_ = kReaderFade;
408 const int chunk = std::min(nS - i, engine_.samplesToNextHop());
411 for (
int ch = 0; ch < nCh; ++ch)
413 const T* in = buffer.getChannel(ch) + i;
414 engine_.pushInput(ch, in, chunk);
415 auto& dry = dryRing_[
static_cast<size_t>(ch)];
417 for (
int k = 0; k < chunk; ++k)
419 dry[
static_cast<size_t>(dp)] = in[k];
420 dp = (dp + 1) & dryMask_;
425 const double ratio = engine_.activeRatio();
426 int64_t rpEnd = readPosInt_;
427 double rfEnd = readPosFrac_;
428 for (
int ch = 0; ch < nCh; ++ch)
430 T* out = buffer.getChannel(ch) + i;
431 const T* acc = engine_.olaData(ch);
432 const auto& dry = dryRing_[
static_cast<size_t>(ch)];
434 int64_t rp = readPosInt_;
435 double rf = readPosFrac_;
437 int fadeLeft = readerFadeLeft_;
439 for (
int k = 0; k < chunk; ++k)
441 T wet = readWet(highReader_, acc, rp, rf);
444 const T w =
static_cast<T
>(fadeLeft) * (T(1) / T(kReaderFade));
445 const T old = readWet(!highReader_, acc, rp, rf);
446 wet += (old - wet) * w;
449 const int dryIdx = (dp - latency_) & dryMask_;
450 const T drySample = dry[
static_cast<size_t>(dryIdx)];
451 const T mixVal =
moveTowards(mixStart, mixTarget, mixMaxStep_ *
static_cast<T
>(i + k + 1));
454 out[k] = drySample * (T(1) - mixVal) + wet * mixVal;
457 const auto adv =
static_cast<int64_t
>(rf);
459 rf -=
static_cast<double>(adv);
460 dp = (dp + 1) & dryMask_;
462 if (ch == 0) { rpEnd = rp; rfEnd = rf; }
476 for (
int k = 0; k < chunk; ++k)
479 const auto adv =
static_cast<int64_t
>(rfEnd);
481 rfEnd -=
static_cast<double>(adv);
485 readPosFrac_ = rfEnd;
486 dryPos_ = (dryPos_ + chunk) & dryMask_;
487 readerFadeLeft_ = std::max(0, readerFadeLeft_ - chunk);
491 engine_.commitInput(chunk, nCh);
496 currentMix_ =
moveTowards(mixStart, mixTarget, mixMaxStep_ *
static_cast<T
>(nS));
512 const T mixTarget = mix_.load(std::memory_order_relaxed);
513 const T mixStart = currentMix_;
514 const int64_t mask = studio_.olaMask();
518 const int need = studio_.samplesToNextHop();
521 studio_.commitInput(0, nCh);
524 const int chunk = std::min(nS - i, need);
525 for (
int ch = 0; ch < nCh; ++ch)
527 const T* in = buffer.getChannel(ch) + i;
528 studio_.pushInput(ch, in, chunk);
529 auto& dry = dryRing_[
static_cast<size_t>(ch)];
531 for (
int k = 0; k < chunk; ++k)
533 dry[
static_cast<size_t>(dp)] = in[k];
534 dp = (dp + 1) & dryMask_;
538 const double ratio = studio_.activeRatio();
539 int64_t rpEnd = studioReadInt_;
540 double rfEnd = studioReadFrac_;
541 for (
int ch = 0; ch < nCh; ++ch)
543 T* out = buffer.getChannel(ch) + i;
544 const T* acc = studio_.olaData(ch);
545 const auto& dry = dryRing_[
static_cast<size_t>(ch)];
546 int64_t rp = studioReadInt_;
547 double rf = studioReadFrac_;
549 for (
int k = 0; k < chunk; ++k)
551 const T wet = reader_.readRing(acc, mask, rp, rf);
552 const T drySample = dry[
static_cast<size_t>((dp - latency_) & dryMask_)];
553 const T mixVal =
moveTowards(mixStart, mixTarget, mixMaxStep_ *
static_cast<T
>(i + k + 1));
554 out[k] = drySample * (T(1) - mixVal) + wet * mixVal;
556 const auto adv =
static_cast<int64_t
>(rf);
558 rf -=
static_cast<double>(adv);
559 dp = (dp + 1) & dryMask_;
561 if (ch == 0) { rpEnd = rp; rfEnd = rf; }
565 for (
int k = 0; k < chunk; ++k)
568 const auto adv =
static_cast<int64_t
>(rfEnd);
570 rfEnd -=
static_cast<double>(adv);
573 studioReadInt_ = rpEnd;
574 studioReadFrac_ = rfEnd;
575 dryPos_ = (dryPos_ + chunk) & dryMask_;
580 const double c = studio_.nextStreamCentre() +
static_cast<double>(studio_.synthHop());
581 const double here =
static_cast<double>(studioReadInt_) + studioReadFrac_;
582 studio_.steerTimeline(
static_cast<double>(studioOut_) + (c - here) / ratio
583 -
static_cast<double>(latencyStudio_));
584 studio_.commitInput(chunk, nCh);
587 currentMix_ =
moveTowards(mixStart, mixTarget, mixMaxStep_ *
static_cast<T
>(nS));
591 [[nodiscard]]
static int studioDefaultFrame(
double sampleRate)
noexcept
594 while (n < (1 << 16) &&
static_cast<double>(n) * 1.5 < 0.0427 * sampleRate) n <<= 1;
599 [[nodiscard]] T readWet(
bool high,
const T* acc, int64_t ip,
double frac)
const noexcept
601 return high ? reader_.readRing(acc, accumMask_, ip, frac)
602 : readCatmullRom(acc, ip, frac);
606 [[nodiscard]] T readCatmullRom(
const T* acc, int64_t ip,
double frac)
const noexcept
608 const int64_t m = accumMask_;
609 const T x0 = acc[
static_cast<size_t>((ip - 1) & m)];
610 const T x1 = acc[
static_cast<size_t>(ip & m)];
611 const T x2 = acc[
static_cast<size_t>((ip + 1) & m)];
612 const T x3 = acc[
static_cast<size_t>((ip + 2) & m)];
613 const T f =
static_cast<T
>(frac);
614 return x1 + T(0.5) * f * (x2 - x0
615 + f * (T(2) * x0 - T(5) * x1 + T(4) * x2 - x3
616 + f * (T(3) * (x1 - x2) + x3 - x0)));
621 void publishEngineParams() noexcept
623 typename detail::PhaseVocoderEngine<T>::Params p;
624 p.targetSemitones =
static_cast<double>(semitones_.load(std::memory_order_relaxed));
625 p.transientPreserve = transientPreserve_.load(std::memory_order_relaxed);
626 p.formantPreserve = formantPreserve_.load(std::memory_order_relaxed);
627 engine_.publishParams(p);
628 typename detail::StudioVocoder<T>::Params q;
629 q.targetSemitones = p.targetSemitones;
630 q.transientPreserve = p.transientPreserve;
631 q.formantPreserve = p.formantPreserve;
632 studio_.publishParams(q);
636 int numChannels_ = 0;
637 std::atomic<bool> prepared_ {
false };
639 int readOffset_ = 2560;
643 int64_t accumMask_ = 8191;
645 detail::PhaseVocoderEngine<T> engine_;
646 detail::StudioVocoder<T> studio_;
647 bool studioActive_ =
true;
648 int latencyStudio_ = 5184;
649 int latencyLegacy_ = 4096;
650 int64_t studioReadInt_ = 0;
651 double studioReadFrac_ = 0.0;
652 int64_t studioOut_ = 0;
653 SincInterpolator<T> reader_;
656 static constexpr int kReaderFade = 64;
657 bool highReader_ =
false;
658 int readerFadeLeft_ = 0;
660 std::vector<std::vector<T>> dryRing_;
663 int64_t readPosInt_ = 0;
664 double readPosFrac_ = 0.0;
665 T currentMix_ = T(1);
666 T mixMaxStep_ = T(1.0 / 960.0);
668 std::atomic<T> semitones_ { T(0) };
669 std::atomic<T> mix_ { T(1) };
670 std::atomic<bool> transientPreserve_ {
true };
671 std::atomic<bool> formantPreserve_ {
false };