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"
49template <
typename T =
float>
79 static_cast<float>(
pan_.load(std::memory_order_relaxed)));
88 delayL_.prepareMs(monoSpec,
static_cast<double>(maxMs) + 1.0);
89 delayR_.prepareMs(monoSpec,
static_cast<double>(maxMs) + 1.0);
111 algo =
static_cast<Algorithm>(std::clamp(
static_cast<int>(algo), 0,
113 algorithm_.store(algo, std::memory_order_relaxed);
123 if (!std::isfinite(position))
return;
127 pan_.store(std::clamp(position, T(-1), T(1)), std::memory_order_relaxed);
137 if (buffer.getNumChannels() < 2)
return;
145 const int gN = buffer.getNumSamples();
146 for (
int ch = 0; ch < buffer.getNumChannels(); ++ch)
148 T* d = buffer.getChannel(ch);
149 for (
int i = 0; i < gN; ++i)
150 if (!std::isfinite(d[i])) d[i] = T(0);
160 float pTarget =
static_cast<float>(
pan_.load(std::memory_order_relaxed));
163 switch (
algorithm_.load(std::memory_order_relaxed))
191 if (!std::isfinite(ms))
return;
196 if (!std::isfinite(ms))
return;
197 haasMaxDelay_.store(std::max(0.0f, ms), std::memory_order_relaxed);
201 if (!std::isfinite(hz))
return;
202 spectralFreq_.store(std::clamp(hz, 20.0f, 20000.0f), std::memory_order_relaxed);
206 if (!std::isfinite(dB))
return;
215 if (!std::isfinite(ms))
return;
216 smoothingTime_.store(std::max(0.0f, ms), std::memory_order_relaxed);
224 [[nodiscard]] std::vector<uint8_t>
getState()
const
227 w.
write(
"pan",
pan_.load(std::memory_order_relaxed));
228 w.
write(
"algorithm",
static_cast<int32_t
>(
algorithm_.load(std::memory_order_relaxed)));
255 T* L = buffer.getChannel(0);
256 T* R = buffer.getChannel(1);
257 const int n = buffer.getNumSamples();
258 constexpr T
halfPi = pi<T> / T(2);
264 const T gL = std::cos(angle);
265 const T gR = std::sin(angle);
266 for (
int i = 0; i < n; ++i)
274 for (
int i = 0; i < n; ++i)
277 T angle = (p * T(0.5) + T(0.5)) *
halfPi;
287 T* L = buffer.getChannel(0);
288 T* R = buffer.getChannel(1);
289 const int n = buffer.getNumSamples();
292 T targetP =
static_cast<T
>(panTarget);
299 const T baseMs = T(4000) /
static_cast<T
>(
sampleRate_);
300 delayL_.setDelayMs(baseMs + itdMax * std::max(T(0), targetP));
301 delayR_.setDelayMs(baseMs + itdMax * std::max(T(0), -targetP));
303 for (
int i = 0; i < n; ++i)
317 T absp = std::abs(p);
318 T farAtten = T(1) - absp * T(0.5);
319 T leakage = absp * T(0.3);
325 l_temp = L[i] * farAtten + R[i] * leakage;
326 r_temp = R[i] + L[i] * leakage * T(0.5);
331 l_temp = L[i] + R[i] * leakage * T(0.5);
332 r_temp = R[i] * farAtten + L[i] * leakage;
337 L[i] =
delayL_.processSample(0, l_temp);
338 R[i] =
delayR_.processSample(0, r_temp);
344 T* L = buffer.getChannel(0);
345 T* R = buffer.getChannel(1);
346 const int n = buffer.getNumSamples();
347 constexpr T
halfPi = pi<T> / T(2);
349 for (
int i = 0; i < n; ++i)
352 T mid = (L[i] + R[i]) * T(0.5);
353 T side = (L[i] - R[i]) * T(0.5);
360 T angle = (p * T(0.5) + T(0.5)) *
halfPi;
361 T gL =
fastCos(angle) * sqrt2<T>;
362 T gR =
fastSin(angle) * sqrt2<T>;
364 L[i] = mid * gL + side;
365 R[i] = mid * gR - side;
371 T* L = buffer.getChannel(0);
372 T* R = buffer.getChannel(1);
373 const int n = buffer.getNumSamples();
374 constexpr T
halfPi = pi<T> / T(2);
376 for (
int i = 0; i < n; ++i)
379 T mid = (L[i] + R[i]) * T(0.5);
380 T side = (L[i] - R[i]) * T(0.5);
382 T angle = (p * T(0.5) + T(0.5)) *
halfPi;
386 T sideGainL = std::clamp(
fastCos(angle) * sqrt2<T>, T(0), T(1));
387 T sideGainR = std::clamp(
fastSin(angle) * sqrt2<T>, T(0), T(1));
389 L[i] = mid + (side * sideGainL);
390 R[i] = mid - (side * sideGainR);
396 T haasMax = T(
haasMaxDelay_.load(std::memory_order_relaxed));
397 T pT =
static_cast<T
>(panTarget);
402 const T baseMs = T(4000) /
static_cast<T
>(
sampleRate_);
403 delayL_.setDelayMs(baseMs + haasMax * std::max(T(0), pT));
404 delayR_.setDelayMs(baseMs + haasMax * std::max(T(0), -pT));
406 T* L = buffer.getChannel(0);
407 T* R = buffer.getChannel(1);
408 const int n = buffer.getNumSamples();
410 for (
int i = 0; i < n; ++i)
418 L[i] =
delayL_.processSample(0, L[i]);
419 R[i] =
delayR_.processSample(0, R[i]);
428 T* L = buffer.getChannel(0);
429 T* R = buffer.getChannel(1);
430 const int n = buffer.getNumSamples();
431 constexpr int kSubBlock = 4;
433 for (
int start = 0; start < n; start += kSubBlock)
435 const int end = std::min(n, start + kSubBlock);
437 for (
int i = start; i < end; ++i)
441 for (
int i = start; i < end; ++i)
453 const float pan =
static_cast<float>(targetPan);
460 T gainLdB = -targetPan *
static_cast<T
>(sMaxGain);
461 T gainRdB = targetPan *
static_cast<T
>(sMaxGain);
466 sampleRate_,
static_cast<double>(sFreq),
static_cast<double>(gainLdB)));
468 sampleRate_,
static_cast<double>(sFreq),
static_cast<double>(gainRdB)));
Non-owning view over audio channel data.
Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates.
void setCoeffsNow(const BiquadCoeffs &c) noexcept
Stream-owner direct set: makes c the active set immediately.
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: a change settles (to 96%) in this many ms along a critically damped curv...
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
std::atomic< float > haasMaxDelay_
void setSpectralMaxGain(float dB) noexcept
static float panTimeConstantMs(float smoothingMs) noexcept
Smoothers::CriticallyDampedSmoother panSmoother_
void applySpectral(AudioBufferView< T > buffer, float) noexcept
void reset() noexcept
Clears delay lines and filter states to prevent ghost echoes.
void applyMidPan(AudioBufferView< T > buffer, float) 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 (the pi/2 offset costs float about half a...
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.
Critically damped smoother (no overshoot, exact Q=0.5).
void reset(double sampleRate, float timeConstantMilliseconds, float initialValue=0.0f) noexcept
float getCurrentValue() const noexcept
void setTargetValue(float newTarget) noexcept
float getNextValue() noexcept
bool isSmoothing() const noexcept