Bob Larkin
Well-known member
I have been putting together some communications object blocks with floating point data interfaces. This format of Audio objects has been discussed on this forum https://forum.pjrc.com/threads/40568-Floating-Point-Audio-Library-Extension, and documented by Chip Audette in his OpenAudio_ArduinoLibrary library and the Tympan library. It occurred to me that one of the blocks might have application in the 16-bit audio applications and so I converted it to have a fixed point Q15 interface, the same as the Teensy Audio Library. This note is just about the 16-bit integer version and makes this block available for such use.
The AudioFilterEqualizer designs, analyzes and applies a general FIR-filter-based multi-band equalizer. The number of bands is essentially unlimited (51 now) and the frequency limits of the bands are arbitrary. The relative gain for each band is set by specifying a dB level. The Fourier transform of this multi-band response is windowed by a Kaiser window that allows a side-lobe specified parameter to determine the trade-off between rate of transition between bands and the side-lobe levels for a steep transition. The number of coefficients (taps) of the FIR filter is variable from 4 to 250.
Many details are covered in the introductory material to the .h file listed below. In summary, the equalizer object is instantiated as an AudioFilterEqualizer object with a single input and output. It needs no 'begin' function as it comes up running a 4-tap pass-through filter. The equalizer details are entered by a equalizerNew() function that can also be called on-the-fly to change settings. The equalizer is specified by a pair of float arrays supplied by the .INO. Likewise, an int16_t array is .INO supplied to hold the FIR coefficients.
An equalizerResponse() function gives back the dB response that is actually achieved, including quantization effects. A float32_t array to hold these is again .INO supplied
This block is more general than most graphic equalizers, in that the the lower attenuation level is not limited to -12 dB. This allows standard LP, HP, BP and BR responses to be included. The biggest limitation in using the equalizer is the low frequency response. This is limited by the number of IR taps. At a sample rate of 44.1 kHz the 200 tap response below 100 or 200 Hz is in the right direction, but somewhat approximate.There are ways to improve this, but it remains a factor. That said, for communications applications where the lowest frequency of interest is around 200 Hz, it is not a problem.
The internally designed FIR filter is symmetric which means that the delay through the filter is constant for all frequencies (linear phase).
Here is the include AudioFilterEqualizer_I16.h file, with supplementary info.
Next is the AudioFilterEqualizer_I16.cpp file.
And two simple .INO files to show usage follow
Bug or success reports on any of this would be appreciated would be appreciated on any of this. It is all a work in progress.
Thanks, Bob
The AudioFilterEqualizer designs, analyzes and applies a general FIR-filter-based multi-band equalizer. The number of bands is essentially unlimited (51 now) and the frequency limits of the bands are arbitrary. The relative gain for each band is set by specifying a dB level. The Fourier transform of this multi-band response is windowed by a Kaiser window that allows a side-lobe specified parameter to determine the trade-off between rate of transition between bands and the side-lobe levels for a steep transition. The number of coefficients (taps) of the FIR filter is variable from 4 to 250.
Many details are covered in the introductory material to the .h file listed below. In summary, the equalizer object is instantiated as an AudioFilterEqualizer object with a single input and output. It needs no 'begin' function as it comes up running a 4-tap pass-through filter. The equalizer details are entered by a equalizerNew() function that can also be called on-the-fly to change settings. The equalizer is specified by a pair of float arrays supplied by the .INO. Likewise, an int16_t array is .INO supplied to hold the FIR coefficients.
An equalizerResponse() function gives back the dB response that is actually achieved, including quantization effects. A float32_t array to hold these is again .INO supplied
This block is more general than most graphic equalizers, in that the the lower attenuation level is not limited to -12 dB. This allows standard LP, HP, BP and BR responses to be included. The biggest limitation in using the equalizer is the low frequency response. This is limited by the number of IR taps. At a sample rate of 44.1 kHz the 200 tap response below 100 or 200 Hz is in the right direction, but somewhat approximate.There are ways to improve this, but it remains a factor. That said, for communications applications where the lowest frequency of interest is around 200 Hz, it is not a problem.
The internally designed FIR filter is symmetric which means that the delay through the filter is constant for all frequencies (linear phase).
Here is the include AudioFilterEqualizer_I16.h file, with supplementary info.
Code:
/*
* AudioFilterEqualizer_I16
*
* Created: Bob Larkin W7PUA 14 May 2020
*
* This is a direct translation of the receiver audio equalizer written
* by this author for the open-source DSP-10 radio in 1999. See
* http://www.janbob.com/electron/dsp10/dsp10.htm and
* http://www.janbob.com/electron/dsp10/uhf3_35a.zip
* This version processes blocks of 16-bit integer audio (as opposed to
* the Chip Audette style F32 floating point baudio.)
*
* Credit and thanks to PJRC, Paul Stoffregen and Teensy products for the audio
* system and library that this is built upon as well as the float32
* work of Chip Audette embodied in the OpenAudio_ArduinoLibrary. Many thanks
* for the library structures and wonderful Teensy products.
*
* This equalizer is specified by an array of 'nBands' frequency bands
* each of of arbitrary frequency span. The first band always starts at
* 0.0 Hz, and that value is not entered. Each band is specified by the upper
* frequency limit to the band.
* The last band always ends at half of the sample frequency, which for 44117 Hz
* sample frequency would be 22058.5. Each band is specified by its upper
* frequency in an .INO supplied array feq[]. The dB level of that band is
* specified by a value, in dB, arranged in an .INO supplied array
* aeq[]. Thus a trivial bass/treble control might look like:
* nBands = 3;
* feq[] = {300.0, 1500.0, 22058.5};
* float32_t bass = -2.5; // in dB, relative to anything
* float32_t treble = 6.0;
* aeq[] = {bass, 0.0, treble};
*
* It may be obvious that this equalizer is a more general case of the common
* functions such as low-pass, band-pass, notch, etc. For instance, a pair
* of band pass filters would look like:
* nBands = 5;
* feq[] = {500.0, 700.0, 2000.0, 2200.0, 22058.5};
* aeq[] = {-100.0, 0.0, -100.0, 2.0, -100.0};
* where we added 2 dB of gain to the 2200 to 2400 Hz filter, relative to the 500
* to 700 Hz band.
*
* An octave band equalizer is made by starting at some low frequency, say 40 Hz for the
* first band. The lowest frequency band will be from 0.0 Hz up to that first frequency.
* Next multiply the first frequency by 2, creating in our example, a band from 40.0
* to 80 Hz. This is continued until the last frequency is about 22058 Hz.
* This works out to require 10 bands, as follows:
* nBands = 10;
* feq[] = { 40.0, 80.0, 160.0, 320.0, 640.0, 1280.0, 2560.0, 5120.0, 10240.0, 22058.5};
* aeq[] = { 5.0, 4.0, 2.0, -3.0, -4.0, -1.0, 3.0, 6.0, 3.0, 0.5 };
*
* For a "half octave" equalizer, multiply each upper band limit by the square root of 2 = 1.414
* to get the next band limit. For that case, feq[] would start with a sequence
* like 40, 56.56, 80.00, 113.1, 160.0, ... for a total of about 20 bands.
*
* How well all of this is achieved depends on the number of FIR coefficients
* being used. In the Teensy 3.6 / 4.0 the resourses allow a hefty number,
* say 201, of coefficients to be used without stealing all the processor time
* (see Timing, below). The coefficient and FIR memory is sized for a maximum of
* 250 coefficients, but can be recompiled for bigger with the define FIR_MAX_COEFFS.
* To simplify calculations, the number of FIR coefficients should be odd. If not
* odd, the number will be reduced by one, quietly.
*
* If you try to make the bands too narrow for the number of FIR coeffficients,
* the approximation to the desired curve becomes poor. This can all be evaluated
* by the function getResponse(nPoints, pResponse) which fills an .INO-supplied array
* pResponse[nPoints] with the frequency response of the equalizer in dB. The nPoints
* are spread evenly between 0.0 and half of the sample frequency.
*
* Initialization is a 2-step process. This makes it practical to change equalizer
* levels on-the-fly. The constructor starts up with a 4-tap FIR setup for direct
* pass through. Then the setup() in the .INO can specify the equalizer.
* The newEqualizer() function has several parameters, the number of equalizer bands,
* the frequencies of the bands, and the sidelobe level. All of these can be changed
* dynamically. This function can be changed dynamically, but it may be desireable to
* mute the audio during the change to prevent clicks.
*
* This 16-bit integer version adjusts the maximum coefficient size to scale16 in the calls
* to both equalizerNew() and getResponse(). Broadband equalizers can work with full-scale
* 32767.0f sorts of levels, where narrow band filtering may need smaller values to
* prevent overload. Experiment and check carefully. Use lower values if there are doubts.
*
* For a pass-through function, something like this (which can be intermixed with fancy equalizers):
* float32_t fBand[] = {10000.0f, 22058.5f};
* float32_t dbBand[] = {0.0f, 0.0f};
* equalize1.equalizerNew(2, &fBand[0], &dbBand[0], 4, &equalizeCoeffs[0], 30.0f, 32767.0f);
*
* Measured Q15 timing of update() for a 128 sample block on a T3.6:
* Fixed time 11.6 microseconds
* Per FIR Coefficient time 5.6 microseconds
* Total for 200 FIR Coefficients = 1140 microseconds (39.3% of fs=44117 Hz available time)
*
* Copyright (c) 2020 Bob Larkin
* Any snippets of code from PJRC used here brings with it
* the associated license.
*
* In addition, work here is covered by MIT LIcense:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef _filter_equalizer_h
#define _filter_equalizer_h
#include "Arduino.h"
#include "arm_math.h"
#include "Audio.h"
#include "AudioStream.h"
#ifndef MF_PI
#define MF_PI 3.1415926f
#endif
// Temporary timing test
#define TEST_TIME_EQ 0
#define EQUALIZER_MAX_COEFFS 250
#define ERR_EQ_BANDS 1
#define ERR_EQ_SIDELOBES 2
#define ERR_EQ_NFIR 3
class AudioFilterEqualizer : public AudioStream
{
public:
AudioFilterEqualizer(void): AudioStream(1,inputQueueArray) {
// Initialize FIR instance (ARM DSP Math Library) with default simple passthrough FIR
if (arm_fir_init_q15(&fir_inst, nFIRused, (q15_t *)cf16, &StateQ15[0], AUDIO_BLOCK_SAMPLES)
!= ARM_MATH_SUCCESS) {
cf16 = NULL;
}
}
uint16_t equalizerNew(uint16_t _nBands, float32_t *feq, float32_t *adb,
uint16_t _nFIR, int16_t *_cf, float32_t kdb, float32_t scale16);
void getResponse(uint16_t nFreq, float32_t *rdb, float32_t scale16);
void update(void);
private:
audio_block_t *inputQueueArray[1];
uint16_t block_size = AUDIO_BLOCK_SAMPLES;
int16_t firStart[4] = {0, 32767, 0, 0}; // Initialize to passthrough
int16_t* cf16 = firStart; // pointer to current coefficients
uint16_t nFIR = 4; // Number of coefficients
uint16_t nFIRdesign = 3; // used in designing filter
uint16_t nFIRused = 4; // Adjusted nFIR, nFIR-1 for nFIR odd.
uint16_t nBands = 2;
float32_t sample_rate_Hz = AUDIO_SAMPLE_RATE;
// *Temporary* - TEST_TIME allows measuring time in microseconds for each part of the update()
#if TEST_TIME_EQ
elapsedMicros tElapse;
int32_t iitt = 999000; // count up to a million during startup
#endif
// ARM DSP Math library filter instance
arm_fir_instance_q15 fir_inst;
int16_t StateQ15[AUDIO_BLOCK_SAMPLES + EQUALIZER_MAX_COEFFS]; // max, max
/* float i0f(float x) Returns the modified Bessel function Io(x).
* Algorithm is based on Abromowitz and Stegun, Handbook of Mathematical
* Functions, and Press, et. al., Numerical Recepies in C.
* All in 32-bit floating point
*/
float i0f(float x) {
float af, bf, cf;
if( (af=fabsf(x)) < 3.75f ) {
cf = x/3.75f;
cf = cf*cf;
bf=1.0f+cf*(3.515623f+cf*(3.089943f+cf*(1.20675f+cf*(0.265973f+
cf*(0.0360768f+cf*0.0045813f)))));
}
else {
cf = 3.75f/af;
bf=(expf(af)/sqrtf(af))*(0.3989423f+cf*(0.0132859f+cf*(0.0022532f+
cf*(-0.0015756f+cf*(0.0091628f+cf*(-0.0205771f+cf*(0.0263554f+
cf*(-0.0164763f+cf*0.0039238f))))))));
}
return bf;
}
};
#endif
Next is the AudioFilterEqualizer_I16.cpp file.
Code:
/* AudioFilterEqualizer_I16.cpp
*
* Bob Larkin, W7PUA 14 May 2020
*
* See AudioFilterEqualizer_I16.h for much more explanation on usage.
*
* Copyright (c) 2020 Bob Larkin
* Any snippets of code from PJRC and used here brings with it
* the associated license.
*
* In addition, work here is covered by MIT LIcense:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "AudioFilterEqualizer_I16.h"
void AudioFilterEqualizer::update(void) {
audio_block_t *block, *block_new;
#if TEST_TIME_EQ
if (iitt++ >1000000) iitt = -10;
uint32_t t1, t2;
t1 = tElapse;
#endif
block = receiveReadOnly();
if (!block) return;
// Check for coefficients
if (cf16 == NULL) {
release(block);
return;
}
// get a block for the FIR output
block_new = allocate();
if (block_new) {
//apply the FIR
arm_fir_q15(&fir_inst, block->data, block_new->data, AUDIO_BLOCK_SAMPLES);
transmit(block_new); // send the FIR output
release(block_new);
}
release(block);
#if TEST_TIME_EQ
t2 = tElapse;
if(iitt++ < 0) {Serial.print("At AudioEqualizer end, microseconds = "); Serial.println (t2 - t1); }
t1 = tElapse;
#endif
}
/* equalizerNew() calculates the Equalizer FIR filter coefficients. Works from:
* uint16_t equalizerNew(uint16_t _nBands, float32_t *feq, float32_t *adb,
uint16_t _nFIR, int16_t *_cf, float32_t kdb, float32_t scale16)
* nBands Number of equalizer bands
* feq Pointer to array feq[] of nBands breakpoint frequencies, fractions of sample rate, Hz
* adb Pointer to array aeq[] of nBands levels, in dB, for the feq[] defined frequency bands
* nFIR The number of FIR coefficients (taps) used in the equalzer
* cf Pointer to an array of int16 to hold FIR coefficients
* kdb A parameter that trades off sidelobe levels for sharpness of band transition.
* kdb=30 sharp cutoff, higher sidelobes
* kdb=60 slow cutoff, low sidelobes
* scale16 A float number that sets the maximum int value for coefficients. Max 32768.0f
*
* The arrays, feq[], aeq[] and cf[] are supplied by the calling .INO
*
* Returns: 0 if successful, or an error code if not.
* Errors: 1 = ERR_EQ_BANDS = Too many bands, 50 max
* 2 = ERR_EQ_SIDELOBES = Sidelobe level out of range, must be > 0
* 3 = ERR_EQ_NFIR = nFIR out of range
*
* Note - This function runs at setup time, and there is no need to fret about
* processor speed. Likewise, local arrays are created on the stack and memory space is
* available for other use when this function closes.
*/
uint16_t AudioFilterEqualizer::equalizerNew(uint16_t _nBands, float32_t* feq, float32_t* adb,
uint16_t _nFIR, int16_t* pcf16, float32_t kdb, float32_t scale16) {
uint16_t i, j;
uint16_t nHalfFIR;
float32_t beta, kbes;
float32_t q, xj2, scaleXj2, WindowWt;
float32_t cf[250];
float32_t fNorm[50]; // Normalized to the sampling frequency
float32_t aVolts[50]; // Convert from dB to "quasi-Volts"
cf16 = pcf16; // Set the private copies
nFIR = _nFIR;
nBands = _nBands;
// Check range of nFIR. q15 FIR requires even number of coefficients,
// but for historic reasons, we design odd number FIR. So add a
// variable nFIRused that is even, and one more than the design value.
if (nFIR<4 || nFIR>EQUALIZER_MAX_COEFFS)
return ERR_EQ_NFIR;
if (2*(nFIR/2) == nFIR) { // nFIR even
nFIRdesign = nFIR - 1;
nFIRused = nFIR;
}
else { // nFIR odd
nFIRdesign = nFIR - 2; // Avoid this
nFIRused = nFIR - 1;
}
nHalfFIR = (nFIRdesign - 1)/2; // If nFIRdesign=199, nHalfFIR=99
for (int kk = 0; kk<nFIRdesign; kk++) // To be sure, zero the coefficients
cf[kk] = 0.0f;
// Convert dB to Voltage ratios, frequencies to fractions of sampling freq
if(nBands <2 || nBands>50) return ERR_EQ_BANDS;
for (i=0; i<nBands; i++) {
aVolts[i]=powf(10.0, (0.05*adb[i]));
fNorm[i]=feq[i]/sample_rate_Hz;
}
/* Find FIR coefficients, the Fourier transform of the frequency
* response. This is done by dividing the response into a sequence
* of nBands rectangular frequency blocks, each of a different level.
* We can precalculate the sinc Fourier transform for each rectangular band.
* The linearity of the Fourier transform allows us to sum the transforms
* of the individual blocks to get pre-windowed coefficients.
*
* Numbering example for nFIRdesign==199:
* Subscript 0 to 98 is 99 taps; 100 to 198 is 99 taps; 99+1+99=199 taps
* The center coef ( for nFIRdesign=199 taps, nHalfFIR=99 ) is a
* special case that comes from sin(0)/0 and treated first:
*/
cf[nHalfFIR] = 2.0f*(aVolts[0]*fNorm[0]); // Coefficient "99"
for(i=1; i<nBands; i++) {
cf[nHalfFIR] += 2.0f*aVolts[i]*(fNorm[i]-fNorm[i-1]);
}
for (j=1; j<=nHalfFIR; j++) { // Coefficients "100 to 198"
q = MF_PI*(float32_t)j;
// First, deal with the zero frequency end band that is "low-pass."
cf[j+nHalfFIR] = aVolts[0]*sinf(fNorm[0]*2.0f*q)/q;
// and then the rest of the bands that have low and high frequencies
for(i=1; i<nBands; i++)
cf[j+nHalfFIR] += aVolts[i]*( (sinf(fNorm[i]*2.0f*q)/q) - (sinf(fNorm[i-1]*2.0f*q)/q) );
}
/* At this point, the cf[] coefficients are simply truncated sin(x)/x, creating
* very high sidelobe responses. To reduce the sidelobes, a windowing function is applied.
* This has the side affect of increasing the rate of cutoff for sharp frequency changes.
* The only windowing function available here is that of James Kaiser. This has a number
* of desirable features. These include being able to tradeoff sidelobe level
* for rate of cutoff cutoff between frequency bands.
* We specify it in terms of kdb, the highest sidelobe, in dB, next to a sharp cutoff. For
* calculating the windowing vector, we need a Kaiser parameter beta, found as follows:
*/
if (kdb<0) return ERR_EQ_SIDELOBES;
if (kdb>50)
beta = 0.1102f*(kdb-8.7f);
else if (kdb>20.96f && kdb<=50.0f)
beta = 0.58417f*powf((kdb-20.96f), 0.4f) + 0.07886f*(kdb-20.96f);
else
beta=0.0f;
// i0f is the floating point in & out zero'th order modified Bessel function
kbes = 1.0f / i0f(beta); // An additional derived parameter used in loop
// Apply the Kaiser window, j = 0 is center coeff window value
scaleXj2 = 2.0f/(float32_t)nFIRdesign;
scaleXj2 *= scaleXj2;
for (j=0; j<=nHalfFIR; j++) { // For 199 Taps, this is 0 to 99
xj2 = (int16_t)(0.5f+(float32_t)j);
xj2 = scaleXj2*xj2*xj2;
WindowWt=kbes*(i0f(beta*sqrtf(1.0-xj2)));
cf[nHalfFIR + j] *= WindowWt; // Apply the Kaiser window to upper half
cf[nHalfFIR - j] = cf[nHalfFIR +j]; // and create the lower half
}
// Find the biggest to decide the scaling factor for the FIR filter.
// Then we will scale the coefficients according to scale16
float32_t cfmax = 0.0f;
for (j=0; j<=nHalfFIR; j++) // 0 to 99 for nFIRdesign=199
if (cfmax < fabsf(cf[j])) cfmax=fabsf(cf[j]);
// scale16 is a float number, such as 16384.0, that sets the maximum +/-
// value for coefficients. This is a complex subject that needs more discussion
// than we can put here. The following scales the coefficients and converts to 16 bit:
for (j=0; j<nFIRdesign; j++)
cf16[j] = (int)(scale16*cf[j]/cfmax);
// nFIRused id always even. nFIRdesign is always odd. So add a zero
cf16[nFIRdesign] = 0;
// The following puts the numbers into the fir_inst structure
arm_fir_init_q15(&fir_inst, nFIRused, (int16_t *)cf16, &StateQ15[0], (uint32_t)block_size);
return 0;
}
/* Calculate response in dB. Leave nFreq-point-result in array rdb[] that is supplied
* by the calling .INO See Parks and Burris, "Digital Filter Design," p27 (Type 1).
*/
void AudioFilterEqualizer::getResponse(uint16_t nFreq, float32_t *rdb, float32_t scale16) {
uint16_t i, j;
float32_t bt;
float32_t piOnNfreq;
uint16_t nHalfFIR;
float32_t cf[nFIR];
nHalfFIR = (nFIRdesign - 1)/2; // If nFIRdesign=199, nHalfFIR=99
for (i=0; i<nFIRdesign; i++)
cf[i] = ((float32_t)cf16[i]) / scale16;
piOnNfreq = MF_PI / (float32_t)nFreq;
for (i=0; i<nFreq; i++) {
bt = cf[nHalfFIR]; // Center coefficient
for (j=0; j<nHalfFIR; j++) // Add in the others twice, as they are symmetric
bt += 2.0f*cf[j]*cosf(piOnNfreq*(float32_t)((nHalfFIR-j)*i));
rdb[i] = 20.0f*log10f(fabsf(bt)); // Convert to dB
}
}
And two simple .INO files to show usage follow
Code:
/* TestEqualizer2.ino Bob Larkin 10 May 2020
* This is a test of the Filter Equalizer for Teensy Audio.
* It also tests the .getResponse() function for determining
* the actual filter response in dB.
* This version is for 16-bit Teensy Audio Library.
* A float32 version is available.
*/
#include "Audio.h"
#include "AudioFilterEqualizer_I16.h"
AudioInputI2S i2s1;
AudioSynthWaveformSine sine1; // Test signal
AudioFilterEqualizer equalize1;
AudioRecordQueue queue1; // The LSB output
AudioConnection patchCord1(sine1, 0, equalize1, 0);
AudioConnection patchCord2(equalize1, 0, queue1, 0);
// This 10-band octave band equalizer is set strangely in order to demonstrate the Equalizer
float32_t fBand[] = { 40.0, 80.0, 160.0, 320.0, 640.0, 1280.0, 2560.0, 5120.0, 10240.0, 22058.5};
float32_t dbBand[] = {10.0, 8.0, 6.0, 3.0, -2.0, 0.0, 0.0, 6.0, 10.0, -100};
float32_t scaleCoeff = 16384.0f; // Max allowed size of FIR coefficients, depends on equalizer use
int16_t equalizeCoeffs[250];
int16_t dt1[128];
int16_t *pq1, *pd1;
int16_t i;
float32_t dBResponse[500]; // Show lots of detail for a plot
void setup(void) {
AudioMemory(10);
Serial.begin(300); delay(1000);
Serial.println("*** Test Audio Equalizer ***");
// Sine wave is default +/- 8192 max/min
sine1.frequency(1000.0f);
// Initialize the equalizer with 10 bands, 200 FIR coefficients and -60 dB sidelobes, 16384 Max coefficient
uint16_t eq = equalize1.equalizerNew(10, &fBand[0], &dbBand[0], 200, &equalizeCoeffs[0], 60, scaleCoeff);
if (eq == ERR_EQ_BANDS)
Serial.println("Equalizer failed: Invalid number of frequency bands.");
else if (eq == ERR_EQ_SIDELOBES)
Serial.println("Equalizer failed: Invalid sidelobe specification.");
else if (eq == ERR_EQ_NFIR)
Serial.println("Equalizer failed: Invalid number of FIR coefficients.");
else
Serial.println("Equalizer initialized successfully.");
// Get frequency response in dB for 500 points, uniformly spaced from 0 to 21058 Hz
// this is an AudioFilterEqualizer function called as
// void getResponse(uint16_t nFreq, float32_t *rdb, float32_t scale16);
equalize1.getResponse(500, dBResponse, scaleCoeff);
Serial.println("Freq Hz, Response dB");
for(int16_t m=0; m<500; m++) { // Print the response in Hz, dB, suitable for a spread sheet
Serial.print((float32_t)m * 22058.5f / 500.0f);
Serial.print(",");
Serial.println(dBResponse[m], 7);
}
i = -10;
}
void loop(void) {
if(i<0) i++; // Get past startup data
if(i==0) queue1.begin();
// Print a 128 sample of the filtered output with sine1 as an input.
// This "if" will be active for i == 0
if (queue1.available() >= 1 && i >= 0) {
pq1 = queue1.readBuffer();
pd1 = &dt1[0];
for(uint k=0; k<128; k++)
*pd1++ = *pq1++;
i=1; // Only collect 1 block
queue1.freeBuffer();
queue1.end(); // No more data to queue1
}
if(i == 1) {
// Uncomment the next 3 lines to printout a sample of the sine wave.
Serial.println("128 Samples: ");
for (uint k=0; k<128; k++)
Serial.println (dt1[k]);
i = 2;
}
}
Code:
/* TestEqualizer2Audio.ino Bob Larkin 10 May 2020
* This is a test of the Filter Equalizer for Teensy Audio.
* Runs two different equalizers, switching every 5 seconds to demonstrate
* dynamic filter changing.
*/
#include "Audio.h"
#include "AudioFilterEqualizer_I16.h"
// Signals from left ADC to Equalizer to left DAC using Teensy Audio Adaptor
AudioInputI2S i2sIn;
AudioFilterEqualizer equalize1;
AudioOutputI2S i2sOut;
AudioConnection patchCord1(i2sIn, 0, equalize1, 0);
AudioConnection patchCord2(equalize1, 0, i2sOut, 0);
AudioControlSGTL5000 codec;
// Some sort of octave band equalizer as a one alternative, 10 bands
float32_t fBand1[] = { 40.0, 80.0, 160.0, 320.0, 640.0, 1280.0, 2560.0, 5120.0, 10240.0, 22058.5};
float32_t dbBand1[] = {10.0, 2.0, -2.0, -5.0, -2.0, -4.0, -20.0, 6.0, 10.0, -100};
float32_t scaleCoeff = 32765.0f; // Max allowed size of FIR coefficients; a smidge under 2^15
// To contrast, put a strange bandpass filter as an alternative, 5 bands
float32_t fBand2[] = { 300.0, 500.0, 800.0, 1000.0, 22058.5};
float32_t dbBand2[] = {-60.0, 0.0, -20.0, 0.0, -60.0};
int16_t equalizeCoeffs[200];
int16_t k = 0;
void setup(void) {
AudioMemory(10);
Serial.begin(300); delay(1000);
Serial.println("*** Test Audio Equalizer ***");
codec.enable();
codec.inputSelect(AUDIO_INPUT_LINEIN);
// Initialize the equalizer with 10 bands, fBand1[] 199 FIR coefficients
// -65 dB sidelobes, 16384 Max coefficient value
uint16_t eq = equalize1.equalizerNew(10, &fBand1[0], &dbBand1[0], 200, &equalizeCoeffs[0], 60, scaleCoeff);
if (eq == ERR_EQ_BANDS)
Serial.println("Equalizer failed: Invalid number of frequency bands.");
else if (eq == ERR_EQ_SIDELOBES)
Serial.println("Equalizer failed: Invalid sidelobe specification.");
else if (eq == ERR_EQ_NFIR)
Serial.println("Equalizer failed: Invalid number of FIR coefficients.");
else
Serial.println("Equalizer initialized successfully.");
for (k=0; k<200; k++) Serial.println(equalizeCoeffs[k]);
}
void loop(void) {
// Change between two filters every 5 seconds.
// To run with just the 10-band equalizer, comment out the entire loop with "/* ... */"
if(k == 0) {
k = 1;
equalize1.equalizerNew(10, &fBand1[0], &dbBand1[0], 200, &equalizeCoeffs[0], 65, scaleCoeff);
}
else { // Swap back and forth
k = 0;
equalize1.equalizerNew(5, &fBand2[0], &dbBand2[0], 190, &equalizeCoeffs[0], 40, scaleCoeff);
}
delay(5000); // Interrupts will keep the audio going
}
Bug or success reports on any of this would be appreciated would be appreciated on any of this. It is all a work in progress.
Thanks, Bob