Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
delay_line.hpp
Go to the documentation of this file.
1// sbl/dsp/primitives/delay_line.hpp — Circular buffer delay line
2//
3// Circular buffer with integer, linear, and 4-point Hermite cubic reads.
4// Caller-provided storage. Supports both fixed and per-sample modulated
5// read positions.
6//
7// Domain: Time — stores and retrieves audio across time.
8// See docs/design-philosophy.md for the domain manipulation framework.
9//
10// Usage:
11// float buf[4096] = {};
12// sbl::dsp::primitives::DelayLine dl;
13// dl.init(buf, 4096);
14// dl.write(sample);
15// float out = dl.read(delay_samples); // linear interpolation
16// float out = dl.read_cubic(delay_samples); // 4-point Hermite
17//
18// // Block processing (fused write+read with per-sample modulation):
19// dl.process(in, out, delay_per_sample, frames);
20
21#ifndef SBL_DSP_PRIMITIVES_DELAY_LINE_HPP_
22#define SBL_DSP_PRIMITIVES_DELAY_LINE_HPP_
23
24#include <cstdint>
25
26namespace sbl::dsp::primitives {
27
28class DelayLine {
29public:
30 /// @note All public methods are ISR-safe — bounded computation, no I/O.
31
32 /**
33 * @brief Construct with caller-provided buffer
34 * @param buffer Float buffer (must outlive the DelayLine)
35 * @param max_delay Maximum delay in samples (buffer size)
36 */
37 DelayLine(float* buffer, uint32_t max_delay)
38 : buffer_(buffer), max_delay_(max_delay), write_pos_(0) {}
39
40 /// Default constructor for deferred initialization (must call init() before use)
41 DelayLine() = default;
42
43 /**
44 * @brief Initialize after default construction
45 * @param buffer Float buffer (must outlive the DelayLine)
46 * @param max_delay Maximum delay in samples (buffer size)
47 */
48 void init(float* buffer, uint32_t max_delay) {
49 buffer_ = buffer;
50 max_delay_ = max_delay;
51 write_pos_ = 0;
52 }
53
54 /**
55 * @brief Write a sample to the delay line
56 *
57 * Advances the write pointer after storing. The write pointer always
58 * points to the next write location.
59 */
60 void write(float sample) {
61 buffer_[write_pos_] = sample;
62 ++write_pos_;
63 if (write_pos_ >= max_delay_) {
64 write_pos_ = 0;
65 }
66 }
67
68 /**
69 * @brief Read at fractional delay with linear interpolation
70 *
71 * @param delay_samples Delay in samples (1.0 = most recently written sample).
72 * Clamped to [1.0, max_delay - 1].
73 * @return Linearly interpolated sample
74 */
75 float read(float delay_samples) const {
76 // Clamp to valid range for linear interpolation
77 if (delay_samples < 1.0f) delay_samples = 1.0f;
78 float max_f = static_cast<float>(max_delay_ - 1);
79 if (delay_samples > max_f) delay_samples = max_f;
80
81 // Split into integer and fractional parts
82 uint32_t int_delay = static_cast<uint32_t>(delay_samples);
83 float frac = delay_samples - static_cast<float>(int_delay);
84
85 // Read two adjacent samples
86 float y0 = read_at(int_delay);
87 float y1 = read_at(int_delay + 1);
88
89 // Linear interpolation
90 return y0 + frac * (y1 - y0);
91 }
92
93 /**
94 * @brief Read at fractional delay with 4-point Hermite cubic interpolation
95 *
96 * Higher quality than linear — reduces artifacts for reverb tank modulation
97 * and pitch shifting. Uses samples at [d-1, d, d+1, d+2] around the read
98 * position.
99 *
100 * @param delay_samples Delay in samples (1.0 = most recently written sample).
101 * Clamped to [2.0, max_delay - 2].
102 * @return Hermite-interpolated sample
103 */
104 float read_cubic(float delay_samples) const {
105 // Clamp to valid range for cubic interpolation (need 1 sample before, 2 after)
106 if (delay_samples < 2.0f) delay_samples = 2.0f;
107 float max_f = static_cast<float>(max_delay_ - 2);
108 if (delay_samples > max_f) delay_samples = max_f;
109
110 uint32_t int_delay = static_cast<uint32_t>(delay_samples);
111 float frac = delay_samples - static_cast<float>(int_delay);
112
113 // 4 samples: y0=d-1, y1=d, y2=d+1, y3=d+2
114 float y0 = read_at(int_delay - 1);
115 float y1 = read_at(int_delay);
116 float y2 = read_at(int_delay + 1);
117 float y3 = read_at(int_delay + 2);
118
119 // Hermite interpolation (4-point, 3rd-order)
120 float a = -0.5f * y0 + 1.5f * y1 - 1.5f * y2 + 0.5f * y3;
121 float b = y0 - 2.5f * y1 + 2.0f * y2 - 0.5f * y3;
122 float c = -0.5f * y0 + 0.5f * y2;
123 float d = y1;
124
125 return ((a * frac + b) * frac + c) * frac + d;
126 }
127
128 /**
129 * @brief Block processing with per-sample modulated delay
130 *
131 * For each frame: writes in[i], reads at delay[i], outputs to out[i].
132 * The delay array provides per-sample modulation (e.g., base delay + LFO).
133 * Uses linear interpolation.
134 *
135 * in and out may alias (in-place processing is safe since each sample
136 * is read from in before write, and output is written after read).
137 *
138 * @param in Input audio buffer
139 * @param out Output audio buffer
140 * @param delay Per-sample delay values in samples
141 * @param frames Number of frames to process
142 */
143 void process(const float* in, float* out,
144 const float* delay, uint16_t frames) {
145 for (uint16_t i = 0; i < frames; ++i) {
146 write(in[i]);
147 out[i] = read(delay[i]);
148 }
149 }
150
151 /** @brief Zero all samples in the buffer and reset write position */
152 void reset() {
153 if (buffer_) {
154 for (uint32_t i = 0; i < max_delay_; ++i) {
155 buffer_[i] = 0.0f;
156 }
157 }
158 write_pos_ = 0;
159 }
160
161 /** @brief Maximum delay in samples (buffer size) */
162 uint32_t max_delay() const { return max_delay_; }
163
164 /** @brief Current write position (for external tap reads) */
165 uint32_t write_pos() const { return write_pos_; }
166
167private:
168 /**
169 * @brief Read sample at integer delay from write position
170 * @param delay Integer delay in samples (1 = most recently written)
171 */
172 float read_at(uint32_t delay) const {
173 // write_pos_ points to the *next* write location.
174 // delay=1 reads the most recently written sample (write_pos_ - 1).
175 uint32_t pos = (write_pos_ + max_delay_ - delay) % max_delay_;
176 return buffer_[pos];
177 }
178
179 float* buffer_ = nullptr;
180 uint32_t max_delay_ = 0;
181 uint32_t write_pos_ = 0;
182};
183
184} // namespace sbl::dsp::primitives
185
186#endif // SBL_DSP_PRIMITIVES_DELAY_LINE_HPP_
float read(float delay_samples) const
Read at fractional delay with linear interpolation.
DelayLine(float *buffer, uint32_t max_delay)
Construct with caller-provided buffer.
uint32_t max_delay() const
Maximum delay in samples (buffer size)
void write(float sample)
Write a sample to the delay line.
float read_cubic(float delay_samples) const
Read at fractional delay with 4-point Hermite cubic interpolation.
DelayLine()=default
Default constructor for deferred initialization (must call init() before use)
void init(float *buffer, uint32_t max_delay)
Initialize after default construction.
uint32_t write_pos() const
Current write position (for external tap reads)
void reset()
Zero all samples in the buffer and reset write position.
void process(const float *in, float *out, const float *delay, uint16_t frames)
Block processing with per-sample modulated delay.
Stateful, single-concern building blocks.