ESPHome 2026.10.2
Loading...
Searching...
No Matches
i2s_audio_speaker.cpp
Go to the documentation of this file.
1#include "i2s_audio_speaker.h"
2
3#ifdef USE_ESP32
4
5#include <driver/gpio.h>
6#include <driver/i2s_std.h>
7
10
12#include "esphome/core/hal.h"
13#include "esphome/core/log.h"
14
15#include "esp_timer.h"
16
17#include <cmath>
18
19namespace esphome::i2s_audio {
20
21static const char *const TAG = "i2s_audio.speaker";
22
23// Software volume control maps the user-facing (0.0, 1.0) range linearly to a dB reduction in
24// [-49.0, 0.0] dB; 0.0 is silence.
25static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f;
26
27// Rate at which the software gain moves toward a new target.
28static constexpr uint32_t GAIN_RAMP_MS_PER_DB = 1;
29
31 this->event_group_ = xEventGroupCreate();
32
33 if (this->event_group_ == nullptr) {
34 ESP_LOGE(TAG, "Event group creation failed");
35 this->mark_failed();
36 return;
37 }
38
39 // Initialize volume control. When audio_dac is configured, this sets the DAC volume and mute state.
40 // When no audio_dac is configured, this initializes software volume control.
41 this->set_volume(this->volume_);
42 this->set_mute_state(this->mute_state_);
43
44 // Until the I2S driver first starts, dout sits in its reset state (often pulled high, or a JTAG
45 // function on the ESP32-S3), which keeps a SPDIF optical transmitter lit. Park it low now.
46 this->park_dout_pin_();
47}
48
49void I2SAudioSpeakerBase::park_dout_pin_() {
50 gpio_reset_pin(this->dout_pin_);
51 gpio_set_direction(this->dout_pin_, GPIO_MODE_OUTPUT);
52 gpio_set_level(this->dout_pin_, 0);
53}
54
56 ESP_LOGCONFIG(TAG,
57 "Speaker:\n"
58 " Pin: %d\n"
59 " Buffer duration: %" PRIu32,
60 static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_);
61 if (this->timeout_.has_value()) {
62 ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value());
63 }
64}
65
67 uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
68
69 // A stop that arrives while stopped cancels any start that has not been processed yet
71 if ((event_group_bits & stop_bits) && (this->state_ == speaker::STATE_STOPPED)) {
72 xEventGroupClearBits(this->event_group_, stop_bits | SpeakerEventGroupBits::COMMAND_START);
73 event_group_bits &= ~(stop_bits | SpeakerEventGroupBits::COMMAND_START);
74 }
75
76 if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) {
78 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
79 }
80
81 // Handle the task's state
82 if (event_group_bits & SpeakerEventGroupBits::TASK_STARTING) {
83 ESP_LOGD(TAG, "Starting");
84 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
85 }
86 if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) {
87 ESP_LOGV(TAG, "Started");
88 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
90#ifdef USE_AUDIO_DAC
91 // The task raises TASK_RUNNING only after enabling the channel, so the clocks are already running
92 if (this->audio_dac_ != nullptr) {
94 }
95#endif // USE_AUDIO_DAC
96 }
97 if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
98 ESP_LOGV(TAG, "Stopping");
99 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
101 }
102 if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPED) {
103 ESP_LOGD(TAG, "Stopped");
104
105 vTaskDelete(this->speaker_task_handle_);
106 this->speaker_task_handle_ = nullptr;
107
108 this->stop_i2s_driver_();
109 // ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at
110 // the top of loop(): the audio source's task can raise a start at any point above, including
111 // during stop_i2s_driver_(), and nothing would ever re-issue it.
112 const EventBits_t bits_before_clear = xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
113 if (bits_before_clear & SpeakerEventGroupBits::COMMAND_START) {
114 ESP_LOGD(TAG, "Start requested while stopping; keeping the request");
115 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
116 }
117 this->status_clear_error();
118
119 this->on_task_stopped();
120
122 }
123
124 if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) {
125 ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)");
126 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
127 }
128
129 // Handle the speaker's state
130 switch (this->state_) {
132 if (this->status_has_error()) {
133 break;
134 }
135
136 // Still starting up or winding down from a previous run
137 if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) {
138 break;
139 }
140
141 if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) {
142 ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
143 this->status_momentary_error("driver-failure", 1000);
144 break;
145 }
146
147 // Seed the ramp at the live target so this run adopts it instantly rather than fading to it
148 // from wherever the previous run left off. Posted here, not in the task: the ramp's mailbox
149 // allows one writer, and that is the main loop.
150 this->post_software_gain_(0);
151 xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
152 &this->speaker_task_handle_);
153
154 if (this->speaker_task_handle_ == nullptr) {
155 ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
156 this->status_momentary_error("task-failure", 1000);
157 this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
158 }
159 break;
160 case speaker::STATE_RUNNING: // Intentional fallthrough
161 case speaker::STATE_STOPPING: // Intentional fallthrough
163 break;
164 }
165}
166
167void I2SAudioSpeakerBase::set_volume(float volume) {
169 this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
170}
171
172void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
174 this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
175}
176
177void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) {
178#ifdef USE_AUDIO_DAC
179 if (this->audio_dac_ != nullptr) {
180 return; // Hardware volume; the ramp stays at unity
181 }
182#endif // USE_AUDIO_DAC
183 // Software volume control. The ramp treats 0 dB as unity and skips processing there.
184 float target_db;
185 if (this->is_silent_()) {
186 target_db = -INFINITY;
187 } else if (this->volume_ >= 1.0f) {
188 target_db = 0.0f;
189 } else {
190 target_db = remap<float, float>(this->volume_, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f);
191 }
192 this->gain_ramp_.set_target_db_at_rate(target_db, rate_samples);
193}
194
195size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
196 if (this->is_failed()) {
197 ESP_LOGE(TAG, "Setup failed; cannot play audio");
198 return 0;
199 }
200
202 this->start();
203 }
204
205 if (this->state_ != speaker::STATE_RUNNING) {
206 // Unable to write data to a running speaker, so delay the max amount of time so it can get ready
207 vTaskDelay(ticks_to_wait);
208 ticks_to_wait = 0;
209 }
210
211 size_t bytes_written = 0;
212 if (this->state_ == speaker::STATE_RUNNING) {
213 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
214 if (temp_ring_buffer != nullptr) {
215 // The weak_ptr locks successfully only while the speaker task owns the ring buffer, so it is safe to write
216 bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
217 }
218 }
219
220 return bytes_written;
221}
222
224 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
225 if (temp_ring_buffer != nullptr) {
226 return temp_ring_buffer->available() > 0;
227 }
228 return false;
229}
230
231void I2SAudioSpeakerBase::speaker_task(void *params) {
232 I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) params;
233 this_speaker->run_speaker_task();
234}
235
237 if (!this->is_ready() || this->is_failed() || this->status_has_error())
238 return;
239 if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
240 return;
241
242 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
243}
244
245void I2SAudioSpeakerBase::stop() { this->stop_(false); }
246
247void I2SAudioSpeakerBase::finish() { this->stop_(true); }
248
249void I2SAudioSpeakerBase::stop_(bool wait_on_empty) {
250 if (!this->is_ready() || this->is_failed())
251 return;
252
253 // Always set the bit, even when stopped, so loop() can cancel a start that is still pending
254 if (wait_on_empty) {
256 } else {
257 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
258 }
259}
260
261esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg,
262 size_t event_queue_size) {
263 esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL);
264 if (err != ESP_OK) {
265 ESP_LOGE(TAG, "I2S channel allocation failed: %s", esp_err_to_name(err));
266 this->parent_->unlock();
267 return err;
268 }
269
270 err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
271 if (err != ESP_OK) {
272 ESP_LOGE(TAG, "Failed to initialize I2S channel");
273 i2s_del_channel(this->tx_handle_);
274 this->tx_handle_ = nullptr;
275 this->parent_->unlock();
276 return err;
277 }
278
279 if (this->i2s_event_queue_ == nullptr) {
280 this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t));
281 } else {
282 // Reset queue to clear any stale events from previous task
283 xQueueReset(this->i2s_event_queue_);
284 }
285
286 // Lockstep records queue. One record per in-flight DMA buffer; sized to match the I2S event queue
287 // so a fully-saturated DMA pipeline cannot overflow either side before drain.
288 if (this->write_records_queue_ == nullptr) {
289 this->write_records_queue_ = xQueueCreate(event_queue_size, sizeof(uint32_t));
290 } else {
291 xQueueReset(this->write_records_queue_);
292 }
293
294 if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) {
295 ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)");
296 i2s_del_channel(this->tx_handle_);
297 this->tx_handle_ = nullptr;
298 this->parent_->unlock();
299 return ESP_ERR_NO_MEM;
300 }
301
302 return ESP_OK;
303}
304
305void I2SAudioSpeakerBase::stop_i2s_driver_() {
306 if (this->tx_handle_ != nullptr) {
307 i2s_channel_disable(this->tx_handle_);
308 i2s_del_channel(this->tx_handle_);
309 this->tx_handle_ = nullptr;
310
311 // i2s_del_channel() leaves dout wired to this port's data-out signal in the GPIO matrix: it only
312 // clears an internal reservation mask, never the esp_rom_gpio_connect_out_signal() routing that
313 // setup installed. If another speaker reuses this port (shared bus), its audio still reaches our
314 // dout. Detach the pin and drive it low so a stale output stops driving downstream hardware: a
315 // SPDIF optical transmitter would otherwise stay lit, and an analog DAC would emit noise.
316 this->park_dout_pin_();
317 }
318 this->parent_->unlock();
319}
320
321bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
322 int64_t now = esp_timer_get_time();
323
324 BaseType_t need_yield1 = pdFALSE;
325 BaseType_t need_yield2 = pdFALSE;
326 BaseType_t need_yield3 = pdFALSE;
327
328 I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx;
329
330 if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
331 // Queue is full, so discard the oldest event. The lockstep queues are now desynced; the task resyncs them.
332 int64_t dummy;
333 xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
334 xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT, &need_yield2);
335 }
336
337 xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
338
339 return need_yield1 | need_yield2 | need_yield3;
340}
341
343 // Stop DMA so no more completion events arrive while the queues are rebuilt
344 i2s_channel_disable(this->tx_handle_);
345 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT);
346
347 uint32_t frames = extra_frames;
348 uint32_t record_frames = 0;
349 while (xQueueReceive(this->write_records_queue_, &record_frames, 0) == pdTRUE) {
350 frames += record_frames;
351 }
352 xQueueReset(this->i2s_event_queue_);
353
354 if (frames > 0) {
355 ESP_LOGV(TAG, "Crediting %" PRIu32 " dropped frames as played", frames);
357 }
358}
359
360void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
361#ifdef USE_AUDIO_DAC
362 if (this->audio_dac_ != nullptr) {
363 return; // Hardware volume; the ramp is never targeted
364 }
365#endif // USE_AUDIO_DAC
366 const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1);
367 this->gain_ramp_.process(data, static_cast<uint8_t>(bytes_per_sample),
368 this->current_stream_info_.bytes_to_samples(bytes_read));
369}
370
371void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
372#ifdef USE_ESP32_VARIANT_ESP32
373 // For ESP32 16-bit mono mode, adjacent samples need to be swapped.
374 if (this->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16) {
375 int16_t *samples = reinterpret_cast<int16_t *>(data);
376 size_t sample_count = bytes_read / sizeof(int16_t);
377 for (size_t i = 0; i + 1 < sample_count; i += 2) {
378 int16_t tmp = samples[i];
379 samples[i] = samples[i + 1];
380 samples[i + 1] = tmp;
381 }
382 }
383#endif // USE_ESP32_VARIANT_ESP32
384}
385
386} // namespace esphome::i2s_audio
387
388#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
void status_momentary_error(const char *name, uint32_t length=5000)
Set error status flag and automatically clear it after a timeout.
bool is_failed() const
Definition component.h:272
void status_clear_error()
Definition component.h:295
bool is_ready() const
bool status_has_error() const
Definition component.h:280
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
uint8_t get_channels() const
Definition audio.h:29
uint32_t bytes_to_samples(size_t bytes) const
Convert bytes to samples.
Definition audio.h:48
virtual void on_audio_started()
Called from the main loop by the speaker driving this DAC once its audio clocks are running.
Definition audio_dac.h:19
void drain_lockstep_(uint32_t extra_frames)
Plays the provided audio data.
esp_audio_libs::gain::GainRamp gain_ramp_
void apply_software_volume_(uint8_t *data, size_t bytes_read)
Apply software volume control by running the samples through the gain ramp.
std::weak_ptr< ring_buffer::RingBuffer > audio_ring_buffer_
void swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read)
Swap adjacent 16-bit mono samples for ESP32 (non-variant) hardware quirk.
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
virtual void set_volume(float volume)
Definition speaker.h:74
audio_dac::AudioDac * audio_dac_
Definition speaker.h:135
CallbackManager< void(uint32_t, int64_t)> audio_output_callback_
Definition speaker.h:138
virtual void set_mute_state(bool mute_state)
Definition speaker.h:85
virtual bool has_buffered_data() const =0
audio::AudioStreamInfo audio_stream_info_
Definition speaker.h:131
bool is_silent_() const
Whether the output should be silent: muted, or the volume is effectively zero.
Definition speaker.h:114
T remap(U value, U min, U max, T min_out, T max_out)
Remap value from the range (min, max) to (min_out, max_out).
Definition helpers.h:884
int64_t esp_timer_get_time(void)
static void uint32_t
uint16_t length
Definition tt21100.cpp:0
spi_device_handle_t handle