ESPHome 2025.5.0
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
10namespace esphome {
11namespace sound_level {
12
13static const char *const TAG = "sound_level";
14
15static const uint32_t AUDIO_BUFFER_DURATION_MS = 30;
16static const uint32_t RING_BUFFER_DURATION_MS = 120;
17
18// Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2
19static const double MAX_SAMPLE_SQUARED_DENOMINATOR = INT16_MIN * INT16_MIN;
20
22 ESP_LOGCONFIG(TAG, "Sound Level Component:");
23 ESP_LOGCONFIG(TAG, " Measurement Duration: %" PRIu32 " ms", measurement_duration_ms_);
24 LOG_SENSOR(" ", "Peak:", this->peak_sensor_);
25
26 LOG_SENSOR(" ", "RMS:", this->rms_sensor_);
27}
28
30 this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
31 std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
32 if (this->ring_buffer_.use_count() == 2) {
33 // ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write
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("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 // Copy data from ring buffer into the transfer buffer - don't block to avoid slowing the main loop
84 this->audio_buffer_->transfer_data_from_source(0);
85
86 if (this->audio_buffer_->available() == 0) {
87 // No new audio available for processing
88 return;
89 }
90
91 const uint32_t samples_in_window =
93 const uint32_t samples_available_to_process =
95 const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process);
96
97 // MicrophoneSource always provides int16 samples due to Python codegen settings
98 const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_buffer_->get_buffer_start());
99
100 // Process all the new audio samples
101 for (uint32_t i = 0; i < samples_to_process; ++i) {
102 // Squaring int16 samples won't overflow an int32
103 int32_t squared_sample = static_cast<int32_t>(audio_data[i]) * static_cast<int32_t>(audio_data[i]);
104
105 if (this->peak_sensor_ != nullptr) {
106 this->squared_peak_ = std::max(this->squared_peak_, squared_sample);
107 }
108
109 if (this->rms_sensor_ != nullptr) {
110 // Squared sum is an uint64 type - at max levels, an uint32 type would overflow after ~8 samples
111 this->squared_samples_sum_ += squared_sample;
112 }
113
114 ++this->sample_count_;
115 }
116
117 // Remove the processed samples from ``audio_buffer_``
118 this->audio_buffer_->decrease_buffer_length(
119 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_buffer_ != nullptr) {
161 return true;
162 }
163
164 // Allocate a transfer buffer
166 this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS));
167 if (this->audio_buffer_ == nullptr) {
168 this->status_momentary_error("Failed to allocate transfer buffer", 15000);
169 return false;
170 }
171
172 // Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it
173 this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation
174 std::shared_ptr<RingBuffer> temp_ring_buffer =
176 if (temp_ring_buffer.use_count() == 0) {
177 this->status_momentary_error("Failed to allocate ring buffer", 15000);
178 this->stop_();
179 return false;
180 } else {
181 this->ring_buffer_ = temp_ring_buffer;
182 this->audio_buffer_->set_source(temp_ring_buffer);
183 }
184
185 this->status_clear_error();
186 return true;
187}
188
190
191} // namespace sound_level
192} // namespace esphome
193
194#endif
void status_momentary_error(const std::string &name, uint32_t length=5000)
bool status_has_warning() const
bool status_has_error() const
void status_set_warning(const char *message="unspecified")
void status_clear_warning()
static std::unique_ptr< RingBuffer > create(size_t len)
static std::unique_ptr< AudioSourceTransferBuffer > create(size_t buffer_size)
Creates a new source transfer buffer.
size_t ms_to_bytes(uint32_t ms) const
Converts duration to bytes.
Definition audio.h:73
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
void add_data_callback(std::function< void(const std::vector< uint8_t > &)> &&data_callback)
audio::AudioStreamInfo get_audio_stream_info()
Gets the AudioStreamInfo of the data after processing.
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:39
void start()
Starts the MicrophoneSource to start measuring sound levels.
std::unique_ptr< audio::AudioSourceTransferBuffer > audio_buffer_
Definition sound_level.h:51
microphone::MicrophoneSource * microphone_source_
Definition sound_level.h:46
void stop()
Stops the MicrophoneSource.
std::weak_ptr< RingBuffer > ring_buffer_
Definition sound_level.h:52
void stop_()
Internal start command that, if necessary, allocates audio_buffer_ and a ring buffer which / audio_bu...
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7