Skip to content

Commit 5c75fba

Browse files
committed
AdaptiveResamplingStream DRAFT
1 parent d583769 commit 5c75fba

File tree

5 files changed

+306
-106
lines changed

5 files changed

+306
-106
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#pragma once
2+
3+
#include "AudioTools/CoreAudio/AudioBasic/KalmanFilter.h"
4+
#include "AudioTools/CoreAudio/AudioBasic/PIDController.h"
5+
#include "AudioTools/CoreAudio/AudioStreams.h"
6+
#include "AudioTools/CoreAudio/ResampleStream.h"
7+
8+
namespace audio_tools {
9+
10+
/**
11+
* @brief An Audio Stream backed by a buffer (queue) which tries to correct
12+
* jitter and automatically adjusts for the slightly different clock rates
13+
* between an audio source and audio target. Use separate tasks to write and
14+
* read the data. Also make sure that you protect the access with a mutex or
15+
* provide a thread-safe buffer!
16+
*
17+
* The resamping step size is calculated with the help of a PID controller.
18+
* The fill level is smoothed using a Kalman filter.
19+
*
20+
* @ingroup buffers
21+
* @author Phil Schatzmann
22+
*/
23+
class AdaptiveResamplingStream : public AudioStream {
24+
public:
25+
/**
26+
* @brief Construct a new AdaptiveResamplingStream object
27+
*
28+
* @param buffer Reference to the buffer used for audio data
29+
* @param stepRangePercent Allowed resampling range in percent (default: 0.05)
30+
*/
31+
AdaptiveResamplingStream(BaseBuffer<uint8_t>& buffer,
32+
float stepRangePercent = 0.05) {
33+
p_buffer = &buffer;
34+
setStepRangePercent(stepRangePercent);
35+
addNotifyAudioChange(resample_stream);
36+
}
37+
38+
/**
39+
* @brief Initialize the stream and internal components.
40+
*
41+
* @return true if initialization was successful, false otherwise
42+
*/
43+
bool begin() {
44+
if (p_buffer == nullptr) return false;
45+
queue_stream.setBuffer(*p_buffer);
46+
queue_stream.begin();
47+
resample_stream.setAudioInfo(audioInfo());
48+
resample_stream.setStream(queue_stream);
49+
resample_stream.begin(audioInfo());
50+
float from_step = 1.0 - resample_range;
51+
float to_step = 1.0 + resample_range;
52+
return pid.begin(1.0, from_step, to_step, p, i, d);
53+
}
54+
55+
/**
56+
* @brief Write audio data to the buffer.
57+
*
58+
* @param data Pointer to the data to write
59+
* @param len Number of bytes to write
60+
* @return size_t Number of bytes actually written
61+
*/
62+
size_t write(const uint8_t* data, size_t len) override {
63+
if (p_buffer == 0) return 0;
64+
size_t result = p_buffer->writeArray(data, len);
65+
recalculate();
66+
return result;
67+
}
68+
69+
/**
70+
* @brief End the stream and release resources.
71+
*/
72+
void end() {
73+
queue_stream.end();
74+
resample_stream.end();
75+
}
76+
77+
/**
78+
* @brief Read resampled audio data from the buffer.
79+
*
80+
* @param data Pointer to the buffer to fill
81+
* @param len Number of bytes to read
82+
* @return size_t Number of bytes actually read
83+
*/
84+
size_t readBytes(uint8_t* data, size_t len) override {
85+
if (p_buffer->available() == 0) return 0;
86+
87+
return resample_stream.readBytes(data, len);
88+
}
89+
90+
/**
91+
* @brief Recalculate the resampling step size based on buffer fill level.
92+
*
93+
* @return float The new step size
94+
*/
95+
float recalculate() {
96+
if (p_buffer == nullptr) return step_size;
97+
98+
// calculate new resampling step size
99+
level_percent = p_buffer->levelPercent();
100+
kalman_filter.addMeasurement(level_percent);
101+
step_size = pid.calculate(50.0, kalman_filter.calculate());
102+
103+
// log step size every 10th read
104+
if (read_count++ % 10 == 0) {
105+
LOGI("step_size: %f", step_size);
106+
}
107+
108+
// return resampled result
109+
resample_stream.setStepSize(step_size);
110+
return step_size;
111+
}
112+
113+
/**
114+
* @brief Set the allowed resampling range as a percent.
115+
*
116+
* @param rangePercent Allowed range in percent (e.g., 0.05 for ±0.05%)
117+
*/
118+
void setStepRangePercent(float rangePercent) {
119+
resample_range = rangePercent / 100.0;
120+
}
121+
122+
/**
123+
* @brief Get the current actual buffer fill level in percent.
124+
*
125+
* @return float Current fill level (0-100)
126+
*/
127+
float levelPercentActual() {
128+
if (p_buffer == nullptr) return 0.0f;
129+
return p_buffer->levelPercent();
130+
}
131+
132+
/**
133+
* @brief Get the fill level at the last calculation in percent.
134+
*
135+
* @return float Last calculated fill level (0-100)
136+
*/
137+
float levelPercent() { return level_percent; }
138+
139+
/**
140+
* @brief Set the Kalman filter parameters.
141+
*
142+
* @param process_noise Process noise covariance (Q)
143+
* @param measurement_noise Measurement noise covariance (R)
144+
*/
145+
void setKalmanParameters(float process_noise, float measurement_noise) {
146+
kalman_filter.begin(process_noise, measurement_noise);
147+
}
148+
149+
/**
150+
* @brief Set the PID controller parameters.
151+
*
152+
* @param p_value Proportional gain
153+
* @param i_value Integral gain
154+
* @param d_value Derivative gain
155+
*/
156+
void setPIDParameters(float p_value, float i_value, float d_value) {
157+
p = p_value;
158+
i = i_value;
159+
d = d_value;
160+
}
161+
162+
protected:
163+
PIDController pid; // PID controller for adaptive resampling step size (p=0.005, i=0.00005, d=0.0001)
164+
QueueStream<uint8_t> queue_stream; // Internal queue stream for buffering audio data
165+
BaseBuffer<uint8_t>* p_buffer = nullptr; // Pointer to the user-provided buffer
166+
ResampleStream resample_stream; // Resample stream for adjusting playback rate
167+
KalmanFilter kalman_filter{0.01f, 0.1f}; // Kalman filter for smoothing buffer fill level
168+
float step_size = 1.0; // Current resampling step size
169+
float resample_range = 0; // Allowed resampling range (fraction)
170+
float p = 0.005; // PID proportional gain
171+
float i = 0.00005; // PID integral gain
172+
float d = 0.0001; // PID derivative gain
173+
float level_percent = 0.0; // Last calculated buffer fill level (percent)
174+
uint32_t read_count = 0; // Read operation counter
175+
};
176+
177+
} // namespace audio_tools
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#pragma once
2+
3+
namespace audio_tools {
4+
5+
/**
6+
* @brief Simple 1D Kalman Filter for smoothing measurements.
7+
*
8+
* This class implements a basic one-dimensional Kalman filter for smoothing
9+
* noisy measurements. It is useful for sensor data fusion and signal processing
10+
* applications where noise reduction is required.
11+
*
12+
* The Kalman filter uses two main parameters:
13+
* - @b process_noise (Q): Represents the expected process variance. A typical
14+
* default is 0.01.
15+
* - @b measurement_noise (R): Represents the expected measurement variance. A
16+
* typical default is 1.0.
17+
*
18+
* Lower values for Q make the filter trust the model more (less responsive to
19+
* changes), while higher values for R make the filter trust the measurements
20+
* less (more smoothing).
21+
*
22+
* @note Reasonable defaults for many sensor applications are Q = 0.01 and R
23+
* = 1.0, but these should be tuned for your specific use case.
24+
* @ingroup filters
25+
* @author Phil Schatzmann
26+
*/
27+
class KalmanFilter {
28+
public:
29+
/**
30+
* @brief Construct a new Kalman Filter object
31+
*
32+
* @param process_noise The process noise covariance (Q). Default is 0.01.
33+
* @param measurement_noise The measurement noise covariance (R). Default
34+
* is 1.0.
35+
*/
36+
KalmanFilter(float process_noise = 0.01f, float measurement_noise = 1.0f) {
37+
begin(process_noise, measurement_noise);
38+
}
39+
40+
/**
41+
* @brief reset the filter with new parameters
42+
*
43+
* @param process_noise The process noise covariance (Q). Default is 0.01.
44+
* @param measurement_noise The measurement noise covariance (R). Default
45+
* is 1.0.
46+
*/
47+
48+
/**
49+
* @brief Initialize or reset the filter with new parameters.
50+
*
51+
* @param process_noise The process noise covariance (Q). Default is 0.01.
52+
* @param measurement_noise The measurement noise covariance (R). Default
53+
* is 1.0.
54+
* @return true Always returns true.
55+
*/
56+
bool begin(float process_noise, float measurement_noise) {
57+
Q = process_noise;
58+
R = measurement_noise;
59+
P = 1.0;
60+
X = 0.0;
61+
return true;
62+
}
63+
64+
/**
65+
* @brief Reset the filter state to zero, keeping the current noise
66+
* parameters.
67+
*
68+
* @return true Always returns true.
69+
*/
70+
bool begin() {
71+
X = 0;
72+
return true;
73+
}
74+
75+
/**
76+
* @brief End or clear the filter (sets the estimate to zero).
77+
*/
78+
void end() { X = 0; }
79+
80+
/**
81+
* @brief Updates the filter with a new measurement and returns the filtered
82+
* value.
83+
*
84+
* @param measurement The new measurement to be filtered
85+
* @return float The updated (filtered) estimate
86+
*/
87+
void addMeasurement(float measurement) {
88+
// Prediction update
89+
P = P + Q;
90+
91+
// Measurement update
92+
K = P / (P + R);
93+
X = X + K * (measurement - X);
94+
P = (1 - K) * P;
95+
}
96+
97+
/**
98+
* @brief Returns the current estimated value.
99+
*
100+
* @return float The current estimate
101+
*/
102+
float calculate() { return X; }
103+
104+
protected:
105+
/**
106+
* @brief Process noise covariance (Q)
107+
*/
108+
float Q;
109+
/**
110+
* @brief Measurement noise covariance (R)
111+
*/
112+
float R;
113+
/**
114+
* @brief Estimation error covariance (P)
115+
*/
116+
float P;
117+
/**
118+
* @brief Estimated state (X)
119+
*/
120+
float X;
121+
/**
122+
* @brief Kalman gain (K)
123+
*/
124+
float K;
125+
};
126+
127+
} // namespace audio_tools

src/AudioTools/CoreAudio/AudioBasic/MovingAverage.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ class MovingAverage {
1717
setSize(size);
1818
}
1919

20-
void add(N value) {
20+
void addMeasurement(N value) {
2121
if (this->values.size() == this->size) {
2222
this->values.pop_front();
2323
}
2424
this->values.push_back(value);
2525
}
2626

27-
float average() {
27+
float calculate() {
2828
float sum = 0;
2929
for (int i = 0; i < this->values.size(); i++) {
3030
sum += this->values[i];
File renamed without changes.

0 commit comments

Comments
 (0)