DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
SpectralFreeze.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
27#include "../Core/AudioBuffer.h"
28#include "../Core/AudioSpec.h"
29#include "../Core/DspMath.h"
30#include "../Core/SpectralProcessor.h"
31#include "../Core/StateBlob.h"
32
33#include <algorithm>
34#include <array>
35#include <atomic>
36#include <cmath>
37#include <cstddef>
38#include <cstdint>
39#include <limits>
40#include <numbers>
41#include <utility>
42#include <vector>
43
44#if !defined(DSPARK_NO_EXCEPTIONS)
45#include <stdexcept>
46#endif
47
48namespace dspark {
49
55template <FloatType T>
56class SpectralFreeze final
57{
58public:
60 enum class PhaseMode : std::uint8_t { Tonal, Diffuse };
61
70 void prepare(const AudioSpec& spec, int fftSize = 2048, int hopSize = 0)
71 {
72 if (!validSpec(spec)) return;
73
74 const int newFftSize = sanitizeFftSize(fftSize);
75 const int newHopSize = sanitizeHopSize(newFftSize, hopSize);
76 const int newBins = newFftSize / 2 + 1;
77 const int channels = spec.numChannels;
78
79 // Build the complete replacement off to the side. A valid prepare is
80 // setup-only; if an allocation throws, the prior running instance has
81 // not been partially resized.
82 SpectralProcessor<T> nextProcessor;
83 nextProcessor.prepare(spec, newFftSize, newHopSize);
84
85 const std::size_t channelBins = checkedProduct(
86 static_cast<std::size_t>(channels), static_cast<std::size_t>(newBins));
87 const std::size_t complexChannelBins = checkedProduct(channelBins, 2u);
88
89 std::vector<T> nextScratch(checkedProduct(
90 static_cast<std::size_t>(channels),
91 static_cast<std::size_t>(spec.maxBlockSize)), T(0));
92 std::vector<T> nextRaw(checkedProduct(complexChannelBins, 3u), T(0));
93 std::vector<T> nextMagnitude(channelBins, T(0));
94 std::vector<T> nextCapturedPhase(channelBins, T(0));
95 std::vector<T> nextFrozenPhase(channelBins, T(0));
96 std::vector<T> nextPower(static_cast<std::size_t>(newBins), T(0));
97 std::vector<T> nextOmega(static_cast<std::size_t>(newBins), T(0));
98 std::vector<T> nextOmegaImag(static_cast<std::size_t>(newBins), T(0));
99 std::vector<std::uint32_t> nextActive(
100 static_cast<std::size_t>(newBins), 0u);
101 std::vector<std::uint32_t> nextPeak(
102 static_cast<std::size_t>(newBins), 0u);
103 std::vector<std::uint32_t> nextRegion(
104 static_cast<std::size_t>(newBins), 0u);
105
106 processor_ = std::move(nextProcessor);
107 scratch_ = std::move(nextScratch);
108 rawSpectra_ = std::move(nextRaw);
109 capturedMagnitude_ = std::move(nextMagnitude);
110 capturedPhase_ = std::move(nextCapturedPhase);
111 frozenPhase_ = std::move(nextFrozenPhase);
112 binPower_ = std::move(nextPower);
113 omega_ = std::move(nextOmega);
114 omegaImag_ = std::move(nextOmegaImag);
115 active_ = std::move(nextActive);
116 peak_ = std::move(nextPeak);
117 regionPeak_ = std::move(nextRegion);
118
119 spec_ = spec;
120 fftSize_ = newFftSize;
121 hopSize_ = newHopSize;
122 numBins_ = newBins;
123 prepared_ = true;
124 rebuildScratchPointers();
125 clearStreamState(false);
126 }
127
132 void reset() noexcept
133 {
134 if (!prepared_) return;
135 clearStreamState(true);
136 }
137
139 void setFrozen(bool frozen) noexcept
140 {
141 if (frozen)
142 requested_.fetch_or(kFrozenBit, std::memory_order_relaxed);
143 else
144 requested_.fetch_and(~kFrozenBit, std::memory_order_relaxed);
145 }
146
148 [[nodiscard]] bool isFrozen() const noexcept
149 {
150 return (requested_.load(std::memory_order_relaxed) & kFrozenBit) != 0u;
151 }
152
157 void setPhaseMode(PhaseMode mode) noexcept
158 {
159 if (mode == PhaseMode::Diffuse)
160 requested_.fetch_or(kDiffuseBit, std::memory_order_relaxed);
161 else
162 requested_.fetch_and(~kDiffuseBit, std::memory_order_relaxed);
163 }
164
166 [[nodiscard]] PhaseMode getPhaseMode() const noexcept
167 {
168 return (requested_.load(std::memory_order_relaxed) & kDiffuseBit) != 0u
171 }
172
176 [[nodiscard]] std::vector<uint8_t> getState() const
177 {
178 StateWriter w(stateId("SFRZ"), 1);
179 w.write("frozen", isFrozen());
180 w.write("diffuse", getPhaseMode() == PhaseMode::Diffuse);
181 return w.blob();
182 }
183
187 bool setState(const uint8_t* data, size_t size)
188 {
189 StateReader r(data, size);
190 if (!r.isValid() || r.processorId() != stateId("SFRZ")) return false;
191 setFrozen(r.read("frozen", false));
192 setPhaseMode(r.read("diffuse", false) ? PhaseMode::Diffuse : PhaseMode::Tonal);
193 return true;
194 }
195
197 [[nodiscard]] int getLatency() const noexcept
198 {
199 return prepared_ ? fftSize_ : 0;
200 }
201
203 [[nodiscard]] int getFFTSize() const noexcept
204 {
205 return prepared_ ? fftSize_ : 0;
206 }
207
209 [[nodiscard]] int getHopSize() const noexcept
210 {
211 return prepared_ ? hopSize_ : 0;
212 }
213
215 [[nodiscard]] int getTransitionSamples() const noexcept
216 {
217 return prepared_ ? fftSize_ : 0;
218 }
219
227 void processBlock(AudioBufferView<T> buffer) noexcept
228 {
229 if (!prepared_ || buffer.getNumSamples() <= 0) return;
230
231 const int runtimeChannels = buffer.getNumChannels();
232 const int processedChannels = std::min(runtimeChannels, spec_.numChannels);
233 const int totalSamples = buffer.getNumSamples();
234
235 int offset = 0;
236 while (offset < totalSamples)
237 {
238 const int block = std::min(spec_.maxBlockSize, totalSamples - offset);
239
240 for (int ch = 0; ch < spec_.numChannels; ++ch)
241 {
242 T* const work = scratchChannels_[static_cast<std::size_t>(ch)];
243 if (ch < processedChannels && buffer.getChannel(ch) != nullptr)
244 {
245 const T* const source = buffer.getChannel(ch) + offset;
246 for (int i = 0; i < block; ++i)
247 work[i] = std::isfinite(source[i]) ? source[i] : T(0);
248 }
249 else
250 {
251 std::fill_n(work, block, T(0));
252 }
253 }
254
255 AudioBufferView<T> workView(scratchChannels_.data(),
256 spec_.numChannels, block);
257 auto callback = [this](T* spectrum, int bins) noexcept {
258 processSpectrum(spectrum, bins);
259 };
260 processor_.processBlock(workView, callback);
261
262 for (int ch = 0; ch < processedChannels; ++ch)
263 {
264 T* const destination = buffer.getChannel(ch);
265 if (destination == nullptr) continue;
266 const T* const work = scratchChannels_[static_cast<std::size_t>(ch)];
267 for (int i = 0; i < block; ++i)
268 destination[offset + i] = std::isfinite(work[i]) ? work[i] : T(0);
269 }
270
271 offset += block;
272 }
273 }
274
275private:
276 static constexpr std::uint32_t kFrozenBit = 1u;
277 static constexpr std::uint32_t kDiffuseBit = 2u;
278 static constexpr int kMinFftSize = 256;
279 static constexpr int kMaxFftSize = 32768;
280 static constexpr int kMaxBlockSize = 65536;
281 static constexpr int kMaxChannels = 16;
282 static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
283
284 [[nodiscard]] static bool validSpec(const AudioSpec& spec) noexcept
285 {
286 return std::isfinite(spec.sampleRate) && spec.sampleRate > 0.0
287 && spec.maxBlockSize >= 1 && spec.maxBlockSize <= kMaxBlockSize
288 && spec.numChannels >= 1 && spec.numChannels <= kMaxChannels;
289 }
290
291 [[nodiscard]] static int sanitizeFftSize(int requested) noexcept
292 {
293 requested = std::clamp(requested, kMinFftSize, kMaxFftSize);
294 int result = kMinFftSize;
295 while (result < requested) result <<= 1;
296 return result;
297 }
298
299 [[nodiscard]] static int sanitizeHopSize(int fftSize, int requested) noexcept
300 {
301 if (requested == fftSize / 8 || requested == fftSize / 4
302 || requested == fftSize / 2)
303 return requested;
304 return fftSize / 4;
305 }
306
307 [[nodiscard]] static std::size_t checkedProduct(std::size_t a,
308 std::size_t b)
309 {
310 // All public dimensions are tightly capped; this guard documents the
311 // allocation arithmetic and remains valid if the caps later change.
312 if (a != 0u && b > std::numeric_limits<std::size_t>::max() / a)
313 {
314#if defined(DSPARK_NO_EXCEPTIONS)
315 return 0u;
316#else
317 throw std::length_error("SpectralFreeze allocation size overflow");
318#endif
319 }
320 return a * b;
321 }
322
323 void rebuildScratchPointers() noexcept
324 {
325 scratchChannels_.fill(nullptr);
326 for (int ch = 0; ch < spec_.numChannels; ++ch)
327 {
328 scratchChannels_[static_cast<std::size_t>(ch)] = scratch_.data()
329 + static_cast<std::size_t>(ch)
330 * static_cast<std::size_t>(spec_.maxBlockSize);
331 }
332 }
333
334 void clearStreamState(bool resetProcessor) noexcept
335 {
336 if (resetProcessor) processor_.reset();
337 std::fill(scratch_.begin(), scratch_.end(), T(0));
338 std::fill(rawSpectra_.begin(), rawSpectra_.end(), T(0));
339 std::fill(capturedMagnitude_.begin(), capturedMagnitude_.end(), T(0));
340 std::fill(capturedPhase_.begin(), capturedPhase_.end(), T(0));
341 std::fill(frozenPhase_.begin(), frozenPhase_.end(), T(0));
342 std::fill(binPower_.begin(), binPower_.end(), T(0));
343 std::fill(omega_.begin(), omega_.end(), T(0));
344 std::fill(omegaImag_.begin(), omegaImag_.end(), T(0));
345 std::fill(active_.begin(), active_.end(), 0u);
346 std::fill(peak_.begin(), peak_.end(), 0u);
347 std::fill(regionPeak_.begin(), regionPeak_.end(), 0u);
348
349 olderBank_ = 0;
350 latestBank_ = 1;
351 currentBank_ = 2;
352 completedFrames_ = 0;
353 callbackChannel_ = 0;
354 q_ = 0;
355 frameQ_ = 0;
356 frameRequestWord_ = 0u;
357 captured_ = false;
358 capturedMode_ = PhaseMode::Tonal;
359 magnitudeFloor_ = T(0);
360 nextDiffuseGeneration_ = 0u;
361 diffuseGeneration_ = 0u;
362 diffuseFrame_ = 0u;
363 tonalAdvanceCount_ = 0u;
364 }
365
366 [[nodiscard]] std::size_t rawOffset(int bank, int channel) const noexcept
367 {
368 return (static_cast<std::size_t>(bank)
369 * static_cast<std::size_t>(spec_.numChannels)
370 + static_cast<std::size_t>(channel))
371 * static_cast<std::size_t>(numBins_ * 2);
372 }
373
374 [[nodiscard]] std::size_t channelBinOffset(int channel) const noexcept
375 {
376 return static_cast<std::size_t>(channel)
377 * static_cast<std::size_t>(numBins_);
378 }
379
380 [[nodiscard]] static T principalArgument(T phase) noexcept
381 {
382 constexpr T p = std::numbers::pi_v<T>;
383 constexpr T tau = T(2) * p;
384 T wrapped = std::fmod(phase + p, tau);
385 if (wrapped < T(0)) wrapped += tau;
386 return wrapped - p;
387 }
388
389 [[nodiscard]] static T phaseOf(T real, T imag) noexcept
390 {
391 return std::atan2(imag, real);
392 }
393
394 [[nodiscard]] static T safeMagnitude(T real, T imag) noexcept
395 {
396 return std::hypot(real, imag);
397 }
398
399 void processSpectrum(T* spectrum, int bins) noexcept
400 {
401 if (callbackChannel_ == 0) beginFrame();
402
403 const int channel = callbackChannel_;
404 T* const raw = rawSpectra_.data() + rawOffset(currentBank_, channel);
405 const int usableBins = std::min(bins, numBins_);
406 for (int k = 0; k < usableBins; ++k)
407 {
408 T real = spectrum[2 * k];
409 T imag = spectrum[2 * k + 1];
410 if (!std::isfinite(real) || !std::isfinite(imag))
411 {
412 real = T(0);
413 imag = T(0);
414 spectrum[2 * k] = T(0);
415 spectrum[2 * k + 1] = T(0);
416 }
417 raw[2 * k] = real;
418 raw[2 * k + 1] = imag;
419 }
420
421 if (frameQ_ > 0 && captured_)
422 blendFrozenSpectrum(spectrum, channel);
423
424 ++callbackChannel_;
425 if (callbackChannel_ == spec_.numChannels)
426 {
427 callbackChannel_ = 0;
428 completeFrame();
429 }
430 }
431
432 void beginFrame() noexcept
433 {
434 // This is the audio owner's single requested-control load for the
435 // complete all-channel frame.
436 frameRequestWord_ = requested_.load(std::memory_order_relaxed);
437
438 if (q_ == 0)
439 {
440 if ((frameRequestWord_ & kFrozenBit) == 0u)
441 {
442 captured_ = false;
443 }
444 else if (!captured_ && completedFrames_ >= 2)
445 {
446 capturedMode_ = (frameRequestWord_ & kDiffuseBit) != 0u
448 : PhaseMode::Tonal;
449 captureLatestFrame();
450 captured_ = true;
451
452 if (capturedMode_ == PhaseMode::Tonal)
453 advanceTonalPhase();
454 else
455 diffuseFrame_ = 0u;
456 }
457 }
458
459 frameQ_ = q_;
460 if (frameQ_ > 0 && captured_
461 && capturedMode_ == PhaseMode::Diffuse)
462 prepareDiffuseFrame();
463 }
464
465 void completeFrame() noexcept
466 {
467 const int recycled = olderBank_;
468 olderBank_ = latestBank_;
469 latestBank_ = currentBank_;
470 currentBank_ = recycled;
471 completedFrames_ = std::min(2, completedFrames_ + 1);
472
473 if (!captured_) return;
474
475 if ((frameRequestWord_ & kFrozenBit) != 0u)
476 {
477 if (q_ < fftSize_ / hopSize_) ++q_;
478 }
479 else if (q_ > 0)
480 {
481 --q_;
482 }
483
484 if (q_ == 0 && (frameRequestWord_ & kFrozenBit) == 0u)
485 {
486 captured_ = false;
487 return;
488 }
489
490 if (capturedMode_ == PhaseMode::Tonal)
491 advanceTonalPhase();
492 else if (frameQ_ > 0)
493 ++diffuseFrame_;
494 }
495
496 void captureLatestFrame() noexcept
497 {
498 std::fill(active_.begin(), active_.end(), 0u);
499 std::fill(peak_.begin(), peak_.end(), 0u);
500 std::fill(regionPeak_.begin(), regionPeak_.end(), 0u);
501 std::fill(binPower_.begin(), binPower_.end(), T(0));
502 std::fill(omega_.begin(), omega_.end(), T(0));
503
504 long double globalScale = 0.0L;
505 for (int ch = 0; ch < spec_.numChannels; ++ch)
506 {
507 const T* const latest = rawSpectra_.data() + rawOffset(latestBank_, ch);
508 for (int k = 0; k < numBins_; ++k)
509 {
510 globalScale = std::max(globalScale,
511 std::abs(static_cast<long double>(latest[2 * k])));
512 globalScale = std::max(globalScale,
513 std::abs(static_cast<long double>(latest[2 * k + 1])));
514 }
515 }
516
517 long double maxPower = 0.0L;
518 if (globalScale > 0.0L)
519 {
520 for (int k = 0; k < numBins_; ++k)
521 {
522 long double power = 0.0L;
523 for (int ch = 0; ch < spec_.numChannels; ++ch)
524 {
525 const T* const latest = rawSpectra_.data()
526 + rawOffset(latestBank_, ch);
527 const long double real =
528 static_cast<long double>(latest[2 * k]) / globalScale;
529 const long double imag =
530 static_cast<long double>(latest[2 * k + 1]) / globalScale;
531 power += real * real + imag * imag;
532 }
533 binPower_[static_cast<std::size_t>(k)] = static_cast<T>(power);
534 maxPower = std::max(maxPower, power);
535 }
536 }
537
538 const long double epsilon =
539 static_cast<long double>(std::numeric_limits<T>::epsilon());
540 const long double minimumNormal =
541 static_cast<long double>(std::numeric_limits<T>::min());
542 const long double minimumMagnitude = std::sqrt(minimumNormal);
543 long double normalizedPowerFloor = 0.0L;
544 if (globalScale > 0.0L)
545 {
546 normalizedPowerFloor = 64.0L * epsilon * maxPower;
547 const long double relativeMagnitudeFloor = globalScale
548 * std::sqrt(normalizedPowerFloor);
549 magnitudeFloor_ = static_cast<T>(std::max(
550 relativeMagnitudeFloor, minimumMagnitude));
551 }
552 else
553 {
554 magnitudeFloor_ = static_cast<T>(minimumMagnitude);
555 }
556
557 const long double normalizedMagnitudeFloor =
558 std::sqrt(normalizedPowerFloor);
559 for (int k = 1; k < numBins_ - 1; ++k)
560 {
561 const long double normalizedMagnitude = std::sqrt(
562 static_cast<long double>(binPower_[static_cast<std::size_t>(k)]));
563 const long double rawMagnitude = globalScale * normalizedMagnitude;
564 active_[static_cast<std::size_t>(k)] =
565 normalizedMagnitude > normalizedMagnitudeFloor
566 && rawMagnitude > minimumMagnitude
567 ? 1u
568 : 0u;
569 }
570
571 for (int ch = 0; ch < spec_.numChannels; ++ch)
572 {
573 const T* const latest = rawSpectra_.data() + rawOffset(latestBank_, ch);
574 const std::size_t base = channelBinOffset(ch);
575 capturedMagnitude_[base] = latest[0];
576 capturedPhase_[base] = T(1);
577 frozenPhase_[base] = T(0);
578 const int nyquist = numBins_ - 1;
579 capturedMagnitude_[base + static_cast<std::size_t>(nyquist)] =
580 latest[2 * nyquist];
581 capturedPhase_[base + static_cast<std::size_t>(nyquist)] = T(1);
582 frozenPhase_[base + static_cast<std::size_t>(nyquist)] = T(0);
583
584 for (int k = 1; k < nyquist; ++k)
585 {
586 const std::size_t index = base + static_cast<std::size_t>(k);
587 const T magnitude = active_[static_cast<std::size_t>(k)] != 0u
588 ? safeMagnitude(latest[2 * k], latest[2 * k + 1])
589 : T(0);
590 capturedMagnitude_[index] = magnitude;
591 if (magnitude > T(0))
592 {
593 capturedPhase_[index] = latest[2 * k] / magnitude;
594 frozenPhase_[index] = latest[2 * k + 1] / magnitude;
595 }
596 else
597 {
598 capturedPhase_[index] = T(1);
599 frozenPhase_[index] = T(0);
600 }
601 }
602 }
603
604 if (capturedMode_ == PhaseMode::Tonal)
605 buildTonalRegions();
606
607 diffuseGeneration_ = nextDiffuseGeneration_++;
608 diffuseFrame_ = 0u;
609 tonalAdvanceCount_ = 0u;
610 }
611
612 void buildTonalRegions() noexcept
613 {
614 int peakCount = 0;
615 for (int k = 1; k < numBins_ - 1; ++k)
616 {
617 const std::size_t index = static_cast<std::size_t>(k);
618 if (active_[index] != 0u
619 && binPower_[index] > binPower_[index - 1u]
620 && binPower_[index] >= binPower_[index + 1u])
621 {
622 peak_[index] = 1u;
623 ++peakCount;
624 }
625 }
626
627 if (peakCount == 0)
628 {
629 for (int k = 1; k < numBins_ - 1; ++k)
630 {
631 const std::size_t index = static_cast<std::size_t>(k);
632 if (active_[index] != 0u)
633 {
634 peak_[index] = 1u;
635 regionPeak_[index] = static_cast<std::uint32_t>(k);
636 }
637 }
638 }
639 else
640 {
641 int currentPeak = -1;
642 int nextPeak = -1;
643 for (int k = 1; k < numBins_ - 1; ++k)
644 {
645 if (peak_[static_cast<std::size_t>(k)] != 0u)
646 {
647 currentPeak = k;
648 break;
649 }
650 }
651 for (int k = currentPeak + 1; k < numBins_ - 1; ++k)
652 {
653 if (peak_[static_cast<std::size_t>(k)] != 0u)
654 {
655 nextPeak = k;
656 break;
657 }
658 }
659
660 for (int k = 1; k < numBins_ - 1; ++k)
661 {
662 if (active_[static_cast<std::size_t>(k)] == 0u) continue;
663 while (nextPeak >= 0 && k > (currentPeak + nextPeak) / 2)
664 {
665 currentPeak = nextPeak;
666 nextPeak = -1;
667 for (int p = currentPeak + 1; p < numBins_ - 1; ++p)
668 {
669 if (peak_[static_cast<std::size_t>(p)] != 0u)
670 {
671 nextPeak = p;
672 break;
673 }
674 }
675 }
676 regionPeak_[static_cast<std::size_t>(k)] =
677 static_cast<std::uint32_t>(currentPeak);
678 }
679 }
680
681 for (int p = 1; p < numBins_ - 1; ++p)
682 {
683 if (peak_[static_cast<std::size_t>(p)] == 0u) continue;
684
685 long double binScale = 0.0L;
686 for (int ch = 0; ch < spec_.numChannels; ++ch)
687 {
688 const T* const latest = rawSpectra_.data()
689 + rawOffset(latestBank_, ch);
690 binScale = std::max(binScale,
691 std::abs(static_cast<long double>(latest[2 * p])));
692 binScale = std::max(binScale,
693 std::abs(static_cast<long double>(latest[2 * p + 1])));
694 }
695
696 long double sumWeight = 0.0L;
697 long double realResidual = 0.0L;
698 long double imagResidual = 0.0L;
699 const long double nominal = 2.0L * std::numbers::pi_v<long double>
700 * static_cast<long double>(p) * static_cast<long double>(hopSize_)
701 / static_cast<long double>(fftSize_);
702
703 if (binScale > 0.0L)
704 {
705 for (int ch = 0; ch < spec_.numChannels; ++ch)
706 {
707 const T* const latest = rawSpectra_.data()
708 + rawOffset(latestBank_, ch);
709 const T* const older = rawSpectra_.data()
710 + rawOffset(olderBank_, ch);
711 const long double real =
712 static_cast<long double>(latest[2 * p]) / binScale;
713 const long double imag =
714 static_cast<long double>(latest[2 * p + 1]) / binScale;
715 const long double weight = real * real + imag * imag;
716 const long double latestPhase = std::atan2(
717 static_cast<long double>(latest[2 * p + 1]),
718 static_cast<long double>(latest[2 * p]));
719 const long double olderPhase = std::atan2(
720 static_cast<long double>(older[2 * p + 1]),
721 static_cast<long double>(older[2 * p]));
722 const T residual = principalArgument(static_cast<T>(
723 latestPhase - olderPhase - nominal));
724 realResidual += weight
725 * std::cos(static_cast<long double>(residual));
726 imagResidual += weight
727 * std::sin(static_cast<long double>(residual));
728 sumWeight += weight;
729 }
730 }
731
732 long double rho = 0.0L;
733 const long double residualMagnitude =
734 std::hypot(realResidual, imagResidual);
735 if (residualMagnitude > 64.0L
736 * static_cast<long double>(std::numeric_limits<T>::epsilon())
737 * sumWeight)
738 rho = std::atan2(imagResidual, realResidual);
739
740 const T advance = principalArgument(static_cast<T>(nominal + rho));
741 omega_[static_cast<std::size_t>(p)] = std::cos(advance);
742 omegaImag_[static_cast<std::size_t>(p)] = std::sin(advance);
743 }
744
745 // The two phase banks are a compact unit-complex representation. Peak
746 // entries retain their captured absolute phase; non-peaks retain the
747 // captured phase relative to their region peak. This lets the held
748 // audio path use multiplies instead of transcendental functions.
749 for (int ch = 0; ch < spec_.numChannels; ++ch)
750 {
751 const std::size_t base = channelBinOffset(ch);
752 for (int k = 1; k < numBins_ - 1; ++k)
753 {
754 if (active_[static_cast<std::size_t>(k)] == 0u
755 || peak_[static_cast<std::size_t>(k)] != 0u)
756 continue;
757 const std::size_t index = base + static_cast<std::size_t>(k);
758 const std::size_t peakIndex = base + regionPeak_[
759 static_cast<std::size_t>(k)];
760 const T real = capturedPhase_[index];
761 const T imag = frozenPhase_[index];
762 const T peakReal = capturedPhase_[peakIndex];
763 const T peakImag = frozenPhase_[peakIndex];
764 capturedPhase_[index] = real * peakReal + imag * peakImag;
765 frozenPhase_[index] = imag * peakReal - real * peakImag;
766 }
767 }
768 }
769
770 void advanceTonalPhase() noexcept
771 {
772 ++tonalAdvanceCount_;
773 const bool renormalize = (tonalAdvanceCount_ & 255u) == 0u;
774 for (int ch = 0; ch < spec_.numChannels; ++ch)
775 {
776 const std::size_t base = channelBinOffset(ch);
777 for (int p = 1; p < numBins_ - 1; ++p)
778 {
779 if (peak_[static_cast<std::size_t>(p)] == 0u) continue;
780 const std::size_t index = base + static_cast<std::size_t>(p);
781 const T real = capturedPhase_[index];
782 const T imag = frozenPhase_[index];
783 const T advanceReal = omega_[static_cast<std::size_t>(p)];
784 const T advanceImag = omegaImag_[static_cast<std::size_t>(p)];
785 T nextReal = real * advanceReal - imag * advanceImag;
786 T nextImag = real * advanceImag + imag * advanceReal;
787 if (renormalize)
788 {
789 const T length = std::hypot(nextReal, nextImag);
790 if (length > T(0) && std::isfinite(length))
791 {
792 nextReal /= length;
793 nextImag /= length;
794 }
795 else
796 {
797 nextReal = T(1);
798 nextImag = T(0);
799 }
800 }
801 capturedPhase_[index] = nextReal;
802 frozenPhase_[index] = nextImag;
803 }
804 }
805 }
806
807 [[nodiscard]] static T diffuseAngle(std::uint64_t generation,
808 std::uint64_t frame,
809 std::uint64_t bin) noexcept
810 {
811 std::uint64_t x = UINT64_C(0xD1B54A32D192ED03)
812 ^ (generation * UINT64_C(0x9E3779B97F4A7C15))
813 ^ (frame * UINT64_C(0xBF58476D1CE4E5B9))
814 ^ (bin * UINT64_C(0x94D049BB133111EB));
815 x += UINT64_C(0x9E3779B97F4A7C15);
816 x = (x ^ (x >> 30u)) * UINT64_C(0xBF58476D1CE4E5B9);
817 x = (x ^ (x >> 27u)) * UINT64_C(0x94D049BB133111EB);
818 x ^= x >> 31u;
819 const double uniform = static_cast<double>(x >> 11u) * 0x1.0p-53;
820 return static_cast<T>(2.0 * std::numbers::pi_v<double> * uniform);
821 }
822
823 void prepareDiffuseFrame() noexcept
824 {
825 for (int k = 1; k < numBins_ - 1; ++k)
826 {
827 const T angle = diffuseAngle(diffuseGeneration_, diffuseFrame_,
828 static_cast<std::uint64_t>(k));
829 omega_[static_cast<std::size_t>(k)] = std::cos(angle);
830 omegaImag_[static_cast<std::size_t>(k)] = std::sin(angle);
831 }
832 }
833
834 void targetPhasor(int channel, int bin, T& real, T& imag) const noexcept
835 {
836 const std::size_t base = channelBinOffset(channel);
837 const std::size_t index = base + static_cast<std::size_t>(bin);
838 if (capturedMode_ == PhaseMode::Diffuse)
839 {
840 const T capturedReal = capturedPhase_[index];
841 const T capturedImag = frozenPhase_[index];
842 const T randomReal = omega_[static_cast<std::size_t>(bin)];
843 const T randomImag = omegaImag_[static_cast<std::size_t>(bin)];
844 real = capturedReal * randomReal - capturedImag * randomImag;
845 imag = capturedReal * randomImag + capturedImag * randomReal;
846 return;
847 }
848
849 if (peak_[static_cast<std::size_t>(bin)] != 0u)
850 {
851 real = capturedPhase_[index];
852 imag = frozenPhase_[index];
853 return;
854 }
855
856 const std::size_t peakIndex = base
857 + regionPeak_[static_cast<std::size_t>(bin)];
858 const T peakReal = capturedPhase_[peakIndex];
859 const T peakImag = frozenPhase_[peakIndex];
860 const T relativeReal = capturedPhase_[index];
861 const T relativeImag = frozenPhase_[index];
862 real = peakReal * relativeReal - peakImag * relativeImag;
863 imag = peakReal * relativeImag + peakImag * relativeReal;
864 }
865
866 void blendFrozenSpectrum(T* spectrum, int channel) noexcept
867 {
868 const int transitionSteps = fftSize_ / hopSize_;
869 const std::size_t base = channelBinOffset(channel);
870 const int nyquist = numBins_ - 1;
871
872 if (frameQ_ == transitionSteps)
873 {
874 spectrum[0] = capturedMagnitude_[base];
875 spectrum[1] = T(0);
876 spectrum[2 * nyquist] =
877 capturedMagnitude_[base + static_cast<std::size_t>(nyquist)];
878 spectrum[2 * nyquist + 1] = T(0);
879 for (int k = 1; k < nyquist; ++k)
880 {
881 const std::size_t index = base + static_cast<std::size_t>(k);
882 const T liveMagnitude = safeMagnitude(spectrum[2 * k],
883 spectrum[2 * k + 1]);
884 const T frozenMagnitude = capturedMagnitude_[index];
885 const bool liveActive = liveMagnitude >= magnitudeFloor_;
886 const bool frozenActive = frozenMagnitude >= magnitudeFloor_;
887 if (!liveActive && !frozenActive)
888 {
889 spectrum[2 * k] = T(0);
890 spectrum[2 * k + 1] = T(0);
891 continue;
892 }
893 if (!frozenActive)
894 {
895 const T scale = frozenMagnitude / liveMagnitude;
896 spectrum[2 * k] *= scale;
897 spectrum[2 * k + 1] *= scale;
898 continue;
899 }
900 T targetReal = T(0);
901 T targetImag = T(0);
902 targetPhasor(channel, k, targetReal, targetImag);
903 spectrum[2 * k] = frozenMagnitude * targetReal;
904 spectrum[2 * k + 1] = frozenMagnitude * targetImag;
905 }
906 return;
907 }
908
909 const T phase = std::numbers::pi_v<T> * static_cast<T>(frameQ_)
910 / static_cast<T>(transitionSteps);
911 const T alpha = T(0.5) - T(0.5) * std::cos(phase);
912 const T oneMinusAlpha = T(1) - alpha;
913 spectrum[0] = oneMinusAlpha * spectrum[0]
914 + alpha * capturedMagnitude_[base];
915 spectrum[1] = T(0);
916 spectrum[2 * nyquist] = oneMinusAlpha * spectrum[2 * nyquist]
917 + alpha * capturedMagnitude_[base + static_cast<std::size_t>(nyquist)];
918 spectrum[2 * nyquist + 1] = T(0);
919
920 for (int k = 1; k < nyquist; ++k)
921 {
922 const std::size_t index = base + static_cast<std::size_t>(k);
923 const T liveMagnitude = safeMagnitude(spectrum[2 * k],
924 spectrum[2 * k + 1]);
925 const T frozenMagnitude = capturedMagnitude_[index];
926 const bool liveActive = liveMagnitude >= magnitudeFloor_;
927 const bool frozenActive = frozenMagnitude >= magnitudeFloor_;
928 if (!liveActive && !frozenActive)
929 {
930 spectrum[2 * k] = T(0);
931 spectrum[2 * k + 1] = T(0);
932 continue;
933 }
934
935 T targetReal = T(0);
936 T targetImag = T(0);
937 targetPhasor(channel, k, targetReal, targetImag);
938 T livePhase = phaseOf(spectrum[2 * k], spectrum[2 * k + 1]);
939 T targetPhase = phaseOf(targetReal, targetImag);
940 if (!liveActive) livePhase = targetPhase;
941 if (!frozenActive) targetPhase = livePhase;
942
943 const T magnitude = oneMinusAlpha * liveMagnitude
944 + alpha * frozenMagnitude;
945 const T outputPhase = principalArgument(livePhase
946 + alpha * principalArgument(targetPhase - livePhase));
947 spectrum[2 * k] = magnitude * std::cos(outputPhase);
948 spectrum[2 * k + 1] = magnitude * std::sin(outputPhase);
949 if (!std::isfinite(spectrum[2 * k])
950 || !std::isfinite(spectrum[2 * k + 1]))
951 {
952 spectrum[2 * k] = T(0);
953 spectrum[2 * k + 1] = T(0);
954 }
955 }
956 }
957
958 AudioSpec spec_ {};
959 bool prepared_ = false;
960 int fftSize_ = 0;
961 int hopSize_ = 0;
962 int numBins_ = 0;
963
964 SpectralProcessor<T> processor_;
965 std::vector<T> scratch_;
966 std::array<T*, kMaxChannels> scratchChannels_ {};
967
968 // Three raw banks are current, latest completed and the completed frame
969 // before it. Integer bank rotation avoids frame-sized copies.
970 std::vector<T> rawSpectra_;
971 int olderBank_ = 0;
972 int latestBank_ = 1;
973 int currentBank_ = 2;
974 int completedFrames_ = 0;
975 int callbackChannel_ = 0;
976
977 std::vector<T> capturedMagnitude_;
978 std::vector<T> capturedPhase_;
979 std::vector<T> frozenPhase_;
980 std::vector<T> binPower_;
981 std::vector<T> omega_;
982 std::vector<T> omegaImag_;
983 std::vector<std::uint32_t> active_;
984 std::vector<std::uint32_t> peak_;
985 std::vector<std::uint32_t> regionPeak_;
986
987 int q_ = 0;
988 int frameQ_ = 0;
989 std::uint32_t frameRequestWord_ = 0u;
990 bool captured_ = false;
991 PhaseMode capturedMode_ = PhaseMode::Tonal;
992 T magnitudeFloor_ = T(0);
993 std::uint64_t nextDiffuseGeneration_ = 0u;
994 std::uint64_t diffuseGeneration_ = 0u;
995 std::uint64_t diffuseFrame_ = 0u;
996 std::uint32_t tonalAdvanceCount_ = 0u;
997
998 std::atomic<std::uint32_t> requested_ { 0u };
999};
1000
1001} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Frequency-domain hold with tonal and diffuse phase modes.
int getFFTSize() const noexcept
Returns the prepared FFT size, or zero before prepare.
bool isFrozen() const noexcept
Returns the requested freeze state, not transition completion.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an in-place block without allocation or locking.
int getHopSize() const noexcept
Returns the prepared hop size, or zero before prepare.
int getLatency() const noexcept
Returns the exact input-signal latency, or zero before prepare.
std::vector< uint8_t > getState() const
Serializes the requested freeze state and phase mode (setup/UI threads; allocates)....
void setPhaseMode(PhaseMode mode) noexcept
Publishes the requested phase mode with one atomic RMW. Any representation other than exactly Diffuse...
bool setState(const uint8_t *data, size_t size)
Restores the freeze request and phase mode from a blob (tolerant; rejects foreign ids)....
void setFrozen(bool frozen) noexcept
Publishes the requested freeze state with one atomic RMW.
int getTransitionSamples() const noexcept
Returns the N-sample transition duration, or zero unprepared.
PhaseMode getPhaseMode() const noexcept
Returns the requested phase mode.
PhaseMode
Phase evolution used by the captured spectrum.
void prepare(const AudioSpec &spec, int fftSize=2048, int hopSize=0)
Prepares the processor and allocates all audio-path storage.
void reset() noexcept
Clears WOLA, history, capture and transition state. Requested controls are intentionally preserved.
High-performance STFT analysis-modification-synthesis pipeline.
void prepare(const AudioSpec &spec, int fftSize=2048, int hopSize=0)
Allocates buffers and prepares the WOLA processing state.
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
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
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