DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
TimeStretch.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
278#include "../Core/AudioBuffer.h"
279#include "../Core/AudioSpec.h"
280#include "../Core/DenormalGuard.h"
281#include "../Core/DspMath.h"
282#include "../Core/StateBlob.h"
283#include "detail/PhaseVocoderEngine.h"
284#include "detail/StudioVocoder.h"
285
286#include <algorithm>
287#include <atomic>
288#include <cmath>
289#include <cstddef>
290#include <cstdint>
291#include <vector>
292
293namespace dspark {
294
301template <FloatType T>
302class TimeStretch final
303{
304public:
305 // The published parameter words must never make the audio thread take a
306 // lock to read them.
307 static_assert(std::atomic<bool>::is_always_lock_free,
308 "audio-thread stores must not lock");
309
311 static constexpr T kMinRatio = T(0.5);
312 static constexpr T kMaxRatio = T(2);
313
331 enum class Quality { Standard, Studio };
332
334 void setQuality(Quality q) noexcept
335 {
337 std::memory_order_relaxed);
338 }
339
341 [[nodiscard]] Quality getQuality() const noexcept
342 {
343 return quality_.load(std::memory_order_relaxed);
344 }
345
346 // -- Lifecycle ---------------------------------------------------------------
347
365 void prepare(const AudioSpec& spec, int fftSize = 0)
366 {
367 if (!spec.isValid()) return;
368 if (fftSize != 0 && ((fftSize & (fftSize - 1)) != 0
369 || fftSize < 256 || fftSize > (1 << 20)))
370 return;
371
372 prepared_.store(false, std::memory_order_relaxed);
373
374 numChannels_ = std::max(1, spec.numChannels);
375 // 0 selects each engine's own default frame: 2048 for Standard (its
376 // published rendering) and about 85 ms for Studio (4096 at 44.1 and
377 // 48 kHz, 8192 at 88.2 and 96 kHz), the span its measurements use.
378 fftStandard_ = fftSize != 0 ? fftSize : 2048;
379 fftStudio_ = fftSize != 0 ? fftSize : studioDefaultFrame(spec.sampleRate);
380
381 // The input queue holds what the stretch has not consumed yet. It
382 // needs room for one whole analysis hop (which can be as long as a
383 // frame, and for Studio as long as a frame plus its lookahead), for
384 // one host block, and for the surplus a ratio above 1 accumulates
385 // until the caller stops or the queue refuses more.
386 const int widest = std::max(fftStandard_, fftStudio_);
387 int capacity = 4 * widest + 4 * studioDefaultFrame(spec.sampleRate) + std::max(1, spec.maxBlockSize);
388 queueSize_ = 1;
389 while (queueSize_ < capacity) queueSize_ <<= 1;
390 queueMask_ = queueSize_ - 1;
391
392 // No resample-back stage and no harmonic/percussive split; the
393 // locked analysis hop and the spectral-flux onset detector are both
394 // asked for, because this owner reads the synthesis stream directly
395 // and its hop schedule has to act on strikes the frame-energy test
396 // cannot see over sustained material.
397 engine_.prepare(spec.sampleRate, numChannels_, fftStandard_, false, false, true, true);
398 studio_.prepare(spec.sampleRate, numChannels_, fftStudio_, false);
399
400 queue_.assign(static_cast<size_t>(numChannels_), {});
401 for (int ch = 0; ch < numChannels_; ++ch)
402 queue_[static_cast<size_t>(ch)].assign(static_cast<size_t>(queueSize_), T(0));
403 feed_.assign(static_cast<size_t>(capacity), T(0));
404
405 publishEngineParams();
406 prepared_.store(true, std::memory_order_relaxed);
407 reset();
408 }
409
412 void reset() noexcept
413 {
414 if (!prepared_.load(std::memory_order_relaxed)) return;
415 studioActive_ = quality_.load(std::memory_order_relaxed) == Quality::Studio;
416 engine_.reset();
417 studio_.reset();
418 if (studioActive_)
419 {
420 fftSize_ = fftStudio_;
421 // One frame of analysis-synthesis delay plus the onset
422 // detector's lookahead.
423 latency_ = fftStudio_ + studio_.lookahead();
424 accumMask_ = studio_.olaMask();
425 }
426 else
427 {
428 fftSize_ = fftStandard_;
429 // The reader trails the completed frontier of the synthesis
430 // stream by one synthesis hop. The analysis-synthesis chain
431 // itself accounts for fftSize - synthHop samples of delay, so
432 // that trailing distance puts the reported latency at exactly
433 // one frame.
434 latency_ = fftStandard_;
435 accumMask_ = engine_.olaMask();
436 }
437 synthHop_ = fftSize_ / 4;
438 for (auto& q : queue_) std::fill(q.begin(), q.end(), T(0));
439
440 queueWrite_ = 0;
441 queueRead_ = 0;
442 queued_ = 0;
443 discarded_ = 0;
444 path_ = Path::None;
445 // Start one synthesis hop behind the write head: those cells are
446 // still silent, which is the latency the class reports, and from
447 // there the reader stays exactly that far behind.
448 readPos_ = eWriteHead() - static_cast<int64_t>(latency_ - fftSize_ + synthHop_);
449 openIntake();
450 }
451
452 // -- Parameters ---------------------------------------------------------------
453
462 void setTimeRatio(T ratio) noexcept
463 {
464 if (!std::isfinite(ratio)) return;
465 timeRatio_.store(std::clamp(ratio, kMinRatio, kMaxRatio),
466 std::memory_order_relaxed);
467 publishEngineParams();
468 }
469
478 void setTempoChangePercent(T pct) noexcept
479 {
480 if (!std::isfinite(pct)) return;
481 const T denom = T(1) + pct / T(100);
482 if (!(denom > T(0))) { setTimeRatio(kMaxRatio); return; }
483 setTimeRatio(T(1) / denom);
484 }
485
489 void setTransientPreserve(bool on) noexcept
490 {
491 transientPreserve_.store(on, std::memory_order_relaxed);
492 publishEngineParams();
493 }
494
503 void setPhaseLock(bool on) noexcept
504 {
505 phaseLock_.store(on, std::memory_order_relaxed);
506 publishEngineParams();
507 }
508
510 [[nodiscard]] T getTimeRatio() const noexcept
511 {
512 return timeRatio_.load(std::memory_order_relaxed);
513 }
514
516 [[nodiscard]] bool getTransientPreserve() const noexcept
517 {
518 return transientPreserve_.load(std::memory_order_relaxed);
519 }
520
522 [[nodiscard]] bool getPhaseLock() const noexcept
523 {
524 return phaseLock_.load(std::memory_order_relaxed);
525 }
526
532 [[nodiscard]] int getLatency() const noexcept
533 {
534 return prepared_.load(std::memory_order_relaxed) ? latency_ : 0;
535 }
536
538 [[nodiscard]] int getQueuedInputSamples() const noexcept { return queued_; }
539
540 // -- Rate-changing streaming: the correct path at any ratio -------------
541
550 [[nodiscard]] int getInputCapacity() const noexcept
551 {
552 if (!prepared_.load(std::memory_order_relaxed) || path_ == Path::Adaptor)
553 return 0;
554 return queueSize_ - queued_;
555 }
556
564 [[nodiscard]] int getAvailableOutput() const noexcept
565 {
566 if (!prepared_.load(std::memory_order_relaxed) || path_ != Path::Pull)
567 return 0;
568 return static_cast<int>(std::max<int64_t>(0, eWriteHead() - readPos_));
569 }
570
586 {
587 if (!prepared_.load(std::memory_order_relaxed) || !claimPullPath()) return 0;
588 DenormalGuard guard;
589
590 const int nCh = std::min(in.getNumChannels(), numChannels_);
591 const int count = std::min(std::max(0, in.getNumSamples()), queueSize_ - queued_);
592 int wp = queueWrite_;
593 for (int k = 0; k < count; ++k)
594 {
595 for (int ch = 0; ch < nCh; ++ch)
596 queue_[static_cast<size_t>(ch)][static_cast<size_t>(wp)]
597 = in.getChannel(ch)[k];
598 for (int ch = nCh; ch < numChannels_; ++ch)
599 queue_[static_cast<size_t>(ch)][static_cast<size_t>(wp)] = T(0);
600 wp = (wp + 1) & queueMask_;
601 }
602 queueWrite_ = wp;
603 queued_ += count;
604
605 runReadyFrames();
606 return count;
607 }
608
623 {
624 if (!prepared_.load(std::memory_order_relaxed) || !claimPullPath()) return 0;
625 DenormalGuard guard;
626
627 const int nCh = std::min(out.getNumChannels(), numChannels_);
628 const int count = std::min(std::max(0, out.getNumSamples()),
629 static_cast<int>(std::max<int64_t>(0, eWriteHead() - readPos_)));
630 if (count > 0)
631 {
632 readSynthesis(out, nCh, 0, count);
633 readPos_ += count;
634 }
635 // Reading frees room in the synthesis ring, which may be what was
636 // holding the next analysis frame back.
637 runReadyFrames();
638 return std::max(0, count);
639 }
640
651 [[nodiscard]] int64_t getDiscardedInput() const noexcept { return discarded_; }
652
654 [[nodiscard]] std::vector<uint8_t> getState() const
655 {
656 StateWriter w(stateId("TSTR"), 1);
657 w.write("ratio", static_cast<float>(timeRatio_.load(std::memory_order_relaxed)));
658 w.write("transient", transientPreserve_.load(std::memory_order_relaxed));
659 w.write("phaselock", phaseLock_.load(std::memory_order_relaxed));
660 w.write("quality", static_cast<int32_t>(quality_.load(std::memory_order_relaxed)));
661 return w.blob();
662 }
663
665 bool setState(const uint8_t* data, size_t size)
666 {
667 StateReader r(data, size);
668 if (!r.isValid() || r.processorId() != stateId("TSTR")) return false;
669 setTimeRatio(static_cast<T>(r.read("ratio", 1.0f)));
670 setTransientPreserve(r.read("transient", true));
671 setPhaseLock(r.read("phaselock", true));
672 // A blob written before the quality field existed was rendered by the
673 // Standard engine, so that is what it restores.
674 setQuality(static_cast<Quality>(std::clamp(r.read("quality", 0), 0, 1)));
675 return true;
676 }
677
678 // -- Processing ----------------------------------------------------------------
679
694 void processBlock(AudioBufferView<T> buffer) noexcept
695 {
696 if (!prepared_.load(std::memory_order_relaxed)) return;
697 if (path_ == Path::Pull) return;
698 path_ = Path::Adaptor;
699 DenormalGuard guard;
700
701 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
702 const int nS = buffer.getNumSamples();
703 if (nS <= 0) return;
704
705 // Input is taken into the queue in step with the output leaving it,
706 // never a whole block ahead. That is what the device physically is -
707 // one sample in for every sample out - and it is also what makes the
708 // result independent of the host's block size: every decision below
709 // is taken at a position in the stream where the queue holds exactly
710 // the input the stream has delivered by then, whether the host sends
711 // one sample at a time or four thousand.
712 int taken = 0; // samples of this block already queued
713 int i = 0; // output samples already written
714 while (i < nS)
715 {
716 takeInput(buffer, nCh, taken, i);
717
718 const int64_t avail = eWriteHead() - readPos_;
719 if (avail > 0)
720 {
721 const int take = static_cast<int>(
722 std::min<int64_t>(avail, static_cast<int64_t>(nS - i)));
723 takeInput(buffer, nCh, taken, i + take);
724 readSynthesis(buffer, nCh, i, take);
725 readPos_ += take;
726 i += take;
727 continue;
728 }
729
730 // More synthesis is needed: run one analysis frame. Below a ratio
731 // of 1 the stretch wants more input than the stream has brought,
732 // and no amount of buffering invents it: the output waits, in
733 // silence, for exactly as many samples as are missing, and picks
734 // up where it left off.
735 const int need = eSamplesToNextHop();
736 if (need > queued_)
737 {
738 const int silence = std::min(need - queued_, nS - i);
739 // Take the matching input FIRST: the block is about to be
740 // overwritten with silence, and a sample the stream delivered
741 // must be queued whether or not anything came out against it.
742 takeInput(buffer, nCh, taken, i + silence);
743 for (int ch = 0; ch < nCh; ++ch)
744 std::fill(buffer.getChannel(ch) + i, buffer.getChannel(ch) + i + silence, T(0));
745 i += silence;
746 continue;
747 }
748
749 for (int ch = 0; ch < nCh; ++ch)
750 {
751 dequeueInto(ch, need);
752 ePushInput(ch, feed_.data(), need);
753 }
754 eCommitInput(need, nCh);
755 queueRead_ = (queueRead_ + need) & queueMask_;
756 queued_ -= need;
757 openIntake();
758 }
759 }
760
776 {
777 const int inLen = in.getNumSamples();
778 const int inCh = in.getNumChannels();
779
780 if (!prepared_.load(std::memory_order_relaxed) || inLen <= 0 || inCh <= 0)
781 {
782 out.resize(std::max(0, inCh), std::max(0, inLen));
783 for (int ch = 0; ch < inCh; ++ch)
784 std::copy(in.getChannel(ch), in.getChannel(ch) + inLen, out.getChannel(ch));
785 return;
786 }
787
788 const int nCh = std::min(inCh, numChannels_);
789 beginOffline(nCh);
790 pushOffline(in.getSubView(0, inLen));
792 const int outLen = static_cast<int>(offlineStop_ - offlineSkip_);
793 out.resize(inCh, std::max(0, outLen));
794 if (outLen > 0)
795 {
796 std::vector<T*> ptrs(static_cast<size_t>(nCh));
797 for (int ch = 0; ch < nCh; ++ch) ptrs[static_cast<size_t>(ch)] = out.getChannel(ch);
798 (void)pullOffline(AudioBufferView<T>(ptrs.data(), nCh, outLen));
799 }
800 offlineOpen_ = false;
801
802 // Channels the stretch does not cover would otherwise be left at the
803 // zero resize() gives them; carry them through instead, truncated or
804 // silence-padded to the stretched length.
805 for (int ch = nCh; ch < inCh; ++ch)
806 {
807 const T* src = in.getChannel(ch);
808 T* dst = out.getChannel(ch);
809 const int n = std::min(outLen, inLen);
810 std::copy(src, src + n, dst);
811 std::fill(dst + n, dst + outLen, T(0));
812 }
813 }
814
831 void beginOffline(int numChannels)
832 {
833 offlineOpen_ = false;
834 if (!prepared_.load(std::memory_order_relaxed)) return;
835 reset();
836 path_ = Path::Adaptor; // this path drives the same reader as the adaptor
837 offlineChannels_ = std::clamp(numChannels, 1, numChannels_);
838 offlineRatio_ = studioActive_ ? studio_.activeRatio() : engine_.activeRatio();
839 // A frame carries its content at its centre, so the first output
840 // sample that lines up with input sample 0 sits half an input frame
841 // plus half a stretched frame into the synthesis stream.
842 offlineSkip_ = studioActive_
843 ? std::llround(studio_.streamPositionOf(0.0)) - readPos_
844 : std::lround(0.5 * static_cast<double>(fftSize_) * (1.0 + offlineRatio_));
845 offlineStreamPos_ = 0;
846 offlineReceived_ = 0;
847 offlineStop_ = offlineSkip_;
848 offlineFinished_ = false;
849 offlineStaged_.assign(static_cast<size_t>(offlineChannels_), std::vector<T> {});
850 offlineStagedHead_ = 0;
851 offlineOut_.assign(static_cast<size_t>(offlineChannels_), std::vector<T> {});
852 offlineOutHead_ = 0;
853 offlineOpen_ = true;
854 }
855
858 {
859 if (!offlineOpen_ || offlineFinished_) return;
860 const int n = in.getNumSamples();
861 if (n <= 0) return;
862 for (int ch = 0; ch < offlineChannels_; ++ch)
863 {
864 auto& q = offlineStaged_[static_cast<size_t>(ch)];
865 if (ch < in.getNumChannels())
866 q.insert(q.end(), in.getChannel(ch), in.getChannel(ch) + n);
867 else
868 q.insert(q.end(), static_cast<size_t>(n), T(0));
869 }
870 offlineReceived_ += n;
871 runOffline();
872 }
873
877 {
878 if (!offlineOpen_ || offlineFinished_) return;
879 offlineFinished_ = true;
880 offlineStop_ = offlineSkip_ + static_cast<int64_t>(
881 std::lround(static_cast<double>(offlineReceived_) * offlineRatio_));
882 runOffline();
883 }
884
886 [[nodiscard]] int getOfflineAvailable() const noexcept
887 {
888 if (!offlineOpen_ || offlineOut_.empty()) return 0;
889 return static_cast<int>(offlineOut_[0].size() - offlineOutHead_);
890 }
891
898 {
899 const int n = std::min(out.getNumSamples(), getOfflineAvailable());
900 if (n <= 0) return 0;
901 for (int ch = 0; ch < offlineChannels_ && ch < out.getNumChannels(); ++ch)
902 {
903 const T* src = offlineOut_[static_cast<size_t>(ch)].data() + offlineOutHead_;
904 std::copy(src, src + n, out.getChannel(ch));
905 }
906 offlineOutHead_ += static_cast<size_t>(n);
907 if (offlineOutHead_ > 65536 && offlineOutHead_ * 2 > offlineOut_[0].size())
908 {
909 for (auto& q : offlineOut_)
910 q.erase(q.begin(), q.begin() + static_cast<std::ptrdiff_t>(offlineOutHead_));
911 offlineOutHead_ = 0;
912 }
913 return n;
914 }
915
916private:
927 void runOffline()
928 {
929 const int nCh = offlineChannels_;
930 for (;;)
931 {
932 if (offlineFinished_ && offlineStreamPos_ >= offlineStop_) break;
933 const int64_t avail = eWriteHead() - readPos_;
934 if (avail <= 0)
935 {
936 const int need = eSamplesToNextHop();
937 const int64_t staged = static_cast<int64_t>(offlineStaged_[0].size() - offlineStagedHead_);
938 if (!offlineFinished_ && staged < need) break; // wait for input
939 // Past the input's end the engine is flushed with silence.
940 for (int ch = 0; ch < nCh; ++ch)
941 {
942 const auto& q = offlineStaged_[static_cast<size_t>(ch)];
943 for (int k = 0; k < need; ++k)
944 {
945 const size_t idx = offlineStagedHead_ + static_cast<size_t>(k);
946 feed_[static_cast<size_t>(k)] = (idx < q.size()) ? q[idx] : T(0);
947 }
948 ePushInput(ch, feed_.data(), need);
949 }
950 eCommitInput(need, nCh);
951 offlineStagedHead_ = std::min(offlineStagedHead_ + static_cast<size_t>(need),
952 offlineStaged_[0].size());
953 if (offlineStagedHead_ > 65536)
954 {
955 for (auto& q : offlineStaged_)
956 q.erase(q.begin(), q.begin() + static_cast<std::ptrdiff_t>(offlineStagedHead_));
957 offlineStagedHead_ = 0;
958 }
959 continue;
960 }
961
962 const int64_t limit = offlineFinished_
963 ? offlineStop_
964 : offlineSkip_ + static_cast<int64_t>(
965 std::lround(static_cast<double>(offlineReceived_) * offlineRatio_));
966 const int64_t take = std::min(avail, limit - offlineStreamPos_);
967 if (take <= 0) break;
968 const int64_t from = std::max(offlineStreamPos_, offlineSkip_);
969 const int64_t count = offlineStreamPos_ + take - from;
970 if (count > 0)
971 {
972 for (int ch = 0; ch < nCh; ++ch)
973 {
974 const T* acc = eOla(ch);
975 auto& q = offlineOut_[static_cast<size_t>(ch)];
976 const int64_t rp = readPos_ + (from - offlineStreamPos_);
977 for (int64_t k = 0; k < count; ++k)
978 q.push_back(acc[static_cast<size_t>((rp + k) & accumMask_)]);
979 }
980 }
981 readPos_ += take;
982 offlineStreamPos_ += take;
983 }
984 }
985
986private:
988 void publishEngineParams() noexcept
989 {
990 typename detail::PhaseVocoderEngine<T>::Params p;
991 // The engine carries its stretch target logarithmically, because the
992 // same core drives a pitch shifter where semitones are the natural
993 // unit. Ratio and semitones are the same number in two spellings.
994 p.targetSemitones = 12.0 * std::log2(static_cast<double>(
995 timeRatio_.load(std::memory_order_relaxed)));
996 p.transientPreserve = transientPreserve_.load(std::memory_order_relaxed);
997 p.formantPreserve = false; // nothing resamples the output here
998 p.phaseLock = phaseLock_.load(std::memory_order_relaxed);
999 p.percussiveSplit = false; // no public switch selects the split here
1000 engine_.publishParams(p);
1001
1002 typename detail::StudioVocoder<T>::Params q;
1003 q.targetSemitones = p.targetSemitones;
1004 q.transientPreserve = p.transientPreserve;
1005 q.formantPreserve = false;
1006 studio_.publishParams(q);
1007 }
1008
1030 void openIntake() noexcept
1031 {
1032 intakeSpan_ = std::max(1, synthHop_);
1033 const double ratio = studioActive_ ? studio_.activeRatio() : engine_.activeRatio();
1034 // Never wider than the queue: above unity the window is bounded by
1035 // one analysis hop and the queue holds several frames, so this
1036 // cannot bind there, and stating it is what keeps the loop below
1037 // total.
1038 intakeWant_ = ratio <= 1.0
1039 ? queueSize_ - queued_
1040 : std::clamp(eSamplesToNextHop() - queued_, 0, queueSize_ - queued_);
1041 intakeTaken_ = 0;
1042 intakeErr_ = 0;
1043 }
1044
1046 [[nodiscard]] bool acceptOffered() noexcept
1047 {
1048 if (intakeTaken_ >= intakeWant_) return false;
1049 if (intakeWant_ < intakeSpan_)
1050 {
1051 intakeErr_ += intakeWant_;
1052 if (intakeErr_ < intakeSpan_) return false;
1053 intakeErr_ -= intakeSpan_;
1054 }
1055 ++intakeTaken_;
1056 return true;
1057 }
1058
1069 void takeInput(AudioBufferView<T> buffer, int nCh, int& taken, int upTo) noexcept
1070 {
1071 if (upTo <= taken) return;
1072 int wp = queueWrite_;
1073 int held = queued_;
1074 for (int k = taken; k < upTo; ++k)
1075 {
1076 if (!acceptOffered()) { ++discarded_; continue; }
1077 for (int ch = 0; ch < nCh; ++ch)
1078 queue_[static_cast<size_t>(ch)][static_cast<size_t>(wp)]
1079 = buffer.getChannel(ch)[k];
1080 wp = (wp + 1) & queueMask_;
1081 ++held;
1082 }
1083 queueWrite_ = wp;
1084 queued_ = held;
1085 taken = upTo;
1086 }
1087
1090 [[nodiscard]] bool claimPullPath() noexcept
1091 {
1092 if (path_ == Path::Pull) return true;
1093 if (path_ != Path::None) return false;
1094 path_ = Path::Pull;
1095 // This path's output is the stretched timeline itself, so it starts
1096 // at the first synthesis sample whose overlap-add is complete: one
1097 // frame past the write head, less the analysis hop the first frame
1098 // consumes. Nothing has been fed yet, so that hop is the one the
1099 // engine reports now.
1100 readPos_ = studioActive_
1101 ? static_cast<int64_t>(std::llround(studio_.streamPositionOf(0.0)))
1102 : eWriteHead() + static_cast<int64_t>(fftSize_ - eSamplesToNextHop());
1103 return true;
1104 }
1105
1108 void runReadyFrames() noexcept
1109 {
1110 for (;;)
1111 {
1112 const int need = eSamplesToNextHop();
1113 if (queued_ < need) return;
1114 // A frame writes one whole window ahead of the head; stop before
1115 // it would reach unread output.
1116 if (eWriteHead() + static_cast<int64_t>(fftSize_) - readPos_
1117 > static_cast<int64_t>(accumMask_) + 1)
1118 return;
1119
1120 for (int ch = 0; ch < numChannels_; ++ch)
1121 {
1122 dequeueInto(ch, need);
1123 ePushInput(ch, feed_.data(), need);
1124 }
1125 eCommitInput(need, numChannels_);
1126 queueRead_ = (queueRead_ + need) & queueMask_;
1127 queued_ -= need;
1128 }
1129 }
1130
1134 void dequeueInto(int ch, int count) noexcept
1135 {
1136 const auto& q = queue_[static_cast<size_t>(ch)];
1137 int rp = queueRead_;
1138 for (int k = 0; k < count; ++k)
1139 {
1140 feed_[static_cast<size_t>(k)] = q[static_cast<size_t>(rp)];
1141 rp = (rp + 1) & queueMask_;
1142 }
1143 }
1144
1146 void readSynthesis(AudioBufferView<T> buffer, int nCh, int offset, int count) noexcept
1147 {
1148 for (int ch = 0; ch < nCh; ++ch)
1149 {
1150 const T* acc = eOla(ch);
1151 T* dst = buffer.getChannel(ch) + offset;
1152 const int64_t rp = readPos_;
1153 for (int k = 0; k < count; ++k)
1154 dst[k] = acc[static_cast<size_t>((rp + k) & accumMask_)];
1155 }
1156 }
1157
1158 // -- Engine dispatch (the engine in force is chosen at reset()) ---------------
1159
1160 [[nodiscard]] int64_t eWriteHead() const noexcept
1161 {
1162 return studioActive_ ? studio_.writeHead() : engine_.writeHead();
1163 }
1164 [[nodiscard]] int eSamplesToNextHop() const noexcept
1165 {
1166 return studioActive_ ? studio_.samplesToNextHop() : engine_.samplesToNextHop();
1167 }
1168 void ePushInput(int ch, const T* src, int count) noexcept
1169 {
1170 if (studioActive_) studio_.pushInput(ch, src, count);
1171 else engine_.pushInput(ch, src, count);
1172 }
1173 void eCommitInput(int count, int nCh) noexcept
1174 {
1175 if (studioActive_) studio_.commitInput(count, nCh);
1176 else engine_.commitInput(count, nCh);
1177 }
1178 [[nodiscard]] const T* eOla(int ch) const noexcept
1179 {
1180 return studioActive_ ? studio_.olaData(ch) : engine_.olaData(ch);
1181 }
1182
1184 [[nodiscard]] static int studioDefaultFrame(double sampleRate) noexcept
1185 {
1186 int n = 256;
1187 while (n < (1 << 16) && static_cast<double>(n) * 1.5 < 0.085 * sampleRate) n <<= 1;
1188 return n;
1189 }
1190
1191 // -- Members -------------------------------------------------------------------
1192 int numChannels_ = 0;
1193 std::atomic<bool> prepared_ { false };
1194
1195 int fftSize_ = 2048;
1196 int synthHop_ = 512;
1197 int latency_ = 2048;
1198 int64_t accumMask_ = 8191;
1199
1200 detail::PhaseVocoderEngine<T> engine_;
1201 detail::StudioVocoder<T> studio_;
1202 bool studioActive_ = true;
1203 int fftStandard_ = 2048;
1204 int fftStudio_ = 4096;
1205
1206 std::vector<std::vector<T>> queue_;
1207 std::vector<T> feed_;
1208 int queueSize_ = 0;
1209 int queueMask_ = 0;
1210 int queueWrite_ = 0;
1211 int queueRead_ = 0;
1212 int queued_ = 0;
1213
1215 enum class Path { None, Adaptor, Pull };
1216 Path path_ = Path::None;
1217
1218 int64_t discarded_ = 0;
1219 int intakeSpan_ = 0;
1220 int intakeWant_ = 0;
1221 int intakeTaken_ = 0;
1222 int intakeErr_ = 0;
1223
1224 int64_t readPos_ = 0;
1225
1226 std::atomic<Quality> quality_ { Quality::Studio };
1227 std::atomic<T> timeRatio_ { T(1) };
1228 std::atomic<bool> transientPreserve_ { true };
1229 std::atomic<bool> phaseLock_ { true };
1230
1231 // Offline session (beginOffline() .. finishOffline(), pullOffline()).
1232 bool offlineOpen_ = false;
1233 bool offlineFinished_ = false;
1234 int offlineChannels_ = 0;
1235 double offlineRatio_ = 1.0;
1236 int64_t offlineSkip_ = 0;
1237 int64_t offlineStop_ = 0;
1238 int64_t offlineStreamPos_ = 0;
1239 int64_t offlineReceived_ = 0;
1240 std::vector<std::vector<T>> offlineStaged_;
1241 size_t offlineStagedHead_ = 0;
1242 std::vector<std::vector<T>> offlineOut_;
1243 size_t offlineOutHead_ = 0;
1244};
1245
1246} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
int getNumSamples() const noexcept
Returns the number of samples per channel.
int getNumChannels() const noexcept
Returns the number of channels in this view.
T * getChannel(int ch) const noexcept
Returns a pointer to the sample data for the given channel.
AudioBufferView getSubView(int startSample, int length) const noexcept
Returns a zero-copy sub-view starting at an offset.
Owning audio buffer with contiguous, 32-byte aligned storage.
T * getChannel(int ch) noexcept
Returns a pointer to the sample data.
void resize(int numChannels, int numSamples)
Allocates the buffer for the given dimensions.
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:161
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:204
bool isValid() const noexcept
Definition StateBlob.h:199
uint32_t processorId() const noexcept
Definition StateBlob.h:200
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:53
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:105
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:71
Real-time and offline time stretching, 0.5x to 2x, pitch unchanged.
int getInputCapacity() const noexcept
Room for input right now, in samples (stream owner).
void setTimeRatio(T ratio) noexcept
Sets the stretch as an output/input length ratio.
int getAvailableOutput() const noexcept
Stretched output ready right now, in samples (stream owner).
void processBlock(AudioBufferView< T > buffer) noexcept
Fixed-rate playback adaptor: streaming, in-place, real-time safe.
void prepare(const AudioSpec &spec, int fftSize=0)
Allocates every buffer this class will ever use.
T getTimeRatio() const noexcept
void process(AudioBufferView< const T > in, AudioBuffer< T > &out)
Offline whole-signal stretch (setup thread; allocates out).
Quality
The engine that renders the stretch (see the file overview).
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
int feedInput(AudioBufferView< const T > in) noexcept
Hands input to the stretch; real-time safe (stream owner).
int getQueuedInputSamples() const noexcept
Input samples accepted but not yet consumed by the stretch.
int64_t getDiscardedInput() const noexcept
Input samples the fixed-rate adaptor refused, cumulative since prepare() or reset() (stream owner).
void setTempoChangePercent(T pct) noexcept
Sets the stretch as a tempo change in percent.
void finishOffline()
Ends the input of an offline session; everything left becomes available to pullOffline().
int getLatency() const noexcept
Latency of the fixed-rate processBlock() path in samples: one frame (42.7 ms at the default 2048 fram...
static constexpr T kMinRatio
Smallest and largest stretch this class accepts.
Quality getQuality() const noexcept
int pullOffline(AudioBufferView< T > out)
Writes up to out.getNumSamples() stretched samples of an offline session and returns how many it wrot...
bool getTransientPreserve() const noexcept
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
int pullOutput(AudioBufferView< T > out) noexcept
Takes stretched output; real-time safe (stream owner).
int getOfflineAvailable() const noexcept
Stretched samples ready to pull in an offline session.
static constexpr T kMaxRatio
void setQuality(Quality q) noexcept
Selects the engine (applied at the next prepare()/reset()).
void beginOffline(int numChannels)
Opens an offline stretch fed in blocks (setup thread; allocates).
void pushOffline(AudioBufferView< const T > in)
Feeds the next block of an offline session. No-op outside one.
bool getPhaseLock() const noexcept
void setTransientPreserve(bool on) noexcept
Enables phase reset on detected transients (default on). Without it, attacks are stretched along with...
void setPhaseLock(bool on) noexcept
Enables identity phase locking (default on).
void reset() noexcept
Clears all signal state and empties the input queue (keeps parameters). Belongs to the owner of the s...
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:651
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:53
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45