32#include "../Core/AudioBuffer.h"
33#include "../Core/AudioSpec.h"
34#include "../Core/Biquad.h"
35#include "../Core/DspMath.h"
36#include "../Core/Smoothers.h"
37#include "../Core/StateBlob.h"
48template <
typename T =
float>
84 delayL_.prepareMs(monoSpec,
static_cast<double>(maxMs) + 1.0);
85 delayR_.prepareMs(monoSpec,
static_cast<double>(maxMs) + 1.0);
103 algo =
static_cast<Algorithm>(std::clamp(
static_cast<int>(algo), 0,
105 algorithm_.store(algo, std::memory_order_relaxed);
115 if (!std::isfinite(position))
return;
119 pan_.store(std::clamp(position, T(-1), T(1)), std::memory_order_relaxed);
129 if (buffer.getNumChannels() < 2)
return;
137 const int gN = buffer.getNumSamples();
138 for (
int ch = 0; ch < buffer.getNumChannels(); ++ch)
140 T* d = buffer.getChannel(ch);
141 for (
int i = 0; i < gN; ++i)
142 if (!std::isfinite(d[i])) d[i] = T(0);
152 float pTarget =
static_cast<float>(
pan_.load(std::memory_order_relaxed));
155 switch (
algorithm_.load(std::memory_order_relaxed))
183 if (!std::isfinite(ms))
return;
188 if (!std::isfinite(ms))
return;
189 haasMaxDelay_.store(std::max(0.0f, ms), std::memory_order_relaxed);
193 if (!std::isfinite(hz))
return;
194 spectralFreq_.store(std::clamp(hz, 20.0f, 20000.0f), std::memory_order_relaxed);
198 if (!std::isfinite(dB))
return;
207 if (!std::isfinite(ms))
return;
208 smoothingTime_.store(std::max(0.0f, ms), std::memory_order_relaxed);
216 [[nodiscard]] std::vector<uint8_t>
getState()
const
219 w.
write(
"pan",
pan_.load(std::memory_order_relaxed));
220 w.
write(
"algorithm",
static_cast<int32_t
>(
algorithm_.load(std::memory_order_relaxed)));
247 T* L = buffer.getChannel(0);
248 T* R = buffer.getChannel(1);
249 const int n = buffer.getNumSamples();
250 constexpr T
halfPi = pi<T> / T(2);
256 const T gL = std::cos(angle);
257 const T gR = std::sin(angle);
258 for (
int i = 0; i < n; ++i)
266 for (
int i = 0; i < n; ++i)
269 T angle = (p * T(0.5) + T(0.5)) *
halfPi;
279 T* L = buffer.getChannel(0);
280 T* R = buffer.getChannel(1);
281 const int n = buffer.getNumSamples();
284 T targetP =
static_cast<T
>(panTarget);
291 const T baseMs = T(4000) /
static_cast<T
>(
sampleRate_);
292 delayL_.setDelayMs(baseMs + itdMax * std::max(T(0), targetP));
293 delayR_.setDelayMs(baseMs + itdMax * std::max(T(0), -targetP));
295 for (
int i = 0; i < n; ++i)
309 T absp = std::abs(p);
310 T farAtten = T(1) - absp * T(0.5);
311 T leakage = absp * T(0.3);
317 l_temp = L[i] * farAtten + R[i] * leakage;
318 r_temp = R[i] + L[i] * leakage * T(0.5);
323 l_temp = L[i] + R[i] * leakage * T(0.5);
324 r_temp = R[i] * farAtten + L[i] * leakage;
329 L[i] =
delayL_.processSample(0, l_temp);
330 R[i] =
delayR_.processSample(0, r_temp);
336 T* L = buffer.getChannel(0);
337 T* R = buffer.getChannel(1);
338 const int n = buffer.getNumSamples();
339 constexpr T
halfPi = pi<T> / T(2);
341 for (
int i = 0; i < n; ++i)
344 T mid = (L[i] + R[i]) * T(0.5);
345 T side = (L[i] - R[i]) * T(0.5);
352 T angle = (p * T(0.5) + T(0.5)) *
halfPi;
353 T gL =
fastCos(angle) * sqrt2<T>;
354 T gR =
fastSin(angle) * sqrt2<T>;
356 L[i] = mid * gL + side;
357 R[i] = mid * gR - side;
363 T* L = buffer.getChannel(0);
364 T* R = buffer.getChannel(1);
365 const int n = buffer.getNumSamples();
366 constexpr T
halfPi = pi<T> / T(2);
368 for (
int i = 0; i < n; ++i)
371 T mid = (L[i] + R[i]) * T(0.5);
372 T side = (L[i] - R[i]) * T(0.5);
374 T angle = (p * T(0.5) + T(0.5)) *
halfPi;
378 T sideGainL = std::clamp(
fastCos(angle) * sqrt2<T>, T(0), T(1));
379 T sideGainR = std::clamp(
fastSin(angle) * sqrt2<T>, T(0), T(1));
381 L[i] = mid + (side * sideGainL);
382 R[i] = mid - (side * sideGainR);
388 T haasMax = T(
haasMaxDelay_.load(std::memory_order_relaxed));
389 T pT =
static_cast<T
>(panTarget);
394 const T baseMs = T(4000) /
static_cast<T
>(
sampleRate_);
395 delayL_.setDelayMs(baseMs + haasMax * std::max(T(0), pT));
396 delayR_.setDelayMs(baseMs + haasMax * std::max(T(0), -pT));
398 T* L = buffer.getChannel(0);
399 T* R = buffer.getChannel(1);
400 const int n = buffer.getNumSamples();
402 for (
int i = 0; i < n; ++i)
410 L[i] =
delayL_.processSample(0, L[i]);
411 R[i] =
delayR_.processSample(0, R[i]);
423 T* L = buffer.getChannel(0);
424 T* R = buffer.getChannel(1);
425 const int n = buffer.getNumSamples();
427 for (
int i = 0; i < n; ++i)
440 T gainLdB = -targetPan *
static_cast<T
>(sMaxGain);
441 T gainRdB = targetPan *
static_cast<T
>(sMaxGain);
444 sampleRate_,
static_cast<double>(sFreq),
static_cast<double>(gainLdB)));
446 sampleRate_,
static_cast<double>(sFreq),
static_cast<double>(gainRdB)));
Professional delay line with Hermite interpolation, analog feedback saturation, and stereo processing...
Non-owning view over audio channel data.
Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates.
void setCoeffs(const BiquadCoeffs &c) noexcept
Sets the filter coefficients asynchronously.
void reset() noexcept
Resets all per-channel filter states to zero to avoid ringing/clicks.
T processSample(T input, int channel) noexcept
Processes a single sample for a specific channel.
void setHaasMaxDelay(float ms) noexcept
void setAlgorithm(Algorithm algo) noexcept
Sets the active panning algorithm safely from any thread.
std::atomic< float > binauralMaxITD_
void prepare(const AudioSpec &spec)
Initializes internal delays, filters, and smoothers.
Biquad< T, 1 > spectralR_
std::atomic< Algorithm > algorithm_
void setSmoothingTime(float ms) noexcept
Sets the pan smoothing time. Thread-safe; applied at the top of the next processBlock() (previously i...
std::atomic< float > spectralMaxGain_
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Biquad< T, 1 > spectralL_
void setPan(T position) noexcept
Sets the target pan position (automatable, smoothed).
void setBinauralMaxITD(float ms) noexcept
void applyEqualPower(AudioBufferView< T > buffer, float) noexcept
Smoothers::LinearSmoother panSmoother_
std::atomic< float > haasMaxDelay_
void setSpectralMaxGain(float dB) noexcept
void reset() noexcept
Clears delay lines and filter states to prevent ghost echoes.
void applyMidPan(AudioBufferView< T > buffer, float) noexcept
void applySpectral(AudioBufferView< T > buffer, float panTarget) noexcept
std::atomic< bool > smoothingDirty_
Pan smoother re-config pending.
std::atomic< float > smoothingTime_
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio block in-place. Real-time safe.
void applyHaas(AudioBufferView< T > buffer, float panTarget) noexcept
void setSpectralFrequency(float hz) noexcept
void applyCombinedBinaural(AudioBufferView< T > buffer, float panTarget) noexcept
void updateSpectralFilters(T targetPan) noexcept
std::atomic< float > spectralFreq_
Algorithm
Available panning algorithms.
@ Binaural
Cross-feeding + ITD delay.
@ EqualPower
Standard -3 dB constant-power pan.
@ MidPan
Pans only the centre (mid) image.
@ SidePan
Pans only the stereo (side) image.
@ Haas
Precedence effect via inter-channel delay.
@ Spectral
Frequency-dependent panning via high-shelf.
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
void applySidePan(AudioBufferView< T > buffer, float) noexcept
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
bool isValid() const noexcept
uint32_t processorId() const noexcept
Serializes key/value parameters into a versioned blob.
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
void write(const char *key, float value)
Writes a float parameter.
Main namespace for the DSPark framework.
T fastSin(T x) noexcept
Fast sine approximation (degree-9 odd minimax polynomial).
T fastCos(T x) noexcept
Fast cosine approximation. See fastSin() for accuracy notes (float error is ~7e-6 here: half an ulp m...
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
constexpr T halfPi
Pi / 2 (1.57079...). Quarter period; sin/cos phase offset.
Describes the audio environment for a DSP processor.
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
int maxBlockSize
Maximum number of samples per processing block.
double sampleRate
Sample rate in Hz.
static BiquadCoeffs makeHighShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
High-shelf filter.
Linear ramp smoother for predictable, uniform interpolation.
void reset(double sampleRate, float rampTimeMilliseconds, float initialValue=0.0f) noexcept
float getNextValue() noexcept
void setTargetValue(float newTarget) noexcept
bool isSmoothing() const noexcept
float getCurrentValue() const noexcept