ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
sound_level.cpp
Go to the documentation of this file.
1#include "sound_level.h"
2
3#ifdef USE_ESP32
4
5#include "esphome/core/log.h"
6
7#include <cmath>
8#include <cstdint>
9
11
12static const char *const TAG = "sound_level";
13
14static const uint32_t MAX_FILL_DURATION_MS = 30;
15static const uint32_t RING_BUFFER_DURATION_MS = 120;
16
17// Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2
18static const double MAX_SAMPLE_SQUARED_DENOMINATOR = INT16_MIN * INT16_MIN;
19
21 ESP_LOGCONFIG(TAG,
22 "Sound Level Component:\n"
23 " Measurement Duration: %" PRIu32 " ms",
25 LOG_SENSOR(" ", "Peak:", this->peak_sensor_);
26
27 LOG_SENSOR(" ", "RMS:", this->rms_sensor_);
28}
29
31 this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
32 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
33 if (temp_ring_buffer != nullptr) {
34 temp_ring_buffer->write((void *) data.data(), data.size());
35 }
36 });
37
38 if (!this->microphone_source_->is_passive()) {
39 // Automatically start the microphone if not in passive mode
41 }
42}
43
45 if ((this->peak_sensor_ == nullptr) && (this->rms_sensor_ == nullptr)) {
46 // No sensors configured, nothing to do
47 return;
48 }
49
50 if (this->microphone_source_->is_running() && !this->status_has_error()) {
51 // Allocate buffers
52 if (this->start_()) {
54 }
55 } else {
56 if (!this->status_has_warning()) {
57 this->status_set_warning(LOG_STR("Microphone isn't running, can't compute statistics"));
58
59 // Deallocate buffers, if necessary
60 this->stop_();
61
62 // Reset sensor outputs
63 if (this->peak_sensor_ != nullptr) {
64 this->peak_sensor_->publish_state(NAN);
65 }
66 if (this->rms_sensor_ != nullptr) {
67 this->rms_sensor_->publish_state(NAN);
68 }
69
70 // Reset accumulators
71 this->squared_peak_ = 0;
72 this->squared_samples_sum_ = 0;
73 this->sample_count_ = 0;
74 }
75
76 return;
77 }
78
79 if (this->status_has_error()) {
80 return;
81 }
82
83 // Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop.
84 // pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
85 this->audio_source_->fill(0, false);
86
87 if (this->audio_source_->available() == 0) {
88 // No new audio available for processing
89 return;
90 }
91
92 const uint32_t samples_in_window =
94 const uint32_t samples_available_to_process =
96 const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process);
97
98 // MicrophoneSource always provides int16 samples due to Python codegen settings
99 const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_source_->data());
100
101 // Process all the new audio samples
102 for (uint32_t i = 0; i < samples_to_process; ++i) {
103 // Squaring int16 samples won't overflow an int32
104 int32_t squared_sample = static_cast<int32_t>(audio_data[i]) * static_cast<int32_t>(audio_data[i]);
105
106 if (this->peak_sensor_ != nullptr) {
107 this->squared_peak_ = std::max(this->squared_peak_, squared_sample);
108 }
109
110 if (this->rms_sensor_ != nullptr) {
111 // Squared sum is an uint64 type - at max levels, an uint32 type would overflow after ~8 samples
112 this->squared_samples_sum_ += squared_sample;
113 }
114
115 ++this->sample_count_;
116 }
117
118 // Remove the processed samples from ``audio_source_``
119 this->audio_source_->consume(this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process));
120
121 if (this->sample_count_ == samples_in_window) {
122 // Processed enough samples for the measurement window, compute and publish the sensor values
123 if (this->peak_sensor_ != nullptr) {
124 const float peak_db = 10.0f * log10(static_cast<float>(this->squared_peak_) / MAX_SAMPLE_SQUARED_DENOMINATOR);
125 this->peak_sensor_->publish_state(peak_db);
126
127 this->squared_peak_ = 0; // reset accumulator
128 }
129
130 if (this->rms_sensor_ != nullptr) {
131 // Calculations are done with doubles instead of floats - floats lose precision for even modest window durations
132 const double rms_db = 10.0 * log10((this->squared_samples_sum_ / MAX_SAMPLE_SQUARED_DENOMINATOR) /
133 static_cast<double>(samples_in_window));
134 this->rms_sensor_->publish_state(rms_db);
135
136 this->squared_samples_sum_ = 0; // reset accumulator
137 }
138
139 this->sample_count_ = 0; // reset counter
140 }
141}
142
144 if (this->microphone_source_->is_passive()) {
145 ESP_LOGW(TAG, "Can't start the microphone in passive mode");
146 return;
147 }
148 this->microphone_source_->start();
149}
150
152 if (this->microphone_source_->is_passive()) {
153 ESP_LOGW(TAG, "Can't stop microphone in passive mode");
154 return;
155 }
156 this->microphone_source_->stop();
157}
158
159bool SoundLevelComponent::start_() {
160 if (this->audio_source_ != nullptr) {
161 return true;
162 }
163
164 const auto &stream_info = this->microphone_source_->get_audio_stream_info();
165 const size_t bytes_per_frame = stream_info.frames_to_bytes(1);
166
167 // Allocate a ring buffer for the microphone callback to write into. Round the size down to a multiple
168 // of bytes_per_frame so the wrap boundary stays frame-aligned and avoids unnecessary single-frame splices.
169 this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation
170 const size_t ring_buffer_size =
171 (stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame;
172 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
173 if (temp_ring_buffer == nullptr) {
174 this->status_momentary_error("ring_buffer", 15000);
175 return false;
176 }
177
178 // Zero-copy source that reads directly from the ring buffer's internal storage. Frame-aligned reads
179 // ensure multi-channel frames are never split across the ring buffer's wrap boundary.
181 temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast<uint8_t>(bytes_per_frame));
182 if (this->audio_source_ == nullptr) {
183 this->status_momentary_error("audio_source", 15000);
184 return false;
185 }
186 this->ring_buffer_ = temp_ring_buffer;
187
188 this->status_clear_error();
189 return true;
190}
191
193
194} // namespace esphome::sound_level
195
196#endif
void status_momentary_error(const char *name, uint32_t length=5000)
Set error status flag and automatically clear it after a timeout.
void status_clear_error()
Definition component.h:295
bool status_has_warning() const
Definition component.h:278
bool status_has_error() const
Definition component.h:280
void status_clear_warning()
Definition component.h:289
size_t frames_to_bytes(uint32_t frames) const
Converts frames to bytes.
Definition audio.h:53
uint32_t ms_to_samples(uint32_t ms) const
Converts duration to samples.
Definition audio.h:68
size_t samples_to_bytes(uint32_t samples) const
Converts samples to bytes.
Definition audio.h:58
uint32_t bytes_to_samples(size_t bytes) const
Convert bytes to samples.
Definition audio.h:48
static std::unique_ptr< RingBufferAudioSource > create(std::shared_ptr< ring_buffer::RingBuffer > ring_buffer, size_t max_fill_bytes, uint8_t alignment_bytes=1)
Creates a new ring-buffer-backed audio source after validating its parameters.
void add_data_callback(F &&data_callback)
audio::AudioStreamInfo get_audio_stream_info()
Gets the AudioStreamInfo of the data after processing.
static std::unique_ptr< RingBuffer > create(size_t len, MemoryPreference preference=MemoryPreference::EXTERNAL_FIRST)
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
void start()
Starts the MicrophoneSource to start measuring sound levels.
std::weak_ptr< ring_buffer::RingBuffer > ring_buffer_
Definition sound_level.h:53
microphone::MicrophoneSource * microphone_source_
Definition sound_level.h:47
void stop()
Stops the MicrophoneSource.
void stop_()
Internal start command that, if necessary, allocates a ring buffer and a zero-copy RingBufferAudioSou...
std::unique_ptr< audio::RingBufferAudioSource > audio_source_
Definition sound_level.h:52
static void uint32_t