Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
/* noisemeter-device - Firmware for CivicTechTO's Noisemeter Device
* Copyright (C) 2024 Clyne Sullivan, Nick Barnard
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "board.h"
#include "sos-iir-filter.h"
#include "spl-meter.h"
#include <cmath>
#if defined(USE_MIC_IM64D130A)
/** Sample size time duration to use for Leq calculation (seconds). */
static constexpr auto LEQ_PERIOD = 1.f;
/** Specifies the type of weighting to use for decibel calculation: dBA, dBC, or None/Z. */
static constexpr auto& WEIGHTING = A_weighting;
/** Specifies the microphone's equalization filter. See pre-defined filters or set to 'None'. */
static constexpr auto& MIC_EQUALIZER = None;
/** Valid number of bits in a received I2S data sample. */
static constexpr auto MIC_BITS = 16u;
/** dBFS value expected at MIC_REF_DB (Sensitivity value from datasheet). */
static constexpr auto MIC_SENSITIVITY = -36.f;
/** Value at which point sensitivity is specified in datasheet (dB). */
static constexpr auto MIC_REF_DB = 94.f;
/** Acoustic overload point (dB). */
static constexpr auto MIC_OVERLOAD_DB = 130.f;
/** Noise floor (dB). */
static constexpr auto MIC_NOISE_DB = 30.f;
/** Linear calibration offset to apply to calculated decibel values. */
static constexpr auto MIC_OFFSET_DB = 0.f;
#elif defined(USE_MIC_SPH0645)
static constexpr auto LEQ_PERIOD = 1.f;
static constexpr auto& WEIGHTING = A_weighting;
static constexpr auto& MIC_EQUALIZER = SPH0645LM4H_B_RB;
static constexpr auto MIC_BITS = 18u;
static constexpr auto MIC_SENSITIVITY = -26.f;
static constexpr auto MIC_REF_DB = 94.f;
static constexpr auto MIC_OVERLOAD_DB = 120.f;
static constexpr auto MIC_NOISE_DB = 29.f;
static constexpr auto MIC_OFFSET_DB = 0.f;
#else
#error "Unsupported microphone! Use either SPH0645 or IM64D130A."
#endif
/** Reference amplitude level for the microphone. */
static constexpr auto MIC_REF_AMPL = std::pow(10.f, MIC_SENSITIVITY / 20.f) * ((1 << (MIC_BITS - 1)) - 1);
void SPLMeter::initMicrophone() noexcept
{
i2s_chan_handle_t rx_chan;
/* Setp 1: Determine the I2S channel configuration and allocate RX channel only
* The default configuration can be generated by the helper macro,
* but note that PDM channel can only be registered on I2S_NUM_0 */
i2s_chan_config_t rx_chan_cfg = {
.id = I2S_PORT,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 15,
.dma_frame_num = 400,
.auto_clear_after_cb = false,
.auto_clear_before_cb = false,
.allow_pd = false,
.intr_priority = 0,
};
ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, nullptr, &rx_chan));
#if defined(USE_MIC_IM64D130A)
/* Step 2: Setting the configurations of PDM RX mode and initialize the RX channel
* The slot configuration and clock configuration can be generated by the macros
* These two helper macros is defined in 'i2s_pdm.h' which can only be used in PDM RX mode.
* They can help to specify the slot and clock configurations for initialization or re-configuring */
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
/* The data bit-width of PDM mode is fixed to 16 */
.slot_cfg = I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.clk = I2S_SCK,
.din = I2S_SD,
.invert_flags = { .clk_inv = false, },
},
};
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_chan, &pdm_rx_cfg));
#elif defined(USE_MIC_SPH0645)
i2s_std_config_t std_rx_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_SCK,
.ws = I2S_WS,
.dout = I2S_GPIO_UNUSED,
.din = I2S_SD,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_chan, &std_rx_cfg));
#endif
/* Step 3: Enable the rx channels before reading data */
ESP_ERROR_CHECK(i2s_channel_enable(rx_chan));
i2s_handle = rx_chan;
}
std::optional<float> SPLMeter::readMicrophoneData() noexcept
{
const auto bread = i2sRead();
#if defined(USE_MIC_IM64D130A)
// I2S samples are 16-bit integers stored into samples's memory.
// These need to be expanded into 32-bit floats for processing.
// Iterate in reverse to avoid overwriting data.
const auto sread = bread / sizeof(int16_t);
auto src = reinterpret_cast<const int16_t *>(samples.data()) + sread;
auto dst = samples.data() + sread;
for (int i = 0; i < sread; i++)
*--dst = *--src;
#elif defined(USE_MIC_SPH0645)
// 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
const auto sread = bread / sizeof(int32_t) / 2;
auto src = reinterpret_cast<const int32_t *>(samples.data());
auto dst = samples.data();
for (int i = 0; i < sread; i++) {
const auto conv = src[i * 2] >> ((sizeof(int32_t) * 8u) - MIC_BITS);
dst[i] = conv;
}
#endif
// Apply equalization and calculate Z-weighted sum of squares,
// writes filtered samples back to the same buffer.
const auto fptr = samples.data();
const auto sum_sqr_SPL = MIC_EQUALIZER.filter(fptr, fptr, sread);
// Apply weighting and calucate weigthed sum of squares
const auto sum_sqr_weighted = WEIGHTING.filter(fptr, fptr, sread);
// Calculate dB values relative to MIC_REF_AMPL and adjust for microphone reference
const auto short_RMS = std::sqrt(sum_sqr_SPL / sread);
const auto short_SPL_dB = MIC_OFFSET_DB + MIC_REF_DB + 20 * std::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 = MIC_OVERLOAD_DB;
} else if (std::isnan(short_SPL_dB) || (short_SPL_dB < MIC_NOISE_DB)) {
Leq_sum_sqr = MIC_NOISE_DB;
}
// Accumulate Leq sum
Leq_sum_sqr += sum_sqr_weighted;
Leq_samples += sread;
// When we gather enough samples, calculate new Leq value
if (Leq_samples >= SAMPLE_RATE * LEQ_PERIOD) {
const auto Leq_RMS = std::sqrt(Leq_sum_sqr / Leq_samples);
Leq_sum_sqr = 0;
Leq_samples = 0;
return MIC_OFFSET_DB + MIC_REF_DB + 20 * std::log10(Leq_RMS / MIC_REF_AMPL); // Leq dB
} else {
return {};
}
}
size_t SPLMeter::i2sRead() noexcept
{
// 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
size_t bread;
#if defined(USE_MIC_IM64D130A)
i2s_channel_read(i2s_handle, samples.data(), samples.size() * sizeof(int16_t), &bread, portMAX_DELAY);
#elif defined(USE_MIC_SPH0645)
i2s_channel_read(i2s_handle, samples.data(), samples.size() * sizeof(int32_t), &bread, portMAX_DELAY);
#endif
return bread;
}
b8134799cThis file is pinned to the selected release. Download the original to open it in your design tool.