Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
6132f705eThis 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;
}