From 2700341f93626d3e6e0ade157e883cb261135c24 Mon Sep 17 00:00:00 2001 From: Benji Date: Sat, 15 Apr 2023 20:55:36 +0200 Subject: [PATCH] Attempt at making PR #7 work --- .vscode/arduino.json | 6 +- .vscode/c_cpp_properties.json | 3 - .vscode/settings.json | 47 +-- README.md | 2 +- esp32-db-signaller.ino | 561 ---------------------------------- esp32-i2s-slm.ino | 221 ++++++++++++++ filters.h | 137 +++++++++ i2s_mic.h | 238 +++++++++++++++ misc/ESP32-Pinout.png | Bin 39996 -> 0 bytes pins.txt | 10 - sos-iir-filter.h | 279 +++++++---------- 11 files changed, 706 insertions(+), 798 deletions(-) delete mode 100644 esp32-db-signaller.ino create mode 100644 esp32-i2s-slm.ino create mode 100644 filters.h create mode 100644 i2s_mic.h delete mode 100644 misc/ESP32-Pinout.png delete mode 100644 pins.txt diff --git a/.vscode/arduino.json b/.vscode/arduino.json index 0e64de5..6d6c340 100644 --- a/.vscode/arduino.json +++ b/.vscode/arduino.json @@ -1,8 +1,4 @@ { - "configuration": "FlashFreq=80,UploadSpeed=921600,DebugLevel=none,EraseFlash=none", "board": "esp32:esp32:firebeetle32", - "port": "/dev/ttyUSB0", - "sketch": "esp32-db-signaller.ino", - "output": "../build", - "programmer": "esptool" + "port": "/dev/ttyUSB0" } \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 0a8743b..d945049 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -215,9 +215,6 @@ "/home/benji/.arduino15/packages/esp32/hardware/esp32/2.0.5/tools/sdk/esp32/dio_qspi/include", "/home/benji/.arduino15/packages/esp32/hardware/esp32/2.0.5/cores/esp32", "/home/benji/.arduino15/packages/esp32/hardware/esp32/2.0.5/variants/firebeetle32", - "/home/benji/Arduino/libraries/IRremoteESP8266/src", - "/home/benji/Arduino/libraries/FastLED/src", - "/home/benji/.arduino15/packages/esp32/hardware/esp32/2.0.5/libraries/Preferences/src", "/home/benji/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/gcc8_4_0-esp-2021r2-patch3/xtensa-esp32-elf/include/c++/8.4.0", "/home/benji/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/gcc8_4_0-esp-2021r2-patch3/xtensa-esp32-elf/include/c++/8.4.0/xtensa-esp32-elf", "/home/benji/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/gcc8_4_0-esp-2021r2-patch3/xtensa-esp32-elf/include/c++/8.4.0/backward", diff --git a/.vscode/settings.json b/.vscode/settings.json index 812ef69..c969551 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,50 +1,5 @@ { "files.associations": { - "cmath": "cpp", - "array": "cpp", - "atomic": "cpp", - "*.tcc": "cpp", - "cctype": "cpp", - "clocale": "cpp", - "cstdarg": "cpp", - "cstddef": "cpp", - "cstdint": "cpp", - "cstdio": "cpp", - "cstdlib": "cpp", - "cstring": "cpp", - "cwchar": "cpp", - "cwctype": "cpp", - "deque": "cpp", - "unordered_map": "cpp", - "vector": "cpp", - "exception": "cpp", - "algorithm": "cpp", - "functional": "cpp", - "iterator": "cpp", - "memory": "cpp", - "memory_resource": "cpp", - "numeric": "cpp", - "optional": "cpp", - "random": "cpp", - "string": "cpp", - "string_view": "cpp", - "system_error": "cpp", - "tuple": "cpp", - "type_traits": "cpp", - "utility": "cpp", - "fstream": "cpp", - "initializer_list": "cpp", - "iosfwd": "cpp", - "iostream": "cpp", - "istream": "cpp", - "limits": "cpp", - "new": "cpp", - "ostream": "cpp", - "sstream": "cpp", - "stdexcept": "cpp", - "streambuf": "cpp", - "cinttypes": "cpp", - "typeinfo": "cpp", - "map": "cpp" + "cmath": "cpp" } } \ No newline at end of file diff --git a/README.md b/README.md index 629cc9b..03781bb 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,6 @@ And the microphone response after equalization should look like: Theoretically, i.e. with factory calibrated ICS-4343x, this should get you ±1dB(A) measurement within 20Hz-20KHz range. -The code in this repository is mostly intended as example how you can integrate resonable noise measurement (i.e. *L*Aeq, Equivalent Continuous Sound Level) in your projects. +The code in this repository is mostly intended as example how you can integrate resonable noise measurement (i.e. *L*Aeq, Equivalent Continuous Sound Level) in your projects. You can find a bit more information in my [hackday.io](https://hackaday.io/project/166867-esp32-i2s-slm) project. diff --git a/esp32-db-signaller.ino b/esp32-db-signaller.ino deleted file mode 100644 index 4be24d2..0000000 --- a/esp32-db-signaller.ino +++ /dev/null @@ -1,561 +0,0 @@ -/* - * Display A-weighted sound level measured by I2S Microphone - * - * (c)2019 Ivan Kostoski - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/* - * Sketch samples audio data from I2S microphone, processes the data - * with digital IIR filters and calculates A or C weighted Equivalent - * Continuous Sound Level (Leq) - * - * I2S is setup to sample data at Fs=48000KHz (fixed value due to - * design of digital IIR filters). Data is read from I2S queue - * in 'sample blocks' (default 125ms block, equal to 6000 samples) - * by 'i2s_reader_task', filtered trough two IIR filters (equalizer - * and weighting), summed up and pushed into 'samples_queue' as - * sum of squares of filtered samples. The main task then pulls data - * from the queue and calculates decibel value relative to microphone - * reference amplitude, derived from datasheet sensitivity dBFS - * value, number of bits in I2S data, and the reference value for - * which the sensitivity is specified (typically 94dB, pure sine - * wave at 1KHz). - * - * Displays line on the small OLED screen with 'short' LAeq(125ms) - * response and numeric LAeq(1sec) dB value from the signal RMS. - */ - -// -// Configuration -// - -int deciLight = 1; - -// Define IR -#include -#include -#include -const uint16_t kRecvPin = 4; -IRrecv irrecv(kRecvPin); -decode_results results; - -// Define FastLED -#include -#define LED_TYPE NEOPIXEL -#define NUM_LEDS 7 // How many LEDs are attached to the Arduino? -#define DATA_PIN 2 // Which pin on the Arduino is connected to the LEDs? -int brightness = 255; // LED brightness, 0 (min) to 255 (max) -CRGB leds[NUM_LEDS]; - -// Define preferences -#include -Preferences preferences; -int dB_min_default = 40; -int dB_max_default = 60; - -// Define mic -#include -#include "sos-iir-filter.h" -#define LEQ_PERIOD 0.15 // second(s) -#define WEIGHTING A_weighting // Also avaliable: 'C_weighting' or 'None' (Z_weighting) -#define LEQ_UNITS "LAeq" // customize based on above weighting used -#define DB_UNITS "dBA" // customize based on above weighting used - -// NOTE: Some microphones require at least DC-Blocker filter -#define MIC_EQUALIZER INMP441 // See below for defined IIR filters or set to 'None' to disable -#define MIC_OFFSET_DB 3.0103 // Default offset (sine-wave RMS vs. dBFS). Modify this value for linear calibration - -// Customize these values from microphone datasheet -#define MIC_SENSITIVITY -26 // dBFS value expected at MIC_REF_DB (Sensitivity value from datasheet) -#define MIC_REF_DB 94.0 // Value at which point sensitivity is specified in datasheet (dB) -#define MIC_OVERLOAD_DB 116.0 // dB - Acoustic overload point -#define MIC_NOISE_DB 29 // dB - Noise floor -#define MIC_BITS 24 // valid number of bits in I2S data -#define MIC_CONVERT(s) (s >> (SAMPLE_BITS - MIC_BITS)) - -// Calculate reference amplitude value at compile time -constexpr double MIC_REF_AMPL = pow(10, double(MIC_SENSITIVITY) / 20) * ((1 << (MIC_BITS - 1)) - 1); - -// -// I2S pins - Can be routed to almost any (unused) ESP32 pin. -// SD can be any pin, inlcuding input only pins (36-39). -// SCK (i.e. BCLK) and WS (i.e. L/R CLK) must be output capable pins -// -// Below ones are just example for my board layout, put here the pins you will use -// -#define I2S_WS 15 -#define I2S_SCK 14 -#define I2S_SD 32 - -// I2S peripheral to use (0 or 1) -#define I2S_PORT I2S_NUM_0 - -// -// Equalizer IIR filters to flatten microphone frequency response -// See respective .m file for filter design. Fs = 48Khz. -// -// Filters are represented as Second-Order Sections cascade with assumption -// that b0 and a0 are equal to 1.0 and 'gain' is applied at the last step -// B and A coefficients were transformed with GNU Octave: -// [sos, gain] = tf2sos(B, A) -// See: https://www.dsprelated.com/freebooks/filters/Series_Second_Order_Sections.html -// NOTE: SOS matrix 'a1' and 'a2' coefficients are negatives of tf2sos output -// - -// TDK/InvenSense INMP441 -// Datasheet: https://www.invensense.com/wp-content/uploads/2015/02/INMP441.pdf -// B ~= [1.00198, -1.99085, 0.98892] -// A ~= [1.0, -1.99518, 0.99518] -SOS_IIR_Filter INMP441 = { - gain : 1.00197834654696, - sos : {// Second-Order Sections {b1, b2, -a1, -a2} - {-1.986920458344451, +0.986963226946616, +1.995178510504166, -0.995184322194091}} -}; - -// -// Weighting filters -// - -// -// A-weighting IIR Filter, Fs = 48KHz -// (By Dr. Matt L., Source: https://dsp.stackexchange.com/a/36122) -// B = [0.169994948147430, 0.280415310498794, -1.120574766348363, 0.131562559965936, 0.974153561246036, -0.282740857326553, -0.152810756202003] -// A = [1.0, -2.12979364760736134, 0.42996125885751674, 1.62132698199721426, -0.96669962900852902, 0.00121015844426781, 0.04400300696788968] -SOS_IIR_Filter A_weighting = { - gain : 0.169994948147430, - sos : {// Second-Order Sections {b1, b2, -a1, -a2} - {-2.00026996133106, +1.00027056142719, -1.060868438509278, -0.163987445885926}, - {+4.35912384203144, +3.09120265783884, +1.208419926363593, -0.273166998428332}, - {-0.70930303489759, -0.29071868393580, +1.982242159753048, -0.982298594928989}} -}; - -// -// C-weighting IIR Filter, Fs = 48KHz -// Designed by invfreqz curve-fitting, see respective .m file -// B = [-0.49164716933714026, 0.14844753846498662, 0.74117815661529129, -0.03281878334039314, -0.29709276192593875, -0.06442545322197900, -0.00364152725482682] -// A = [1.0, -1.0325358998928318, -0.9524000181023488, 0.8936404694728326 0.2256286147169398 -0.1499917107550188, 0.0156718181681081] -SOS_IIR_Filter C_weighting = { - gain: -0.491647169337140, - sos: { - {+1.4604385758204708, +0.5275070373815286, +1.9946144559930252, -0.9946217070140883}, - {+0.2376222404939509, +0.0140411206016894, -1.3396585608422749, -0.4421457807694559}, - {-2.0000000000000000, +1.0000000000000000, +0.3775800047420818, -0.0356365756680430} - } -}; - -// -// Sampling -// -#define SAMPLE_RATE 48000 // Hz, fixed to design of IIR filters -#define SAMPLE_BITS 32 // bits -#define SAMPLE_T int32_t -#define SAMPLES_SHORT (SAMPLE_RATE / 8) // ~125ms -#define SAMPLES_LEQ (SAMPLE_RATE * LEQ_PERIOD) -#define DMA_BANK_SIZE (SAMPLES_SHORT / 16) -#define DMA_BANKS 32 - -// Data we push to 'samples_queue' -struct sum_queue_t -{ - // Sum of squares of mic samples, after Equalizer filter - float sum_sqr_SPL; - // Sum of squares of weighted mic samples - float sum_sqr_weighted; - // Debug only, FreeRTOS ticks we spent processing the I2S data - uint32_t proc_ticks; -}; -QueueHandle_t samples_queue; - -// Static buffer for block of samples -float samples[SAMPLES_SHORT] __attribute__((aligned(4))); - -// -// I2S Microphone sampling setup -// -void mic_i2s_init() { - // Setup I2S to sample mono channel for SAMPLE_RATE * SAMPLE_BITS - // NOTE: Recent update to Arduino_esp32 (1.0.2 -> 1.0.3) - // seems to have swapped ONLY_LEFT and ONLY_RIGHT channels - const i2s_config_t i2s_config = { - mode : i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), - sample_rate : SAMPLE_RATE, - bits_per_sample : i2s_bits_per_sample_t(SAMPLE_BITS), - channel_format : I2S_CHANNEL_FMT_ONLY_RIGHT, - communication_format : i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), - intr_alloc_flags : ESP_INTR_FLAG_LEVEL1, - dma_buf_count : DMA_BANKS, - dma_buf_len : DMA_BANK_SIZE, - use_apll : true, - tx_desc_auto_clear : false, - fixed_mclk : 0 - }; - // I2S pin mapping - const i2s_pin_config_t pin_config = { - bck_io_num : I2S_SCK, - ws_io_num : I2S_WS, - data_out_num : -1, // not used - data_in_num : I2S_SD - }; - - i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); - - i2s_set_pin(I2S_PORT, &pin_config); -} - -// -// I2S Reader Task -// -// Rationale for separate task reading I2S is that IIR filter -// processing can be scheduled to different core on the ESP32 -// while main task can do something else, like update the -// display in the example -// -// As this is intended to run as separate high-priority task, -// we only do the minimum required work with the I2S data -// until it is 'compressed' into sum of squares -// -// FreeRTOS priority and stack size (in 32-bit words) -#define I2S_TASK_PRI 4 -#define I2S_TASK_STACK 2048 -// -void mic_i2s_reader_task(void *parameter) { - mic_i2s_init(); - - // Discard first block, microphone may have startup time (i.e. INMP441 up to 83ms) - size_t bytes_read = 0; - i2s_read(I2S_PORT, &samples, SAMPLES_SHORT * sizeof(int32_t), &bytes_read, portMAX_DELAY); - - while (true) { - // Block and wait for microphone values from I2S - // - // Data is moved from DMA buffers to our 'samples' buffer by the driver ISR - // and when there is requested ammount of data, task is unblocked - // - // Note: i2s_read does not care it is writing in float[] buffer, it will write - // integer values to the given address, as received from the hardware peripheral. - i2s_read(I2S_PORT, &samples, SAMPLES_SHORT * sizeof(SAMPLE_T), &bytes_read, portMAX_DELAY); - - TickType_t start_tick = xTaskGetTickCount(); - - // Convert (including shifting) integer microphone values to floats, - // using the same buffer (assumed sample size is same as size of float), - // to save a bit of memory - SAMPLE_T *int_samples = (SAMPLE_T *)&samples; - for (int i = 0; i < SAMPLES_SHORT; i++) - samples[i] = MIC_CONVERT(int_samples[i]); - - sum_queue_t q; - // Apply equalization and calculate Z-weighted sum of squares, - // writes filtered samples back to the same buffer. - q.sum_sqr_SPL = MIC_EQUALIZER.filter(samples, samples, SAMPLES_SHORT); - - // Apply weighting and calucate weigthed sum of squares - q.sum_sqr_weighted = WEIGHTING.filter(samples, samples, SAMPLES_SHORT); - - // Debug only. Ticks we spent filtering and summing block of I2S data - q.proc_ticks = xTaskGetTickCount() - start_tick; - - // Send the sums to FreeRTOS queue where main task will pick them up - // and further calcualte decibel values (division, logarithms, etc...) - xQueueSend(samples_queue, &q, portMAX_DELAY); - } -} - -// -// Setup and main loop -// -// Note: Use doubles, not floats, here unless you want to pin -// the task to whichever core it happens to run on at the moment -// -void setup() { - - // LED setup - delay(2000); - FastLED.addLeds(leds, NUM_LEDS); - FastLED.setDither(false); - FastLED.setCorrection(TypicalLEDStrip); - FastLED.setBrightness(brightness); - FastLED.setMaxPowerInVoltsAndMilliamps(5, 420); - set_max_power_indicator_LED(13); - fill_solid(leds, NUM_LEDS, CRGB::Black); - FastLED.show(); - - // If needed, now you can actually lower the CPU frquency, - // i.e. if you want to (slightly) reduce ESP32 power consumption - setCpuFrequencyMhz(80); // It should run as low as 80MHz - - Serial.begin(115200); - delay(1000); // Safety - - // IR setup - irrecv.enableIRIn(); // Start the receiver - while (!Serial) // Wait for the serial connection to be establised. - delay(50); - - // Create FreeRTOS queue - samples_queue = xQueueCreate(8, sizeof(sum_queue_t)); - - // Create the I2S reader FreeRTOS task - // NOTE: Current version of ESP-IDF will pin the task - // automatically to the first core it happens to run on - // (due to using the hardware FPU instructions). - // For manual control see: xTaskCreatePinnedToCore - xTaskCreate(mic_i2s_reader_task, "Mic I2S Reader", I2S_TASK_STACK, NULL, I2S_TASK_PRI, NULL); - - -} - -void loop() { - - sum_queue_t q; - uint32_t Leq_samples = 0; - double Leq_sum_sqr = 0; - double Leq_dB = 0; - - // Read sum of samaples, calculated by 'i2s_reader_task' - while (xQueueReceive(samples_queue, &q, portMAX_DELAY)) { - - preferences.begin("traffic", false); - // Preferences setup - unsigned int dB_min = preferences.getUInt("dB_min", dB_min_default); - unsigned int dB_max = preferences.getUInt("dB_max", dB_max_default); - - if (irrecv.decode(&results)) { - switch (results.value) { - case 0xF700FF: - // Serial.println("Bright+"); - if (brightness < 204) { - brightness = brightness + 51; - } else { - brightness = 255; - } - FastLED.setBrightness(brightness); - FastLED.show(); - break; - case 0xF7807F: - // Serial.println("Bright-"); - if (brightness > 51) { - brightness = brightness - 51; - } else { - brightness = 10; - } - FastLED.setBrightness(brightness); - FastLED.show(); - break; - case 0xF740BF: - // Serial.println("Off"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Black); - FastLED.show(); - break; - case 0xF7C03F: - // Serial.println("On"); - deciLight = 1; - break; - case 0xF720DF: - // Serial.println("Red"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Red); - FastLED.show(); - break; - case 0xF7A05F: - // Serial.println("Green"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Green); - FastLED.show(); - break; - case 0xF7609F: - // Serial.println("Blue"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Blue); - FastLED.show(); - break; - case 0xF7E01F: - // Serial.println("White"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::White); - FastLED.show(); - break; - case 0xF710EF: - // Serial.println("Tomato"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Tomato); - FastLED.show(); - break; - case 0xF7906F: - // Serial.println("LightGreen"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::LightGreen); - FastLED.show(); - break; - case 0xF750AF: - // Serial.println("SkyBlue"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::SkyBlue); - FastLED.show(); - break; - case 0xF7D02F: - // Serial.println("Flash"); - dB_min++; - if (deciLight == 1) { - preferences.putUInt("dB_min", dB_min); - fill_solid(leds, NUM_LEDS, CRGB::Black); - FastLED.show(); - Serial.println(dB_min); - } - break; - case 0xF730CF: - // Serial.println("OrangeRed"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::OrangeRed); - FastLED.show(); - break; - case 0xF7B04F: - // Serial.println("Cyan"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Cyan); - FastLED.show(); - break; - case 0xF7708F: - // Serial.println("RebeccaPurple"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Purple); - FastLED.show(); - break; - case 0xF7F00F: - // Serial.println("Strobe"); - if (deciLight == 1) { - dB_min--; - preferences.putUInt("dB_min", dB_min); - fill_solid(leds, NUM_LEDS, CRGB::Black); - FastLED.show(); - Serial.println(dB_min); - } - break; - case 0xF708F7: - // Serial.println("Orange"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Orange); - FastLED.show(); - break; - case 0xF78877: - // Serial.println("Turquoise"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Turquoise); - FastLED.show(); - break; - case 0xF748B7: - // Serial.println("Purple"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::MediumPurple); - FastLED.show(); - break; - case 0xF7C837: - // Serial.println("Fade"); - if (deciLight == 1) { - dB_max++; - preferences.putUInt("dB_max", dB_max); - fill_solid(leds, NUM_LEDS, CRGB::Black); - FastLED.show(); - Serial.println(dB_max); - } - break; - case 0xF728D7: - // Serial.println("Yellow"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Yellow); - FastLED.show(); - break; - case 0xF7A857: - // Serial.println("DarkCyan"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::DarkCyan); - FastLED.show(); - break; - case 0xF76897: - // Serial.println("Plum"); - deciLight = 0; - fill_solid(leds, NUM_LEDS, CRGB::Plum); - FastLED.show(); - break; - case 0xF7E817: - // Serial.println("Smooth"); - if (deciLight == 1) { - dB_max--; - preferences.putUInt("dB_max", dB_max); - fill_solid(leds, NUM_LEDS, CRGB::Black); - FastLED.show(); - Serial.println(dB_max); - } - break; - default: - Serial.println("Unknown button pressed"); - Serial.print(results.value, HEX); - Serial.println(""); - } - irrecv.resume(); // Receive the next value - } - delay(100); - preferences.end(); - - // Calculate dB values relative to MIC_REF_AMPL and adjust for microphone reference - double short_RMS = sqrt(double(q.sum_sqr_SPL) / SAMPLES_SHORT); - double short_SPL_dB = MIC_OFFSET_DB + MIC_REF_DB + 20 * log10(short_RMS / MIC_REF_AMPL); - - // In case of acoustic overload or below noise floor measurement, report infinty Leq value - if (short_SPL_dB > MIC_OVERLOAD_DB) { - Leq_sum_sqr = INFINITY; - } - else if (isnan(short_SPL_dB) || (short_SPL_dB < MIC_NOISE_DB)) { - Leq_sum_sqr = -INFINITY; - } - - // Accumulate Leq sum - Leq_sum_sqr += q.sum_sqr_weighted; - Leq_samples += SAMPLES_SHORT; - - // When we gather enough samples, calculate new Leq value - if (Leq_samples >= SAMPLE_RATE * LEQ_PERIOD) { - double Leq_RMS = sqrt(Leq_sum_sqr / Leq_samples); - Leq_dB = MIC_OFFSET_DB + MIC_REF_DB + 20 * log10(Leq_RMS / MIC_REF_AMPL); - Leq_sum_sqr = 0; - Leq_samples = 0; - - // Serial output, customize (or remove) as needed - Serial.printf("Current dB value: %.1f\n", Leq_dB); - - // Debug only - // Serial.printf("%u processing ticks\n", q.proc_ticks); - - if (deciLight == 1) { - if (Leq_dB < dB_min) { - fill_solid(leds, NUM_LEDS, CRGB::Green); - FastLED.show(); - } - else if (Leq_dB < dB_max) { - fill_solid(leds, NUM_LEDS, CRGB::Yellow); - FastLED.show(); - } - else { - fill_solid(leds, NUM_LEDS, CRGB::Red); - FastLED.show(); - } - } - - } - - } -} \ No newline at end of file diff --git a/esp32-i2s-slm.ino b/esp32-i2s-slm.ino new file mode 100644 index 0000000..5a2d392 --- /dev/null +++ b/esp32-i2s-slm.ino @@ -0,0 +1,221 @@ +/* + * Display A-weighted sound level measured by I2S Microphone + * + * (c)2019 Ivan Kostoski + * (c)2021 Bim Overbohm (split into files, template) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * Sketch samples audio data from I2S microphone, processes the data + * with digital IIR filters and calculates A or C weighted Equivalent + * Continuous Sound Level (Leq) + * + * I2S is setup to sample data at Fs=48000KHz (fixed value due to + * design of digital IIR filters). Data is read from I2S queue + * in 'sample blocks' (default 125ms block, equal to 6000 samples) + * by 'i2s_reader_task', filtered trough two IIR filters (equalizer + * and weighting), summed up and pushed into 'samples_queue' as + * sum of squares of filtered samples. The main task then pulls data + * from the queue and calculates decibel value relative to microphone + * reference amplitude, derived from datasheet sensitivity dBFS + * value, number of bits in I2S data, and the reference value for + * which the sensitivity is specified (typically 94dB, pure sine + * wave at 1KHz). + * + * Displays line on the small OLED screen with 'short' LAeq(125ms) + * response and numeric LAeq(1sec) dB value from the signal RMS. + */ + +#include "i2s_mic.h" +#include "filters.h" + +// +// Configuration +// + +#define LEQ_PERIOD 1 // second(s) +#define WEIGHTING C_weighting // Also avaliable: 'C_weighting' or 'None' (Z_weighting) +#define LEQ_UNITS "LAeq" // customize based on above weighting used +#define DB_UNITS "dBA" // customize based on above weighting used +#define USE_DISPLAY 0 + +// NOTE: Some microphones require at least DC-Blocker filter +#define MIC_EQUALIZER ICS43434 // See below for defined IIR filters or set to 'None' to disable +#define MIC_OFFSET_DB 3.0103 // Default offset (sine-wave RMS vs. dBFS). Modify this value for linear calibration + +// Customize these values from microphone datasheet +#define MIC_SENSITIVITY -26 // dBFS value expected at MIC_REF_DB (Sensitivity value from datasheet) +#define MIC_REF_DB 94.0 // Value at which point sensitivity is specified in datasheet (dB) +#define MIC_OVERLOAD_DB 116.0 // dB - Acoustic overload point +#define MIC_NOISE_DB 29 // dB - Noise floor +#define MIC_BITS 24 // valid number of bits in I2S data +#define MIC_CONVERT(s) (s >> (SAMPLE_BITS - MIC_BITS)) +#define MIC_TIMING_SHIFT false // Set to one to fix MSB timing for some microphones, i.e. SPH0645LM4H-x + +// Calculate reference amplitude value at compile time +constexpr double MIC_REF_AMPL = pow(10, double(MIC_SENSITIVITY) / 20) * ((1 << (MIC_BITS - 1)) - 1); + +#define SAMPLE_RATE_HZ 48000 // Hz, fixed to design of IIR filters. Determines maximum frequency that can be analysed by the FFT Fmax=sampleF/2. +#define SAMPLE_COUNT 2048 // ~40ms sample time, must be power-of-two +// Static buffer for block of samples +float samples[SAMPLE_COUNT] __attribute__((aligned(4))); + +// +// I2S pins - Can be routed to almost any (unused) ESP32 pin. +// SD can be any pin, inlcuding input only pins (36-39). +// SCK (i.e. BCLK) and WS (i.e. L/R CLK) must be output capable pins +// +// Below ones are just example for my board layout, put here the pins you will use +// +#define I2S_WS 15 +#define I2S_SCK 14 +#define I2S_SD 32 + +// I2S peripheral to use (0 or 1) +#define I2S_PORT I2S_NUM_0 + +// Set up microphone +auto mic = Microphone_I2S(MIC_EQUALIZER); + +// +// Setup your display library (and geometry) here +// +#if (USE_DISPLAY > 0) +// ThingPulse/esp8266-oled-ssd1306, you may need the latest source and PR#198 for 64x48 +#include +#define OLED_GEOMETRY GEOMETRY_64_48 +//#define OLED_GEOMETRY GEOMETRY_128_32 +//#define OLED_GEOMETRY GEOMETRY_128_64 +#define OLED_FLIP_V 1 +SSD1306Wire display(0x3c, SDA, SCL, OLED_GEOMETRY); +#endif + +// +// Setup and main loop +// +// Note: Use doubles, not floats, here unless you want to pin +// the task to whichever core it happens to run on at the moment +// +void setup() +{ + // If needed, now you can actually lower the CPU frquency, + // i.e. if you want to (slightly) reduce ESP32 power consumption + setCpuFrequencyMhz(80); // It should run as low as 80MHz + + Serial.begin(115200); + delay(1000); // Safety + +#if (USE_DISPLAY > 0) + display.init(); +#if (OLED_FLIP_V > 0) + display.flipScreenVertically(); +#endif + display.setFont(ArialMT_Plain_16); +#endif + + mic.begin(); + Serial.println("Starting sampling from mic"); + mic.startSampling(); + + uint32_t Leq_samples = 0; + double Leq_sum_sqr = 0; + double Leq_dB = 0; + + // Read equalized samples read from microphone + while (xQueueReceive(mic.sampleQueue(), &samples, portMAX_DELAY)) + { + + // Sum of squares of mic equalized samples + float sum_sqr_SPL = MIC_EQUALIZER.calculateSumOfSquares(samples, samples, SAMPLE_COUNT); + // Sum of squares of equalized + weighted mic samples + float sum_sqr_weighted = WEIGHTING.calculateSumOfSquares(samples, samples, SAMPLE_COUNT); + + // Calculate dB values relative to MIC_REF_AMPL and adjust for microphone reference + double short_RMS = sqrt(double(sum_sqr_SPL) / SAMPLE_COUNT); + double short_SPL_dB = MIC_OFFSET_DB + MIC_REF_DB + 20 * log10(short_RMS / MIC_REF_AMPL); + + // In case of acoustic overload or below noise floor measurement, report infinty Leq value + if (short_SPL_dB > MIC_OVERLOAD_DB) + { + Leq_sum_sqr = INFINITY; + } + else if (isnan(short_SPL_dB) || (short_SPL_dB < MIC_NOISE_DB)) + { + Leq_sum_sqr = -INFINITY; + } + + // Accumulate Leq sum + Leq_sum_sqr += sum_sqr_weighted; + Leq_samples += SAMPLE_COUNT; + + // When we gather enough samples, calculate new Leq value + if (Leq_samples >= SAMPLE_RATE_HZ * LEQ_PERIOD) + { + double Leq_RMS = sqrt(Leq_sum_sqr / Leq_samples); + Leq_dB = MIC_OFFSET_DB + MIC_REF_DB + 20 * log10(Leq_RMS / MIC_REF_AMPL); + Leq_sum_sqr = 0; + Leq_samples = 0; + + // Serial output, customize (or remove) as needed + Serial.printf("%.1f\n", Leq_dB); + + // Debug only + //Serial.printf("%u processing ticks\n", q.proc_ticks); + } + +#if (USE_DISPLAY > 0) + + // + // Example code that displays the measured value. + // You should customize the below code for your display + // and display library used. + // + + display.clear(); + + // It is important to somehow notify when the deivce is out of its range + // as the calculated values are very likely with big error + if (Leq_dB > MIC_OVERLOAD_DB) + { + // Display 'Overload' if dB value is over the AOP + display.drawString(0, 24, "Overload"); + } + else if (isnan(Leq_dB) || (Leq_dB < MIC_NOISE_DB)) + { + // Display 'Low' if dB value is below noise floor + display.drawString(0, 24, "Low"); + } + + // The 'short' Leq line + double short_Leq_dB = MIC_OFFSET_DB + MIC_REF_DB + 20 * log10(sqrt(double(sum_sqr_weighted) / SAMPLE_COUNT) / MIC_REF_AMPL); + uint16_t len = min(max(0, int(((short_Leq_dB - MIC_NOISE_DB) / MIC_OVERLOAD_DB) * (display.getWidth() - 1))), display.getWidth() - 1); + display.drawHorizontalLine(0, 0, len); + display.drawHorizontalLine(0, 1, len); + display.drawHorizontalLine(0, 2, len); + + // The Leq numeric decibels + display.drawString(0, 4, String(Leq_dB, 1) + " " + DB_UNITS); + + display.display(); + +#endif // USE_DISPLAY + } +} + +void loop() +{ + // Nothing here.. +} diff --git a/filters.h b/filters.h new file mode 100644 index 0000000..6c3c21e --- /dev/null +++ b/filters.h @@ -0,0 +1,137 @@ +/* + * Display A-weighted sound level measured by I2S Microphone + * + * (c)2019 Ivan Kostoski (original version) + * (c)2021 Bim Overbohm (split into files, template) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include "sos-iir-filter.h" + +// +// IIR Filters +// + +// Dummy filter. For testing only +const SOS_IIR_Filter None( + 1.0, + {} +); + +// DC-Blocker filter - removes DC component from I2S data +// See: https://www.dsprelated.com/freebooks/filters/DC_Blocker.html +// a1 = -0.9992 should heavily attenuate frequencies below 10Hz +const SOS_IIR_Filter DC_BLOCKER( + 1.0, + {{-1.0, 0.0, +0.9992, 0.0}} +); + +// +// Equalizer IIR filters to flatten microphone frequency response +// See respective .m file for filter design. Fs = 48Khz. +// +// Filters are represented as Second-Order Sections cascade with assumption +// that b0 and a0 are equal to 1.0 and 'gain' is applied at the last step +// B and A coefficients were transformed with GNU Octave: +// [sos, gain] = tf2sos(B, A) +// See: https://www.dsprelated.com/freebooks/filters/Series_Second_Order_Sections.html +// NOTE: SOS matrix 'a1' and 'a2' coefficients are negatives of tf2sos output +// + +// TDK/InvenSense ICS-43434 +// Datasheet: https://www.invensense.com/wp-content/uploads/2016/02/DS-000069-ICS-43434-v1.1.pdf +// B = [0.477326418836803, -0.486486982406126, -0.336455844522277, 0.234624646917202, 0.111023257388606]; +// A = [1.0, -1.93073383849136326, 0.86519456089576796, 0.06442838283825100, 0.00111249298800616]; +const SOS_IIR_Filter ICS43434( + 0.477326418836803, + {// Second-Order Sections {b1, b2, -a1, -a2} + {+0.96986791463971267, 0.23515976355743193, -0.06681948004769928, -0.00111521990688128}, + {-1.98905931743624453, 0.98908924206960169, +1.99755331853906037, -0.99755481510122113}} +); + +// TDK/InvenSense ICS-43432 +// Datasheet: https://www.invensense.com/wp-content/uploads/2015/02/ICS-43432-data-sheet-v1.3.pdf +// B = [-0.45733702338341309 1.12228667105574775 -0.77818278904413563, 0.00968926337978037, 0.10345668405223755] +// A = [1.0, -3.3420781082912949, 4.4033694320978771, -3.0167072679918010, 1.2265536567647031, -0.2962229189311990, 0.0251085747458112] +const SOS_IIR_Filter ICS43432( + -0.457337023383413, + {// Second-Order Sections {b1, b2, -a1, -a2} + {-0.544047931916859, -0.248361759321800, +0.403298891662298, -0.207346186351843}, + {-1.909911869441421, +0.910830292683527, +1.790285722826743, -0.804085812369134}, + {+0.000000000000000, +0.000000000000000, +1.148493493802252, -0.150599527756651}} +); + +// TDK/InvenSense INMP441 +// Datasheet: https://www.invensense.com/wp-content/uploads/2015/02/INMP441.pdf +// B ~= [1.00198, -1.99085, 0.98892] +// A ~= [1.0, -1.99518, 0.99518] +const SOS_IIR_Filter INMP441( + 1.00197834654696, + {// Second-Order Sections {b1, b2, -a1, -a2} + {-1.986920458344451, +0.986963226946616, +1.995178510504166, -0.995184322194091}} +); + +// Infineon IM69D130 Shield2Go +// Datasheet: https://www.infineon.com/dgdl/Infineon-IM69D130-DS-v01_00-EN.pdf?fileId=5546d462602a9dc801607a0e46511a2e +// B ~= [1.001240684967527, -1.996936108836337, 0.995703101823006] +// A ~= [1.0, -1.997675693595542, 0.997677044195563] +// With additional DC blocking component +const SOS_IIR_Filter IM69D130( + 1.00124068496753, + {// Second-Order Sections {b1, b2, -a1, -a2} + {-1.0, 0.0, +0.9992, 0}, // DC blocker, a1 = -0.9992 + {-1.994461610298131, 0.994469278738208, +1.997675693595542, -0.997677044195563}} +); + +// Knowles SPH0645LM4H-B, rev. B +// https://cdn-shop.adafruit.com/product-files/3421/i2S+Datasheet.PDF +// B ~= [1.001234, -1.991352, 0.990149] +// A ~= [1.0, -1.993853, 0.993863] +// With additional DC blocking component +const SOS_IIR_Filter SPH0645LM4H_B_RB( + 1.00123377961525, + {// Second-Order Sections {b1, b2, -a1, -a2} + {-1.0, 0.0, +0.9992, 0}, // DC blocker, a1 = -0.9992 + {-1.988897663539382, +0.988928479008099, +1.993853376183491, -0.993862821429572}} + ); + +// +// Weighting filters +// + +// A-weighting IIR Filter, Fs = 48KHz +// (By Dr. Matt L., Source: https://dsp.stackexchange.com/a/36122) +// B = [0.169994948147430, 0.280415310498794, -1.120574766348363, 0.131562559965936, 0.974153561246036, -0.282740857326553, -0.152810756202003] +// A = [1.0, -2.12979364760736134, 0.42996125885751674, 1.62132698199721426, -0.96669962900852902, 0.00121015844426781, 0.04400300696788968] +const SOS_IIR_Filter A_weighting( + 0.169994948147430, + {// Second-Order Sections {b1, b2, -a1, -a2} + {-2.00026996133106, +1.00027056142719, -1.060868438509278, -0.163987445885926}, + {+4.35912384203144, +3.09120265783884, +1.208419926363593, -0.273166998428332}, + {-0.70930303489759, -0.29071868393580, +1.982242159753048, -0.982298594928989}} +); + +// C-weighting IIR Filter, Fs = 48KHz +// Designed by invfreqz curve-fitting, see respective .m file +// B = [-0.49164716933714026, 0.14844753846498662, 0.74117815661529129, -0.03281878334039314, -0.29709276192593875, -0.06442545322197900, -0.00364152725482682] +// A = [1.0, -1.0325358998928318, -0.9524000181023488, 0.8936404694728326 0.2256286147169398 -0.1499917107550188, 0.0156718181681081] +const SOS_IIR_Filter C_weighting( + -0.491647169337140, + { + {+1.4604385758204708, +0.5275070373815286, +1.9946144559930252, -0.9946217070140883}, + {+0.2376222404939509, +0.0140411206016894, -1.3396585608422749, -0.4421457807694559}, + {-2.0000000000000000, +1.0000000000000000, +0.3775800047420818, -0.0356365756680430}} +); diff --git a/i2s_mic.h b/i2s_mic.h new file mode 100644 index 0000000..743ca2b --- /dev/null +++ b/i2s_mic.h @@ -0,0 +1,238 @@ +/* + * Display A-weighted sound level measured by I2S Microphone + * + * (c)2019 Ivan Kostoski (original version) + * (c)2021 Bim Overbohm (split into files, template) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "sos-iir-filter.h" + +//#define SERIAL_OUTPUT + +// I2S microphone connnection +// NR_OF_SAMPLES = number of microphone samples to take and return in queue +// I2S pins - Can be routed to almost any (unused) ESP32 pin. +// SD can be any pin, including input only pins (36-39). +// SCK (i.e. BCLK) and WS (i.e. L/R CLK) must be output capable pins +// PIN_WS = I2S word select pin +// PIN_SCK = I2S clock pin +// PIN_SD = I2S data pin +// I2S_PORT = I2S port to use +// MIC_BITS = number of valid bits in microphone data +// MSB_SHIFT = set to true to fix MSB timing for some microphones, i.e. SPH0645LM4H-x +// SAMPLE_RATE = microphone sample rate. must be 48kHz to fit filter design +template +class Microphone_I2S +{ + static constexpr unsigned TASK_PRIO = 4; // FreeRTOS priority + static constexpr unsigned TASK_STACK = 2048; // FreeRTOS stack size (in 32-bit words) + +public: + using SAMPLE_T = int32_t; + using SampleBuffer = float[NR_OF_SAMPLES]; + static const constexpr uint32_t SAMPLE_BITS = sizeof(SAMPLE_T) * 8; + + /// @brief Create new I2S microphone. + /// @param filter Microphone IIR filter function to apply to samples + Microphone_I2S(const SOS_IIR_Filter &filter) + : m_filter(filter) + { + } + + void begin() + { +#ifdef SERIAL_OUTPUT + Serial.print("Installing microphone I2S driver at "); + if (I2S_PORT == I2S_NUM_0) + { + Serial.println("I2S0"); + } + else if (I2S_PORT == I2S_NUM_1) + { + Serial.println("I2S1"); + } + else + { + Serial.println("unknown port"); + } +#endif + // Setup I2S to sample mono channel for SAMPLE_RATE * SAMPLE_BITS + // NOTE: Recent update to Arduino_esp32 (1.0.2 -> 1.0.3) + // seems to have swapped ONLY_LEFT and ONLY_RIGHT channels + i2s_config_t i2s_config{}; + i2s_config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX); + i2s_config.sample_rate = SAMPLE_RATE; + i2s_config.bits_per_sample = i2s_bits_per_sample_t(SAMPLE_BITS); + i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; + i2s_config.communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB); + i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; + i2s_config.dma_buf_count = 2; + i2s_config.dma_buf_len = NR_OF_SAMPLES; + i2s_config.use_apll = true; + //i2s_config.tx_desc_auto_clear = false; + i2s_config.fixed_mclk = 0; + i2s_driver_install(I2S_PORT, &i2s_config, 0, nullptr); + + // I2S pin mapping +#ifdef SERIAL_OUTPUT + Serial.println("Installing microphone I2S pin mapping"); +#endif + i2s_pin_config_t pin_config{}; + pin_config.bck_io_num = PIN_SCK; + pin_config.ws_io_num = PIN_WS; + pin_config.data_out_num = -1; // not used + pin_config.data_in_num = PIN_SD; + if (MSB_SHIFT) + { + // Undocumented (?!) manipulation of I2S peripheral registers + // to fix MSB timing issues with some I2S microphones + REG_SET_BIT(I2S_TIMING_REG(I2S_PORT), BIT(9)); + REG_SET_BIT(I2S_CONF_REG(I2S_PORT), I2S_RX_MSB_SHIFT); + } + i2s_set_pin(I2S_PORT, &pin_config); + +#ifdef SERIAL_OUTPUT + Serial.println("Creating microphone I2S sample queue"); +#endif + //FIXME: There is a known issue with esp-idf and sampling rates, see: + // https://github.com/espressif/esp-idf/issues/2634 + // In the meantime, the below line seems to set sampling rate at ~47999.992Hz + // fifs_req=24576000, sdm0=149, sdm1=212, sdm2=5, odir=2 -> fifs_reached=24575996 + //NOTE: This seems to be fixed in ESP32 Arduino 1.0.4, esp-idf 3.2 + // Should be safe to remove... + //#include + //rtc_clk_apll_enable(1, 149, 212, 5, 2); + // Create FreeRTOS queue + m_sampleQueue = xQueueCreate(2, sizeof(SampleBuffer)); + // Create the I2S reader FreeRTOS task + // NOTE: Current version of ESP-IDF will pin the task + // automatically to the first core it happens to run on + // (due to using the hardware FPU instructions). + // For manual control see: xTaskCreatePinnedToCore +#ifdef SERIAL_OUTPUT + Serial.println("Creating microphone I2S reader task"); +#endif + xTaskCreate(readerTask, "Microphone_I2S reader", TASK_STACK, this, TASK_PRIO, nullptr); + } + + /// @brief Get the queue that stores new sample buffers. + QueueHandle_t sampleQueue() const + { + return m_sampleQueue; + } + + /// @brief Start sampling from microphone. + void startSampling() + { + m_isSampling = true; + } + + /// @brief Stop sampling from microphone. + void stopSampling() + { + m_isSampling = false; + } + +private: + static void readerTask(void *parameter) + { +#ifdef SERIAL_OUTPUT + Serial.println("Mic reader task started"); +#endif + auto object = reinterpret_cast(parameter); + // Discard first blocks, microphone may have startup time (i.e. INMP441 up to 83ms) + size_t bytes_read = 0; + for (int i = 0; i < 5; i++) + { + i2s_read(I2S_PORT, &object->m_sampleBuffer, NR_OF_SAMPLES * sizeof(SAMPLE_T), &bytes_read, portMAX_DELAY); + } + while (true) + { + if (object->m_isSampling) + { + // Block and wait for microphone values from I2S + // Data is moved from DMA buffers to our m_sampleBuffer by the driver ISR + // and when there is requested amount of data, task is unblocked + // + // Note: i2s_read does not care it is writing in float[] buffer, it will write + // integer values to the given address, as received from the hardware peripheral. + i2s_read(I2S_PORT, &object->m_sampleBuffer, NR_OF_SAMPLES * sizeof(SAMPLE_T), &bytes_read, portMAX_DELAY); + + // Debug only. Ticks we spent filtering and summing block of I2S data + //TickType_t start_tick = xTaskGetTickCount(); + + // Convert (including shifting) integer microphone values to floats, + // using the same buffer (assumed sample size is same as size of float), + // to save a bit of memory + auto int_samples = reinterpret_cast(&object->m_sampleBuffer); + for (int i = 0; i < NR_OF_SAMPLES; i++) + { + object->m_sampleBuffer[i] = int_samples[i] >> (SAMPLE_BITS - MIC_BITS); + } + + // filter values and apply gain setting + object->m_filter.applyFilters(object->m_sampleBuffer, object->m_sampleBuffer, NR_OF_SAMPLES); + object->m_filter.applyGain(object->m_sampleBuffer, object->m_sampleBuffer, NR_OF_SAMPLES); + + // Debug only. Ticks we spent filtering and summing block of I2S data + //auto proc_ticks = xTaskGetTickCount() - start_tick; + + // Send the sums to FreeRTOS queue where main task will pick them up + // and further calculate decibel values (division, logarithms, etc...) + xQueueSend(object->m_sampleQueue, &object->m_sampleBuffer, portMAX_DELAY); + + // Debug only. Print raw microphone sample values + /*int vMin = 1000000; + int vMax = -vMin; + int vAvg = 0; + int vNan = 0; + for (unsigned int k = 0; k < bytes_read; k++) + { + if (isnan(object->m_sampleBuffer[k]) || isinf(object->m_sampleBuffer[k])) + { + object->m_sampleBuffer[k] = 0; + vNan++; + } + if (object->m_sampleBuffer[k] < vMin) + { + vMin = object->m_sampleBuffer[k]; + } + if (object->m_sampleBuffer[k] > vMax) + { + vMax = object->m_sampleBuffer[k]; + } + vAvg += object->m_sampleBuffer[k]; + } + vAvg /= bytes_read; + Serial.print("Min: "); Serial.print(vMin, 3); + Serial.print(", Max: "); Serial.print(vMax, 3); + Serial.print(", Avg: "); Serial.print(vAvg, 3); + Serial.print(", NAN or INF: "); Serial.println(vNan);*/ + } + } + } + + SOS_IIR_Filter m_filter; + QueueHandle_t m_sampleQueue; + SampleBuffer m_sampleBuffer __attribute__((aligned(4))); + bool m_isSampling = false; +}; diff --git a/misc/ESP32-Pinout.png b/misc/ESP32-Pinout.png deleted file mode 100644 index d9c4487d963aa5aaf15559c14fbd249b492562c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39996 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 z zUL;5$I6;HEF7EE`WV86PXduCY1PH+)xCU69#TF;H1z8pk1P{()K|bEQ@2>8uZhk$} z)iqY#GtZCiX@G#uv7~S!%wEF5%w4>#t=ZiG0?y#J`dJ5G16gv_W|2gtN!}wzeNC~=`rn+3o@mrl? z90@N;EO%9Dz41$|*mrNrNyHuOB`$BAsG>jt(3uTXW49^1S1X4ds zD|`4MK_;HWBi`uDIjt5#{`t$zlM~K=mABdAHfLj*c!s032Xn7-CbI3d+L~Oc|F&y+ zWg?vL;D|$#;<}&nI!>iLS|LZRX2G;i1?5?DE<)&SpEt$;Wc}Fd*&w;Pf8RL|51fho@Cu`FU{IB72>a*mHU`BGZcmDF#4Mp6 zv!Vt}2pQvu#1sp+=?*+-vgWH1Xd}CYfjS0GZQIO|%kNtD+cuUUmePlBa320(b|S;P8nX4UzF_dCA9mJ0Xbqohgh9S7l(<={}4XU+s^F#r)w<-ThwN{uRh(q+vHZZx8WZ69itGGU2~C4stQ)7AIKZlx zAvD5t*7!X+7S4l&+c^5UYr8#U z5Kq-Iip+=$i;0TYgLQs#wsq%bq5k0qaikmHN*=d>mU?XIt0SZWRa<0jUC7T`i6 z)-R;G(Tt>LuVEeTYh!EV$b4d4Vf|#HcO`3i_(!T|1Y{S~p`60TcAL_KC>|bxzf3}T zq5|o6NhzTpB(7tU(L%@Aw&68*w{D4mjCkW~GZ&$+c2^1tsKkoUtEG>s`-#rnc~L%z zfNrt$dcx@sM=z{kQLtQkIy<06j(2K`QKw$vun;G>;OWTQFNQ>iJrY^Tm6w$Hm64KX zeKDpSX?{U}9MS<##-I@gn>b+Ldl?z4cB&W2O)3_M@?I%EYcpH6LLQJs{gIa56lQGRt{6fIu2p*e? zWi@8zyMKGCkdiUj(E&n-uPk$PI zOX57Rv+1Ds_^=czk<6(HDBD&h-!}3{r-K(FCGoVU*twryxIT#_;Oro)-d8g++O4)* zc?BMvHoePmIxzH;!4%Vl(f@2EhNEj5DSPDUp{_Ju&?&!Ou+>QGVcoH|i=|BVcp9Oe zQ|4o-m#CQrKnvFNL88)JVhmw&84`$8jtrik|4jvb>wm~Wq_%7s6*2ABbfG)yH= z&mD6>hTQw@-5F@LDiXd92@dV4M8I;F*{+@xl&gRAGwmG#e`I?$w&9L{C@-kp!QzKM zb?buv{Uq=^6VpR!D;*UTM2eq(FTlZJ20b`fw*N&J$;4C$Q&v*4l#x*L`1AGZ$B;J} z{Xht_XXH9z-|0G+V$jkMI%&MiKz{;6ZDN_`Tp6n5y~Q1XQ&CRoz*fcmJDg@&MuYV zBDEK=!b4l=a5o;dd!qSWtOMh3N?u~zef-V1a&n~%p{Ed$Co>docshz17Z^DMxl+ru z`(!fX+GG3tL0Wz9M(Ha~Y6~xPVk@k83~5IR2U;*f4x|*6<)Kg~o;_zC5&9=fD?2$a zf34x>+>B!~d}#A5(Wq*G9U&fXiVNWk{uNs5(5fJo_Yb+`1{QkAZPsb&cN;hOgj+Lq z)tq%LN339=749Mm6;MUlnHy3%g6{0OJU@FZg^-z$y=e&T)zDmgCbI$yKO9$(;te`T z?r6lZ6vPv;Df;-D)~Vm4Ovo9fwtrK#LFPV|3^Ka5Gj1t4pOkpGVnp8!>yATyF(}(>j{ShBCWgxr4n7mjhT~wrJ5@2| zz1HYhG?NdAoDl+DpmU+|vj5?)b{nPUGv+uinuKJ;>bUZWf2=J{Sowo^0u^LaFEZ6) znAA!PFlWeb)Rbks)g^k*&$?vu5Yk`2`*N@yS;KOd8heE$DoyfG7`}|>U}A}sB!3}j zZzeZZtL_&83C?X|kN~%6S-6tli>BXq{KRWO5PMp_stnP~dyJu1bDt6}swL|FRYU8{ zFwC@>SA~DNBwE!~@$Palu}LnLZ8}JB|MPe~^nDR{@JyHl93tU6??^jv1WP`xx7^!Z zy|TcQ#u{*M;MKDbdXS2GTtVnu2761$iEseNp9Jz*G^sFlpc-HO__1lmIX#Wf`*|Mg zxtY$7j_ip0GaT5{<;{xVFF%xNt5UUIu89v(3Q}9sIRJawbGDd3bH0|J+n~6pX9gZF zaY~jOWWUp&k2dpe>21_VyrB`2P7XRGo1Mg}Djt2>d#si+%H!H#;%KR@LUVc;(PZ&= z8E0eRLa`=eO89rrAAi~x2K;@Eh4u$AjSOk|{jJ0^LgPI%x%do|Mn>gbu`FPfPc&O- zL3%|d)ttWh#G_y2tMNS}qQsMeQ4h7FyVN*pm8F}xJse&ljQ-JF~hAK zEoJ<&exes;;D@cgjpQt83n7i>R?d&JqBQOV^zz#-oH6IO$sup1E;90k`@q;&VZQuU z8#axBB{K!V5nw%}Gu&Atkb71`Ixb808}8oYgS4<|rNR!wRfu8y5m9C^<{v@*z}Q&k zZ{6MyHt#pfZ+}1{VgYFG-L`74$=KTE*uVL@L8jdO3%c@V+O#d0d$JL7cG5*c(*h0A z6CTfJ9OshdA!%fI&-4l!2v4y|B5xLBBoZ>DxXC&(w%`TMi__+f(%axroZJbUv;-qn8^uoqt(u$mX24H1MMx_mm1jTRRpMyq)fDp;h~>7Ocs- zi^7m2j<@Ek%u7mOZSLj2H;FYTGKT+)3;qXhR6^$c7eoAS`1z4v{(tHJ0dEbF#~piC z$iaY{pDnO2klF^|j@4X0%-fP_UsZoU!28m@RcTI<)=MTRK4#$h-d6HP+G+dwwW|Tw zBkA?5%SrYeRHQHryPs$o32?W5k{i^Kz*ERbzmvJ)AoV%yv_8Ri+*ujyK(xW@5vNG= z;QtBL8izxGYSf=EGH=hu5tcUKQ##cl)`w znHLv2Axr)a{n_&l%kD!L*tqsc&2;CNyz5Km2f!@xy+9cw`xULEeSqlY5Oq#3x_yxd zW--3mwPlpnbtCfLP^`Gi%;W?iH0tt(zM4|dui|ABWkm2wD-hbJ>YO{hwgPYZLrlI% zmrwNQh_$Et^4kOFo%@EAAAWd%#)i z%-*@2rheXGFQ1itn=N*({<}(s^`bCkiy0M*9!)m7G&mktYam$l{ec=qPaHFhz}7gE zr#1IPTWFDK%|eYP`*f=V`Y;&}Af4A>D(ix|$DWzDul*dUwE8J9XUYjEAm#1i{il|C z{7av|s)F0UyIJA}mFpT;3|Pc{bQ8faE-1zATkUyP_FC(i^KGVrPVPR0ToXGCE3MXH zyyQjVDPB!SUsZYk0@sjaw%1)3o|I{{FRix|eTMf&^vmV?RH-$9sThex&h#Eht0OV+ z77LCj$|gS<8S@YBSQ*=93c%#i2nVSLD^3Y-GV11b4;E?bSPN0;X>>m!ctG$6!;ssv zrBxdffH(DAq@KFx&w|6U;`>gzSll$Z=Sl`HN6Mv7<%p8RtrM=M=-v^&zhHc^qiLvn z13XZV3=yZRkVLFP^r0nuz?$F(MHv z?1!e1G2HH9nST7ab{aQ|DWQ4JhcyP}?vjKbpMbX@yoysJEY+c1YSOtKd5CWoo{Fsf zTNr=j`@4Sm#3th(khxikbkh0Y%30=CkU9*!*k1PzJ|W!#gW@8OFm_J9t(IeKKW7*8 zN#K_yh%6>_tX>xo#-JDnv1CY7^2E6RHVYbyJRdcT`2E05oibq5Q&m3OVvJ6M+gh^?KW97`X=i2A_Tp-v z2E1EBcsDm)?QStYsUV}P*Ktl8XXh*#TpCyTZhbWOLxOXdlf*tn-VGu=w{s$N?$LD< zle0Y{yU5q@ih+ZAQi)~T6_CZ+(Tbl$?GsA7qd~XxXBo8RL7QDp|0RHj%in&?uA@t}E!BN+a;R-OlOnVy)Off&eLX0iYh7gz@dHj~ihLeHR^Vz(`tMu=>ioHH zD=&i-EfU(GLggu8Jd@2TAQ$C;tk-kd)ccihdsSbVdVQXnxuTl4*U1s$dBuIMnY7oC zM3e#-03$v)@D7DS3_PYp1tm`hq{eFO-NiWsFmtycx^zu5DJM_vhAlwdR(Rnlw>2ixaz zweTDlZsK5!nwG9?_zt$Kvf}=3z*A8Ek&yo8SKhk#FDpYn%#)HIOM6_E`KxLfMQ4n2 zz6R%l|MdpnfK7yn31~im^t>JumoZu^R(YW@3mNqJ z1mT|<9`nM z=E+OpQ6BX5)SqKNM@%^3k5Mdv(_1NvsO5H)m9$nhORQ#WHipVUXhPNs>9u86RDk3Z zMObX%qMU;`q!L*ARJrTTl&x6O2b5kTfDd0p!jH;0KRU6qTR6LE#?cW3MZxc+#;K*)$bm{qrF+cejL2&p?p#V7NWd9`)x}DI; zW*yS8`;9A{Om{f%odGZw;j#GRNkYxOUrXDIayliW&9llsdry9UM*v3aPEDdezorwK zB8`ojJ=*_OP&+)WP+v!5D6MqhLhAFh;AMuGqkG`+u4~rcm+%|DZ+750CpxS1xqm{5 zmcN!D*}9Y1m-y|6_%OF%3n^Y|=YzpPQFe9vvbl~eUCb*-9Z{f$51l5rWryc-y7K0* zR@r#$vd#Mq!*kg>S1s&#{VmJjp|v<2Jy-#e!`nV;H;=Nbf$X|Lt~cvD7VlJL zmum-99i_7|!P0jap}$2lwa_6-x(Z`#7T#`<*RV#w^|{y3y!)|ggWut#ImJx*;R;=Z z%8c-}By-z)&n@=j40rM~^_1qzRa23e!_!#hY%GM69fc-wN5QA5;ZG-}{Ncu861v_e zGx|d^Mh55k=p+pTEm|>A|74@-09Fk{t6a5Pt?IY5xK}4(h2@JA0+yRg_s%^C_jIkp%M(z=R{mN+^XKbnVPdZZ5I%fZ8xm zb(ZKc`|?2l9cK)SE4QeKoEgrA9z$MN(^fGjKq{n4*sfP#l}NcGT!Pet|ZGp`uL8O)UmI!cMz>Ap4^Ohez$v8F%! z4O*>>wwYo;)%LzQnwReX1C_6nAGs$)PRZ;6YDQKv&4PodKsax&-`W+B3U%oAaafa_ z6JdE%tdT_rIRE3M!~%!p5e+5&DgDX&wO9t2bj6s<{V;Tj@?5B$HkeNGSN8Ll$+zr-)bc;^+#+TcJZq4;ulI z5>bEWUR*&ocM!lV5EpP1ScQ$@@4IBlYAvIJ83wrRO*?BcB&O z?4GfWV3&Jwx?iJ=JV*vFEPG7d`sWH2J5Iz~@=%Fe0@N~k=j5=H&}|t--AAwczIrl; zR9{HcZJo_AfcML#@0N#dtA`UXxzO{H+3)qwbTxJL;G}^ZQQ9|ir1OQl5l+PinzEvlZs*f zF)U!|bb~8Ha*uL!HppLM@pRp{yo1|x{1O%9Ee11hyuq$q>||@6)h@XtxvlqH{ysZ^ rcaLgzo87y{dhoG;zeP(}7v7VsHE4iIanN!Yk4;rkO93Wt^YQ-yYIGKf diff --git a/pins.txt b/pins.txt deleted file mode 100644 index aa74747..0000000 --- a/pins.txt +++ /dev/null @@ -1,10 +0,0 @@ -LED -Signal (Y) D2 - -MIC -I2S SD (Y) D32 -I2S WS (B) D15 -I2S SCK (G) D14 - -IR -Signal (Y) D4 \ No newline at end of file diff --git a/sos-iir-filter.h b/sos-iir-filter.h index ac832aa..f3ea9ec 100644 --- a/sos-iir-filter.h +++ b/sos-iir-filter.h @@ -2,6 +2,7 @@ * ESP32 Second-Order Sections IIR Filter implementation * * (c)2019 Ivan Kostoski + * (c)2021 Bim Overbohm (split into files, template) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,187 +18,121 @@ * along with this program. If not, see . */ -#ifndef SOS_IIR_FILTER_H -#define SOS_IIR_FILTER_H +#pragma once -#include +#include +#include +#include -struct SOS_Coefficients { - float b1; - float b2; - float a1; - float a2; +struct SOS_Coefficients +{ + float b1; + float b2; + float a1; + float a2; }; -struct SOS_Delay_State { - float w0 = 0; - float w1 = 0; +struct SOS_Delay_State +{ + float w0 = 0.0F; + float w1 = 0.0F; }; -extern "C" { - int sos_filter_f32(float *input, float *output, int len, const SOS_Coefficients &coeffs, SOS_Delay_State &w); -} -__asm__ ( - // - // ESP32 implementation of IIR Second-Order Section filter - // Assumes a0 and b0 coefficients are one (1.0) - // - // float* a2 = input; - // float* a3 = output; - // int a4 = len; - // float* a5 = coeffs; - // float* a6 = w; - // float a7 = gain; - // - ".text \n" - ".align 4 \n" - ".global sos_filter_f32 \n" - ".type sos_filter_f32,@function\n" - "sos_filter_f32: \n" - " entry a1, 16 \n" - " lsi f0, a5, 0 \n" // float f0 = coeffs.b1; - " lsi f1, a5, 4 \n" // float f1 = coeffs.b2; - " lsi f2, a5, 8 \n" // float f2 = coeffs.a1; - " lsi f3, a5, 12 \n" // float f3 = coeffs.a2; - " lsi f4, a6, 0 \n" // float f4 = w[0]; - " lsi f5, a6, 4 \n" // float f5 = w[1]; - " loopnez a4, 1f \n" // for (; len>0; len--) { - " lsip f6, a2, 4 \n" // float f6 = *input++; - " madd.s f6, f2, f4 \n" // f6 += f2 * f4; // coeffs.a1 * w0 - " madd.s f6, f3, f5 \n" // f6 += f3 * f5; // coeffs.a2 * w1 - " mov.s f7, f6 \n" // f7 = f6; // b0 assumed 1.0 - " madd.s f7, f0, f4 \n" // f7 += f0 * f4; // coeffs.b1 * w0 - " madd.s f7, f1, f5 \n" // f7 += f1 * f5; // coeffs.b2 * w1 -> result - " ssip f7, a3, 4 \n" // *output++ = f7; - " mov.s f5, f4 \n" // f5 = f4; // w1 = w0 - " mov.s f4, f6 \n" // f4 = f6; // w0 = f6 - " 1: \n" // } - " ssi f4, a6, 0 \n" // w[0] = f4; - " ssi f5, a6, 4 \n" // w[1] = f5; - " movi.n a2, 0 \n" // return 0; - " retw.n \n" -); - -extern "C" { - float sos_filter_sum_sqr_f32(float *input, float *output, int len, const SOS_Coefficients &coeffs, SOS_Delay_State &w, float gain); +extern "C" +{ + int sos_filter_f32(float *input, float *output, int len, const SOS_Coefficients &coeffs, SOS_Delay_State &w); } -__asm__ ( - // - // ESP32 implementation of IIR Second-Order section filter with applied gain. - // Assumes a0 and b0 coefficients are one (1.0) - // Returns sum of squares of filtered samples - // - // float* a2 = input; - // float* a3 = output; - // int a4 = len; - // float* a5 = coeffs; - // float* a6 = w; - // float a7 = gain; - // - ".text \n" - ".align 4 \n" - ".global sos_filter_sum_sqr_f32 \n" - ".type sos_filter_sum_sqr_f32,@function \n" - "sos_filter_sum_sqr_f32: \n" - " entry a1, 16 \n" - " lsi f0, a5, 0 \n" // float f0 = coeffs.b1; - " lsi f1, a5, 4 \n" // float f1 = coeffs.b2; - " lsi f2, a5, 8 \n" // float f2 = coeffs.a1; - " lsi f3, a5, 12 \n" // float f3 = coeffs.a2; - " lsi f4, a6, 0 \n" // float f4 = w[0]; - " lsi f5, a6, 4 \n" // float f5 = w[1]; - " wfr f6, a7 \n" // float f6 = gain; - " const.s f10, 0 \n" // float sum_sqr = 0; - " loopnez a4, 1f \n" // for (; len>0; len--) { - " lsip f7, a2, 4 \n" // float f7 = *input++; - " madd.s f7, f2, f4 \n" // f7 += f2 * f4; // coeffs.a1 * w0 - " madd.s f7, f3, f5 \n" // f7 += f3 * f5; // coeffs.a2 * w1; - " mov.s f8, f7 \n" // f8 = f7; // b0 assumed 1.0 - " madd.s f8, f0, f4 \n" // f8 += f0 * f4; // coeffs.b1 * w0; - " madd.s f8, f1, f5 \n" // f8 += f1 * f5; // coeffs.b2 * w1; - " mul.s f9, f8, f6 \n" // f9 = f8 * f6; // f8 * gain -> result - " ssip f9, a3, 4 \n" // *output++ = f9; - " mov.s f5, f4 \n" // f5 = f4; // w1 = w0 - " mov.s f4, f7 \n" // f4 = f7; // w0 = f7; - " madd.s f10, f9, f9 \n" // f10 += f9 * f9; // sum_sqr += f9 * f9; - " 1: \n" // } - " ssi f4, a6, 0 \n" // w[0] = f4; - " ssi f5, a6, 4 \n" // w[1] = f5; - " rfr a2, f10 \n" // return sum_sqr; - " retw.n \n" // -); - - -/** - * Envelops above asm functions into C++ class - */ -struct SOS_IIR_Filter { - - const int num_sos; - const float gain; - SOS_Coefficients* sos = NULL; - SOS_Delay_State* w = NULL; - - // Dynamic constructor - SOS_IIR_Filter(size_t num_sos, const float gain, const SOS_Coefficients _sos[] = NULL): num_sos(num_sos), gain(gain) { - if (num_sos > 0) { - sos = new SOS_Coefficients[num_sos]; - if ((sos != NULL) && (_sos != NULL)) memcpy(sos, _sos, num_sos * sizeof(SOS_Coefficients)); - w = new SOS_Delay_State[num_sos](); +__asm__( + // + // ESP32 implementation of IIR Second-Order Section filter + // Assumes a0 and b0 coefficients are one (1.0) + // + // float* a2 = input; + // float* a3 = output; + // int a4 = len; + // float* a5 = coeffs; + // float* a6 = w; + // float a7 = gain; + // + ".text \n" + ".align 4 \n" + ".global sos_filter_f32 \n" + ".type sos_filter_f32,@function\n" + "sos_filter_f32: \n" + " entry a1, 16 \n" + " lsi f0, a5, 0 \n" // float f0 = coeffs.b1; + " lsi f1, a5, 4 \n" // float f1 = coeffs.b2; + " lsi f2, a5, 8 \n" // float f2 = coeffs.a1; + " lsi f3, a5, 12 \n" // float f3 = coeffs.a2; + " lsi f4, a6, 0 \n" // float f4 = w[0]; + " lsi f5, a6, 4 \n" // float f5 = w[1]; + " loopnez a4, 1f \n" // for (; len>0; len--) { + " lsip f6, a2, 4 \n" // float f6 = *input++; + " madd.s f6, f2, f4 \n" // f6 += f2 * f4; // coeffs.a1 * w0 + " madd.s f6, f3, f5 \n" // f6 += f3 * f5; // coeffs.a2 * w1 + " mov.s f7, f6 \n" // f7 = f6; // b0 assumed 1.0 + " madd.s f7, f0, f4 \n" // f7 += f0 * f4; // coeffs.b1 * w0 + " madd.s f7, f1, f5 \n" // f7 += f1 * f5; // coeffs.b2 * w1 -> result + " ssip f7, a3, 4 \n" // *output++ = f7; + " mov.s f5, f4 \n" // f5 = f4; // w1 = w0 + " mov.s f4, f6 \n" // f4 = f6; // w0 = f6 + " 1: \n" // } + " ssi f4, a6, 0 \n" // w[0] = f4; + " ssi f5, a6, 4 \n" // w[1] = f5; + " movi.n a2, 0 \n" // return 0; + " retw.n \n"); + +/// @brief Envelops above asm function into C++ class +class SOS_IIR_Filter +{ +public: + /// @brief Constructor + SOS_IIR_Filter(float gain, const std::vector& sos) + : m_gain(gain) + , m_sos(sos) + , m_w(sos.size()) + { + }; + + /// @brief Apply defined IIR Filter(s) to input array of floats and write values to output + auto applyFilters(float *input, float *output, size_t len) const -> void + { + float *source = input; + for (decltype(m_sos.size()) i = 0; i < m_sos.size(); i++) + { + sos_filter_f32(source, output, len, m_sos[i], m_w[i]); + source = output; + } } - }; - - // Template constructor for const filter declaration - template - SOS_IIR_Filter(const float gain, const SOS_Coefficients (&sos)[Array_Size]): SOS_IIR_Filter(Array_Size, gain, sos) {}; - - /** - * Apply defined IIR Filter to input array of floats, write filtered values to output, - * and return sum of squares of all filtered values - */ - inline float filter(float* input, float* output, size_t len) { - if ((num_sos < 1) || (sos == NULL) || (w == NULL)) return 0; - float* source = input; - // Apply all but last Second-Order-Section - for(int i=0; i<(num_sos-1); i++) { - sos_filter_f32(source, output, len, sos[i], w[i]); - source = output; - } - // Apply last SOS with gain and return the sum of squares of all samples - return sos_filter_sum_sqr_f32(source, output, len, sos[num_sos-1], w[num_sos-1], gain); - } - ~SOS_IIR_Filter() { - if (w != NULL) delete[] w; - if (sos != NULL) delete[] sos; - } - -}; - -// -// For testing only -// -struct No_IIR_Filter { - const int num_sos = 0; - const float gain = 1.0; - - No_IIR_Filter() {}; - - inline float filter(float* input, float* output, size_t len) { - float sum_sqr = 0; - float s; - for(int i=0; i void + { + if (input != nullptr && output != nullptr) + { + for (decltype(len) i = 0; i < len; i++) + { + output[i] = input[i] * m_gain; + } + } } - if (input != output) { - for(int i=0; i float + { + float result = 0.0F; + if (arr1 != nullptr && arr2 != nullptr) + { + for (decltype(len) i = 0; i < len; i++) + { + result += arr1[i] * arr2[i]; + } + } + return result; } - return sum_sqr; - }; - + +private: + float m_gain = 1.0F; + std::vector m_sos{}; + mutable std::vector m_w{}; }; - -No_IIR_Filter None; - -#endif // SOS_IIR_FILTER_H