DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
FrequencyShifter.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
29#include "../Core/AudioBuffer.h"
30#include "../Core/AudioSpec.h"
31#include "../Core/DenormalGuard.h"
32#include "../Core/DspMath.h"
33#include "../Core/Hilbert.h"
34#include "../Core/StateBlob.h"
35
36#include <algorithm>
37#include <atomic>
38#include <cmath>
39#include <cstddef>
40#include <cstdint>
41#include <numbers>
42#include <vector>
43
44namespace dspark {
45
60template <FloatType T>
62{
63public:
73 void prepare(const AudioSpec& spec)
74 {
75 if (!spec.isValid()) return; // release-safe: keep previous state
76
77 sampleRate_ = spec.sampleRate;
78 numChannels_ = spec.numChannels;
79
80 // Zero-allocations on audio thread: allocate all Hilberts during prepare.
81 hilberts_.resize(numChannels_);
82 for (auto& h : hilberts_) {
83 h.prepare(spec.sampleRate);
84 }
85
86 currentMix_ = mix_.load(std::memory_order_relaxed);
87 reset();
88 }
89
94 void processBlock(AudioBufferView<T> buffer) noexcept
95 {
96 const int numCh = std::min(buffer.getNumChannels(), numChannels_);
97 const int numSamples = buffer.getNumSamples();
98 if (numSamples == 0 || numCh == 0) return;
99
100 // The Hilbert transformer runs per-sample over a 191-tap FIR; denormal
101 // input samples would drop every tap into the x86 slow path. The
102 // framework convention is that per-sample Hilbert callers install their
103 // own guard (see Hilbert.h and Compressor.h).
104 DenormalGuard guard;
105
106 const T targetMix = mix_.load(std::memory_order_relaxed);
107 const T shiftHz = shift_.load(std::memory_order_relaxed);
108
109 // The mix is smoothed with a linear per-block ramp: an unsmoothed
110 // step would jump the dry/wet crossfade audibly. The shift needs no
111 // smoothing - it only changes the speed of the quadrature carrier,
112 // whose phase stays continuous across blocks.
113 const T mixInc = (targetMix - currentMix_) / static_cast<T>(numSamples);
114
115 // 1. Compute rotation matrix coefficients once per block
116 const double w = (shiftHz * 2.0 * std::numbers::pi) / sampleRate_;
117 const T cos_w = static_cast<T>(std::cos(w));
118 const T sin_w = static_cast<T>(std::sin(w));
119
120 // Cache the starting phase components for this block
121 const T startCos = static_cast<T>(std::cos(phase_));
122 const T startSin = static_cast<T>(std::sin(phase_));
123
124 // 2. Process planar channels to maximize cache locality
125 for (int ch = 0; ch < numCh; ++ch)
126 {
127 T* data = buffer.getChannel(ch);
128 auto& hilbert = hilberts_[ch];
129
130 // Local quadrature oscillator state (identical carrier and mix
131 // ramp on every channel)
132 T u = startCos;
133 T v = startSin;
134 T smoothMix = currentMix_;
135
136 for (int i = 0; i < numSamples; ++i)
137 {
138 smoothMix += mixInc;
139
140 // Hilbert processing (I + jQ)
141 auto h = hilbert.process(data[i]);
142
143 // Modulate analytic signal: real part of (I+jQ) * (u+jv)
144 T shifted = h.real * u - h.imag * v;
145
146 // Mix correctly: Use h.real as the phase-aligned dry signal
147 data[i] = h.real + (shifted - h.real) * smoothMix;
148
149 // Advance quadrature oscillator: rotation matrix
150 T next_u = u * cos_w - v * sin_w;
151 T next_v = u * sin_w + v * cos_w;
152 u = next_u;
153 v = next_v;
154 }
155 }
156
157 // Land the mix ramp exactly on the published target (the next block
158 // must start settled - the framework's smoothing convention).
159 currentMix_ = targetMix;
160
161 // 3. Advance absolute phase once per block to prevent float drift
162 phase_ += w * numSamples;
163
164 // Wrap phase precisely
165 constexpr double kTwoPi = 2.0 * std::numbers::pi;
166 phase_ = std::fmod(phase_, kTwoPi);
167 if (phase_ < 0.0) phase_ += kTwoPi;
168 }
169
173 void reset() noexcept
174 {
175 phase_ = 0.0;
176 for (auto& h : hilberts_) {
177 h.reset();
178 }
179 }
180
188 void setShift(T hz) noexcept
189 {
190 if (!std::isfinite(hz)) return;
191 shift_.store(hz, std::memory_order_relaxed);
192 }
193
199 void setMix(T mix) noexcept
200 {
201 if (!std::isfinite(mix)) return;
202 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
203 }
204
206 [[nodiscard]] T getShift() const noexcept { return shift_.load(std::memory_order_relaxed); }
207
209 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
210
217 [[nodiscard]] static constexpr int getLatency() noexcept
218 {
220 }
221
223 [[nodiscard]] std::vector<uint8_t> getState() const
224 {
225 // The blob stores float (setState reads float back); the explicit
226 // casts also keep this overload resolvable when T is double.
227 StateWriter w(stateId("FSHF"), 1);
228 w.write("shift", static_cast<float>(shift_.load(std::memory_order_relaxed)));
229 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
230 return w.blob();
231 }
232
234 bool setState(const uint8_t* data, size_t size)
235 {
236 StateReader r(data, size);
237 if (!r.isValid() || r.processorId() != stateId("FSHF")) return false;
238 setShift(static_cast<T>(r.read("shift", 0.0f)));
239 setMix(static_cast<T>(r.read("mix", 1.0f)));
240 return true;
241 }
242
243private:
244 double sampleRate_ = 44100.0;
245 int numChannels_ = 0;
246
247 // Using double for phase accumulation to prevent drift over long sessions
248 double phase_ = 0.0;
249
250 std::atomic<T> shift_{ T(0) };
251 std::atomic<T> mix_{ T(1) };
252
253 // Smoothed state for the audio thread
254 T currentMix_{ T(1) };
255
256 std::vector<Hilbert<T>> hilberts_;
257};
258
259} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Constant-Hz frequency shift optimized via Quadrature Oscillator.
static constexpr int getLatency() noexcept
Reports the processing latency in samples.
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
void setMix(T mix) noexcept
Sets the dry/wet mix. Smoothed internally.
T getShift() const noexcept
void reset() noexcept
Resets internal filter states and phase accumulator.
void prepare(const AudioSpec &spec)
Prepares the frequency shifter state and allocates internal buffers.
T getMix() const noexcept
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
void setShift(T hz) noexcept
Sets the frequency shift amount in Hz.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio in-place applying the frequency shift.
static constexpr int getLatencySamples() noexcept
FIR group-delay latency applied to both outputs, in samples.
Definition Hilbert.h:152
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
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:639
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
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43