DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
PhaseCorrelation.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
44#include "../Core/AudioBuffer.h"
45#include "../Core/AudioSpec.h"
46#include "../Core/DspMath.h"
47
48#include <algorithm>
49#include <array>
50#include <atomic>
51#include <bit>
52#include <cmath>
53#include <cstddef>
54#include <cstdint>
55#include <type_traits>
56
57namespace dspark {
58
65template <FloatType T>
67{
68public:
71 {
72 float mid;
73 float side;
74 };
75
76 static constexpr int kGonioSize = 1024;
77
78 // -- Lifecycle ---------------------------------------------------------------
79
89 void prepare(const AudioSpec& spec, double windowMs = 300.0) noexcept
90 {
91 if (!spec.isValid() || !std::isfinite(spec.sampleRate)) return;
92 if (!std::isfinite(windowMs)) windowMs = 300.0;
93 const double fs = spec.sampleRate;
94 windowMs_ = std::clamp(windowMs, 1.0, 600000.0);
95 alpha_ = 1.0 - std::exp(-1.0 / (windowMs_ * 0.001 * fs));
96 // Decimate the goniometer feed to ~24k points/s - plenty for displays.
97 gonioDecim_ = std::max(1, static_cast<int>(std::llround(std::min(fs / 24000.0, 65536.0))));
98 prepared_.store(true, std::memory_order_relaxed);
99 reset();
100 }
101
109 void reset() noexcept
110 {
111 lr_ = ll_ = rr_ = 0.0;
112 decimCount_ = 0;
113 for (auto& p : gonio_)
114 p.store(0u, std::memory_order_relaxed);
115 gonioWrite_.store(0, std::memory_order_relaxed);
116 correlation_.store(T(0), std::memory_order_relaxed);
117 balance_.store(T(0), std::memory_order_relaxed);
118 }
119
120 // -- Processing -------------------------------------------------------------------
121
130 {
131 if (!prepared_.load(std::memory_order_relaxed)) return;
132
133 const int nS = buffer.getNumSamples();
134 const int nCh = buffer.getNumChannels();
135 if (nS <= 0 || nCh <= 0) return;
136
137 const T* L = buffer.getChannel(0);
138 const T* R = (nCh >= 2) ? buffer.getChannel(1) : L;
139
140 const double alpha = alpha_;
141 double lr = lr_, ll = ll_, rr = rr_;
142 int wp = gonioWrite_.load(std::memory_order_relaxed);
143
144 for (int i = 0; i < nS; ++i)
145 {
146 const double l = static_cast<double>(L[i]);
147 const double r = static_cast<double>(R[i]);
148 lr += alpha * (l * r - lr);
149 ll += alpha * (l * l - ll);
150 rr += alpha * (r * r - rr);
151
152 if (++decimCount_ >= gonioDecim_)
153 {
154 decimCount_ = 0;
155 if (std::isfinite(l) && std::isfinite(r))
156 {
157 GonioPoint p;
158 p.mid = static_cast<float>((l + r) * 0.7071067811865476);
159 p.side = static_cast<float>((l - r) * 0.7071067811865476);
160 gonio_[static_cast<size_t>(wp)].store(std::bit_cast<std::uint64_t>(p),
161 std::memory_order_relaxed);
162 wp = (wp + 1) & (kGonioSize - 1);
163 }
164 }
165 }
166
167 gonioWrite_.store(wp, std::memory_order_release);
168
169 // A non-finite sample would park NaN in the recursive averages for
170 // good (the one-pole never drains it). Discard the poisoned window
171 // and hold the last published readings; the accumulators restart
172 // from zero, so measurement resumes on the next clean block (the
173 // correlation is a ratio - it re-converges immediately once real
174 // signal flows again).
175 if (!std::isfinite(lr + ll + rr))
176 {
177 lr_ = ll_ = rr_ = 0.0;
178 return;
179 }
180
181 // Flush fully decayed averages to true zero: long silences would
182 // otherwise walk the one-pole into double denormals and tax hosts
183 // that do not enable FTZ. Well below any audible floor.
184 if (ll < 1e-100) ll = 0.0;
185 if (rr < 1e-100) rr = 0.0;
186 if (std::abs(lr) < 1e-100) lr = 0.0;
187
188 lr_ = lr;
189 ll_ = ll;
190 rr_ = rr;
191
192 const double denom = std::sqrt(ll * rr);
193 const double corr = (denom > 1e-12) ? std::clamp(lr / denom, -1.0, 1.0) : 0.0;
194 correlation_.store(static_cast<T>(corr), std::memory_order_relaxed);
195
196 const double total = ll + rr;
197 const double bal = (total > 1e-12) ? (rr - ll) / total : 0.0;
198 balance_.store(static_cast<T>(bal), std::memory_order_relaxed);
199 }
200
201 // -- Readout (lock-free, any thread) ------------------------------------------------
202
204 [[nodiscard]] T getCorrelation() const noexcept
205 {
206 return correlation_.load(std::memory_order_relaxed);
207 }
208
210 [[nodiscard]] T getBalance() const noexcept
211 {
212 return balance_.load(std::memory_order_relaxed);
213 }
214
221 int getGonioPoints(GonioPoint* dest, int maxCount) const noexcept
222 {
223 if (dest == nullptr) return 0;
224 const int count = std::clamp(maxCount, 0, kGonioSize);
225 const int wp = gonioWrite_.load(std::memory_order_acquire);
226 for (int i = 0; i < count; ++i)
227 {
228 const auto idx = static_cast<size_t>((wp - count + i) & (kGonioSize - 1));
229 dest[i] = std::bit_cast<GonioPoint>(gonio_[idx].load(std::memory_order_relaxed));
230 }
231 return count;
232 }
233
235 [[nodiscard]] double getWindowMs() const noexcept { return windowMs_; }
236
237private:
238 static_assert(sizeof(GonioPoint) == sizeof(std::uint64_t),
239 "GonioPoint must pack into one 64-bit atomic");
240 static_assert(std::is_trivially_copyable_v<GonioPoint>);
241
242 double windowMs_ = 300.0;
243 double alpha_ = 0.001;
244 std::atomic<bool> prepared_ { false };
245
246 double lr_ = 0.0, ll_ = 0.0, rr_ = 0.0;
247
248 int gonioDecim_ = 2;
249 int decimCount_ = 0;
250 std::array<std::atomic<std::uint64_t>, kGonioSize> gonio_ {};
251 std::atomic<int> gonioWrite_ { 0 };
252
253 std::atomic<T> correlation_ { T(0) };
254 std::atomic<T> balance_ { T(0) };
255};
256
257} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Correlation/balance meter and goniometer data source.
void processBlock(AudioBufferView< const T > buffer) noexcept
Analyzes a block (read-only; the audio is not modified).
int getGonioPoints(GonioPoint *dest, int maxCount) const noexcept
Copies the newest goniometer points (oldest first).
double getWindowMs() const noexcept
void reset() noexcept
Clears the averages, the readouts and the goniometer ring.
T getBalance() const noexcept
T getCorrelation() const noexcept
void prepare(const AudioSpec &spec, double windowMs=300.0) noexcept
Prepares the meter. Allocation-free.
static constexpr int kGonioSize
Goniometer ring length.
Main namespace for the DSPark framework.
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
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
One mid/side point for goniometer displays.