dspx 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -78
- package/binding.gyp +10 -0
- package/dist/FilterBankDesign.d.ts +233 -0
- package/dist/FilterBankDesign.d.ts.map +1 -0
- package/dist/FilterBankDesign.js +247 -0
- package/dist/FilterBankDesign.js.map +1 -0
- package/dist/advanced-dsp.d.ts +6 -6
- package/dist/advanced-dsp.d.ts.map +1 -1
- package/dist/advanced-dsp.js +35 -12
- package/dist/advanced-dsp.js.map +1 -1
- package/dist/backends.d.ts +0 -103
- package/dist/backends.d.ts.map +1 -1
- package/dist/backends.js +0 -217
- package/dist/backends.js.map +1 -1
- package/dist/bindings.d.ts +216 -17
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +503 -42
- package/dist/bindings.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +67 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +38 -8
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +84 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -2
- package/prebuilds/win32-x64/dspx.node +0 -0
- package/scripts/add-dispose-to-tests.js +145 -0
- package/src/native/DspPipeline.cc +777 -143
- package/src/native/DspPipeline.h +13 -0
- package/src/native/FilterBankDesignBindings.cc +241 -0
- package/src/native/IDspStage.h +24 -0
- package/src/native/UtilityBindings.cc +130 -0
- package/src/native/adapters/ClipDetectionStage.h +15 -4
- package/src/native/adapters/ConvolutionStage.h +101 -0
- package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
- package/src/native/adapters/DecimatorStage.h +80 -0
- package/src/native/adapters/DifferentiatorStage.h +13 -0
- package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
- package/src/native/adapters/FilterBankStage.cc +336 -0
- package/src/native/adapters/FilterBankStage.h +170 -0
- package/src/native/adapters/FilterStage.cc +139 -1
- package/src/native/adapters/FilterStage.h +4 -0
- package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
- package/src/native/adapters/IntegratorStage.h +15 -0
- package/src/native/adapters/InterpolatorStage.h +51 -0
- package/src/native/adapters/LinearRegressionStage.h +40 -0
- package/src/native/adapters/LmsStage.h +63 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
- package/src/native/adapters/MovingAverageStage.h +119 -0
- package/src/native/adapters/PeakDetectionStage.h +53 -0
- package/src/native/adapters/RectifyStage.h +14 -0
- package/src/native/adapters/ResamplerStage.h +67 -0
- package/src/native/adapters/RlsStage.h +76 -0
- package/src/native/adapters/RmsStage.h +73 -1
- package/src/native/adapters/SnrStage.h +45 -0
- package/src/native/adapters/SscStage.h +65 -0
- package/src/native/adapters/StftStage.h +62 -0
- package/src/native/adapters/VarianceStage.h +60 -1
- package/src/native/adapters/WampStage.h +59 -0
- package/src/native/adapters/WaveformLengthStage.h +51 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +65 -1
- package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
- package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
- package/src/native/core/FilterBankDesign.h +266 -0
- package/src/native/core/Policies.h +124 -0
- package/src/native/utils/CircularBufferArray.cc +2 -1
- package/src/native/utils/Toon.h +195 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include "Policies.h"
|
|
3
|
+
#include <vector>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
|
|
6
|
+
namespace dsp::core
|
|
7
|
+
{
|
|
8
|
+
/**
|
|
9
|
+
* @brief Implements a Cumulative Moving Average (CMA) filter.
|
|
10
|
+
*
|
|
11
|
+
* CMA is the average of all samples seen since initialization.
|
|
12
|
+
* Formula: CMA(n) = (CMA(n-1) * (n-1) + value(n)) / n
|
|
13
|
+
* or: CMA(n) = sum(values[1..n]) / n
|
|
14
|
+
*
|
|
15
|
+
* Unlike simple moving average (SMA), CMA considers ALL historical data:
|
|
16
|
+
* - SMA: Uses fixed window of recent N samples
|
|
17
|
+
* - CMA: Uses all samples from start to current
|
|
18
|
+
*
|
|
19
|
+
* Properties:
|
|
20
|
+
* - Memory-efficient: Only stores running sum and count
|
|
21
|
+
* - Stable convergence: Influence of new samples decreases over time
|
|
22
|
+
* - No window size needed: Adapts to any data length
|
|
23
|
+
*
|
|
24
|
+
* Use cases:
|
|
25
|
+
* - Long-term averages where all history matters
|
|
26
|
+
* - Baseline estimation from calibration data
|
|
27
|
+
* - Online mean estimation for statistics
|
|
28
|
+
*
|
|
29
|
+
* @tparam T The numeric type of the samples (e.g., float, double).
|
|
30
|
+
*/
|
|
31
|
+
template <typename T>
|
|
32
|
+
class CumulativeMovingAverageFilter
|
|
33
|
+
{
|
|
34
|
+
public:
|
|
35
|
+
/**
|
|
36
|
+
* @brief Constructs a new CMA Filter.
|
|
37
|
+
*/
|
|
38
|
+
CumulativeMovingAverageFilter()
|
|
39
|
+
: m_policy()
|
|
40
|
+
{
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Delete copy constructor and copy assignment
|
|
44
|
+
CumulativeMovingAverageFilter(const CumulativeMovingAverageFilter &) = delete;
|
|
45
|
+
CumulativeMovingAverageFilter &operator=(const CumulativeMovingAverageFilter &) = delete;
|
|
46
|
+
|
|
47
|
+
// Enable move semantics
|
|
48
|
+
CumulativeMovingAverageFilter(CumulativeMovingAverageFilter &&) noexcept = default;
|
|
49
|
+
CumulativeMovingAverageFilter &operator=(CumulativeMovingAverageFilter &&) noexcept = default;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @brief Adds a new sample and updates the CMA.
|
|
53
|
+
* @param newValue The new sample value.
|
|
54
|
+
* @return T The updated CMA value.
|
|
55
|
+
*/
|
|
56
|
+
T addSample(T newValue)
|
|
57
|
+
{
|
|
58
|
+
m_policy.onAdd(newValue);
|
|
59
|
+
return m_policy.getResult(0); // Window count parameter unused for CMA
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @brief Process array of samples in batch (optimized for throughput).
|
|
64
|
+
*
|
|
65
|
+
* @param input Input array of samples
|
|
66
|
+
* @param output Output array (same size as input)
|
|
67
|
+
* @param length Number of samples to process
|
|
68
|
+
*/
|
|
69
|
+
void processArray(const T *input, T *output, size_t length)
|
|
70
|
+
{
|
|
71
|
+
for (size_t i = 0; i < length; ++i)
|
|
72
|
+
{
|
|
73
|
+
output[i] = addSample(input[i]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @brief Gets the current CMA value.
|
|
79
|
+
* @return T The cumulative moving average.
|
|
80
|
+
*/
|
|
81
|
+
T getCma() const { return m_policy.getResult(0); }
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @brief Gets the number of samples processed.
|
|
85
|
+
* @return size_t The total count of samples.
|
|
86
|
+
*/
|
|
87
|
+
size_t getCount() const { return m_policy.getCount(); }
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @brief Clears the CMA state (resets to zero).
|
|
91
|
+
*/
|
|
92
|
+
void clear() { m_policy.clear(); }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @brief Checks if any samples have been processed.
|
|
96
|
+
* @return true if count > 0, false otherwise.
|
|
97
|
+
*/
|
|
98
|
+
bool hasData() const { return m_policy.getCount() > 0; }
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @brief Exports the filter's internal state.
|
|
102
|
+
* @return A pair containing the running sum and sample count.
|
|
103
|
+
*/
|
|
104
|
+
std::pair<T, size_t> getState() const
|
|
105
|
+
{
|
|
106
|
+
return m_policy.getState();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @brief Restores the filter's internal state.
|
|
111
|
+
* @param sum The running sum to restore.
|
|
112
|
+
* @param count The sample count to restore.
|
|
113
|
+
*/
|
|
114
|
+
void setState(T sum, size_t count)
|
|
115
|
+
{
|
|
116
|
+
m_policy.setState(sum, count);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private:
|
|
120
|
+
CmaPolicy<T> m_policy;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
} // namespace dsp::core
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include "Policies.h"
|
|
3
|
+
#include <vector>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
|
|
6
|
+
namespace dsp::core
|
|
7
|
+
{
|
|
8
|
+
/**
|
|
9
|
+
* @brief Implements an Exponential Moving Average (EMA) filter.
|
|
10
|
+
*
|
|
11
|
+
* EMA gives more weight to recent samples and exponentially decaying weight to older samples.
|
|
12
|
+
* Formula: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
|
|
13
|
+
*
|
|
14
|
+
* where α (alpha) is the smoothing factor:
|
|
15
|
+
* - α close to 1: Fast response to changes (less smoothing)
|
|
16
|
+
* - α close to 0: Slow response to changes (more smoothing)
|
|
17
|
+
*
|
|
18
|
+
* Common conversions:
|
|
19
|
+
* - From N-period SMA: α = 2 / (N + 1)
|
|
20
|
+
* - From time constant: α = 1 - exp(-Δt / τ)
|
|
21
|
+
*
|
|
22
|
+
* Unlike simple moving average, EMA does not require a fixed window size,
|
|
23
|
+
* making it memory-efficient for very long averaging periods.
|
|
24
|
+
*
|
|
25
|
+
* @tparam T The numeric type of the samples (e.g., float, double).
|
|
26
|
+
*/
|
|
27
|
+
template <typename T>
|
|
28
|
+
class ExponentialMovingAverageFilter
|
|
29
|
+
{
|
|
30
|
+
public:
|
|
31
|
+
/**
|
|
32
|
+
* @brief Constructs a new EMA Filter with specified alpha.
|
|
33
|
+
* @param alpha The smoothing factor (0 < α ≤ 1).
|
|
34
|
+
* @throws std::invalid_argument if alpha is outside valid range.
|
|
35
|
+
*/
|
|
36
|
+
explicit ExponentialMovingAverageFilter(T alpha)
|
|
37
|
+
: m_policy(alpha)
|
|
38
|
+
{
|
|
39
|
+
if (alpha <= 0 || alpha > 1)
|
|
40
|
+
{
|
|
41
|
+
throw std::invalid_argument("EMA alpha must be in range (0, 1]");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Delete copy constructor and copy assignment
|
|
46
|
+
ExponentialMovingAverageFilter(const ExponentialMovingAverageFilter &) = delete;
|
|
47
|
+
ExponentialMovingAverageFilter &operator=(const ExponentialMovingAverageFilter &) = delete;
|
|
48
|
+
|
|
49
|
+
// Enable move semantics
|
|
50
|
+
ExponentialMovingAverageFilter(ExponentialMovingAverageFilter &&) noexcept = default;
|
|
51
|
+
ExponentialMovingAverageFilter &operator=(ExponentialMovingAverageFilter &&) noexcept = default;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @brief Adds a new sample and updates the EMA.
|
|
55
|
+
* @param newValue The new sample value.
|
|
56
|
+
* @return T The updated EMA value.
|
|
57
|
+
*/
|
|
58
|
+
T addSample(T newValue)
|
|
59
|
+
{
|
|
60
|
+
m_policy.onAdd(newValue);
|
|
61
|
+
return m_policy.getResult(0); // Count parameter unused for EMA
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @brief Process array of samples in batch (optimized for throughput).
|
|
66
|
+
*
|
|
67
|
+
* @param input Input array of samples
|
|
68
|
+
* @param output Output array (same size as input)
|
|
69
|
+
* @param length Number of samples to process
|
|
70
|
+
*/
|
|
71
|
+
void processArray(const T *input, T *output, size_t length)
|
|
72
|
+
{
|
|
73
|
+
for (size_t i = 0; i < length; ++i)
|
|
74
|
+
{
|
|
75
|
+
output[i] = addSample(input[i]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @brief Gets the current EMA value.
|
|
81
|
+
* @return T The current exponential moving average.
|
|
82
|
+
*/
|
|
83
|
+
T getEma() const { return m_policy.getResult(0); }
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @brief Gets the alpha (smoothing factor).
|
|
87
|
+
* @return T The alpha value.
|
|
88
|
+
*/
|
|
89
|
+
T getAlpha() const { return m_policy.getAlpha(); }
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @brief Clears the EMA state.
|
|
93
|
+
*/
|
|
94
|
+
void clear() { m_policy.clear(); }
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @brief Checks if the filter has been initialized with at least one sample.
|
|
98
|
+
* @return true if initialized, false otherwise.
|
|
99
|
+
*/
|
|
100
|
+
bool isInitialized() const
|
|
101
|
+
{
|
|
102
|
+
auto [ema, initialized] = m_policy.getState();
|
|
103
|
+
return initialized;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @brief Exports the filter's internal state.
|
|
108
|
+
* @return A pair containing the current EMA value and initialization flag.
|
|
109
|
+
*/
|
|
110
|
+
std::pair<T, bool> getState() const
|
|
111
|
+
{
|
|
112
|
+
return m_policy.getState();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @brief Restores the filter's internal state.
|
|
117
|
+
* @param ema The EMA value to restore.
|
|
118
|
+
* @param initialized The initialization flag to restore.
|
|
119
|
+
*/
|
|
120
|
+
void setState(T ema, bool initialized)
|
|
121
|
+
{
|
|
122
|
+
m_policy.setState(ema, initialized);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private:
|
|
126
|
+
EmaPolicy<T> m_policy;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
} // namespace dsp::core
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "IirFilter.h"
|
|
4
|
+
#include <vector>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <algorithm>
|
|
9
|
+
|
|
10
|
+
namespace dsp
|
|
11
|
+
{
|
|
12
|
+
namespace core
|
|
13
|
+
{
|
|
14
|
+
/**
|
|
15
|
+
* Filter coefficients structure for a single filter
|
|
16
|
+
*/
|
|
17
|
+
struct FilterCoefficients
|
|
18
|
+
{
|
|
19
|
+
std::vector<float> b; // Numerator coefficients
|
|
20
|
+
std::vector<float> a; // Denominator coefficients
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filter Bank Design Engine
|
|
25
|
+
*
|
|
26
|
+
* Generates sets of bandpass filters covering a frequency range according to
|
|
27
|
+
* psychoacoustic (Mel, Bark) or mathematical (Linear, Log) scales.
|
|
28
|
+
*
|
|
29
|
+
* This is a stateless utility that performs frequency warping and filter design
|
|
30
|
+
* without maintaining any processing state.
|
|
31
|
+
*/
|
|
32
|
+
class FilterBankDesign
|
|
33
|
+
{
|
|
34
|
+
public:
|
|
35
|
+
enum class Scale
|
|
36
|
+
{
|
|
37
|
+
Linear, // Linear spacing in Hz
|
|
38
|
+
Log, // Logarithmic spacing
|
|
39
|
+
Mel, // Mel scale (mimics human hearing)
|
|
40
|
+
Bark // Bark scale (critical band rate)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
enum class Type
|
|
44
|
+
{
|
|
45
|
+
Butterworth, // Maximally flat passband
|
|
46
|
+
Chebyshev1 // Equiripple passband
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Filter bank design options
|
|
51
|
+
*/
|
|
52
|
+
struct DesignOptions
|
|
53
|
+
{
|
|
54
|
+
Scale scale; // Frequency spacing scale
|
|
55
|
+
Type type; // Filter topology
|
|
56
|
+
int count; // Number of bands
|
|
57
|
+
double sampleRate; // Sample rate in Hz
|
|
58
|
+
double minFreq; // Minimum frequency in Hz
|
|
59
|
+
double maxFreq; // Maximum frequency in Hz
|
|
60
|
+
int order; // Filter order per band (steepness)
|
|
61
|
+
double rippleDb = 0.5; // Passband ripple for Chebyshev (dB)
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Design a filter bank with specified options
|
|
66
|
+
*
|
|
67
|
+
* @param opts Design options including scale, count, frequency range
|
|
68
|
+
* @return Vector of filter coefficients (one per band)
|
|
69
|
+
*
|
|
70
|
+
* @throws std::invalid_argument if options are invalid
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Create 24-band Mel-spaced filter bank for speech analysis
|
|
74
|
+
* DesignOptions opts;
|
|
75
|
+
* opts.scale = Scale::Mel;
|
|
76
|
+
* opts.type = Type::Butterworth;
|
|
77
|
+
* opts.count = 24;
|
|
78
|
+
* opts.sampleRate = 44100;
|
|
79
|
+
* opts.minFreq = 20;
|
|
80
|
+
* opts.maxFreq = 8000;
|
|
81
|
+
* opts.order = 2;
|
|
82
|
+
* auto bank = FilterBankDesign::design(opts);
|
|
83
|
+
*/
|
|
84
|
+
static std::vector<FilterCoefficients> design(const DesignOptions &opts)
|
|
85
|
+
{
|
|
86
|
+
// Validate inputs
|
|
87
|
+
if (opts.count <= 0)
|
|
88
|
+
{
|
|
89
|
+
throw std::invalid_argument("Band count must be positive");
|
|
90
|
+
}
|
|
91
|
+
if (opts.minFreq < 0)
|
|
92
|
+
{
|
|
93
|
+
throw std::invalid_argument("Minimum frequency cannot be negative");
|
|
94
|
+
}
|
|
95
|
+
if (opts.minFreq >= opts.maxFreq)
|
|
96
|
+
{
|
|
97
|
+
throw std::invalid_argument("Invalid frequency range: minFreq must be < maxFreq");
|
|
98
|
+
}
|
|
99
|
+
if (opts.maxFreq > opts.sampleRate / 2.0)
|
|
100
|
+
{
|
|
101
|
+
throw std::invalid_argument("Maximum frequency must be <= Nyquist frequency");
|
|
102
|
+
}
|
|
103
|
+
if (opts.order <= 0)
|
|
104
|
+
{
|
|
105
|
+
throw std::invalid_argument("Filter order must be positive");
|
|
106
|
+
}
|
|
107
|
+
if (opts.sampleRate <= 0)
|
|
108
|
+
{
|
|
109
|
+
throw std::invalid_argument("Sample rate must be positive");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Step 1: Convert frequency boundaries to target scale
|
|
113
|
+
double minVal = toScale(opts.minFreq, opts.scale);
|
|
114
|
+
double maxVal = toScale(opts.maxFreq, opts.scale);
|
|
115
|
+
double step = (maxVal - minVal) / opts.count;
|
|
116
|
+
|
|
117
|
+
// Step 2: Generate band edges in target scale, then convert back to Hz
|
|
118
|
+
std::vector<double> boundaries;
|
|
119
|
+
boundaries.reserve(opts.count + 1);
|
|
120
|
+
|
|
121
|
+
for (int i = 0; i <= opts.count; ++i)
|
|
122
|
+
{
|
|
123
|
+
double val = minVal + (i * step);
|
|
124
|
+
double hz = fromScale(val, opts.scale);
|
|
125
|
+
boundaries.push_back(hz);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Step 3: Create filters for each band
|
|
129
|
+
std::vector<FilterCoefficients> bank;
|
|
130
|
+
bank.reserve(opts.count);
|
|
131
|
+
|
|
132
|
+
for (int i = 0; i < opts.count; ++i)
|
|
133
|
+
{
|
|
134
|
+
double fLow = boundaries[i];
|
|
135
|
+
double fHigh = boundaries[i + 1];
|
|
136
|
+
|
|
137
|
+
// For the first band starting at 0 Hz, use a small positive value
|
|
138
|
+
// to avoid DC (bandpass filters can't have 0 Hz as lower bound)
|
|
139
|
+
if (fLow == 0.0)
|
|
140
|
+
{
|
|
141
|
+
fLow = 1.0; // 1 Hz minimum
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Normalize frequencies to [0, 0.5] range (0.5 = Nyquist)
|
|
145
|
+
double nLow = fLow / opts.sampleRate;
|
|
146
|
+
double nHigh = fHigh / opts.sampleRate;
|
|
147
|
+
|
|
148
|
+
// Safety clamping to avoid numerical issues
|
|
149
|
+
nLow = std::max(0.0001, std::min(nLow, 0.4999));
|
|
150
|
+
nHigh = std::max(0.0001, std::min(nHigh, 0.4999));
|
|
151
|
+
|
|
152
|
+
// Ensure proper ordering after clamping
|
|
153
|
+
if (nLow >= nHigh)
|
|
154
|
+
{
|
|
155
|
+
nHigh = nLow + 0.0001;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Design bandpass filter using existing IirFilter factory
|
|
159
|
+
IirFilter<float> filter = (opts.type == Type::Chebyshev1)
|
|
160
|
+
? IirFilter<float>::createChebyshevBandPass(
|
|
161
|
+
nLow, nHigh, opts.order, opts.rippleDb)
|
|
162
|
+
: IirFilter<float>::createButterworthBandPass(
|
|
163
|
+
nLow, nHigh, opts.order);
|
|
164
|
+
|
|
165
|
+
// Extract coefficients
|
|
166
|
+
FilterCoefficients coeffs;
|
|
167
|
+
coeffs.b = filter.getBCoefficients();
|
|
168
|
+
coeffs.a = filter.getACoefficients();
|
|
169
|
+
|
|
170
|
+
bank.push_back(coeffs);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return bank;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get frequency boundaries for a filter bank design
|
|
178
|
+
* Useful for visualization and debugging
|
|
179
|
+
*
|
|
180
|
+
* @param opts Design options
|
|
181
|
+
* @return Vector of boundary frequencies in Hz
|
|
182
|
+
*/
|
|
183
|
+
static std::vector<double> getBoundaries(const DesignOptions &opts)
|
|
184
|
+
{
|
|
185
|
+
if (opts.count <= 0)
|
|
186
|
+
{
|
|
187
|
+
throw std::invalid_argument("Band count must be positive");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
double minVal = toScale(opts.minFreq, opts.scale);
|
|
191
|
+
double maxVal = toScale(opts.maxFreq, opts.scale);
|
|
192
|
+
double step = (maxVal - minVal) / opts.count;
|
|
193
|
+
|
|
194
|
+
std::vector<double> boundaries;
|
|
195
|
+
boundaries.reserve(opts.count + 1);
|
|
196
|
+
|
|
197
|
+
for (int i = 0; i <= opts.count; ++i)
|
|
198
|
+
{
|
|
199
|
+
double val = minVal + (i * step);
|
|
200
|
+
boundaries.push_back(fromScale(val, opts.scale));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return boundaries;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private:
|
|
207
|
+
/**
|
|
208
|
+
* Convert frequency from Hz to target scale
|
|
209
|
+
*/
|
|
210
|
+
static double toScale(double hz, Scale scale)
|
|
211
|
+
{
|
|
212
|
+
switch (scale)
|
|
213
|
+
{
|
|
214
|
+
case Scale::Linear:
|
|
215
|
+
return hz;
|
|
216
|
+
|
|
217
|
+
case Scale::Log:
|
|
218
|
+
return std::log10(hz);
|
|
219
|
+
|
|
220
|
+
case Scale::Mel:
|
|
221
|
+
// Mel scale: f_mel = 2595 * log10(1 + f_hz / 700)
|
|
222
|
+
return 2595.0 * std::log10(1.0 + hz / 700.0);
|
|
223
|
+
|
|
224
|
+
case Scale::Bark:
|
|
225
|
+
// Bark scale (Traunmüller 1990)
|
|
226
|
+
// z = 26.81 * f / (1960 + f) - 0.53
|
|
227
|
+
return 26.81 * hz / (1960.0 + hz) - 0.53;
|
|
228
|
+
|
|
229
|
+
default:
|
|
230
|
+
return hz;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Convert from scale back to Hz
|
|
236
|
+
*/
|
|
237
|
+
static double fromScale(double val, Scale scale)
|
|
238
|
+
{
|
|
239
|
+
switch (scale)
|
|
240
|
+
{
|
|
241
|
+
case Scale::Linear:
|
|
242
|
+
return val;
|
|
243
|
+
|
|
244
|
+
case Scale::Log:
|
|
245
|
+
return std::pow(10.0, val);
|
|
246
|
+
|
|
247
|
+
case Scale::Mel:
|
|
248
|
+
// Inverse Mel: f_hz = 700 * (10^(f_mel / 2595) - 1)
|
|
249
|
+
return 700.0 * (std::pow(10.0, val / 2595.0) - 1.0);
|
|
250
|
+
|
|
251
|
+
case Scale::Bark:
|
|
252
|
+
// Inverse Bark (Traunmüller 1990)
|
|
253
|
+
// f = 1960 * (z + 0.53) / (26.81 - (z + 0.53))
|
|
254
|
+
{
|
|
255
|
+
double adjusted = val + 0.53;
|
|
256
|
+
return 1960.0 * adjusted / (26.81 - adjusted);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
default:
|
|
260
|
+
return val;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
} // namespace core
|
|
266
|
+
} // namespace dsp
|
|
@@ -349,4 +349,128 @@ namespace dsp::core
|
|
|
349
349
|
void setCoefficients(const std::vector<T> &coeffs) { m_coefficients = coeffs; }
|
|
350
350
|
};
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* @brief Policy for Exponential Moving Average (EMA).
|
|
354
|
+
*
|
|
355
|
+
* Implements EMA: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
|
|
356
|
+
* where α (alpha) is the smoothing factor (0 < α ≤ 1).
|
|
357
|
+
*
|
|
358
|
+
* This policy is optimized for scalar operations and can be SIMD-accelerated
|
|
359
|
+
* in batch processing contexts.
|
|
360
|
+
*/
|
|
361
|
+
template <typename T>
|
|
362
|
+
struct EmaPolicy
|
|
363
|
+
{
|
|
364
|
+
T m_ema = 0; // Current EMA value
|
|
365
|
+
T m_alpha; // Smoothing factor
|
|
366
|
+
bool m_initialized = false;
|
|
367
|
+
|
|
368
|
+
explicit EmaPolicy(T alpha)
|
|
369
|
+
: m_alpha(alpha)
|
|
370
|
+
{
|
|
371
|
+
if (alpha <= 0 || alpha > 1)
|
|
372
|
+
{
|
|
373
|
+
throw std::invalid_argument("EMA alpha must be in range (0, 1]");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
void onAdd(T val)
|
|
378
|
+
{
|
|
379
|
+
if (!m_initialized)
|
|
380
|
+
{
|
|
381
|
+
// Initialize with first value
|
|
382
|
+
m_ema = val;
|
|
383
|
+
m_initialized = true;
|
|
384
|
+
}
|
|
385
|
+
else
|
|
386
|
+
{
|
|
387
|
+
// EMA formula: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
|
|
388
|
+
m_ema = m_alpha * val + (static_cast<T>(1) - m_alpha) * m_ema;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
void onRemove(T val)
|
|
393
|
+
{
|
|
394
|
+
// EMA doesn't support removal in sliding window context
|
|
395
|
+
// This should not be called in typical EMA usage
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
void clear()
|
|
399
|
+
{
|
|
400
|
+
m_ema = 0;
|
|
401
|
+
m_initialized = false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
T getResult(size_t count) const
|
|
405
|
+
{
|
|
406
|
+
return m_ema;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// For state serialization
|
|
410
|
+
std::pair<T, bool> getState() const { return {m_ema, m_initialized}; }
|
|
411
|
+
void setState(T ema, bool initialized)
|
|
412
|
+
{
|
|
413
|
+
m_ema = ema;
|
|
414
|
+
m_initialized = initialized;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
T getAlpha() const { return m_alpha; }
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @brief Policy for Cumulative Moving Average (CMA).
|
|
422
|
+
*
|
|
423
|
+
* Implements CMA: CMA(n) = (CMA(n-1) * (n-1) + value(n)) / n
|
|
424
|
+
*
|
|
425
|
+
* Maintains the cumulative average over all samples seen since initialization.
|
|
426
|
+
* More efficient than recalculating from scratch each time.
|
|
427
|
+
*/
|
|
428
|
+
template <typename T>
|
|
429
|
+
struct CmaPolicy
|
|
430
|
+
{
|
|
431
|
+
T m_sum = 0; // Running sum of all values
|
|
432
|
+
size_t m_count = 0; // Total number of samples seen
|
|
433
|
+
|
|
434
|
+
void onAdd(T val)
|
|
435
|
+
{
|
|
436
|
+
m_sum += val;
|
|
437
|
+
m_count++;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
void onRemove(T val)
|
|
441
|
+
{
|
|
442
|
+
// CMA doesn't support removal in typical usage
|
|
443
|
+
// If called, decrement count and sum
|
|
444
|
+
if (m_count > 0)
|
|
445
|
+
{
|
|
446
|
+
m_sum -= val;
|
|
447
|
+
m_count--;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
void clear()
|
|
452
|
+
{
|
|
453
|
+
m_sum = 0;
|
|
454
|
+
m_count = 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
T getResult(size_t windowCount) const
|
|
458
|
+
{
|
|
459
|
+
// Use the policy's internal count, not the window count
|
|
460
|
+
if (m_count == 0)
|
|
461
|
+
return 0;
|
|
462
|
+
return m_sum / static_cast<T>(m_count);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// For state serialization
|
|
466
|
+
std::pair<T, size_t> getState() const { return {m_sum, m_count}; }
|
|
467
|
+
void setState(T sum, size_t count)
|
|
468
|
+
{
|
|
469
|
+
m_sum = sum;
|
|
470
|
+
m_count = count;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
size_t getCount() const { return m_count; }
|
|
474
|
+
};
|
|
475
|
+
|
|
352
476
|
} // namespace dsp::core
|
|
@@ -240,7 +240,8 @@ size_t CircularBufferArray<T>::expireOld(double currentTimestamp)
|
|
|
240
240
|
double cutoff_time = currentTimestamp - windowDuration_ms;
|
|
241
241
|
|
|
242
242
|
// Remove samples from tail while they're older than cutoff
|
|
243
|
-
|
|
243
|
+
// FIX: changed < to <= to correctly expire samples at the cutoff and avoid an additional windowduration delay
|
|
244
|
+
while (count > 0 && timestamps[tail] <= cutoff_time)
|
|
244
245
|
{
|
|
245
246
|
tail = (tail + 1) % capacity;
|
|
246
247
|
--count;
|