├── .gitignore ├── configs ├── wdt_include.h ├── secrets.yaml ├── minimal-example-config.yaml ├── sensor-community-example-config.yaml └── advanced-example-config.yaml ├── .yamllint ├── .pylintrc ├── .github └── workflows │ └── ci.yaml ├── components ├── i2s │ ├── i2s.h │ ├── __init__.py │ └── i2s.cpp └── sound_level_meter │ ├── sound_level_meter.h │ ├── __init__.py │ └── sound_level_meter.cpp ├── .clang-format ├── math └── dsptools.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .esphome 2 | __pycache__ -------------------------------------------------------------------------------- /configs/wdt_include.h: -------------------------------------------------------------------------------- 1 | #include -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | line-length: 4 | max: 100 -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | max-line-length=120 3 | 4 | disable= 5 | missing-docstring -------------------------------------------------------------------------------- /configs/secrets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | wifi_ssid: "..." 3 | wifi_password: "........" 4 | ota_password: "..." 5 | api_password: "..." 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | pull_request: 8 | 9 | jobs: 10 | ci: 11 | name: ${{ matrix.name }} 12 | runs-on: ubuntu-latest 13 | env: 14 | ESPHOME_VERSION: 2023.11.6 15 | PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - id: cache 29 | name: Cache 30 | uses: actions/cache@v3 31 | with: 32 | path: | 33 | ~/.platformio 34 | .venv 35 | key: esphome-${{ env.ESPHOME_VERSION }} 36 | 37 | - name: Set up virtualenv 38 | run: | 39 | python -m venv .venv 40 | source .venv/bin/activate 41 | pip install --upgrade pip 42 | pip install esphome==${{ env.ESPHOME_VERSION }} yamllint pylint 43 | if: steps.cache.outputs.cache-hit != 'true' 44 | 45 | - name: Init virtualenv 46 | run: | 47 | source .venv/bin/activate 48 | echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH 49 | echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV 50 | 51 | - name: Run yamllint 52 | run: | 53 | yamllint $(git ls-files '*.yaml') 54 | 55 | - name: Run pylint 56 | run: | 57 | pylint $(git ls-files '*.py') 58 | 59 | - name: Run clang-format 60 | run: | 61 | clang-format-13 --dry-run --Werror $(git ls-files '*.cpp' '*.h') 62 | 63 | - name: Compile configs 64 | run: | 65 | for f in configs/*-example-config.yaml 66 | do 67 | sed 's!github://stas-sl/esphome-sound-level-meter!../components!' -i $f 68 | esphome compile $f 69 | done 70 | -------------------------------------------------------------------------------- /configs/minimal-example-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | esphome: 3 | name: sound-level-meter 4 | 5 | external_components: 6 | - source: github://stas-sl/esphome-sound-level-meter 7 | 8 | esp32: 9 | board: esp32dev 10 | framework: 11 | type: arduino 12 | 13 | logger: 14 | level: DEBUG 15 | 16 | i2s: 17 | bck_pin: 23 18 | ws_pin: 18 19 | din_pin: 19 20 | sample_rate: 48000 # default: 48000 21 | bits_per_sample: 32 # default: 32 22 | 23 | # right shift samples. 24 | # for example if mic has 24 bit resolution, and i2s configured as 32 bits, 25 | # then audio data will be aligned left (MSB) and LSB will be padded with 26 | # zeros, so you might want to shift them right by 8 bits 27 | bits_shift: 8 # default: 0 28 | 29 | sound_level_meter: 30 | # update_interval specifies over which interval to aggregate audio data 31 | # you can specify default update_interval on top level, but you can also 32 | # override it further by specifying it on sensor level 33 | update_interval: 1s # default: 60s 34 | 35 | # buffer_size is in samples (not bytes), so for float data type 36 | # number of bytes will be buffer_size * 4 37 | buffer_size: 1024 # default: 1024 38 | 39 | # see your mic datasheet to find sensitivity and reference SPL. 40 | # those are used to convert dB FS to db SPL 41 | mic_sensitivity: -26dB # default: empty 42 | mic_sensitivity_ref: 94dB # default: empty 43 | 44 | # for flexibility sensors are organized hierarchically into groups. 45 | # each group can have any number of filters, sensors and nested groups. 46 | # for examples if there is a top level group A with filter A and nested 47 | # group B with filter B, then for sensors inside group B filters A 48 | # and then B will be applied: 49 | # groups: 50 | # # group A 51 | # - filters: 52 | # - filter A 53 | # groups: 54 | # # group B 55 | # - filters: 56 | # - filter B 57 | # sensors: 58 | # - sensor X 59 | groups: 60 | - sensors: 61 | - type: eq 62 | name: Leq_1s 63 | -------------------------------------------------------------------------------- /components/i2s/i2s.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "esphome/core/defines.h" 6 | #include "esphome/core/component.h" 7 | #include "esphome/core/hal.h" 8 | #include "esphome/core/log.h" 9 | 10 | namespace esphome { 11 | namespace i2s { 12 | 13 | class I2SComponent : public Component { 14 | public: 15 | void set_ws_pin(InternalGPIOPin *ws_pin); 16 | void set_bck_pin(InternalGPIOPin *bck_pin); 17 | void set_din_pin(InternalGPIOPin *din_pin); 18 | void set_dout_pin(InternalGPIOPin *dout_pin); 19 | void set_sample_rate(uint32_t sample_rate); 20 | uint32_t get_sample_rate() const; 21 | void set_bits_per_sample(uint8_t bits_per_sample); 22 | uint8_t get_bits_per_sample() const; 23 | void set_dma_buf_count(int dma_buf_count); 24 | int get_dma_buf_count() const; 25 | void set_dma_buf_len(int dma_buf_len); 26 | int get_dma_buf_len() const; 27 | void set_use_apll(bool use_apll); 28 | bool get_use_apll() const; 29 | void set_bits_shift(uint8_t bits_shift); 30 | uint8_t get_bits_shift() const; 31 | bool read(uint8_t *data, size_t len, size_t *bytes_read, TickType_t ticks_to_wait = portMAX_DELAY); 32 | bool read_samples(int32_t *data, size_t num_samples, size_t *samples_read, TickType_t ticks_to_wait = portMAX_DELAY); 33 | bool read_samples(int16_t *data, size_t num_samples, size_t *samples_read, TickType_t ticks_to_wait = portMAX_DELAY); 34 | bool read_samples(float *data, size_t num_samples, size_t *samples_read, TickType_t ticks_to_wait = portMAX_DELAY); 35 | bool read_samples(std::vector &data, TickType_t ticks_to_wait = portMAX_DELAY); 36 | virtual void setup() override; 37 | virtual void dump_config() override; 38 | virtual float get_setup_priority() const override; 39 | 40 | protected: 41 | InternalGPIOPin *ws_pin_{nullptr}; 42 | InternalGPIOPin *bck_pin_{nullptr}; 43 | InternalGPIOPin *din_pin_{nullptr}; 44 | InternalGPIOPin *dout_pin_{nullptr}; 45 | 46 | uint32_t sample_rate_{48000}; 47 | uint8_t bits_per_sample_{32}; 48 | uint8_t port_num_{0}; 49 | int dma_buf_count_{8}; 50 | int dma_buf_len_{256}; 51 | bool use_apll_{false}; 52 | uint8_t bits_shift_{0}; 53 | }; 54 | } // namespace i2s 55 | } // namespace esphome 56 | -------------------------------------------------------------------------------- /components/i2s/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome import pins 4 | from esphome.const import CONF_ID 5 | from esphome.core import coroutine_with_priority 6 | 7 | DEPENDENCIES = ["esp32"] 8 | CODEOWNERS = ["@stas-sl"] 9 | 10 | i2s_ns = cg.esphome_ns.namespace("i2s") 11 | I2SComponent = i2s_ns.class_("I2SComponent", cg.Component) 12 | 13 | MULTI_CONF = True 14 | 15 | CONF_WS_PIN = "ws_pin" 16 | CONF_BCK_PIN = "bck_pin" 17 | CONF_DIN_PIN = "din_pin" 18 | CONF_DOUT_PIN = "dout_pin" 19 | CONF_SAMPLE_RATE = "sample_rate" 20 | CONF_BITS_PER_SAMPLE = "bits_per_sample" 21 | CONF_DMA_BUF_COUNT = "dma_buf_count" 22 | CONF_DMA_BUF_LEN = "dma_buf_len" 23 | CONF_USE_APLL = "use_apll" 24 | CONF_BITS_SHIFT = "bits_shift" 25 | 26 | CONFIG_SCHEMA = cv.All( 27 | cv.Schema( 28 | { 29 | cv.GenerateID(): cv.declare_id(I2SComponent), 30 | cv.Required(CONF_WS_PIN): pins.internal_gpio_output_pin_schema, 31 | cv.Required(CONF_BCK_PIN): pins.internal_gpio_output_pin_schema, 32 | cv.Optional(CONF_DIN_PIN): pins.internal_gpio_input_pin_schema, 33 | cv.Optional(CONF_DOUT_PIN): pins.internal_gpio_output_pin_schema, 34 | cv.Optional(CONF_SAMPLE_RATE, 48000): cv.positive_not_null_int, 35 | cv.Optional(CONF_BITS_PER_SAMPLE, 32): cv.one_of(8, 16, 24, 32, int=True), 36 | cv.Optional(CONF_DMA_BUF_COUNT, 8): cv.positive_not_null_int, 37 | cv.Optional(CONF_DMA_BUF_LEN, 256): cv.positive_not_null_int, 38 | cv.Optional(CONF_USE_APLL, False): cv.boolean, 39 | cv.Optional(CONF_BITS_SHIFT, 0): cv.int_range(0, 32), 40 | } 41 | ).extend(cv.COMPONENT_SCHEMA), 42 | cv.has_at_least_one_key(CONF_DIN_PIN, CONF_DOUT_PIN), 43 | ) 44 | 45 | 46 | @coroutine_with_priority(1.0) 47 | async def to_code(config): 48 | cg.add_global(i2s_ns.using) 49 | var = cg.new_Pvariable(config[CONF_ID]) 50 | await cg.register_component(var, config) 51 | ws_pin = await cg.gpio_pin_expression(config[CONF_WS_PIN]) 52 | cg.add(var.set_ws_pin(ws_pin)) 53 | bck_pin = await cg.gpio_pin_expression(config[CONF_BCK_PIN]) 54 | cg.add(var.set_bck_pin(bck_pin)) 55 | if CONF_DIN_PIN in config: 56 | din_pin = await cg.gpio_pin_expression(config[CONF_DIN_PIN]) 57 | cg.add(var.set_din_pin(din_pin)) 58 | if CONF_DOUT_PIN in config: 59 | dout_pin = await cg.gpio_pin_expression(config[CONF_DOUT_PIN]) 60 | cg.add(var.set_dout_pin(dout_pin)) 61 | cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) 62 | cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) 63 | cg.add(var.set_dma_buf_count(config[CONF_DMA_BUF_COUNT])) 64 | cg.add(var.set_dma_buf_len(config[CONF_DMA_BUF_LEN])) 65 | cg.add(var.set_use_apll(config[CONF_USE_APLL])) 66 | cg.add(var.set_bits_shift(config[CONF_BITS_SHIFT])) 67 | -------------------------------------------------------------------------------- /configs/sensor-community-example-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | esphome: 3 | name: sound-level-meter 4 | includes: 5 | # should contain single line: #include 6 | - wdt_include.h 7 | 8 | on_boot: 9 | then: 10 | - lambda: !lambda |- 11 | // increase watchdog timeout to 30 seconds 12 | // so that ESP32 don't crash on long http requests 13 | // which might happen with sensor.community quite often 14 | esp_task_wdt_init(30, false); 15 | 16 | external_components: 17 | - source: github://stas-sl/esphome-sound-level-meter 18 | 19 | esp32: 20 | board: esp32dev 21 | framework: 22 | type: arduino 23 | 24 | logger: 25 | level: DEBUG 26 | 27 | api: 28 | password: !secret api_password 29 | reboot_timeout: 0s 30 | 31 | ota: 32 | password: !secret ota_password 33 | 34 | http_request: 35 | # default is 5 seconds, but you might consider increasing it, 36 | # if data will be missing in sensor.community 37 | timeout: 5s 38 | 39 | wifi: 40 | ssid: !secret wifi_ssid 41 | password: !secret wifi_password 42 | reboot_timeout: 0s 43 | fast_connect: true 44 | 45 | web_server: 46 | port: 80 47 | 48 | i2s: 49 | ws_pin: 18 50 | bck_pin: 23 51 | din_pin: 19 52 | sample_rate: 48000 53 | bits_per_sample: 32 54 | dma_buf_count: 8 55 | dma_buf_len: 256 56 | use_apll: true 57 | bits_shift: 8 58 | 59 | sound_level_meter: 60 | update_interval: 150s # to match original sensor.community firmware settings 61 | warmup_interval: 500ms 62 | mic_sensitivity: -26dB 63 | mic_sensitivity_ref: 94dB 64 | groups: 65 | - filters: 66 | - type: sos 67 | coeffs: 68 | # A-weighting: 69 | # b0 b1 b2 a1 a2 70 | - [0.16999495, 0.741029, 0.52548885, -0.11321865, -0.056549273] 71 | - [1., -2.00027, 1.0002706, -0.03433284, -0.79215795] 72 | - [1., -0.709303, -0.29071867, -1.9822421, 0.9822986] 73 | sensors: 74 | - type: eq 75 | name: LAeq_1min 76 | id: LAeq_1min 77 | unit_of_measurement: dBA 78 | - type: max 79 | name: LAmax_125ms_1min 80 | id: LAmax_125ms_1min 81 | # I believe, previously 35ms was used in DNMS FW, 82 | # but later it was changed to 125ms 83 | window_size: 125ms 84 | unit_of_measurement: dBA 85 | - type: min 86 | name: LAmin_125ms_1min 87 | id: LAmin_125ms_1min 88 | window_size: 125ms 89 | unit_of_measurement: dBA 90 | 91 | interval: 92 | - interval: 150s 93 | then: 94 | - if: 95 | condition: 96 | lambda: 'return id(LAeq_1min).has_state();' 97 | then: 98 | - http_request.post: 99 | verify_ssl: false 100 | url: http://api.sensor.community/v1/push-sensor-data/ 101 | headers: 102 | X-Pin: 15 103 | X-Sensor: esp32-... # replace with your sensor ID 104 | Content-Type: application/json 105 | json: |- 106 | root["software_version"] = "ESPHome " ESPHOME_VERSION; 107 | auto values = root.createNestedArray("sensordatavalues"); 108 | 109 | auto LA_eq = values.createNestedObject(); 110 | LA_eq["value_type"] = "noise_LAeq"; 111 | LA_eq["value"] = id(LAeq_1min).state; 112 | 113 | auto LA_min = values.createNestedObject(); 114 | LA_min["value_type"] = "noise_LA_min"; 115 | LA_min["value"] = id(LAmin_125ms_1min).state; 116 | 117 | auto LA_max = values.createNestedObject(); 118 | LA_max["value_type"] = "noise_LA_max"; 119 | LA_max["value"] = id(LAmax_125ms_1min).state; 120 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | AccessModifierOffset: -1 3 | AlignAfterOpenBracket: Align 4 | AlignConsecutiveAssignments: false 5 | AlignConsecutiveDeclarations: false 6 | AlignEscapedNewlines: DontAlign 7 | AlignOperands: true 8 | AlignTrailingComments: true 9 | AllowAllParametersOfDeclarationOnNextLine: true 10 | AllowShortBlocksOnASingleLine: false 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: All 13 | AllowShortIfStatementsOnASingleLine: false 14 | AllowShortLoopsOnASingleLine: false 15 | AlwaysBreakAfterReturnType: None 16 | AlwaysBreakBeforeMultilineStrings: false 17 | AlwaysBreakTemplateDeclarations: MultiLine 18 | BinPackArguments: true 19 | BinPackParameters: true 20 | BraceWrapping: 21 | AfterClass: false 22 | AfterControlStatement: false 23 | AfterEnum: false 24 | AfterFunction: false 25 | AfterNamespace: false 26 | AfterObjCDeclaration: false 27 | AfterStruct: false 28 | AfterUnion: false 29 | AfterExternBlock: false 30 | BeforeCatch: false 31 | BeforeElse: false 32 | IndentBraces: false 33 | SplitEmptyFunction: true 34 | SplitEmptyRecord: true 35 | SplitEmptyNamespace: true 36 | BreakBeforeBinaryOperators: None 37 | BreakBeforeBraces: Attach 38 | BreakBeforeInheritanceComma: false 39 | BreakInheritanceList: BeforeColon 40 | BreakBeforeTernaryOperators: true 41 | BreakConstructorInitializersBeforeComma: false 42 | BreakConstructorInitializers: BeforeColon 43 | BreakAfterJavaFieldAnnotations: false 44 | BreakStringLiterals: true 45 | ColumnLimit: 120 46 | CommentPragmas: '^ IWYU pragma:' 47 | CompactNamespaces: false 48 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 49 | ConstructorInitializerIndentWidth: 4 50 | ContinuationIndentWidth: 4 51 | Cpp11BracedListStyle: true 52 | DerivePointerAlignment: false 53 | DisableFormat: false 54 | ExperimentalAutoDetectBinPacking: false 55 | FixNamespaceComments: true 56 | ForEachMacros: 57 | - foreach 58 | - Q_FOREACH 59 | - BOOST_FOREACH 60 | IncludeBlocks: Preserve 61 | IncludeCategories: 62 | - Regex: '^' 63 | Priority: 2 64 | - Regex: '^<.*\.h>' 65 | Priority: 1 66 | - Regex: '^<.*' 67 | Priority: 2 68 | - Regex: '.*' 69 | Priority: 3 70 | IncludeIsMainRegex: '([-_](test|unittest))?$' 71 | IndentCaseLabels: true 72 | IndentPPDirectives: None 73 | IndentWidth: 2 74 | IndentWrappedFunctionNames: false 75 | KeepEmptyLinesAtTheStartOfBlocks: false 76 | MacroBlockBegin: '' 77 | MacroBlockEnd: '' 78 | MaxEmptyLinesToKeep: 1 79 | NamespaceIndentation: None 80 | PenaltyBreakAssignment: 2 81 | PenaltyBreakBeforeFirstCallParameter: 1 82 | PenaltyBreakComment: 300 83 | PenaltyBreakFirstLessLess: 120 84 | PenaltyBreakString: 1000 85 | PenaltyBreakTemplateDeclaration: 10 86 | PenaltyExcessCharacter: 1000000 87 | PenaltyReturnTypeOnItsOwnLine: 2000 88 | PointerAlignment: Right 89 | RawStringFormats: 90 | - Language: Cpp 91 | Delimiters: 92 | - cc 93 | - CC 94 | - cpp 95 | - Cpp 96 | - CPP 97 | - 'c++' 98 | - 'C++' 99 | CanonicalDelimiter: '' 100 | BasedOnStyle: google 101 | - Language: TextProto 102 | Delimiters: 103 | - pb 104 | - PB 105 | - proto 106 | - PROTO 107 | EnclosingFunctions: 108 | - EqualsProto 109 | - EquivToProto 110 | - PARSE_PARTIAL_TEXT_PROTO 111 | - PARSE_TEST_PROTO 112 | - PARSE_TEXT_PROTO 113 | - ParseTextOrDie 114 | - ParseTextProtoOrDie 115 | CanonicalDelimiter: '' 116 | BasedOnStyle: google 117 | ReflowComments: true 118 | SortIncludes: false 119 | SortUsingDeclarations: false 120 | SpaceAfterCStyleCast: true 121 | SpaceAfterTemplateKeyword: false 122 | SpaceBeforeAssignmentOperators: true 123 | SpaceBeforeCpp11BracedList: false 124 | SpaceBeforeCtorInitializerColon: true 125 | SpaceBeforeInheritanceColon: true 126 | SpaceBeforeParens: ControlStatements 127 | SpaceBeforeRangeBasedForLoopColon: true 128 | SpaceInEmptyParentheses: false 129 | SpacesBeforeTrailingComments: 2 130 | SpacesInAngles: false 131 | SpacesInContainerLiterals: false 132 | SpacesInCStyleCastParentheses: false 133 | SpacesInParentheses: false 134 | SpacesInSquareBrackets: false 135 | Standard: Auto 136 | TabWidth: 2 137 | UseTab: Never 138 | -------------------------------------------------------------------------------- /math/dsptools.py: -------------------------------------------------------------------------------- 1 | # source: https://github.com/yurochka/dsptools 2 | 3 | # pylint: skip-file 4 | 5 | import numpy as np 6 | 7 | 8 | def invfreqz(h, w, nb, na, wt=None, gauss=False, real=True, maxiter=30, tol=0.01): 9 | """ 10 | Parameters: 11 | h: Frequency response array 12 | w: Normalized frequency array (from zero to pi) 13 | nb: numerator order 14 | na: denominator order 15 | wt: weight array, same length with h 16 | gauss: whether to use Gauss-Newton method, default False 17 | real: whether real or complex filter, default True 18 | maxiter: maximum number of iteration when using Gauss-Newton method, default 30 19 | tol: tolerance when using Gauss-Newton method, default 0.01 20 | """ 21 | if len(h) != len(w): 22 | raise ValueError('H and W should be of equal length.') 23 | nb = nb + 1 24 | nm = max(nb - 1, na) 25 | OM_a = np.mat(np.arange(0, nm + 1)) 26 | OM_m = OM_a.T * np.mat(w) 27 | OM = np.exp(-1j * OM_m) 28 | Dva_a = np.transpose(OM[1:(na + 1)]) 29 | h_t = np.transpose(np.mat(h)) 30 | Dva_b = h_t * np.mat(np.ones(na)) 31 | Dva = np.multiply(Dva_a, Dva_b) 32 | Dvb = -np.transpose(OM[0:nb]) 33 | D_a = np.hstack((Dva, Dvb)) 34 | if wt is None: 35 | wf = np.transpose(np.mat(np.ones_like(h))) 36 | else: 37 | wf = np.sqrt(np.transpose(np.mat(wt))) 38 | D_b = wf * np.mat(np.ones((1, na + nb))) 39 | D = np.multiply(D_a, D_b) 40 | if real: 41 | R = np.real(D.H * D) 42 | Vd = np.real(D.H * np.multiply(-h_t, wf)) 43 | else: 44 | R = D.H * D 45 | Vd = D.H * np.multiply(-h_t, wf) 46 | th = R.I * Vd 47 | tht = th.T.getA() 48 | a = np.append([1], tht[0][0:na]) 49 | b = tht[0][na:(na + nb)] 50 | if not gauss: 51 | return b, a 52 | else: 53 | indb = np.arange(len(b)) 54 | indg = np.arange(len(a)) 55 | a = polystab(a) 56 | GC_b = np.mat(b) * OM[indb, :] 57 | GC_a = np.mat(a) * OM[indg, :] 58 | GC = np.transpose(GC_b / GC_a) 59 | e = np.multiply(GC - h_t, wf) 60 | Vcap = e.H * e 61 | t = np.mat(np.append(a[1:(na + 1)], b[0:nb])).T 62 | gndir = 2 * tol + 1 63 | l = 0 64 | st = 0 65 | while (np.linalg.norm(gndir) > tol) and (l < maxiter) and (st != 1): 66 | l = l + 1 67 | D31_a = np.transpose(OM[1:(na + 1), :]) 68 | D31_b = - GC / np.transpose(a * OM[0:(na + 1), :]) 69 | D31_c = np.mat(np.ones((1, na))) 70 | D31 = np.multiply(D31_a, D31_b * D31_c) 71 | D32_a = np.transpose(OM[0:nb, :]) 72 | D32_b = np.transpose(a * OM[0:(na + 1), :]) 73 | D32_c = np.mat(np.ones((1, nb))) 74 | D32 = D32_a / (D32_b * D32_c) 75 | D3_a = np.hstack((D31, D32)) 76 | D3_b = wf * np.mat(np.ones((1, na + nb))) 77 | D3 = np.multiply(D3_a, D3_b) 78 | e = np.multiply(GC - h_t, wf) 79 | if real: 80 | R = np.real(D3.H * D3) 81 | Vd = np.real(D3.H * e) 82 | else: 83 | R = D3.H * D3 84 | Vd = D3.H * e 85 | gndir = R.I * Vd 86 | ll = 0 87 | k = 1 88 | V1 = np.mat(Vcap + 1) 89 | while (V1[0][0] > Vcap) and (ll < 20): 90 | t1 = t - k * gndir 91 | if ll == 19: 92 | t1 = t 93 | t1_v = np.transpose(t1).getA()[0] 94 | a = polystab(np.append([1], t1_v[0:na])) 95 | t1_v[0:na] = a[1:(na + 1)] 96 | b = t1_v[na:(na + nb)] 97 | GC_b = np.mat(b) * OM[indb, :] 98 | GC_a = np.mat(a) * OM[indg, :] 99 | GC = np.transpose(GC_b / GC_a) 100 | V1_a = np.multiply(GC - h_t, wf) 101 | V1 = V1_a.H * V1_a 102 | t1 = np.mat(np.append(a[1:(na + 1)], b[0:nb])).T 103 | k = k / 2 104 | ll = ll + 1 105 | if ll == 20: 106 | st = 1 107 | if ll == 10: 108 | gndir = Vd / np.linalg.norm(R) * R.shape[0] 109 | k = 1 110 | t = t1 111 | Vcap = V1[0][0] 112 | return b, a 113 | 114 | 115 | def polystab(a): 116 | if len(a) <= 1: 117 | return a 118 | else: 119 | v = np.roots(a) 120 | for i in range(len(v)): 121 | if v[i] != 0: 122 | vs = 0.5 * (np.sign(abs(v[i]) - 1) + 1) 123 | v[i] = (1 - vs) * v[i] + vs / v[i].conj() 124 | ind = np.nonzero(v) 125 | b = a[ind[0][0]] * np.poly(v) 126 | if np.iscomplex(b).any(): 127 | b = np.real(b) 128 | return b 129 | -------------------------------------------------------------------------------- /components/sound_level_meter/sound_level_meter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "esphome/core/component.h" 7 | #include "esphome/core/automation.h" 8 | #include "esphome/components/sensor/sensor.h" 9 | #include "esphome/components/i2s/i2s.h" 10 | 11 | namespace esphome { 12 | namespace sound_level_meter { 13 | class SensorGroup; 14 | class SoundLevelMeterSensor; 15 | class Filter; 16 | 17 | class SoundLevelMeter : public Component { 18 | friend class SoundLevelMeterSensor; 19 | 20 | public: 21 | void set_update_interval(uint32_t update_interval); 22 | uint32_t get_update_interval(); 23 | void set_buffer_size(uint32_t buffer_size); 24 | uint32_t get_buffer_size(); 25 | uint32_t get_sample_rate(); 26 | void set_i2s(i2s::I2SComponent *i2s); 27 | void add_group(SensorGroup *group); 28 | void set_warmup_interval(uint32_t warmup_interval); 29 | void set_task_stack_size(uint32_t task_stack_size); 30 | void set_task_priority(uint8_t task_priority); 31 | void set_task_core(uint8_t task_core); 32 | void set_mic_sensitivity(optional mic_sensitivity); 33 | optional get_mic_sensitivity(); 34 | void set_mic_sensitivity_ref(optional mic_sensitivity_ref); 35 | optional get_mic_sensitivity_ref(); 36 | void set_offset(optional offset); 37 | optional get_offset(); 38 | virtual void setup() override; 39 | virtual void loop() override; 40 | virtual void dump_config() override; 41 | void turn_on(); 42 | void turn_off(); 43 | void toggle(); 44 | bool is_on(); 45 | 46 | protected: 47 | i2s::I2SComponent *i2s_{nullptr}; 48 | std::vector groups_; 49 | size_t buffer_size_{256}; 50 | uint32_t warmup_interval_{500}; 51 | uint32_t task_stack_size_{1024}; 52 | uint8_t task_priority_{1}; 53 | uint8_t task_core_{1}; 54 | optional mic_sensitivity_{}; 55 | optional mic_sensitivity_ref_{}; 56 | optional offset_{}; 57 | std::queue> defer_queue_; 58 | std::mutex defer_mutex_; 59 | uint32_t update_interval_{60000}; 60 | bool is_on_{true}; 61 | std::mutex on_mutex_; 62 | std::condition_variable on_cv_; 63 | 64 | static void task(void *param); 65 | // epshome's scheduler is not thred safe, so we have to use custom thread safe implementation 66 | // to execute sensor updates in main loop 67 | void defer(std::function &&f); 68 | void reset(); 69 | }; 70 | 71 | class SensorGroup { 72 | public: 73 | void set_parent(SoundLevelMeter *parent); 74 | void add_sensor(SoundLevelMeterSensor *sensor); 75 | void add_group(SensorGroup *group); 76 | void add_filter(Filter *filter); 77 | void process(std::vector &buffer); 78 | void dump_config(const char *prefix); 79 | void reset(); 80 | 81 | protected: 82 | SoundLevelMeter *parent_{nullptr}; 83 | std::vector groups_; 84 | std::vector sensors_; 85 | std::vector filters_; 86 | }; 87 | 88 | class SoundLevelMeterSensor : public sensor::Sensor { 89 | friend class SensorGroup; 90 | 91 | public: 92 | void set_parent(SoundLevelMeter *parent); 93 | void set_update_interval(uint32_t update_interval); 94 | virtual void process(std::vector &buffer) = 0; 95 | void defer_publish_state(float state); 96 | 97 | protected: 98 | SoundLevelMeter *parent_{nullptr}; 99 | uint32_t update_samples_{0}; 100 | float adjust_dB(float dB, bool is_rms = true); 101 | 102 | virtual void reset() = 0; 103 | }; 104 | 105 | class SoundLevelMeterSensorEq : public SoundLevelMeterSensor { 106 | public: 107 | virtual void process(std::vector &buffer) override; 108 | 109 | protected: 110 | double sum_{0.}; 111 | uint32_t count_{0}; 112 | 113 | virtual void reset() override; 114 | }; 115 | 116 | class SoundLevelMeterSensorMax : public SoundLevelMeterSensor { 117 | public: 118 | void set_window_size(uint32_t window_size); 119 | virtual void process(std::vector &buffer) override; 120 | 121 | protected: 122 | uint32_t window_samples_{0}; 123 | float sum_{0.f}; 124 | float max_{std::numeric_limits::min()}; 125 | uint32_t count_sum_{0}, count_max_{0}; 126 | 127 | virtual void reset() override; 128 | }; 129 | 130 | class SoundLevelMeterSensorMin : public SoundLevelMeterSensor { 131 | public: 132 | void set_window_size(uint32_t window_size); 133 | virtual void process(std::vector &buffer) override; 134 | 135 | protected: 136 | uint32_t window_samples_{0}; 137 | float sum_{0.f}; 138 | float min_{std::numeric_limits::max()}; 139 | uint32_t count_sum_{0}, count_min_{0}; 140 | 141 | virtual void reset() override; 142 | }; 143 | 144 | class SoundLevelMeterSensorPeak : public SoundLevelMeterSensor { 145 | public: 146 | virtual void process(std::vector &buffer) override; 147 | 148 | protected: 149 | float peak_{0.f}; 150 | uint32_t count_{0}; 151 | 152 | virtual void reset() override; 153 | }; 154 | 155 | class Filter { 156 | friend class SensorGroup; 157 | 158 | public: 159 | virtual void process(std::vector &data) = 0; 160 | 161 | protected: 162 | virtual void reset() = 0; 163 | }; 164 | 165 | class SOS_Filter : public Filter { 166 | public: 167 | SOS_Filter(std::initializer_list> &&coeffs); 168 | virtual void process(std::vector &data) override; 169 | 170 | protected: 171 | std::vector> coeffs_; // {b0, b1, b2, a1, a2} 172 | std::vector> state_; 173 | 174 | virtual void reset() override; 175 | }; 176 | 177 | template class TurnOnAction : public Action { 178 | public: 179 | explicit TurnOnAction(SoundLevelMeter *sound_level_meter) : sound_level_meter_(sound_level_meter) {} 180 | 181 | void play(Ts... x) override { this->sound_level_meter_->turn_on(); } 182 | 183 | protected: 184 | SoundLevelMeter *sound_level_meter_; 185 | }; 186 | 187 | template class TurnOffAction : public Action { 188 | public: 189 | explicit TurnOffAction(SoundLevelMeter *sound_level_meter) : sound_level_meter_(sound_level_meter) {} 190 | 191 | void play(Ts... x) override { this->sound_level_meter_->turn_off(); } 192 | 193 | protected: 194 | SoundLevelMeter *sound_level_meter_; 195 | }; 196 | 197 | template class ToggleAction : public Action { 198 | public: 199 | explicit ToggleAction(SoundLevelMeter *sound_level_meter) : sound_level_meter_(sound_level_meter) {} 200 | 201 | void play(Ts... x) override { this->sound_level_meter_->toggle(); } 202 | 203 | protected: 204 | SoundLevelMeter *sound_level_meter_; 205 | }; 206 | 207 | } // namespace sound_level_meter 208 | } // namespace esphome -------------------------------------------------------------------------------- /components/i2s/i2s.cpp: -------------------------------------------------------------------------------- 1 | #include "i2s.h" 2 | 3 | namespace esphome { 4 | namespace i2s { 5 | 6 | static const char *const TAG = "i2s"; 7 | 8 | void I2SComponent::set_ws_pin(InternalGPIOPin *ws_pin) { this->ws_pin_ = ws_pin; } 9 | void I2SComponent::set_bck_pin(InternalGPIOPin *bck_pin) { this->bck_pin_ = bck_pin; } 10 | void I2SComponent::set_din_pin(InternalGPIOPin *din_pin) { this->din_pin_ = din_pin; } 11 | void I2SComponent::set_dout_pin(InternalGPIOPin *dout_pin) { this->dout_pin_ = dout_pin; } 12 | void I2SComponent::set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } 13 | uint32_t I2SComponent::get_sample_rate() const { return this->sample_rate_; } 14 | void I2SComponent::set_bits_per_sample(uint8_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } 15 | uint8_t I2SComponent::get_bits_per_sample() const { return this->bits_per_sample_; } 16 | void I2SComponent::set_dma_buf_count(int dma_buf_count) { this->dma_buf_count_ = dma_buf_count; } 17 | int I2SComponent::get_dma_buf_count() const { return this->dma_buf_count_; } 18 | void I2SComponent::set_dma_buf_len(int dma_buf_len) { this->dma_buf_len_ = dma_buf_len; } 19 | int I2SComponent::get_dma_buf_len() const { return this->dma_buf_len_; } 20 | void I2SComponent::set_use_apll(bool use_apll) { this->use_apll_ = use_apll; } 21 | bool I2SComponent::get_use_apll() const { return this->use_apll_; } 22 | void I2SComponent::set_bits_shift(uint8_t bits_shift) { this->bits_shift_ = bits_shift; } 23 | uint8_t I2SComponent::get_bits_shift() const { return this->bits_shift_; } 24 | float I2SComponent::get_setup_priority() const { return setup_priority::BUS; } 25 | 26 | void I2SComponent::dump_config() { 27 | ESP_LOGCONFIG(TAG, "I2S %d:", this->port_num_); 28 | LOG_PIN(" WS Pin: ", this->ws_pin_); 29 | LOG_PIN(" BCK Pin: ", this->bck_pin_); 30 | LOG_PIN(" DIN Pin: ", this->din_pin_); 31 | LOG_PIN(" DOUT Pin: ", this->dout_pin_); 32 | ESP_LOGCONFIG(TAG, " Sample Rate: %u", this->sample_rate_); 33 | ESP_LOGCONFIG(TAG, " Bits Per Sample: %u", this->bits_per_sample_); 34 | ESP_LOGCONFIG(TAG, " DMA Buf Count: %u", this->dma_buf_count_); 35 | ESP_LOGCONFIG(TAG, " DMA Buf Len: %u", this->dma_buf_len_); 36 | ESP_LOGCONFIG(TAG, " Use APLL: %s", YESNO(this->use_apll_)); 37 | ESP_LOGCONFIG(TAG, " Bits Shift: %u", this->bits_shift_); 38 | } 39 | 40 | bool I2SComponent::read(uint8_t *data, size_t len, size_t *bytes_read, TickType_t ticks_to_wait) { 41 | esp_err_t err = i2s_read(i2s_port_t(this->port_num_), data, len, bytes_read, ticks_to_wait); 42 | 43 | if (err != ESP_OK) { 44 | ESP_LOGW(TAG, "i2s_read failed: %s", esp_err_to_name(err)); 45 | return false; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | bool I2SComponent::read_samples(int32_t *data, size_t num_samples, size_t *samples_read, TickType_t ticks_to_wait) { 52 | if (this->bits_per_sample_ <= 16) { 53 | ESP_LOGE(TAG, 54 | "read_samples: int32 output data pointer should be used with 24 or 32 bit sampling, but current " 55 | "bits_per_samples is %u", 56 | this->bits_per_sample_); 57 | return false; 58 | } 59 | size_t bytes_read; 60 | bool ok = this->read(reinterpret_cast(data), num_samples * sizeof(int32_t), &bytes_read, ticks_to_wait); 61 | if (ok) { 62 | *samples_read = bytes_read / sizeof(int32_t); 63 | if (this->bits_shift_ > 0) 64 | for (int i = 0; i < *samples_read; i++) 65 | data[i] >>= this->bits_shift_; 66 | return ok; 67 | } else { 68 | return false; 69 | } 70 | } 71 | 72 | bool I2SComponent::read_samples(int16_t *data, size_t num_samples, size_t *samples_read, TickType_t ticks_to_wait) { 73 | if (this->bits_per_sample_ > 16) { 74 | ESP_LOGE(TAG, 75 | "read_samples: int16 output data pointer should be used with 8 or 16 bit sampling, but current " 76 | "bits_per_samples is %u", 77 | this->bits_per_sample_); 78 | return false; 79 | } 80 | size_t bytes_read; 81 | bool ok = this->read(reinterpret_cast(data), num_samples * sizeof(int16_t), &bytes_read, ticks_to_wait); 82 | if (ok) { 83 | *samples_read = bytes_read / sizeof(int16_t); 84 | if (this->bits_shift_ > 0) 85 | for (int i = 0; i < *samples_read; i++) 86 | data[i] >>= this->bits_shift_; 87 | return ok; 88 | } else { 89 | return false; 90 | } 91 | } 92 | 93 | bool I2SComponent::read_samples(float *data, size_t num_samples, size_t *samples_read, TickType_t ticks_to_wait) { 94 | uint8_t bytes_per_sample = this->bits_per_sample_ <= 16 ? 2 : 4; 95 | const float max_value = (1UL << (bytes_per_sample * 8 - this->bits_shift_ - 1)) - 1; 96 | bool ok; 97 | if (this->bits_per_sample_ <= 16) 98 | ok = this->read_samples(reinterpret_cast(data), num_samples, samples_read, ticks_to_wait); 99 | else 100 | ok = this->read_samples(reinterpret_cast(data), num_samples, samples_read, ticks_to_wait); 101 | if (ok) { 102 | if (this->bits_per_sample_ <= 16) { 103 | int16_t *data_i16 = reinterpret_cast(data); 104 | for (int i = *samples_read - 1; i >= 0; i--) 105 | data[i] = data_i16[i] / max_value; 106 | } else { 107 | int32_t *data_i32 = reinterpret_cast(data); 108 | for (int i = *samples_read - 1; i >= 0; i--) 109 | data[i] = data_i32[i] / max_value; 110 | } 111 | return ok; 112 | } else { 113 | return false; 114 | } 115 | } 116 | 117 | bool I2SComponent::read_samples(std::vector &data, TickType_t ticks_to_wait) { 118 | size_t samples_read; 119 | bool result = this->read_samples(data.data(), data.capacity(), &samples_read, ticks_to_wait); 120 | data.resize(samples_read); 121 | return result; 122 | } 123 | 124 | void I2SComponent::setup() { 125 | static uint8_t next_port_num = 0; 126 | this->port_num_ = next_port_num++; 127 | 128 | ESP_LOGCONFIG(TAG, "Setting up I2S %u ...", this->port_num_); 129 | 130 | i2s_config_t i2s_config = {.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), // TODO: make it configurable 131 | .sample_rate = this->sample_rate_, 132 | .bits_per_sample = i2s_bits_per_sample_t(this->bits_per_sample_), 133 | .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, // TODO: make it configurable 134 | .communication_format = I2S_COMM_FORMAT_STAND_I2S, 135 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 136 | .dma_buf_count = this->dma_buf_count_, 137 | .dma_buf_len = this->dma_buf_len_, 138 | .use_apll = this->use_apll_, 139 | .tx_desc_auto_clear = false, 140 | .fixed_mclk = 0, 141 | .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, 142 | .bits_per_chan = i2s_bits_per_chan_t(0)}; 143 | 144 | i2s_pin_config_t i2s_pin_config = { 145 | .mck_io_num = I2S_PIN_NO_CHANGE, 146 | .bck_io_num = this->bck_pin_->get_pin(), 147 | .ws_io_num = this->ws_pin_->get_pin(), 148 | .data_out_num = this->dout_pin_ != nullptr ? this->dout_pin_->get_pin() : I2S_PIN_NO_CHANGE, 149 | .data_in_num = this->din_pin_ != nullptr ? this->din_pin_->get_pin() : I2S_PIN_NO_CHANGE}; 150 | 151 | esp_err_t err = i2s_driver_install(i2s_port_t(this->port_num_), &i2s_config, 0, NULL); 152 | if (err != ESP_OK) { 153 | ESP_LOGW(TAG, "i2s_driver_install failed: %s", esp_err_to_name(err)); 154 | this->mark_failed(); 155 | return; 156 | } 157 | 158 | err = i2s_set_pin(i2s_port_t(this->port_num_), &i2s_pin_config); 159 | if (err != ESP_OK) { 160 | ESP_LOGW(TAG, "i2s_set_pin failed: %s", esp_err_to_name(err)); 161 | this->mark_failed(); 162 | return; 163 | } 164 | } 165 | } // namespace i2s 166 | } // namespace esphome 167 | -------------------------------------------------------------------------------- /components/sound_level_meter/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-name-in-module,invalid-name,unused-argument 2 | 3 | import esphome.codegen as cg 4 | import esphome.config_validation as cv 5 | from esphome import automation 6 | from esphome.automation import maybe_simple_id 7 | from esphome.components import sensor, i2s 8 | from esphome.const import ( 9 | CONF_ID, 10 | CONF_SENSORS, 11 | CONF_FILTERS, 12 | CONF_WINDOW_SIZE, 13 | CONF_UPDATE_INTERVAL, 14 | CONF_TYPE, 15 | UNIT_DECIBEL, 16 | STATE_CLASS_MEASUREMENT 17 | ) 18 | 19 | CODEOWNERS = ["@stas-sl"] 20 | DEPENDENCIES = ["esp32", "i2s"] 21 | AUTO_LOAD = ["sensor"] 22 | MULTI_CONF = True 23 | 24 | sound_level_meter_ns = cg.esphome_ns.namespace("sound_level_meter") 25 | SoundLevelMeter = sound_level_meter_ns.class_("SoundLevelMeter", cg.Component) 26 | SoundLevelMeterSensor = sound_level_meter_ns.class_("SoundLevelMeterSensor", sensor.Sensor) 27 | SoundLevelMeterSensorEq = sound_level_meter_ns.class_("SoundLevelMeterSensorEq", SoundLevelMeterSensor, sensor.Sensor) 28 | SoundLevelMeterSensorMax = sound_level_meter_ns.class_("SoundLevelMeterSensorMax", SoundLevelMeterSensor, sensor.Sensor) 29 | SoundLevelMeterSensorMin = sound_level_meter_ns.class_("SoundLevelMeterSensorMin", SoundLevelMeterSensor, sensor.Sensor) 30 | SoundLevelMeterSensorPeak = sound_level_meter_ns.class_( 31 | "SoundLevelMeterSensorPeak", SoundLevelMeterSensor, sensor.Sensor) 32 | SensorGroup = sound_level_meter_ns.class_("SensorGroup") 33 | Filter = sound_level_meter_ns.class_("Filter") 34 | SOS_Filter = sound_level_meter_ns.class_("SOS_Filter", Filter) 35 | ToggleAction = sound_level_meter_ns.class_("ToggleAction", automation.Action) 36 | TurnOffAction = sound_level_meter_ns.class_("TurnOffAction", automation.Action) 37 | TurnOnAction = sound_level_meter_ns.class_("TurnOnAction", automation.Action) 38 | 39 | 40 | CONF_I2S_ID = "i2s_id" 41 | CONF_GROUPS = "groups" 42 | CONF_EQ = "eq" 43 | CONF_MAX = "max" 44 | CONF_MIN = "min" 45 | CONF_PEAK = "peak" 46 | CONF_BUFFER_SIZE = "buffer_size" 47 | CONF_SOS = "sos" 48 | CONF_COEFFS = "coeffs" 49 | CONF_WARMUP_INTERVAL = "warmup_interval" 50 | CONF_TASK_STACK_SIZE = "task_stack_size" 51 | CONF_TASK_PRIORITY = "task_priority" 52 | CONF_TASK_CORE = "task_core" 53 | CONF_MIC_SENSITIVITY = "mic_sensitivity" 54 | CONF_MIC_SENSITIVITY_REF = "mic_sensitivity_ref" 55 | CONF_OFFSET = "offset" 56 | CONF_IS_ON = "is_on" 57 | 58 | 59 | CONFIG_SENSOR_SCHEMA = cv.typed_schema( 60 | { 61 | CONF_EQ: sensor.sensor_schema( 62 | SoundLevelMeterSensorEq, 63 | unit_of_measurement=UNIT_DECIBEL, 64 | accuracy_decimals=2, 65 | state_class=STATE_CLASS_MEASUREMENT 66 | ).extend({ 67 | cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds 68 | }), 69 | CONF_MAX: sensor.sensor_schema( 70 | SoundLevelMeterSensorMax, 71 | unit_of_measurement=UNIT_DECIBEL, 72 | accuracy_decimals=2, 73 | state_class=STATE_CLASS_MEASUREMENT 74 | ).extend({ 75 | cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds, 76 | cv.Required(CONF_WINDOW_SIZE): cv.positive_time_period_milliseconds 77 | }), 78 | CONF_MIN: sensor.sensor_schema( 79 | SoundLevelMeterSensorMin, 80 | unit_of_measurement=UNIT_DECIBEL, 81 | accuracy_decimals=2, 82 | state_class=STATE_CLASS_MEASUREMENT 83 | ).extend({ 84 | cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds, 85 | cv.Required(CONF_WINDOW_SIZE): cv.positive_time_period_milliseconds 86 | }), 87 | CONF_PEAK: sensor.sensor_schema( 88 | SoundLevelMeterSensorPeak, 89 | unit_of_measurement=UNIT_DECIBEL, 90 | accuracy_decimals=2, 91 | state_class=STATE_CLASS_MEASUREMENT 92 | ).extend({ 93 | cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds 94 | }) 95 | } 96 | ) 97 | 98 | CONFIG_FILTER_SCHEMA = cv.typed_schema( 99 | { 100 | CONF_SOS: cv.Schema({ 101 | cv.GenerateID(): cv.declare_id(SOS_Filter), 102 | cv.Required(CONF_COEFFS): [[cv.float_]] 103 | }) 104 | } 105 | ) 106 | 107 | 108 | def config_group_schema(value): 109 | return CONFIG_GROUP_SCHEMA(value) 110 | 111 | 112 | CONFIG_GROUP_SCHEMA = ( 113 | cv.Schema({ 114 | cv.GenerateID(): cv.declare_id(SensorGroup), 115 | cv.Optional(CONF_FILTERS): [CONFIG_FILTER_SCHEMA], 116 | cv.Optional(CONF_SENSORS): [CONFIG_SENSOR_SCHEMA], 117 | cv.Optional(CONF_GROUPS): [config_group_schema] 118 | }) 119 | ) 120 | 121 | CONFIG_SCHEMA = ( 122 | cv.Schema( 123 | { 124 | cv.GenerateID(): cv.declare_id(SoundLevelMeter), 125 | cv.GenerateID(CONF_I2S_ID): cv.use_id(i2s.I2SComponent), 126 | cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.positive_time_period_milliseconds, 127 | cv.Optional(CONF_IS_ON, default=True): cv.boolean, 128 | cv.Optional(CONF_BUFFER_SIZE, default=1024): cv.positive_not_null_int, 129 | cv.Optional(CONF_WARMUP_INTERVAL, default="500ms"): cv.positive_time_period_milliseconds, 130 | cv.Optional(CONF_TASK_STACK_SIZE, default=4096): cv.positive_not_null_int, 131 | cv.Optional(CONF_TASK_PRIORITY, default=2): cv.uint8_t, 132 | cv.Optional(CONF_TASK_CORE, default=1): cv.int_range(0, 1), 133 | cv.Optional(CONF_MIC_SENSITIVITY): cv.decibel, 134 | cv.Optional(CONF_MIC_SENSITIVITY_REF): cv.decibel, 135 | cv.Optional(CONF_OFFSET): cv.decibel, 136 | cv.Required(CONF_GROUPS): [CONFIG_GROUP_SCHEMA] 137 | } 138 | ) 139 | .extend(cv.COMPONENT_SCHEMA) 140 | ) 141 | 142 | SOUND_LEVEL_METER_ACTION_SCHEMA = maybe_simple_id({ 143 | cv.GenerateID(): cv.use_id(SoundLevelMeter) 144 | }) 145 | 146 | 147 | async def groups_to_code(config, component, parent): 148 | for gc in config: 149 | g = cg.new_Pvariable(gc[CONF_ID]) 150 | cg.add(g.set_parent(component)) 151 | cg.add(parent.add_group(g)) 152 | if CONF_FILTERS in gc: 153 | for fc in gc[CONF_FILTERS]: 154 | f = None 155 | if fc[CONF_TYPE] == CONF_SOS: 156 | f = cg.new_Pvariable(fc[CONF_ID], fc[CONF_COEFFS]) 157 | if f is not None: 158 | cg.add(g.add_filter(f)) 159 | if CONF_GROUPS in gc: 160 | await groups_to_code(gc[CONF_GROUPS], component, g) 161 | if CONF_SENSORS in gc: 162 | for sc in gc[CONF_SENSORS]: 163 | s = await sensor.new_sensor(sc) 164 | cg.add(s.set_parent(component)) 165 | if CONF_WINDOW_SIZE in sc: 166 | cg.add(s.set_window_size(sc[CONF_WINDOW_SIZE])) 167 | if CONF_UPDATE_INTERVAL in sc: 168 | cg.add(s.set_update_interval(sc[CONF_UPDATE_INTERVAL])) 169 | cg.add(g.add_sensor(s)) 170 | 171 | 172 | async def to_code(config): 173 | var = cg.new_Pvariable(config[CONF_ID]) 174 | await cg.register_component(var, config) 175 | i2s_component = await cg.get_variable(config[CONF_I2S_ID]) 176 | cg.add(var.set_i2s(i2s_component)) 177 | cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) 178 | cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) 179 | cg.add(var.set_warmup_interval(config[CONF_WARMUP_INTERVAL])) 180 | cg.add(var.set_task_stack_size(config[CONF_TASK_STACK_SIZE])) 181 | cg.add(var.set_task_priority(config[CONF_TASK_PRIORITY])) 182 | cg.add(var.set_task_core(config[CONF_TASK_CORE])) 183 | if CONF_MIC_SENSITIVITY in config: 184 | cg.add(var.set_mic_sensitivity(config[CONF_MIC_SENSITIVITY])) 185 | if CONF_MIC_SENSITIVITY_REF in config: 186 | cg.add(var.set_mic_sensitivity_ref(config[CONF_MIC_SENSITIVITY_REF])) 187 | if CONF_OFFSET in config: 188 | cg.add(var.set_offset(config[CONF_OFFSET])) 189 | if not config[CONF_IS_ON]: 190 | cg.add(var.turn_off()) 191 | await groups_to_code(config[CONF_GROUPS], var, var) 192 | 193 | 194 | @automation.register_action("sound_level_meter.toggle", ToggleAction, SOUND_LEVEL_METER_ACTION_SCHEMA) 195 | @automation.register_action("sound_level_meter.turn_off", TurnOffAction, SOUND_LEVEL_METER_ACTION_SCHEMA) 196 | @automation.register_action("sound_level_meter.turn_on", TurnOnAction, SOUND_LEVEL_METER_ACTION_SCHEMA) 197 | async def switch_toggle_to_code(config, action_id, template_arg, args): 198 | paren = await cg.get_variable(config[CONF_ID]) 199 | return cg.new_Pvariable(action_id, template_arg, paren) 200 | -------------------------------------------------------------------------------- /configs/advanced-example-config.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable rule:brackets rule:commas 2 | --- 3 | esphome: 4 | name: sound-level-meter 5 | 6 | external_components: 7 | - source: github://stas-sl/esphome-sound-level-meter 8 | 9 | esp32: 10 | board: esp32dev 11 | framework: 12 | type: arduino 13 | 14 | logger: 15 | level: DEBUG 16 | 17 | i2s: 18 | bck_pin: 23 19 | ws_pin: 18 20 | din_pin: 19 21 | sample_rate: 48000 # default: 48000 22 | bits_per_sample: 32 # default: 32 23 | dma_buf_count: 8 # default: 8 24 | dma_buf_len: 256 # default: 256 25 | use_apll: true # default: false 26 | 27 | # right shift samples. 28 | # for example if mic has 24 bit resolution, and 29 | # i2s configured as 32 bits, then audio data will be aligned left (MSB) 30 | # and LSB will be padded with zeros, so you might want to shift them right by 8 bits 31 | bits_shift: 8 # default: 0 32 | 33 | sound_level_meter: 34 | id: sound_level_meter1 35 | 36 | # update_interval specifies over which interval to aggregate audio data 37 | # you can specify default update_interval on top level, but you can also override 38 | # it further by specifying it on sensor level 39 | update_interval: 60s # default: 60s 40 | 41 | # you can disable (turn off) component by default (on boot) 42 | # and turn it on later when needed via sound_level_meter.turn_on/toggle actions; 43 | # when used with switch it might conflict/being overriden by 44 | # switch state restoration logic, so you have to either disable it in 45 | # switch config and then is_on property here will have effect, 46 | # or completely rely on switch state restoration/initialization and 47 | # any value set here will be ignored 48 | is_on: true # default: true 49 | 50 | # buffer_size is in samples (not bytes), so for float data type 51 | # number of bytes will be buffer_size * 4 52 | buffer_size: 1024 # default: 1024 53 | 54 | # ignore audio data at startup for this long 55 | warmup_interval: 500ms # default: 500ms 56 | 57 | # audio processing runs in a separate task, you can change its settings below 58 | task_stack_size: 4096 # default: 4096 59 | task_priority: 2 # default: 2 60 | task_core: 1 # default: 1 61 | 62 | # see your mic datasheet to find sensitivity and reference SPL. 63 | # those are used to convert dB FS to db SPL 64 | mic_sensitivity: -26dB # default: empty 65 | mic_sensitivity_ref: 94dB # default: empty 66 | # additional offset if needed 67 | offset: 0dB # default: empty 68 | 69 | # for flexibility sensors are organized hierarchically into groups. each group 70 | # could have any number of filters, sensors and nested groups. 71 | # for examples if there is a top level group A with filter A and nested group B 72 | # with filter B, then for sensors inside group B filters A and then B will be 73 | # applied: 74 | # groups: 75 | # # group A 76 | # - filters: 77 | # - filter A 78 | # groups: 79 | # # group B 80 | # - filters: 81 | # - filter B 82 | # sensors: 83 | # - sensor X 84 | groups: 85 | # group 1 (mic eq) 86 | - filters: 87 | # for now only SOS filter type is supported, see math/filter-design.ipynb 88 | # to learn how to create or convert other filter types to SOS 89 | - type: sos 90 | coeffs: 91 | # INMP441: 92 | # b0 b1 b2 a1 a2 93 | - [ 1.0019784 , -1.9908513 , 0.9889158 , -1.9951786 , 0.99518436] 94 | 95 | # nested groups 96 | groups: 97 | # group 1.1 (no weighting) 98 | - sensors: 99 | # 'eq' type sensor calculates Leq (average) sound level over specified period 100 | - type: eq 101 | name: LZeq_1s 102 | id: LZeq_1s 103 | # you can override updated_interval specified on top level 104 | # individually per each sensor 105 | update_interval: 1s 106 | 107 | # you can have as many sensors of same type, but with different 108 | # other parameters (e.g. update_interval) as needed 109 | - type: eq 110 | name: LZeq_1min 111 | id: LZeq_1min 112 | unit_of_measurement: dBZ 113 | 114 | # 'max' sensor type calculates Lmax with specified window_size. 115 | # for example, if update_interval is 60s and window_size is 1s 116 | # then it will calculate 60 Leq values for each second of audio data 117 | # and the result will be max of them 118 | - type: max 119 | name: LZmax_1s_1min 120 | id: LZmax_1s_1min 121 | window_size: 1s 122 | unit_of_measurement: dBZ 123 | 124 | # same as 'max', but 'min' 125 | - type: min 126 | name: LZmin_1s_1min 127 | id: LZmin_1s_1min 128 | window_size: 1s 129 | unit_of_measurement: dBZ 130 | 131 | # it finds max single sample over whole update_interval 132 | - type: peak 133 | name: LZpeak_1min 134 | id: LZpeak_1min 135 | unit_of_measurement: dBZ 136 | 137 | # group 1.2 (A-weighting) 138 | - filters: 139 | # for now only SOS filter type is supported, see math/filter-design.ipynb 140 | # to learn how to create or convert other filter types to SOS 141 | - type: sos 142 | coeffs: 143 | # A-weighting: 144 | # b0 b1 b2 a1 a2 145 | - [ 0.16999495 , 0.741029 , 0.52548885 , -0.11321865 , -0.056549273] 146 | - [ 1. , -2.00027 , 1.0002706 , -0.03433284 , -0.79215795 ] 147 | - [ 1. , -0.709303 , -0.29071867 , -1.9822421 , 0.9822986 ] 148 | sensors: 149 | - type: eq 150 | name: LAeq_1min 151 | id: LAeq_1min 152 | unit_of_measurement: dBA 153 | - type: max 154 | name: LAmax_1s_1min 155 | id: LAmax_1s_1min 156 | window_size: 1s 157 | unit_of_measurement: dBA 158 | - type: min 159 | name: LAmin_1s_1min 160 | id: LAmin_1s_1min 161 | window_size: 1s 162 | unit_of_measurement: dBA 163 | - type: peak 164 | name: LApeak_1min 165 | id: LApeak_1min 166 | unit_of_measurement: dBA 167 | 168 | # group 1.3 (C-weighting) 169 | - filters: 170 | # for now only SOS filter type is supported, see math/filter-design.ipynb 171 | # to learn how to create or convert other filter types to SOS 172 | - type: sos 173 | coeffs: 174 | # C-weighting: 175 | # b0 b1 b2 a1 a2 176 | - [-0.49651518 , -0.12296628 , -0.0076134163, -0.37165618 , 0.03453208 ] 177 | - [ 1. , 1.3294908 , 0.44188643 , 1.2312505 , 0.37899444 ] 178 | - [ 1. , -2. , 1. , -1.9946145 , 0.9946217 ] 179 | sensors: 180 | - type: eq 181 | name: LCeq_1min 182 | id: LCeq_1min 183 | unit_of_measurement: dBC 184 | - type: max 185 | name: LCmax_1s_1min 186 | id: LCmax_1s_1min 187 | window_size: 1s 188 | unit_of_measurement: dBC 189 | - type: min 190 | name: LCmin_1s_1min 191 | id: LCmin_1s_1min 192 | window_size: 1s 193 | unit_of_measurement: dBC 194 | - type: peak 195 | name: LCpeak_1min 196 | id: LCpeak_1min 197 | unit_of_measurement: dBC 198 | 199 | 200 | # automation 201 | # available actions: 202 | # - sound_level_meter.turn_on 203 | # - sound_level_meter.turn_off 204 | # - sound_level_meter.toggle 205 | switch: 206 | - platform: template 207 | name: "Sound Level Meter Switch" 208 | # if you want is_on property on component to have effect, then set 209 | # restore_mode to DISABLED, or alternatively you can use other modes 210 | # (more on them in esphome docs), then is_on property on the component will 211 | # be overriden by the switch 212 | restore_mode: DISABLED # ALWAYS_OFF | ALWAYS_ON | RESTORE_DEFAULT_OFF | RESTORE_DEFAULT_ON 213 | lambda: |- 214 | return id(sound_level_meter1).is_on(); 215 | turn_on_action: 216 | - sound_level_meter.turn_on 217 | turn_off_action: 218 | - sound_level_meter.turn_off 219 | 220 | button: 221 | - platform: template 222 | name: "Sound Level Meter Toggle Button" 223 | on_press: 224 | - sound_level_meter.toggle: sound_level_meter1 225 | 226 | binary_sensor: 227 | - platform: gpio 228 | pin: GPIO0 229 | name: "Sound Level Meter GPIO Toggle" 230 | on_press: 231 | - sound_level_meter.toggle: sound_level_meter1 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPHome Sound Level Meter [![CI](https://github.com/stas-sl/esphome-sound-level-meter/actions/workflows/ci.yaml/badge.svg)](https://github.com/stas-sl/esphome-sound-level-meter/actions/workflows/ci.yaml) 2 | 3 | This component was made to measure environmental noise levels (Leq, Lmin, Lmax, Lpeak) with different frequency weightings over configured time intervals. It is heavily based on awesome work by Ivan Kostoski: [esp32-i2s-slm](https://github.com/ikostoski/esp32-i2s-slm) (his [hackaday.io project](https://hackaday.io/project/166867-esp32-i2s-slm)). 4 | 5 | image 6 | 7 | Typical weekly traffic noise recorded with a microphone located 50m from a medium traffic road: 8 | image 9 | 10 | Add it to your ESPHome config: 11 | 12 | ```yaml 13 | external_components: 14 | - source: github://stas-sl/esphome-sound-level-meter # add @tag if you want to use a specific version (e.g @v1.0.0) 15 | ``` 16 | 17 | For configuration options see [minimal-example-config.yaml](configs/minimal-example-config.yaml) or [advanced-example-config.yaml](configs/advanced-example-config.yaml): 18 | 19 | ```yaml 20 | i2s: 21 | bck_pin: 23 22 | ws_pin: 18 23 | din_pin: 19 24 | sample_rate: 48000 # default: 48000 25 | bits_per_sample: 32 # default: 32 26 | dma_buf_count: 8 # default: 8 27 | dma_buf_len: 256 # default: 256 28 | use_apll: true # default: false 29 | 30 | # right shift samples. 31 | # for example if mic has 24 bit resolution, and 32 | # i2s configured as 32 bits, then audio data will be aligned left (MSB) 33 | # and LSB will be padded with zeros, so you might want to shift them right by 8 bits 34 | bits_shift: 8 # default: 0 35 | 36 | sound_level_meter: 37 | id: sound_level_meter1 38 | 39 | # update_interval specifies over which interval to aggregate audio data 40 | # you can specify default update_interval on top level, but you can also override 41 | # it further by specifying it on sensor level 42 | update_interval: 60s # default: 60s 43 | 44 | # you can disable (turn off) component by default (on boot) 45 | # and turn it on later when needed via sound_level_meter.turn_on/toggle actions; 46 | # when used with switch it might conflict/being overriden by 47 | # switch state restoration logic, so you have to either disable it in 48 | # switch config and then is_on property here will have effect, 49 | # or completely rely on switch state restoration/initialization and 50 | # any value set here will be ignored 51 | is_on: true # default: true 52 | 53 | # buffer_size is in samples (not bytes), so for float data type 54 | # number of bytes will be buffer_size * 4 55 | buffer_size: 1024 # default: 1024 56 | 57 | # ignore audio data at startup for this long 58 | warmup_interval: 500ms # default: 500ms 59 | 60 | # audio processing runs in a separate task, you can change its settings below 61 | task_stack_size: 4096 # default: 4096 62 | task_priority: 2 # default: 2 63 | task_core: 1 # default: 1 64 | 65 | # see your mic datasheet to find sensitivity and reference SPL. 66 | # those are used to convert dB FS to db SPL 67 | mic_sensitivity: -26dB # default: empty 68 | mic_sensitivity_ref: 94dB # default: empty 69 | # additional offset if needed 70 | offset: 0dB # default: empty 71 | 72 | # for flexibility sensors are organized hierarchically into groups. each group 73 | # could have any number of filters, sensors and nested groups. 74 | # for examples if there is a top level group A with filter A and nested group B 75 | # with filter B, then for sensors inside group B filters A and then B will be 76 | # applied: 77 | # groups: 78 | # # group A 79 | # - filters: 80 | # - filter A 81 | # groups: 82 | # # group B 83 | # - filters: 84 | # - filter B 85 | # sensors: 86 | # - sensor X 87 | groups: 88 | # group 1 (mic eq) 89 | - filters: 90 | # for now only SOS filter type is supported, see math/filter-design.ipynb 91 | # to learn how to create or convert other filter types to SOS 92 | - type: sos 93 | coeffs: 94 | # INMP441: 95 | # b0 b1 b2 a1 a2 96 | - [ 1.0019784 , -1.9908513 , 0.9889158 , -1.9951786 , 0.99518436] 97 | 98 | # nested groups 99 | groups: 100 | # group 1.1 (no weighting) 101 | - sensors: 102 | # 'eq' type sensor calculates Leq (average) sound level over specified period 103 | - type: eq 104 | name: LZeq_1s 105 | id: LZeq_1s 106 | # you can override updated_interval specified on top level 107 | # individually per each sensor 108 | update_interval: 1s 109 | 110 | # you can have as many sensors of same type, but with different 111 | # other parameters (e.g. update_interval) as needed 112 | - type: eq 113 | name: LZeq_1min 114 | id: LZeq_1min 115 | unit_of_measurement: dBZ 116 | 117 | # 'max' sensor type calculates Lmax with specified window_size. 118 | # for example, if update_interval is 60s and window_size is 1s 119 | # then it will calculate 60 Leq values for each second of audio data 120 | # and the result will be max of them 121 | - type: max 122 | name: LZmax_1s_1min 123 | id: LZmax_1s_1min 124 | window_size: 1s 125 | unit_of_measurement: dBZ 126 | 127 | # same as 'max', but 'min' 128 | - type: min 129 | name: LZmin_1s_1min 130 | id: LZmin_1s_1min 131 | window_size: 1s 132 | unit_of_measurement: dBZ 133 | 134 | # it finds max single sample over whole update_interval 135 | - type: peak 136 | name: LZpeak_1min 137 | id: LZpeak_1min 138 | unit_of_measurement: dBZ 139 | 140 | # group 1.2 (A-weighting) 141 | - filters: 142 | # for now only SOS filter type is supported, see math/filter-design.ipynb 143 | # to learn how to create or convert other filter types to SOS 144 | - type: sos 145 | coeffs: 146 | # A-weighting: 147 | # b0 b1 b2 a1 a2 148 | - [ 0.16999495 , 0.741029 , 0.52548885 , -0.11321865 , -0.056549273] 149 | - [ 1. , -2.00027 , 1.0002706 , -0.03433284 , -0.79215795 ] 150 | - [ 1. , -0.709303 , -0.29071867 , -1.9822421 , 0.9822986 ] 151 | sensors: 152 | - type: eq 153 | name: LAeq_1min 154 | id: LAeq_1min 155 | unit_of_measurement: dBA 156 | - type: max 157 | name: LAmax_1s_1min 158 | id: LAmax_1s_1min 159 | window_size: 1s 160 | unit_of_measurement: dBA 161 | - type: min 162 | name: LAmin_1s_1min 163 | id: LAmin_1s_1min 164 | window_size: 1s 165 | unit_of_measurement: dBA 166 | - type: peak 167 | name: LApeak_1min 168 | id: LApeak_1min 169 | unit_of_measurement: dBA 170 | 171 | # group 1.3 (C-weighting) 172 | - filters: 173 | # for now only SOS filter type is supported, see math/filter-design.ipynb 174 | # to learn how to create or convert other filter types to SOS 175 | - type: sos 176 | coeffs: 177 | # C-weighting: 178 | # b0 b1 b2 a1 a2 179 | - [-0.49651518 , -0.12296628 , -0.0076134163, -0.37165618 , 0.03453208 ] 180 | - [ 1. , 1.3294908 , 0.44188643 , 1.2312505 , 0.37899444 ] 181 | - [ 1. , -2. , 1. , -1.9946145 , 0.9946217 ] 182 | sensors: 183 | - type: eq 184 | name: LCeq_1min 185 | id: LCeq_1min 186 | unit_of_measurement: dBC 187 | - type: max 188 | name: LCmax_1s_1min 189 | id: LCmax_1s_1min 190 | window_size: 1s 191 | unit_of_measurement: dBC 192 | - type: min 193 | name: LCmin_1s_1min 194 | id: LCmin_1s_1min 195 | window_size: 1s 196 | unit_of_measurement: dBC 197 | - type: peak 198 | name: LCpeak_1min 199 | id: LCpeak_1min 200 | unit_of_measurement: dBC 201 | 202 | 203 | # automation 204 | # available actions: 205 | # - sound_level_meter.turn_on 206 | # - sound_level_meter.turn_off 207 | # - sound_level_meter.toggle 208 | switch: 209 | - platform: template 210 | name: "Sound Level Meter Switch" 211 | # if you want is_on property on component to have effect, then set 212 | # restore_mode to DISABLED, or alternatively you can use other modes 213 | # (more on them in esphome docs), then is_on property on the component will 214 | # be overriden by the switch 215 | restore_mode: DISABLED # ALWAYS_OFF | ALWAYS_ON | RESTORE_DEFAULT_OFF | RESTORE_DEFAULT_ON 216 | lambda: |- 217 | return id(sound_level_meter1).is_on(); 218 | turn_on_action: 219 | - sound_level_meter.turn_on 220 | turn_off_action: 221 | - sound_level_meter.turn_off 222 | 223 | button: 224 | - platform: template 225 | name: "Sound Level Meter Toggle Button" 226 | on_press: 227 | - sound_level_meter.toggle: sound_level_meter1 228 | 229 | binary_sensor: 230 | - platform: gpio 231 | pin: GPIO0 232 | name: "Sound Level Meter GPIO Toggle" 233 | on_press: 234 | - sound_level_meter.toggle: sound_level_meter1 235 | ``` 236 | 237 | ### Filter design (math) 238 | 239 | Check out [filter-design notebook](math/filter-design.ipynb) to learn how those SOS coefficients were calculated. 240 | 241 | ### Performance 242 | 243 | In Ivan's project SOS filters are implemented using ESP32 assembler, so they are really fast. A quote from him: 244 | 245 | > Well, now you can lower the frequency of ESP32 down to 80MHz (i.e. for battery operation) and filtering and summation of I2S data will still take less than 15% of single core processing time. At 240MHz, filtering 1/8sec worth of samples with 2 x 6th-order IIR filters takes less than 5ms. 246 | 247 | I'm not so familiar with assembler and it is hard to understand and maintain, so I implemented filtering in regular C++. Looks like the performance is not that bad. At 80MHz filtering and summation takes ~210ms per 1s of audio (48000 samples), which is 21% of single core processing time (vs. 15% if implemented in ASM). At 240MHz same task takes 67ms (vs. 5x8=40ms in ASM). 248 | 249 | | CPU Freq | # SOS | Sensors | Sample Rate | Buffer size | Time (per 1s audio) | 250 | | -------- | ----- | ------------------------------ | ----------- | ----------- | ------------------- | 251 | | 80MHz | 0 | 1 Leq | 48000 | 1024 | 57 ms | 252 | | 80MHz | 6 | 1 Leq | 48000 | 1024 | 204 ms | 253 | | 80MHz | 6 | 1 Lmax | 48000 | 1024 | 211 ms | 254 | | 80MHz | 6 | 1 Lpeak | 48000 | 1024 | 207 ms | 255 | | 240MHz | 0 | 1 Leq | 48000 | 1024 | 18 ms | 256 | | 240MHz | 6 | 1 Leq | 48000 | 1024 | 67 ms | 257 | | 240MHz | 6 | 1 Leq, 1 Lpeak, 1 Lmax, 1 Lmin | 48000 | 1024 | 90 ms | 258 | 259 | ### Supported platforms 260 | 261 | Tested with ESPHome version 2023.2.0, platforms: 262 | - [x] ESP32 (Arduino v2.0.5, ESP-IDF v4.4.2) 263 | - [x] ESP32-IDF (ESP-IDF v4.4.2) 264 | 265 | ### Sending data to sensor.community 266 | 267 | See [sensor-community-example-config.yaml](configs/sensor-community-example-config.yaml) 268 | 269 | ### References 270 | 271 | 1. [ESP32-I2S-SLM hackaday.io project](https://hackaday.io/project/166867-esp32-i2s-slm) 272 | 1. [Measuring Audible Noise in Real-Time hackaday.io project](https://hackaday.io/project/162059-street-sense/log/170825-measuring-audible-noise-in-real-time) 273 | 1. [What are LAeq and LAFmax?](https://www.nti-audio.com/en/support/know-how/what-are-laeq-and-lafmax) 274 | 1. [Noise measuring @ smartcitizen.me](https://docs.smartcitizen.me/Components/sensors/air/Noise) 275 | 1. [EspAudioSensor](https://revspace.nl/EspAudioSensor) 276 | 1. [Design of a digital A-weighting filter with arbitrary sample rate (dsp.stackexchange.com)](https://dsp.stackexchange.com/questions/36077/design-of-a-digital-a-weighting-filter-with-arbitrary-sample-rate) 277 | 1. [How to compute dBFS? (dsp.stackexchange.com)](https://dsp.stackexchange.com/questions/8785/how-to-compute-dbfs) 278 | 1. [Microphone Specification Explained](https://invensense.tdk.com/wp-content/uploads/2015/02/AN-1112-v1.1.pdf) 279 | 1. [esp32-i2s-slm source code](https://github.com/ikostoski/esp32-i2s-slm) 280 | 1. [DNMS source code](https://github.com/hbitter/DNMS) 281 | 1. [NoiseLevel source code](https://github.com/bertrik/NoiseLevel) 282 | -------------------------------------------------------------------------------- /components/sound_level_meter/sound_level_meter.cpp: -------------------------------------------------------------------------------- 1 | #include "sound_level_meter.h" 2 | 3 | namespace esphome { 4 | namespace sound_level_meter { 5 | 6 | static const char *const TAG = "sound_level_meter"; 7 | 8 | // By definition dBFS value of a full-scale sine wave equals to 0. 9 | // Since the RMS of the full-scale sine wave is 1/sqrt(2), multiplying rms(signal) by sqrt(2) 10 | // ensures that the formula evaluates to 0 when signal is a full-scale sine wave. 11 | // This is equivalent to adding DBFS_OFFSET 12 | // see: https://dsp.stackexchange.com/a/50947/65262 13 | static constexpr float DBFS_OFFSET = 20 * log10(sqrt(2)); 14 | 15 | /* SoundLevelMeter */ 16 | 17 | void SoundLevelMeter::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } 18 | uint32_t SoundLevelMeter::get_update_interval() { return this->update_interval_; } 19 | void SoundLevelMeter::set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; } 20 | uint32_t SoundLevelMeter::get_buffer_size() { return this->buffer_size_; } 21 | uint32_t SoundLevelMeter::get_sample_rate() { return this->i2s_->get_sample_rate(); } 22 | void SoundLevelMeter::set_i2s(i2s::I2SComponent *i2s) { this->i2s_ = i2s; } 23 | void SoundLevelMeter::add_group(SensorGroup *group) { this->groups_.push_back(group); } 24 | void SoundLevelMeter::set_warmup_interval(uint32_t warmup_interval) { this->warmup_interval_ = warmup_interval; } 25 | void SoundLevelMeter::set_task_stack_size(uint32_t task_stack_size) { this->task_stack_size_ = task_stack_size; } 26 | void SoundLevelMeter::set_task_priority(uint8_t task_priority) { this->task_priority_ = task_priority; } 27 | void SoundLevelMeter::set_task_core(uint8_t task_core) { this->task_core_ = task_core; } 28 | void SoundLevelMeter::set_mic_sensitivity(optional mic_sensitivity) { this->mic_sensitivity_ = mic_sensitivity; } 29 | optional SoundLevelMeter::get_mic_sensitivity() { return this->mic_sensitivity_; } 30 | void SoundLevelMeter::set_mic_sensitivity_ref(optional mic_sensitivity_ref) { 31 | this->mic_sensitivity_ref_ = mic_sensitivity_ref; 32 | } 33 | optional SoundLevelMeter::get_mic_sensitivity_ref() { return this->mic_sensitivity_ref_; } 34 | void SoundLevelMeter::set_offset(optional offset) { this->offset_ = offset; } 35 | optional SoundLevelMeter::get_offset() { return this->offset_; } 36 | 37 | void SoundLevelMeter::dump_config() { 38 | ESP_LOGCONFIG(TAG, "Sound Level Meter:"); 39 | ESP_LOGCONFIG(TAG, " Buffer Size: %u (samples)", this->buffer_size_); 40 | ESP_LOGCONFIG(TAG, " Warmup Interval: %u ms", this->warmup_interval_); 41 | ESP_LOGCONFIG(TAG, " Task Stack Size: %u", this->task_stack_size_); 42 | ESP_LOGCONFIG(TAG, " Task Priority: %u", this->task_priority_); 43 | ESP_LOGCONFIG(TAG, " Task Core: %u", this->task_core_); 44 | LOG_UPDATE_INTERVAL(this); 45 | if (this->groups_.size() > 0) { 46 | ESP_LOGCONFIG(TAG, " Groups:"); 47 | for (int i = 0; i < this->groups_.size(); i++) { 48 | ESP_LOGCONFIG(TAG, " Group %u:", i); 49 | this->groups_[i]->dump_config(" "); 50 | } 51 | } 52 | } 53 | 54 | void SoundLevelMeter::setup() { 55 | xTaskCreatePinnedToCore(SoundLevelMeter::task, "sound_level_meter", this->task_stack_size_, this, 56 | this->task_priority_, nullptr, this->task_core_); 57 | } 58 | 59 | void SoundLevelMeter::loop() { 60 | std::lock_guard lock(this->defer_mutex_); 61 | if (!this->defer_queue_.empty()) { 62 | auto &f = this->defer_queue_.front(); 63 | f(); 64 | this->defer_queue_.pop(); 65 | } 66 | } 67 | 68 | void SoundLevelMeter::turn_on() { 69 | std::lock_guard lock(this->on_mutex_); 70 | this->reset(); 71 | this->is_on_ = true; 72 | this->on_cv_.notify_one(); 73 | ESP_LOGD(TAG, "Turned on"); 74 | } 75 | 76 | void SoundLevelMeter::turn_off() { 77 | std::lock_guard lock(this->on_mutex_); 78 | this->reset(); 79 | this->is_on_ = false; 80 | this->on_cv_.notify_one(); 81 | ESP_LOGD(TAG, "Turned off"); 82 | } 83 | 84 | void SoundLevelMeter::toggle() { 85 | if (this->is_on_) 86 | this->turn_off(); 87 | else 88 | this->turn_on(); 89 | } 90 | 91 | bool SoundLevelMeter::is_on() { return this->is_on_; } 92 | 93 | void SoundLevelMeter::task(void *param) { 94 | SoundLevelMeter *this_ = reinterpret_cast(param); 95 | std::vector buffer(this_->buffer_size_); 96 | 97 | auto warmup_start = millis(); 98 | while (millis() - warmup_start < this_->warmup_interval_) 99 | this_->i2s_->read_samples(buffer); 100 | 101 | uint32_t process_time = 0, process_count = 0; 102 | uint64_t process_start; 103 | while (1) { 104 | { 105 | std::unique_lock lock(this_->on_mutex_); 106 | this_->on_cv_.wait(lock, [this_] { return this_->is_on_; }); 107 | } 108 | if (this_->i2s_->read_samples(buffer)) { 109 | process_start = esp_timer_get_time(); 110 | 111 | for (auto *g : this_->groups_) 112 | g->process(buffer); 113 | 114 | process_time += esp_timer_get_time() - process_start; 115 | process_count += buffer.size(); 116 | 117 | auto sr = this_->get_sample_rate(); 118 | if (process_count >= sr * (this_->update_interval_ / 1000.f)) { 119 | auto t = uint32_t(float(process_time) / process_count * (sr / 1000.f)); 120 | ESP_LOGD(TAG, "Processing time per 1s of audio data (%u samples): %u ms", sr, t); 121 | process_time = process_count = 0; 122 | } 123 | } 124 | delay(1); // allow task wdt trigger 125 | } 126 | } 127 | 128 | void SoundLevelMeter::defer(std::function &&f) { 129 | std::lock_guard lock(this->defer_mutex_); 130 | this->defer_queue_.push(std::move(f)); 131 | } 132 | 133 | void SoundLevelMeter::reset() { 134 | for (auto *g : this->groups_) 135 | g->reset(); 136 | } 137 | 138 | /* SensorGroup */ 139 | 140 | void SensorGroup::set_parent(SoundLevelMeter *parent) { this->parent_ = parent; } 141 | void SensorGroup::add_sensor(SoundLevelMeterSensor *sensor) { this->sensors_.push_back(sensor); } 142 | void SensorGroup::add_group(SensorGroup *group) { this->groups_.push_back(group); } 143 | void SensorGroup::add_filter(Filter *filter) { this->filters_.push_back(filter); } 144 | 145 | void SensorGroup::dump_config(const char *prefix) { 146 | ESP_LOGCONFIG(TAG, "%sSensors:", prefix); 147 | for (auto *s : this->sensors_) 148 | LOG_SENSOR((std::string(prefix) + " ").c_str(), "Sound Pressure Level", s); 149 | 150 | if (this->groups_.size() > 0) { 151 | ESP_LOGCONFIG(TAG, "%sGroups:", prefix); 152 | for (int i = 0; i < this->groups_.size(); i++) { 153 | ESP_LOGCONFIG(TAG, "%s Group %u:", prefix, i); 154 | this->groups_[i]->dump_config((std::string(prefix) + " ").c_str()); 155 | } 156 | } 157 | } 158 | 159 | void SensorGroup::process(std::vector &buffer) { 160 | std::vector &&data = this->filters_.size() > 0 ? std::vector(buffer) : buffer; 161 | if (this->filters_.size() > 0) 162 | for (auto f : this->filters_) 163 | f->process(data); 164 | 165 | for (auto s : this->sensors_) 166 | s->process(data); 167 | 168 | for (auto g : this->groups_) 169 | g->process(data); 170 | } 171 | 172 | void SensorGroup::reset() { 173 | for (auto f : this->filters_) 174 | f->reset(); 175 | for (auto s : this->sensors_) 176 | s->reset(); 177 | for (auto g : this->groups_) 178 | g->reset(); 179 | } 180 | 181 | /* SoundLevelMeterSensor */ 182 | 183 | void SoundLevelMeterSensor::set_parent(SoundLevelMeter *parent) { 184 | this->parent_ = parent; 185 | this->update_samples_ = parent->get_sample_rate() * (parent->get_update_interval() / 1000.f); 186 | } 187 | 188 | void SoundLevelMeterSensor::set_update_interval(uint32_t update_interval) { 189 | this->update_samples_ = this->parent_->get_sample_rate() * (update_interval / 1000.f); 190 | } 191 | 192 | void SoundLevelMeterSensor::defer_publish_state(float state) { 193 | this->parent_->defer([this, state]() { this->publish_state(state); }); 194 | } 195 | 196 | float SoundLevelMeterSensor::adjust_dB(float dB, bool is_rms) { 197 | // see: https://dsp.stackexchange.com/a/50947/65262 198 | if (is_rms) 199 | dB += DBFS_OFFSET; 200 | 201 | // see: https://invensense.tdk.com/wp-content/uploads/2015/02/AN-1112-v1.1.pdf 202 | // dBSPL = dBFS + mic_sensitivity_ref - mic_sensitivity 203 | // e.g. dBSPL = dBFS + 94 - (-26) = dBFS + 120 204 | if (this->parent_->get_mic_sensitivity().has_value() && this->parent_->get_mic_sensitivity_ref().has_value()) 205 | dB += *this->parent_->get_mic_sensitivity_ref() - *this->parent_->get_mic_sensitivity(); 206 | 207 | if (this->parent_->get_offset().has_value()) 208 | dB += *this->parent_->get_offset(); 209 | 210 | return dB; 211 | } 212 | 213 | /* SoundLevelMeterSensorEq */ 214 | 215 | void SoundLevelMeterSensorEq::process(std::vector &buffer) { 216 | // as adding small floating point numbers with large ones might lead 217 | // to precision loss, we first accumulate local sum for entire buffer 218 | // and only in the end add it to global sum which could become quite large 219 | // for large accumulating periods (like 1 hour), therefore global sum (this->sum_) 220 | // is of type double 221 | float local_sum = 0; 222 | for (int i = 0; i < buffer.size(); i++) { 223 | local_sum += buffer[i] * buffer[i]; 224 | this->count_++; 225 | if (this->count_ == this->update_samples_) { 226 | float dB = 10 * log10((sum_ + local_sum) / count_); 227 | dB = this->adjust_dB(dB); 228 | this->defer_publish_state(dB); 229 | this->sum_ = 0; 230 | this->count_ = 0; 231 | local_sum = 0; 232 | } 233 | } 234 | this->sum_ += local_sum; 235 | } 236 | 237 | void SoundLevelMeterSensorEq::reset() { 238 | this->sum_ = 0.; 239 | this->count_ = 0; 240 | this->defer_publish_state(NAN); 241 | } 242 | 243 | /* SoundLevelMeterSensorMax */ 244 | 245 | void SoundLevelMeterSensorMax::set_window_size(uint32_t window_size) { 246 | this->window_samples_ = this->parent_->get_sample_rate() * (window_size / 1000.f); 247 | } 248 | 249 | void SoundLevelMeterSensorMax::process(std::vector &buffer) { 250 | for (int i = 0; i < buffer.size(); i++) { 251 | this->sum_ += buffer[i] * buffer[i]; 252 | this->count_sum_++; 253 | if (this->count_sum_ == this->window_samples_) { 254 | this->max_ = std::max(this->max_, this->sum_ / this->count_sum_); 255 | this->sum_ = 0.f; 256 | this->count_sum_ = 0; 257 | } 258 | this->count_max_++; 259 | if (this->count_max_ == this->update_samples_) { 260 | float dB = 10 * log10(this->max_); 261 | dB = this->adjust_dB(dB); 262 | this->defer_publish_state(dB); 263 | this->max_ = std::numeric_limits::min(); 264 | this->count_max_ = 0; 265 | } 266 | } 267 | } 268 | 269 | void SoundLevelMeterSensorMax::reset() { 270 | this->sum_ = 0.f; 271 | this->max_ = std::numeric_limits::min(); 272 | this->count_max_ = 0; 273 | this->count_sum_ = 0; 274 | this->defer_publish_state(NAN); 275 | } 276 | 277 | /* SoundLevelMeterSensorMin */ 278 | 279 | void SoundLevelMeterSensorMin::set_window_size(uint32_t window_size) { 280 | this->window_samples_ = this->parent_->get_sample_rate() * (window_size / 1000.f); 281 | } 282 | 283 | void SoundLevelMeterSensorMin::process(std::vector &buffer) { 284 | for (int i = 0; i < buffer.size(); i++) { 285 | this->sum_ += buffer[i] * buffer[i]; 286 | this->count_sum_++; 287 | if (this->count_sum_ == this->window_samples_) { 288 | this->min_ = std::min(this->min_, this->sum_ / this->count_sum_); 289 | this->sum_ = 0.f; 290 | this->count_sum_ = 0; 291 | } 292 | this->count_min_++; 293 | if (this->count_min_ == this->update_samples_) { 294 | float dB = 10 * log10(this->min_); 295 | dB = this->adjust_dB(dB); 296 | this->defer_publish_state(dB); 297 | this->min_ = std::numeric_limits::max(); 298 | this->count_min_ = 0; 299 | } 300 | } 301 | } 302 | 303 | void SoundLevelMeterSensorMin::reset() { 304 | this->sum_ = 0.f; 305 | this->min_ = std::numeric_limits::max(); 306 | this->count_min_ = 0; 307 | this->count_sum_ = 0; 308 | this->defer_publish_state(NAN); 309 | } 310 | 311 | /* SoundLevelMeterSensorPeak */ 312 | 313 | void SoundLevelMeterSensorPeak::process(std::vector &buffer) { 314 | for (int i = 0; i < buffer.size(); i++) { 315 | this->peak_ = std::max(this->peak_, abs(buffer[i])); 316 | this->count_++; 317 | if (this->count_ == this->update_samples_) { 318 | float dB = 20 * log10(this->peak_); 319 | dB = this->adjust_dB(dB, false); 320 | this->defer_publish_state(dB); 321 | this->peak_ = 0.f; 322 | this->count_ = 0; 323 | } 324 | } 325 | } 326 | 327 | void SoundLevelMeterSensorPeak::reset() { 328 | this->peak_ = 0.f; 329 | this->count_ = 0; 330 | this->defer_publish_state(NAN); 331 | } 332 | 333 | /* SOS_Filter */ 334 | 335 | SOS_Filter::SOS_Filter(std::initializer_list> &&coeffs) { 336 | this->coeffs_.resize(coeffs.size()); 337 | this->state_.resize(coeffs.size(), {}); 338 | int i = 0; 339 | for (auto &row : coeffs) 340 | std::copy(row.begin(), row.end(), coeffs_[i++].begin()); 341 | } 342 | 343 | // direct form 2 transposed 344 | void SOS_Filter::process(std::vector &data) { 345 | int n = data.size(); 346 | int m = this->coeffs_.size(); 347 | for (int j = 0; j < m; j++) { 348 | for (int i = 0; i < n; i++) { 349 | // y[i] = b0 * x[i] + s0 350 | float yi = this->coeffs_[j][0] * data[i] + this->state_[j][0]; 351 | // s0 = b1 * x[i] - a1 * y[i] + s1 352 | this->state_[j][0] = this->coeffs_[j][1] * data[i] - this->coeffs_[j][3] * yi + this->state_[j][1]; 353 | // s1 = b2 * x[i] - a2 * y[i] 354 | this->state_[j][1] = this->coeffs_[j][2] * data[i] - this->coeffs_[j][4] * yi; 355 | 356 | data[i] = yi; 357 | } 358 | } 359 | } 360 | 361 | void SOS_Filter::reset() { 362 | for (auto &s : this->state_) 363 | s = {0.f, 0.f}; 364 | } 365 | } // namespace sound_level_meter 366 | } // namespace esphome 367 | --------------------------------------------------------------------------------