DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Hilbert.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
25#include "DspMath.h"
26#include "DenormalGuard.h"
27#include "SimdOps.h" // SIMD dot product for the FIR convolution
28
29#include <algorithm>
30#include <array>
31#include <cassert>
32#include <cmath>
33#include <span>
34
35namespace dspark {
36
61template <FloatType T>
63{
64public:
65 struct Result
66 {
67 T real;
68 T imag;
69 };
70
77 void prepare(double sampleRate) noexcept
78 {
79 assert(sampleRate > 0.0);
80 if (!(sampleRate > 0.0)) return;
81
82 sampleRate_ = sampleRate;
83 reset();
84 isPrepared_ = true;
85 }
86
99 [[nodiscard]] inline Result process(T input) noexcept
100 {
101 delay_[static_cast<size_t>(writePos_)] = input;
102 delay_[static_cast<size_t>(writePos_ + kTaps)] = input;
103
104 // Contiguous window of the last kTaps samples, oldest first:
105 // window[j] = x[n - (kTaps - 1 - j)].
106 const T* window = delay_.data() + writePos_ + 1;
107
108 // imag = sum_k h[k] * x[n-k]: with an oldest-first window this is the
109 // dot product against the REVERSED kernel. The pointer is cached in a
110 // member so the hot path never touches the magic-static init guard.
111 const T imag = simd::dotProduct(kernelData_, window, kTaps);
112
113 // real = x[n - kCenter]: index kTaps-1-kCenter == kCenter (odd length).
114 const T re = window[kCenter];
115
116 writePos_ = (writePos_ + 1 == kTaps) ? 0 : (writePos_ + 1);
117 return { re, imag };
118 }
119
126 void processBlock(std::span<const T> input,
127 std::span<T> outReal,
128 std::span<T> outImag) noexcept
129 {
130 assert(isPrepared_);
131 assert(outReal.size() >= input.size() && outImag.size() >= input.size());
132 DenormalGuard dg;
133
134 const size_t numSamples =
135 std::min(input.size(), std::min(outReal.size(), outImag.size()));
136 for (size_t i = 0; i < numSamples; ++i)
137 {
138 const auto res = process(input[i]);
139 outReal[i] = res.real;
140 outImag[i] = res.imag;
141 }
142 }
143
145 void reset() noexcept
146 {
147 delay_.fill(T(0));
148 writePos_ = 0;
149 }
150
152 [[nodiscard]] static constexpr int getLatencySamples() noexcept { return kCenter; }
153
154private:
155 // 191-tap FIR: < 0.1% analytic ripple above ~1 kHz at 48 kHz (< 2.5% at
156 // 500 Hz), 95-sample group delay. Odd length keeps a true integer center tap.
157 static constexpr int kTaps = 191;
158 static constexpr int kCenter = kTaps / 2;
159
163 [[nodiscard]] static const std::array<T, kTaps>& reversedKernel() noexcept
164 {
165 static const std::array<T, kTaps> kernel = []
166 {
167 std::array<T, kTaps> k {};
168 constexpr double kPi = 3.14159265358979323846;
169 for (int i = 0; i < kTaps; ++i)
170 {
171 // Ideal Hilbert kernel h[n] = 2/(pi*n) for odd n, 0 for even n,
172 // windowed with Blackman to control ripple / sideband leakage.
173 const int n = i - kCenter;
174 double ideal = ((n == 0) || (n % 2 == 0)) ? 0.0 : (2.0 / (kPi * static_cast<double>(n)));
175 double w = 0.42
176 - 0.5 * std::cos(2.0 * kPi * static_cast<double>(i) / static_cast<double>(kTaps - 1))
177 + 0.08 * std::cos(4.0 * kPi * static_cast<double>(i) / static_cast<double>(kTaps - 1));
178 k[static_cast<size_t>(kTaps - 1 - i)] = static_cast<T>(ideal * w);
179 }
180 return k;
181 }();
182 return kernel;
183 }
184
185 double sampleRate_ = 0.0;
186 bool isPrepared_ = false;
187 int writePos_ = 0;
188
189 // Cached at construction so process() skips the magic-static guard check
190 // (the static outlives every instance; copies stay valid).
191 const T* kernelData_ = reversedKernel().data();
192
193 // Mirrored delay line (double-write) for contiguous SIMD reads. No
194 // over-alignment: the window starts at a variable offset every sample, so
195 // the SIMD dot product uses unaligned loads regardless.
196 std::array<T, kTaps * 2> delay_{};
197};
198
199} // namespace dspark
RAII scope guard that disables denormal (subnormal) float arithmetic.
Core mathematical utilities for digital signal processing.
SIMD-accelerated buffer operations for real-time audio processing.
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
90-degree phase-differencing network (analytic-signal generator).
Definition Hilbert.h:63
static constexpr int getLatencySamples() noexcept
FIR group-delay latency applied to both outputs, in samples.
Definition Hilbert.h:152
void reset() noexcept
Resets the delay line. Mandatory when seeking or starting playback.
Definition Hilbert.h:145
void prepare(double sampleRate) noexcept
Prepares the transformer. The FIR kernel is sample-rate independent; sampleRate is accepted for API s...
Definition Hilbert.h:77
Result process(T input) noexcept
Processes one sample, returning the analytic signal {real, imag}.
Definition Hilbert.h:99
void processBlock(std::span< const T > input, std::span< T > outReal, std::span< T > outImag) noexcept
Processes a block of samples. Optimized for CPU cache.
Definition Hilbert.h:126
float dotProduct(const float *DSPARK_RESTRICT a, const float *DSPARK_RESTRICT b, int count) noexcept
Computes the dot product of two arrays.
Definition SimdOps.h:419
Main namespace for the DSPark framework.
T real
In-phase component (input delayed by the FIR group delay).
Definition Hilbert.h:67
T imag
Quadrature component (90-deg shifted).
Definition Hilbert.h:68