arduino-audio-tools
Loading...
Searching...
No Matches
AdaptiveResamplingBuffer.h
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4
9
10namespace audio_tools {
11
51class AdaptiveResamplingBuffer : public BaseBuffer<uint8_t>,
52 public AudioInfoSupport {
53 public:
59
67 float stepRangePercent = 5.0f) {
68 setBuffer(buffer);
69 setStepRangePercent(stepRangePercent);
70 }
71
83 void setStartFillPercent(float percent) { start_fill_percent = percent; }
84
85 // resample_stream internally stores a pointer to the sibling
86 // queue_stream member (via setStream()). A copy would leave the copy's
87 // resample_stream pointing at the original's queue_stream, silently
88 // sharing/dangling state - so copying (and, since neither QueueStream
89 // nor ResampleStream defines a real move, moving) is disabled.
92
98 void setBuffer(BaseBuffer<uint8_t>& buffer) { p_buffer = &buffer; }
99
104 void setAudioInfo(AudioInfo info) override {
105 audio_info = info;
106 // if we are already running, apply the change immediately
108 }
109
110 AudioInfo audioInfo() override { return audio_info; }
111
117 bool begin() {
118 if (p_buffer == nullptr) return false;
119 if (resample_range <= 0.0f) {
120 LOGW(
121 "resample_range is 0: call setStepRangePercent() before begin() "
122 "to enable adaptive resampling");
123 }
128 // This is a jitter buffer for an endless/live stream: a momentarily
129 // empty backing buffer is a transient underflow, not end of stream.
130 resample_stream.transformationReader().setEofOnZeroReads(false);
131 // readArray()/available() are typically driven from the same loop that
132 // also has to keep a downstream time-critical consumer fed (e.g. a
133 // StreamCopy pumping an I2S output with only a few ms of DMA buffer).
134 // The default zero-read handling blocks in delay() - up to
135 // MAX_ZERO_READ_COUNT times - waiting for more input on every
136 // transient underflow; on that calling pattern, blocking here is what
137 // starves the downstream consumer, not the resampling math itself. So
138 // give up on a transient underflow immediately instead of stalling.
139 resample_stream.transformationReader().setZeroReadDelay(0);
140 last_time_ms = 0;
141 is_active = true;
142 is_primed = false;
144 // pid.calculate() returns a correction in [-resample_range, +resample_range]
145 // which is subtracted from the feedforward center in recalculate().
146 return pid.begin(1.0, resample_range, -resample_range, p, i, d);
147 }
148
152 void end() {
153 is_active = false;
156 recalc_count = 0;
157 last_time_ms = 0;
158 has_peek = false;
159 }
160
162 bool write(uint8_t data) override { return writeArray(&data, 1) == 1; }
163
166 int writeArray(const uint8_t data[], int len) override {
167 if (p_buffer == nullptr) return 0;
168 int result = p_buffer->writeArray(data, len);
169 total_bytes_written += result;
170 recalculate();
171 return result;
172 }
173
175 bool read(uint8_t& result) override { return readArray(&result, 1) == 1; }
176
179 int readArray(uint8_t data[], int len) override {
180 if (p_buffer == nullptr || len <= 0 || !checkPrimed()) return 0;
181 int written = 0;
182 if (has_peek) {
183 data[written++] = peek_byte;
184 has_peek = false;
185 }
186 if (written < len) {
187 written += resample_stream.readBytes(data + written, len - written);
188 }
189 total_bytes_read += written;
190 return written;
191 }
192
195 bool peek(uint8_t& result) override {
196 if (!checkPrimed()) return false;
197 if (!has_peek) {
198 if (resample_stream.readBytes(&peek_byte, 1) != 1) return false;
199 has_peek = true;
200 }
201 result = peek_byte;
202 return true;
203 }
204
209 void reset() override {
210 if (p_buffer != nullptr) p_buffer->reset();
211 has_peek = false;
212 pid.reset();
214 step_size = 1.0f;
216 last_time_ms = 0;
217 is_primed = false;
219 if (is_active) {
223 }
224 }
225
233 int available() override {
234 if (p_buffer == nullptr || !checkPrimed()) return 0;
235 int raw_available = p_buffer->available();
236 float estimate = step_size > 0.0f ? raw_available / step_size : raw_available;
237 return (has_peek ? 1 : 0) + (int)estimate;
238 }
239
241 int availableForWrite() override {
242 if (p_buffer == nullptr) return 0;
243 return p_buffer->availableForWrite();
244 }
245
247 uint8_t* address() override { return nullptr; }
248
250 size_t size() override { return p_buffer == nullptr ? 0 : p_buffer->size(); }
251
253 float levelPercent() override {
254 if (p_buffer == nullptr) return 0.0f;
255 return p_buffer->levelPercent();
256 }
257
264 float recalculate() {
265 if (p_buffer == nullptr) return step_size;
266
267 // Throttle: writeArray() calls us on every write (e.g. once per
268 // received USB packet, possibly ~1ms apart). Actually redoing the
269 // PID/Kalman math that often ties the PID's dt - and therefore its
270 // derivative term - to the caller's write cadence rather than a
271 // deliberate control-loop interval, so a single write-sized jump in
272 // the buffer fill level can dominate/saturate the derivative term.
273 // Skip the recomputation (and its float math) until enough real time
274 // has passed; in between, callers keep getting the last computed
275 // step size, which is already applied to resample_stream.
276 uint32_t now_ms = millis();
277 if (min_recalc_interval_ms > 0 && last_time_ms != 0 &&
278 (uint32_t)(now_ms - last_time_ms) < min_recalc_interval_ms) {
279 return step_size;
280 }
281
282 // determine the actual elapsed time so the PID's integral/derivative
283 // terms don't depend on how often/how large the caller's writes are
284 float dt = last_time_ms == 0 ? 1.0f : (now_ms - last_time_ms) / 1000.0f;
285 if (dt <= 0.0f) dt = 0.001f;
286 pid.setDt(dt);
287 last_time_ms = now_ms;
288
289 // calculate new resampling step size
292 // A larger step size makes the resampler consume the buffered input
293 // faster, so a low fill level (correction > 0) must reduce the step
294 // size, and a high fill level (correction < 0) must increase it.
295 float correction = pid.calculate(50.0, kalman_filter.calculate());
296 // Feedforward + feedback: center on the long-run average step size
297 // implied by cumulative bytes written vs. read (a low-noise estimate
298 // of genuine producer/consumer clock drift) and let the PID's
299 // correction only handle short-term jitter around that center. This
300 // is far more stable than centering on a fixed 1.0 and relying solely
301 // on the PID to chase both drift and jitter from a noisy instantaneous
302 // fill level.
303 float new_step_size = averageStepSize() - correction;
304 // averageStepSize() is only sanity-clamped to a generous [0.5, 2.0] (see
305 // its docs) since it must be able to represent genuine long-run clock
306 // drift; it is NOT bounded by resample_range. Enforce the documented
307 // setStepRangePercent() contract here, on the combined result, so a
308 // skewed drift estimate (e.g. from a transient input underrun briefly
309 // depressing total_bytes_read - see readArray()) can't push playback
310 // speed - and therefore pitch - outside the range the caller asked for.
311 float min_step = 1.0f - resample_range;
312 float max_step = 1.0f + resample_range;
313 if (new_step_size < min_step) new_step_size = min_step;
314 if (new_step_size > max_step) new_step_size = max_step;
315
316 // Slew-rate limit: bound how fast step_size is allowed to move per
317 // second, regardless of how large the jump from the current value to
318 // new_step_size is or how long dt happens to be. Without this, a
319 // wide resample_range combined with an infrequent recalculation
320 // cadence (setMinRecalculateInterval()) lets a single correction snap
321 // to a far-off value and hold it for the whole interval - audible as
322 // a sudden pitch jump/"crack" rather than a smooth bend. This makes
323 // any correction, however large the target, ramp in gradually and
324 // caps the worst-case audible glitch regardless of range/interval
325 // settings.
326 float current_step = step_size;
327 float max_delta = max_step_change_per_sec * dt;
328 float delta = new_step_size - current_step;
329 if (delta > max_delta) delta = max_delta;
330 if (delta < -max_delta) delta = -max_delta;
331 step_size = current_step + delta;
332
333 // log step size every 100th recalculation
334 if (recalc_count++ % 100 == 0) {
335 LOGI("step_size: %f", step_size.load());
336 }
337
339 return step_size;
340 }
341
354 // Deliberately NOT netted against the buffer's current occupancy
355 // (e.g. total_written - p_buffer->available()): that quantity is
356 // itself the result of whatever step size the resampler already
357 // applied, so feeding it back as the next step size's center creates
358 // a circular, self-reinforcing loop - any startup transient gets
359 // permanently amplified instead of forgotten. total_bytes_written and
360 // total_bytes_read are each driven purely by the source/sink and
361 // don't depend on our own past control output, so their ratio is the
362 // step size that would have kept the buffer's fill level unchanged
363 // over the window - a genuine, non-circular drift estimate. Residual
364 // absolute fill-level error is exactly what the PID feedback term
365 // already corrects for.
366 float ratio = (float)total_bytes_written / (float)total_bytes_read;
367 // safety net against a pathological/runaway estimate; real clock drift
368 // is normally well under 1%, so this only guards against bugs/edge cases
369 if (ratio < 0.5f) ratio = 0.5f;
370 if (ratio > 2.0f) ratio = 2.0f;
371 return ratio;
372 }
373
380 void setMinBytesForDriftEstimate(uint32_t bytes) {
382 }
383
403 void setMinRecalculateInterval(uint32_t interval_ms) {
404 min_recalc_interval_ms = interval_ms;
405 }
406
417 void setMaxStepChangeRate(float per_second) {
418 max_step_change_per_sec = per_second;
419 }
420
424
427 uint64_t totalBytesRead() { return total_bytes_read; }
428
434 void setStepRangePercent(float rangePercent) {
435 resample_range = rangePercent / 100.0;
436 }
437
445
448 float stepSize() { return step_size; }
449
452 bool isPrimed() { return checkPrimed(); }
453
460 void setKalmanParameters(float process_noise, float measurement_noise) {
461 kalman_filter.begin(process_noise, measurement_noise);
462 }
463
471 void setPIDParameters(float p_value, float i_value, float d_value) {
472 p = p_value;
473 i = i_value;
474 d = d_value;
475 }
476
477 protected:
483
486 bool checkPrimed() {
487 if (is_primed) return true;
488 if (start_fill_percent <= 0.0f) {
489 is_primed = true;
490 } else if (p_buffer != nullptr &&
492 is_primed = true;
493 }
494 if (is_primed) {
495 // Priming accumulates writes with no matching reads, so the
496 // resampler's first-ever read would have to work through that
497 // whole backlog at once - permanently skewing the cumulative
498 // written-vs-read ratio used by averageStepSize(). Start counting
499 // only from the moment real (post-priming) traffic begins.
501 }
502 return is_primed;
503 }
504
505 PIDController pid; // PID controller for adaptive resampling step size
506 QueueStream<uint8_t> queue_stream; // Internal queue stream for buffering audio data
507 BaseBuffer<uint8_t>* p_buffer = nullptr; // Pointer to the user-provided raw buffer
508 ResampleStream resample_stream; // Resample stream for adjusting playback rate
509 KalmanFilter kalman_filter{0.01f, 0.1f}; // Kalman filter for smoothing buffer fill level
510 AudioInfo audio_info; // Audio format of the buffered data
511 std::atomic<float> step_size{1.0f}; // Current resampling step size (see class docs on thread-safety)
512 float resample_range = 0; // Allowed resampling range (fraction)
513 float p = 0.005; // PID proportional gain
514 float i = 0.00005; // PID integral gain
515 float d = 0.0001; // PID derivative gain
516 float level_percent_smoothed = 0.0; // Last calculated (Kalman-smoothed) fill level (percent)
517 uint32_t recalc_count = 0; // recalculate() call counter (used to throttle logging)
518 uint32_t last_time_ms = 0; // Timestamp of the last actual recalculation, for PID dt
519 uint32_t min_recalc_interval_ms = 0; // 0 = recalculate on every write (default/prior behavior)
520 float max_step_change_per_sec = 0.2f; // max allowed |d(step_size)/dt|, in step units/sec
521 bool is_active = false; // true between begin() and end()
522 uint8_t peek_byte = 0; // one-byte lookahead cache for peek()
523 bool has_peek = false; // true if peek_byte holds an unconsumed byte
524 float start_fill_percent = 50.0f; // required fill level before priming completes (0 = disabled)
525 bool is_primed = false; // true once the start fill level has been reached once
526 uint64_t total_bytes_written = 0; // cumulative raw bytes written since counters were last reset
527 uint64_t total_bytes_read = 0; // cumulative resampled bytes delivered since counters were last reset
528 uint32_t min_bytes_for_drift_estimate = 4096; // warm-up threshold for averageStepSize()
529};
530
531} // namespace audio_tools
#define LOGW(...)
Definition AudioLoggerIDF.h:29
#define LOGI(...)
Definition AudioLoggerIDF.h:28
A BaseBuffer<uint8_t> that wraps a raw (unresampled) backing buffer and transparently resamples on re...
Definition AdaptiveResamplingBuffer.h:52
void setStepRangePercent(float rangePercent)
Set the allowed resampling range as a percent.
Definition AdaptiveResamplingBuffer.h:434
bool has_peek
Definition AdaptiveResamplingBuffer.h:523
uint32_t last_time_ms
Definition AdaptiveResamplingBuffer.h:518
uint32_t min_recalc_interval_ms
Definition AdaptiveResamplingBuffer.h:519
uint8_t * address() override
Not supported: resampled data has no contiguous physical representation.
Definition AdaptiveResamplingBuffer.h:247
size_t size() override
Capacity (in bytes) of the backing buffer.
Definition AdaptiveResamplingBuffer.h:250
void setStartFillPercent(float percent)
Defines the fill level (in percent of the backing buffer's capacity) that must be reached once,...
Definition AdaptiveResamplingBuffer.h:83
AdaptiveResamplingBuffer & operator=(const AdaptiveResamplingBuffer &)=delete
float p
Definition AdaptiveResamplingBuffer.h:513
void setBuffer(BaseBuffer< uint8_t > &buffer)
Set the raw (unresampled) backing buffer.
Definition AdaptiveResamplingBuffer.h:98
void setPIDParameters(float p_value, float i_value, float d_value)
Set the PID controller parameters.
Definition AdaptiveResamplingBuffer.h:471
KalmanFilter kalman_filter
Definition AdaptiveResamplingBuffer.h:509
BaseBuffer< uint8_t > * p_buffer
Definition AdaptiveResamplingBuffer.h:507
AdaptiveResamplingBuffer(const AdaptiveResamplingBuffer &)=delete
int readArray(uint8_t data[], int len) override
Definition AdaptiveResamplingBuffer.h:179
float d
Definition AdaptiveResamplingBuffer.h:515
bool is_active
Definition AdaptiveResamplingBuffer.h:521
bool isPrimed()
Definition AdaptiveResamplingBuffer.h:452
bool write(uint8_t data) override
Writes a single byte to the backing buffer.
Definition AdaptiveResamplingBuffer.h:162
AdaptiveResamplingBuffer(BaseBuffer< uint8_t > &buffer, float stepRangePercent=5.0f)
Construct a new AdaptiveResamplingBuffer object.
Definition AdaptiveResamplingBuffer.h:66
float start_fill_percent
Definition AdaptiveResamplingBuffer.h:524
void setMaxStepChangeRate(float per_second)
Maximum rate at which step_size is allowed to change, in (fractional step) units per second - e....
Definition AdaptiveResamplingBuffer.h:417
bool peek(uint8_t &result) override
Definition AdaptiveResamplingBuffer.h:195
float i
Definition AdaptiveResamplingBuffer.h:514
void resetDriftCounters()
Resets the cumulative counters used by averageStepSize().
Definition AdaptiveResamplingBuffer.h:479
bool begin()
Initialize the buffer and internal components.
Definition AdaptiveResamplingBuffer.h:117
AudioInfo audio_info
Definition AdaptiveResamplingBuffer.h:510
uint32_t recalc_count
Definition AdaptiveResamplingBuffer.h:517
int available() override
Definition AdaptiveResamplingBuffer.h:233
uint64_t total_bytes_read
Definition AdaptiveResamplingBuffer.h:527
uint64_t total_bytes_written
Definition AdaptiveResamplingBuffer.h:526
ResampleStream resample_stream
Definition AdaptiveResamplingBuffer.h:508
int availableForWrite() override
Number of raw bytes that can still be written to the backing buffer.
Definition AdaptiveResamplingBuffer.h:241
QueueStream< uint8_t > queue_stream
Definition AdaptiveResamplingBuffer.h:506
float levelPercent() override
Current actual fill level of the backing buffer in percent (0-100).
Definition AdaptiveResamplingBuffer.h:253
AdaptiveResamplingBuffer()=default
Construct a new AdaptiveResamplingBuffer object You need to call setBuffer() and setStepRangePercent(...
uint32_t min_bytes_for_drift_estimate
Definition AdaptiveResamplingBuffer.h:528
uint8_t peek_byte
Definition AdaptiveResamplingBuffer.h:522
float resample_range
Definition AdaptiveResamplingBuffer.h:512
void end()
End the buffer and release resources.
Definition AdaptiveResamplingBuffer.h:152
float stepSize()
Definition AdaptiveResamplingBuffer.h:448
std::atomic< float > step_size
Definition AdaptiveResamplingBuffer.h:511
bool checkPrimed()
Definition AdaptiveResamplingBuffer.h:486
bool read(uint8_t &result) override
Reads a single resampled byte.
Definition AdaptiveResamplingBuffer.h:175
void setKalmanParameters(float process_noise, float measurement_noise)
Set the Kalman filter parameters.
Definition AdaptiveResamplingBuffer.h:460
void setMinRecalculateInterval(uint32_t interval_ms)
Minimum time between actual PID/Kalman recomputations, in milliseconds. writeArray() calls recalculat...
Definition AdaptiveResamplingBuffer.h:403
float averageStepSize()
Long-run average step size implied by cumulative bytes written vs. bytes read, used as the feedforwar...
Definition AdaptiveResamplingBuffer.h:352
void setMinBytesForDriftEstimate(uint32_t bytes)
Minimum cumulative output bytes that must have been read before averageStepSize() is trusted; below t...
Definition AdaptiveResamplingBuffer.h:380
float levelPercentSmoothed()
Get the Kalman-smoothed fill level from the last recalculate() call, in percent.
Definition AdaptiveResamplingBuffer.h:444
void setAudioInfo(AudioInfo info) override
Defines the audio format: needed to correctly interpret and resample the sample frames stored in the ...
Definition AdaptiveResamplingBuffer.h:104
bool is_primed
Definition AdaptiveResamplingBuffer.h:525
float level_percent_smoothed
Definition AdaptiveResamplingBuffer.h:516
uint64_t totalBytesRead()
Definition AdaptiveResamplingBuffer.h:427
void reset() override
Definition AdaptiveResamplingBuffer.h:209
AudioInfo audioInfo() override
provides the actual input AudioInfo
Definition AdaptiveResamplingBuffer.h:110
float max_step_change_per_sec
Definition AdaptiveResamplingBuffer.h:520
uint64_t totalBytesWritten()
Definition AdaptiveResamplingBuffer.h:423
float recalculate()
Recalculate the resampling step size based on buffer fill level. Called automatically by writeArray()...
Definition AdaptiveResamplingBuffer.h:264
int writeArray(const uint8_t data[], int len) override
Definition AdaptiveResamplingBuffer.h:166
PIDController pid
Definition AdaptiveResamplingBuffer.h:505
Supports changes to the sampling rate, bits and channels.
Definition AudioTypes.h:131
Shared functionality of all buffers.
Definition Buffers.h:23
virtual void reset()=0
clears the buffer
virtual int writeArray(const T data[], int len)
Fills the buffer data.
Definition Buffers.h:56
virtual size_t size()=0
virtual int availableForWrite()=0
provides the number of entries that are available to write
virtual float levelPercent()
Returns the level of the buffer in %.
Definition Buffers.h:114
virtual int available()=0
provides the number of entries that are available to read
Simple 1D Kalman Filter for smoothing measurements.
Definition KalmanFilter.h:27
float calculate()
Returns the current estimated value.
Definition KalmanFilter.h:102
bool begin(float process_noise, float measurement_noise)
reset the filter with new parameters
Definition KalmanFilter.h:56
void end()
End or clear the filter (sets the estimate to zero).
Definition KalmanFilter.h:78
void addMeasurement(float measurement)
Updates the filter with a new measurement and returns the filtered value.
Definition KalmanFilter.h:87
A simple header only PID Controller.
Definition PIDController.h:15
float calculate(float target, float measured)
Definition PIDController.h:49
void setDt(float dt)
Definition PIDController.h:35
void reset()
Definition PIDController.h:41
bool begin(float dt, float max, float min, float kp, float ki, float kd)
Definition PIDController.h:23
Stream class which stores the data in a temporary queue buffer. The queue can be consumed e....
Definition BaseStream.h:359
virtual bool begin() override
Activates the output.
Definition BaseStream.h:387
void setBuffer(BaseBuffer< T > &buffer)
Definition BaseStream.h:381
virtual void end() override
stops the processing
Definition BaseStream.h:406
virtual void setStream(Stream &stream) override
Defines/Changes the input & output.
Definition AudioIO.h:270
size_t readBytes(uint8_t *data, size_t len) override
Definition AudioIO.h:299
void end() override
Definition AudioIO.h:321
virtual TransformationReader< ReformatBaseStream > & transformationReader()
Provides access to the TransformationReader.
Definition AudioIO.h:338
Dynamic Resampling. We can use a variable factor to speed up or slow down the playback.
Definition ResampleStream.h:33
void setAudioInfo(AudioInfo newInfo) override
Defines the input AudioInfo.
Definition ResampleStream.h:133
void setStepSize(float step)
influence the sample rate
Definition ResampleStream.h:154
bool begin(ResampleConfig cfg)
Definition ResampleStream.h:63
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition LMSEchoCancellationStream.h:6
uint32_t millis()
Returns the milliseconds since the start.
Definition Arduino.h:260
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:51