ESPHome 2026.11.0-dev
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
21ESPHOME_LOG_TAG(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#ifdef USE_I2S_AUDIO_FULL_DUPLEX
45 // The parent set up before us and already routed dout to the shared TX channel, which is never re-initialized.
46 // Parking would detach the pin for good. SPDIF speakers cannot use a full duplex bus.
47 if (this->parent_->is_full_duplex()) {
48 if (this->parent_->is_failed()) {
49 this->mark_failed(LOG_STR("Full duplex bus setup failed"));
50 }
51 return;
52 }
53#endif
54
55 // Until the I2S driver first starts, dout sits in its reset state (often pulled high, or a JTAG
56 // function on the ESP32-S3), which keeps a SPDIF optical transmitter lit. Park it low now.
57 this->park_dout_pin_();
58}
59
60void I2SAudioSpeakerBase::park_dout_pin_() {
61 gpio_reset_pin(this->dout_pin_);
62 gpio_set_direction(this->dout_pin_, GPIO_MODE_OUTPUT);
63 gpio_set_level(this->dout_pin_, 0);
64}
65
67 ESP_LOGCONFIG(TAG,
68 "Speaker:\n"
69 " Pin: %d\n"
70 " Buffer duration: %" PRIu32 "\n"
71 " Keep alive: %s",
72 static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_, YESNO(this->keep_alive_));
73 if (this->timeout_.has_value()) {
74 ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value());
75 }
76}
77
79 uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
80
81 // A stop that arrives while stopped cancels any start that has not been processed yet
83 if ((event_group_bits & stop_bits) && (this->state_ == speaker::STATE_STOPPED)) {
84 xEventGroupClearBits(this->event_group_, stop_bits | SpeakerEventGroupBits::COMMAND_START);
85 event_group_bits &= ~(stop_bits | SpeakerEventGroupBits::COMMAND_START);
86 }
87
88 if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) {
90 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
91 }
92
93 // Handle the task's state
94 if (event_group_bits & SpeakerEventGroupBits::TASK_STARTING) {
95 ESP_LOGD(TAG, "Starting");
96 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
97 }
98 if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) {
99 ESP_LOGV(TAG, "Started");
100 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
102#ifdef USE_AUDIO_DAC
103 // The task raises TASK_RUNNING only after enabling the channel, so the clocks are already running
104 if (this->audio_dac_ != nullptr) {
106 }
107#endif // USE_AUDIO_DAC
108 }
109 if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
110 ESP_LOGV(TAG, "Stopping");
111 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
113 }
114 if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPED) {
115 ESP_LOGD(TAG, "Stopped");
116
117 vTaskDelete(this->speaker_task_handle_);
118 this->speaker_task_handle_ = nullptr;
119
120 // The task raises this bit before it exits, possibly a loop pass before TASK_STOPPED is seen, so read it
121 // live here; a channel the task could not bring up is rebuilt next time rather than kept
122 const bool task_failed = xEventGroupGetBits(this->event_group_) & SpeakerEventGroupBits::ERR_ESP_NO_MEM;
123 if (task_failed) {
124 ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)");
125 }
126 this->stop_i2s_driver_(this->keep_alive_ && !task_failed);
127 // ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at
128 // the top of loop(): the audio source's task can raise a start at any point above, including
129 // during stop_i2s_driver_(), and nothing would ever re-issue it.
130 const EventBits_t bits_before_clear = xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
131 if (bits_before_clear & SpeakerEventGroupBits::COMMAND_START) {
132 ESP_LOGD(TAG, "Start requested while stopping; keeping the request");
133 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
134 }
135 this->status_clear_error();
136
137 this->on_task_stopped();
138
140 }
141
142 // Handle the speaker's state
143 switch (this->state_) {
145 if (this->status_has_error()) {
146 break;
147 }
148
149 // Still winding down from a previous run
150 if (this->speaker_task_handle_ != nullptr) {
151 break;
152 }
153
154#ifdef USE_I2S_AUDIO_FULL_DUPLEX
155 if (this->parent_->is_tx_in_use()) {
156 // Another speaker on the bus is playing; wait for it to release the channel
157 break;
158 }
159#endif
160 const esp_err_t err = this->start_i2s_driver(this->audio_stream_info_);
161 if (err == ESP_ERR_NOT_SUPPORTED) {
162 // Retrying cannot help; stay stopped so the audio source gives up instead of waiting forever
163 ESP_LOGE(TAG, "Stream format not supported; not starting");
166 break;
167 }
168 if (err != ESP_OK) {
169 ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
170 this->status_momentary_error(1000);
171 break;
172 }
173
174 // Seed the ramp at the live target so this run adopts it instantly rather than fading to it
175 // from wherever the previous run left off. Posted here, not in the task: the ramp's mailbox
176 // allows one writer, and that is the main loop.
177 this->post_software_gain_(0);
178 xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
179 &this->speaker_task_handle_);
180
181 if (this->speaker_task_handle_ == nullptr) {
182 ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
183 this->status_momentary_error(1000);
184 this->stop_i2s_driver_(false); // Stops the driver to return the lock; will be reloaded in next attempt
185 }
186 break;
187 }
188 case speaker::STATE_RUNNING: // Intentional fallthrough
189 case speaker::STATE_STOPPING: // Intentional fallthrough
191 break;
192 }
193}
194
195void I2SAudioSpeakerBase::set_volume(float volume) {
197 this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
198}
199
200void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
202 this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
203}
204
205void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) {
206#ifdef USE_AUDIO_DAC
207 if (this->audio_dac_ != nullptr) {
208 return; // Hardware volume; the ramp stays at unity
209 }
210#endif // USE_AUDIO_DAC
211 // Software volume control. The ramp treats 0 dB as unity and skips processing there.
212 float target_db;
213 if (this->is_silent_()) {
214 target_db = -INFINITY;
215 } else if (this->volume_ >= 1.0f) {
216 target_db = 0.0f;
217 } else {
218 target_db = remap<float, float>(this->volume_, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f);
219 }
220 this->gain_ramp_.set_target_db_at_rate(target_db, rate_samples);
221}
222
223size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
224 if (this->is_failed()) {
225 ESP_LOGE(TAG, "Setup failed; cannot play audio");
226 return 0;
227 }
228
230 this->start();
231 }
232
233 if (this->state_ != speaker::STATE_RUNNING) {
234 // Unable to write data to a running speaker, so delay the max amount of time so it can get ready
235 vTaskDelay(ticks_to_wait);
236 ticks_to_wait = 0;
237 }
238
239 size_t bytes_written = 0;
240 if (this->state_ == speaker::STATE_RUNNING) {
241 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
242 if (temp_ring_buffer != nullptr) {
243 // The weak_ptr locks successfully only while the speaker task owns the ring buffer, so it is safe to write
244 bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
245 }
246 }
247
248 return bytes_written;
249}
250
252 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
253 if (temp_ring_buffer != nullptr) {
254 return temp_ring_buffer->available() > 0;
255 }
256 return false;
257}
258
259void I2SAudioSpeakerBase::speaker_task(void *params) {
260 I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) params;
261 this_speaker->run_speaker_task();
262}
263
265 if (!this->is_ready() || this->is_failed() || this->status_has_error())
266 return;
267 if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
268 return;
269 // play() calls start() on every write, so without this a rejected stream would be retried and logged nonstop
270 if (this->rejected_stream_info_.has_value() && this->rejected_stream_info_.value() == this->audio_stream_info_)
271 return;
272
273 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
274}
275
276void I2SAudioSpeakerBase::stop() { this->stop_(false); }
277
278void I2SAudioSpeakerBase::finish() { this->stop_(true); }
279
280void I2SAudioSpeakerBase::stop_(bool wait_on_empty) {
281 if (!this->is_ready() || this->is_failed())
282 return;
283
284 // Always set the bit, even when stopped, so loop() can cancel a start that is still pending
285 if (wait_on_empty) {
287 } else {
288 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
289 }
290}
291
292esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg,
293 size_t event_queue_size) {
294 esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL);
295 if (err != ESP_OK) {
296 ESP_LOGE(TAG, "I2S channel allocation failed: %s", esp_err_to_name(err));
297 this->parent_->unlock();
298 return err;
299 }
300
301 err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
302 if (err != ESP_OK) {
303 ESP_LOGE(TAG, "Failed to initialize I2S channel");
304 this->delete_channel_();
305 this->parent_->unlock();
306 return err;
307 }
308
309 err = this->prepare_event_queues_(event_queue_size);
310 if (err != ESP_OK) {
311 this->delete_channel_();
312 this->parent_->unlock();
313 }
314 return err;
315}
316
317esp_err_t I2SAudioSpeakerBase::prepare_event_queues_(size_t event_queue_size) {
318 // Both queues are drained by the speaker task before it preloads, whether the channel is new or kept
319 if (this->i2s_event_queue_ == nullptr) {
320 this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t));
321 }
322
323 // Lockstep records queue. One record per in-flight DMA buffer; sized to match the I2S event queue
324 // so a fully-saturated DMA pipeline cannot overflow either side before drain.
325 if (this->write_records_queue_ == nullptr) {
326 this->write_records_queue_ = xQueueCreate(event_queue_size, sizeof(uint32_t));
327 }
328
329 if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) {
330 ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)");
331 return ESP_ERR_NO_MEM;
332 }
333
334 return ESP_OK;
335}
336
337#ifdef USE_I2S_AUDIO_FULL_DUPLEX
338esp_err_t I2SAudioSpeakerBase::acquire_full_duplex_channel_(size_t event_queue_size) {
339 this->tx_handle_ = this->parent_->acquire_tx_channel();
340 if (this->tx_handle_ == nullptr) {
341 ESP_LOGE(TAG, "Full duplex channel unavailable");
342 return ESP_ERR_INVALID_STATE;
343 }
344
345 esp_err_t err = this->prepare_event_queues_(event_queue_size);
346 if (err != ESP_OK) {
347 this->parent_->release_tx_channel();
348 this->tx_handle_ = nullptr;
349 }
350 return err;
351}
352#endif
353
354void I2SAudioSpeakerBase::stop_i2s_driver_(bool keep_channel) {
355#ifdef USE_I2S_AUDIO_FULL_DUPLEX
356 if (this->parent_->is_full_duplex()) {
357 // The parent owns the channel pair and its pins, so only hand the TX channel back
358 if (this->tx_handle_ != nullptr) {
359 this->parent_->release_tx_channel();
360 this->tx_handle_ = nullptr;
361 }
362 return;
363 }
364#endif
365
366 if (this->tx_handle_ != nullptr) {
367 i2s_channel_disable(this->tx_handle_);
368 // A kept channel lets the next start skip i2s_new_channel and i2s_channel_init_std_mode, whose GPIO and
369 // clock reconfiguration is the audible pop
370 if (!keep_channel) {
371 this->delete_channel_();
372 }
373 }
374 this->parent_->unlock();
375}
376
377void I2SAudioSpeakerBase::delete_channel_() {
378 i2s_del_channel(this->tx_handle_);
379 this->tx_handle_ = nullptr;
380
381 // i2s_del_channel() leaves dout wired to this port's data-out signal in the GPIO matrix: it only
382 // clears an internal reservation mask, never the esp_rom_gpio_connect_out_signal() routing that
383 // setup installed. If another speaker reuses this port (shared bus), its audio still reaches our
384 // dout. Detach the pin and drive it low so a stale output stops driving downstream hardware: a
385 // SPDIF optical transmitter would otherwise stay lit, and an analog DAC would emit noise.
386 this->park_dout_pin_();
387}
388
389bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
390 int64_t now = esp_timer_get_time();
391
392 BaseType_t need_yield1 = pdFALSE;
393 BaseType_t need_yield2 = pdFALSE;
394 BaseType_t need_yield3 = pdFALSE;
395
396 I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx;
397
398 if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
399 // Queue is full, so discard the oldest event. The lockstep queues are now desynced; the task resyncs them.
400 int64_t dummy;
401 xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
402 xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT, &need_yield2);
403 }
404
405 xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
406
407 return need_yield1 | need_yield2 | need_yield3;
408}
409
410void I2SAudioSpeakerBase::drain_lockstep_(uint32_t extra_frames) {
411 // Stop DMA so no more completion events arrive while the queues are rebuilt
412 i2s_channel_disable(this->tx_handle_);
413 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT);
414
415 uint32_t frames = extra_frames;
416 uint32_t record_frames = 0;
417 while (xQueueReceive(this->write_records_queue_, &record_frames, 0) == pdTRUE) {
418 frames += record_frames;
419 }
420 xQueueReset(this->i2s_event_queue_);
421
422 if (frames > 0) {
423 ESP_LOGV(TAG, "Crediting %" PRIu32 " dropped frames as played", frames);
425 }
426}
427
428void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
429#ifdef USE_AUDIO_DAC
430 if (this->audio_dac_ != nullptr) {
431 return; // Hardware volume; the ramp is never targeted
432 }
433#endif // USE_AUDIO_DAC
434 const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1);
435 this->gain_ramp_.process(data, static_cast<uint8_t>(bytes_per_sample),
436 this->current_stream_info_.bytes_to_samples(bytes_read));
437}
438
439bool I2SAudioSpeakerBase::esp32_mono_swap_needed(const audio::AudioStreamInfo &stream_info) {
440#ifdef USE_ESP32_VARIANT_ESP32
441 return stream_info.get_channels() == 1 && stream_info.get_bits_per_sample() == 16;
442#else
443 return false;
444#endif // USE_ESP32_VARIANT_ESP32
445}
446
447void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
448#ifdef USE_ESP32_VARIANT_ESP32
449 // For ESP32 16-bit mono mode, adjacent samples need to be swapped.
450 if (this->esp32_mono_swap_needed_()) {
451 int16_t *samples = reinterpret_cast<int16_t *>(data);
452 size_t sample_count = bytes_read / sizeof(int16_t);
453 for (size_t i = 0; i + 1 < sample_count; i += 2) {
454 int16_t tmp = samples[i];
455 samples[i] = samples[i + 1];
456 samples[i + 1] = tmp;
457 }
458 }
459#endif // USE_ESP32_VARIANT_ESP32
460}
461
462} // namespace esphome::i2s_audio
463
464#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
void status_clear_error()
Definition component.h:302
bool is_ready() const
bool status_has_error() const
Definition component.h:282
void status_momentary_error(uint32_t length=5000)
Set error status flag and clear it after length ms.
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
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
esp_audio_libs::gain::GainRamp gain_ramp_
std::weak_ptr< ring_buffer::RingBuffer > audio_ring_buffer_
optional< audio::AudioStreamInfo > rejected_stream_info_
void swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read)
Plays the provided audio data.
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
const char * esp_err_to_name(esp_err_t err)
ESPHOME_LOG_TAG(TAG, "i2s_audio")
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:887
int64_t esp_timer_get_time(void)
static void uint32_t
uint16_t length
Definition tt21100.cpp:0
spi_device_handle_t handle