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