DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
FrequencyShifter.h
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 mixMaxStep_ = static_cast<T>(1.0 / (spec.sampleRate * 0.02));
79 numChannels_ = spec.numChannels;
80
81 // Zero-allocations on audio thread: allocate all Hilberts during prepare.
82 hilberts_.resize(numChannels_);
83 for (auto& h : hilberts_) {
84 h.prepare(spec.sampleRate);
85 }
86
87 currentMix_ = mix_.load(std::memory_order_relaxed);
88 reset();
89 }
90
95 void processBlock(AudioBufferView<T> buffer) noexcept
96 {
97 const int numCh = std::min(buffer.getNumChannels(), numChannels_);
98 const int numSamples = buffer.getNumSamples();
99 if (numSamples == 0 || numCh == 0) return;
100
101 // The Hilbert transformer runs per-sample over a 191-tap FIR; denormal
102 // input samples would drop every tap into the x86 slow path. The
103 // framework convention is that per-sample Hilbert callers install their
104 // own guard (see Hilbert.h and Compressor.h).
105 DenormalGuard guard;
106
107 const T targetMix = mix_.load(std::memory_order_relaxed);
108 const T shiftHz = shift_.load(std::memory_order_relaxed);
109
110 // The mix is rate limited to full scale per 20 ms: an unsmoothed step
111 // would jump the dry/wet crossfade audibly, and a per-block ramp
112 // landed in 0.7 ms with 32-sample blocks. The shift needs no
113 // smoothing - it only changes the speed of the quadrature carrier,
114 // whose phase stays continuous across blocks.
115 const T mixStart = currentMix_;
116
117 // 1. Compute rotation matrix coefficients once per block
118 const double w = (shiftHz * 2.0 * std::numbers::pi) / sampleRate_;
119 const T cos_w = static_cast<T>(std::cos(w));
120 const T sin_w = static_cast<T>(std::sin(w));
121
122 // Cache the starting phase components for this block
123 const T startCos = static_cast<T>(std::cos(phase_));
124 const T startSin = static_cast<T>(std::sin(phase_));
125
126 // 2. Process planar channels to maximize cache locality
127 for (int ch = 0; ch < numCh; ++ch)
128 {
129 T* data = buffer.getChannel(ch);
130 auto& hilbert = hilberts_[ch];
131
132 // Local quadrature oscillator state (identical carrier and mix
133 // ramp on every channel)
134 T u = startCos;
135 T v = startSin;
136
137 for (int i = 0; i < numSamples; ++i)
138 {
139 const T smoothMix = moveTowards(mixStart, targetMix,
140 mixMaxStep_ * static_cast<T>(i + 1));
141
142 // Hilbert processing (I + jQ)
143 auto h = hilbert.process(data[i]);
144
145 // Modulate analytic signal: real part of (I+jQ) * (u+jv)
146 T shifted = h.real * u - h.imag * v;
147
148 // Mix correctly: Use h.real as the phase-aligned dry signal
149 data[i] = h.real + (shifted - h.real) * smoothMix;
150
151 // Advance quadrature oscillator: rotation matrix
152 T next_u = u * cos_w - v * sin_w;
153 T next_v = u * sin_w + v * cos_w;
154 u = next_u;
155 v = next_v;
156 }
157 }
158
159 currentMix_ = moveTowards(mixStart, targetMix, mixMaxStep_ * static_cast<T>(numSamples));
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 T mixMaxStep_{ T(1.0 / 960.0) };
256
257 std::vector<Hilbert<T>> hilberts_;
258};
259
260} // 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:163
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.
T moveTowards(T from, T to, T maxDelta) noexcept
Moves a value toward a target by at most a given distance.
Definition DspMath.h:134
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
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45