ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
i2s_audio_spdif.cpp
Go to the documentation of this file.
1#include "i2s_audio_spdif.h"
2
3#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE)
4
5#include <driver/i2s_std.h>
6
9
10#include "esphome/core/hal.h"
11#include "esphome/core/log.h"
12
13#include "esp_timer.h"
14
15namespace esphome::i2s_audio {
16
17static const char *const TAG = "i2s_audio.spdif";
18
19// SPDIF mode adds overhead as each sample is encapsulated in a subframe;
20// each DMA buffer can hold only 192 samples (~4ms each vs. ~15ms for standard I2S).
21// To match the standard I2S buffering duration, we use more buffers to minimize
22// the impact of the overhead, such as stuttering or audio/silence oscillation.
23// 15 buffers x 4ms = 60ms of DMA buffering (same as 4 x 15ms for standard)
24static constexpr size_t SPDIF_DMA_BUFFERS_COUNT = 15;
25
26// Number of DMA events between upstream callbacks (~16ms = 4 events x 4ms each).
27// Matches non-SPDIF timing to prevent overwhelming upstream sync algorithms.
28static constexpr uint32_t SPDIF_DMA_EVENTS_PER_CALLBACK = 4;
29
30// Brief retry wait used by play() to catch short free-space windows during rapid track transitions.
31static constexpr uint32_t SPDIF_PLAY_RETRY_WAIT_MS = 5;
32
33static constexpr size_t SPDIF_I2S_EVENT_QUEUE_COUNT = 2 * SPDIF_DMA_BUFFERS_COUNT;
34
35// Static callback functions for SPDIF encoder (avoids std::function overhead)
36static esp_err_t spdif_preload_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) {
37 auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx);
38 size_t bytes_written = 0;
39 esp_err_t err = i2s_channel_preload_data(speaker->get_tx_handle(), data, size, &bytes_written);
40 if (err != ESP_OK || bytes_written != size) {
41 ESP_LOGV(TAG, "Preload failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size);
42 return (err != ESP_OK) ? err : ESP_ERR_NO_MEM;
43 }
44 return ESP_OK;
45}
46
47static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) {
48 auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx);
49 size_t bytes_written = 0;
50 esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait);
51 if (err != ESP_OK) {
52 ESP_LOGV(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size);
53 }
54 return err;
55}
56
59 if (this->is_failed()) {
60 return;
61 }
62
63 this->spdif_encoder_ = new SPDIFEncoder();
64 if (!this->spdif_encoder_->setup()) {
65 ESP_LOGE(TAG, "Encoder setup failed");
66 this->mark_failed();
67 return;
68 }
69
70 // Configure channel status block with the sample rate
72
73 // Separate callbacks for preload (during underflow recovery) and normal writes
74 this->spdif_encoder_->set_preload_callback(spdif_preload_cb, this);
75 this->spdif_encoder_->set_write_callback(spdif_write_cb, this);
76}
77
80 ESP_LOGCONFIG(TAG,
81 " SPDIF Mode: YES\n"
82 " Sample Rate: %" PRIu32 " Hz",
83 this->sample_rate_);
84}
85
87
88size_t I2SAudioSpeakerSPDIF::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
89 if (this->is_failed()) {
90 ESP_LOGE(TAG, "Setup failed; cannot play audio");
91 return 0;
92 }
93
94 // In SPDIF mode, keep accepting upstream audio while the speaker task is active.
95 // This avoids transient drops during stop/start transitions.
96 const bool task_active = (this->speaker_task_handle_ != nullptr);
97
99 this->start();
100 }
101
102 if (!task_active && this->state_ != speaker::STATE_RUNNING) {
103 // Unable to write data to a running speaker, so delay the max amount of time so it can get ready
104 vTaskDelay(ticks_to_wait);
105 ticks_to_wait = 0;
106 }
107
108 size_t bytes_written = 0;
109 if (this->state_ == speaker::STATE_RUNNING || task_active) {
110 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
111 if (temp_ring_buffer != nullptr) {
112 // In SPDIF mode, a tiny wait helps avoid transient 0-byte writes during short backpressure windows.
113 TickType_t effective_ticks_to_wait = ticks_to_wait;
114 if (effective_ticks_to_wait == 0) {
115 effective_ticks_to_wait = pdMS_TO_TICKS(1);
116 }
117 bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, effective_ticks_to_wait);
118 if (bytes_written == 0 && length > 0) {
119 // Retry once to catch short free-space windows during rapid seek/track transitions.
120 bytes_written =
121 temp_ring_buffer->write_without_replacement((void *) data, length, pdMS_TO_TICKS(SPDIF_PLAY_RETRY_WAIT_MS));
122 }
123 }
124 }
125
126 return bytes_written;
127}
128
130 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
131
132 // Reset SPDIF encoder at task start to ensure clean state
133 // (previous task may have left stale data in encoder buffer)
134 if (this->spdif_encoder_ != nullptr) {
135 this->spdif_encoder_->reset();
136 }
137
138 // Reset lockstep records queue so it starts paired with the (also-reset) i2s_event_queue_.
139 xQueueReset(this->write_records_queue_);
140
141 // The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info
142 const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1);
143
144 // For SPDIF mode, one DMA buffer = one SPDIF block = 192 PCM frames (~4 ms at 48 kHz),
145 // not the ~15 ms a standard I2S DMA buffer holds. Derive the DMA floor from actual block size.
146 const uint32_t frames_to_fill_single_dma_buffer = SPDIF_BLOCK_SAMPLES;
147 const size_t bytes_to_fill_single_dma_buffer =
148 this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer);
149 const size_t dma_buffers_floor_bytes = bytes_to_fill_single_dma_buffer * SPDIF_DMA_BUFFERS_COUNT;
150
151 // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and
152 // avoids unnecessary single-frame splices. Ensure it is at least large enough to cover all DMA buffers.
153 const size_t requested_ring_buffer_bytes =
154 (this->current_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame;
155 const size_t ring_buffer_size = std::max(dma_buffers_floor_bytes, requested_ring_buffer_bytes);
156
157 bool successful_setup = false;
158 std::unique_ptr<audio::RingBufferAudioSource> audio_source;
159
160 {
161 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
162 audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, bytes_to_fill_single_dma_buffer,
163 static_cast<uint8_t>(bytes_per_frame));
164 if (audio_source != nullptr) {
165 this->audio_ring_buffer_ = temp_ring_buffer;
166 successful_setup = true;
167 }
168 }
169
170 if (!successful_setup) {
171 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
172 } else {
173 // Preload DMA buffers with SPDIF-encoded silence before enabling the channel.
174 // This ensures the first data transmitted is valid SPDIF (not raw zeros from
175 // auto_clear) and prevents phantom DMA events before real audio is available.
176 // Each preloaded block pushes a 0-real-frame record so that the corresponding
177 // on_sent events drain in lockstep without crediting any audio frames.
178 this->spdif_encoder_->set_preload_mode(true);
179 for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) {
180 // i2s_channel_preload_data is non-blocking (returns immediately when the preload buffer fills), so no wait.
181 esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(0);
182 if (preload_err != ESP_OK) {
183 break; // DMA preload buffer full or error
184 }
185 const uint32_t silence_record = 0;
186 xQueueSendToBack(this->write_records_queue_, &silence_record, 0);
187 }
188 this->spdif_encoder_->set_preload_mode(false);
189 this->spdif_encoder_->reset(); // Clean encoder state for the main loop
190
191 // Now register the callback and enable the channel
192 xQueueReset(this->i2s_event_queue_);
193 const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb};
194 i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
195 i2s_channel_enable(this->tx_handle_);
196
197 // Always-fill model: each iteration produces exactly one SPDIF block (= one DMA buffer).
198 // We drain real PCM up to one block from the ring buffer and silence-pad any remainder.
199 // Blocking writes pace the loop at the DMA consumption rate. This mirrors the standard
200 // I2S speaker pattern (PR #16317): fill what you can, then silence-pad whatever is still
201 // missing to complete the DMA buffer.
202 const uint32_t block_duration_us = this->current_stream_info_.frames_to_microseconds(SPDIF_BLOCK_SAMPLES);
203 // Sized to absorb the worst case where every DMA buffer is full when we issue the write.
204 const TickType_t write_timeout_ticks =
205 pdMS_TO_TICKS(((block_duration_us * (SPDIF_DMA_BUFFERS_COUNT + 1)) + 999) / 1000);
206 // Brief read budget when the ring buffer is empty (~half a block).
207 const TickType_t read_timeout_ticks = pdMS_TO_TICKS(((block_duration_us / 2) + 999) / 1000);
208
209 // SPDIF Callback Decimation: fire every 4th DMA event (~16ms), matching non-SPDIF timing.
210 uint32_t spdif_pending_frames = 0;
211 int64_t spdif_pending_timestamp = 0;
212 uint32_t spdif_dma_event_count = 0;
213
214 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
215
216 // SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data
217 // to keep the receiver synced. Exits only via break (stream info change, silence timeout,
218 // lockstep desync, dropped event, or partial-write failure).
219 while (true) {
220 uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
221
222 if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
223 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
224 // The ISR pairs COMMAND_STOP with ERR_DROPPED_EVENT when it has to discard a completion
225 // event; that desyncs the lockstep queues permanently and the only safe recovery is a full
226 // task restart.
227 if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
228 ESP_LOGV(TAG, "Exiting: ISR dropped event, restarting to recover lockstep");
229 break;
230 }
231 // User-initiated stop. In SPDIF continuous mode, transition to silence output rather
232 // than tearing the task down.
234 ESP_LOGV(TAG, "COMMAND_STOP received, continuing in silence mode");
235 }
236 if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) {
237 // SPDIF continuous mode never tears the channel down on graceful stop. Clear the flag and
238 // let the audio simply drain through the always-fill loop into the silence-timeout path.
239 xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY);
240 }
241
242 if (this->audio_stream_info_ != this->current_stream_info_) {
243 ESP_LOGV(TAG, "Exiting: stream info changed");
244 break;
245 }
246
247 // Drain ISR completion events, popping a matching record for each.
248 int64_t write_timestamp;
249 bool lockstep_broken = false;
250 while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) {
251 // Lockstep: pop the matching record (real audio frames packed into this DMA block).
252 // Records are pushed by the task right after each successful block commit, so the FIFO
253 // order matches DMA completion order. Empty records queue here means lockstep broke.
254 uint32_t real_frames = 0;
255 if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) {
256 ESP_LOGV(TAG, "Event without matching write record");
258 lockstep_broken = true;
259 break;
260 }
261
262 // Per-block timestamp adjustment: shift back by the silence-padding portion of the block
263 // so the reported timestamp reflects when the last real sample left the wire.
264 uint32_t frames_sent = real_frames;
265 if (real_frames < SPDIF_BLOCK_SAMPLES) {
266 const uint32_t frames_zeroed = SPDIF_BLOCK_SAMPLES - real_frames;
267 write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed);
268 }
269
270 spdif_dma_event_count++;
271 // Accumulate frames; keep the latest timestamp so the callback reports when the last
272 // sample left the wire, not the first.
273 if (frames_sent > 0) {
274 spdif_pending_timestamp = write_timestamp;
275 spdif_pending_frames += frames_sent;
276 }
277
278 bool decimation_reached = (spdif_dma_event_count >= SPDIF_DMA_EVENTS_PER_CALLBACK);
279 // Partial blocks mark an end-of-stream boundary (silence-padded tail). Fire immediately
280 // so the back-shifted timestamp isn't overwritten by a later full audio block landing
281 // in the same decimation window.
282 bool partial_flush = (real_frames > 0 && real_frames < SPDIF_BLOCK_SAMPLES);
283
284 if (decimation_reached || partial_flush) {
285 if (spdif_pending_frames > 0) {
286 this->audio_output_callback_(spdif_pending_frames, spdif_pending_timestamp);
287 spdif_pending_frames = 0;
288 }
289 spdif_dma_event_count = 0;
290 }
291 }
292 if (lockstep_broken) {
293 ESP_LOGV(TAG, "Exiting: lockstep desync, restarting task");
294 break;
295 }
296
297 // Always-fill: produce exactly one SPDIF block this iteration. The blocking encoder write
298 // paces the task at the DMA consumption rate.
299 uint32_t real_frames_in_block = 0;
300 bool block_committed = false;
301 bool partial_write_failure = false;
302
303 if (!this->pause_state_) {
304 while (real_frames_in_block < SPDIF_BLOCK_SAMPLES) {
305 if (audio_source->available() == 0) {
306 size_t bytes_read = audio_source->fill(read_timeout_ticks, false);
307 if (bytes_read == 0) {
308 break; // No upstream data within the read budget; silence-pad the remainder.
309 }
310 uint8_t *new_data = audio_source->mutable_data();
311 this->apply_software_volume_(new_data, bytes_read);
312 this->swap_esp32_mono_samples_(new_data, bytes_read);
313 }
314
315 const uint32_t frames_still_needed = SPDIF_BLOCK_SAMPLES - real_frames_in_block;
316 const size_t bytes_still_needed = this->current_stream_info_.frames_to_bytes(frames_still_needed);
317 const size_t bytes_to_feed = std::min(audio_source->available(), bytes_still_needed);
318
319 uint32_t blocks_sent = 0;
320 size_t pcm_consumed = 0;
321 esp_err_t err = this->spdif_encoder_->write(audio_source->data(), bytes_to_feed, write_timeout_ticks,
322 &blocks_sent, &pcm_consumed);
323 if (err != ESP_OK) {
324 // A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer;
325 // resuming would credit the next iteration's bytes against an old block. Bail and
326 // let loop() restart the task with a clean encoder.
328 partial_write_failure = true;
329 break;
330 }
331
332 if (pcm_consumed > 0) {
333 audio_source->consume(pcm_consumed);
334 real_frames_in_block += this->current_stream_info_.bytes_to_frames(pcm_consumed);
335 }
336 if (blocks_sent > 0) {
337 block_committed = true;
338 break;
339 }
340 }
341 }
342
343 if (partial_write_failure) {
344 break;
345 }
346
347 if (!block_committed) {
348 // Pad whatever real audio we managed to feed (if any) with silence to complete one block,
349 // or emit a full silence block if the encoder is empty.
350 esp_err_t err = this->spdif_encoder_->flush_with_silence(write_timeout_ticks);
351 if (err != ESP_OK) {
353 break;
354 }
355 }
356
357 // One block committed to DMA; push exactly one record carrying its real-audio frame count.
358 // Failure here means the records queue is full, which violates the lockstep invariant.
359 if (xQueueSendToBack(this->write_records_queue_, &real_frames_in_block, 0) != pdTRUE) {
361 break;
362 }
363
364 // Silence-timeout tracking and graceful-stop reset.
365 if (real_frames_in_block == 0) {
366 if (this->spdif_silence_start_ == 0) {
368 }
369
370 if (this->timeout_.has_value()) {
371 const uint32_t silence_duration = millis() - this->spdif_silence_start_;
372 if (silence_duration >= this->timeout_.value()) {
373 ESP_LOGV(TAG, "Silence timeout reached (%" PRIu32 "ms) - stopping speaker", silence_duration);
374 break;
375 }
376 }
377 } else if (this->spdif_silence_start_ != 0) {
378 uint32_t silence_duration = millis() - this->spdif_silence_start_;
379 if (silence_duration > 100) {
380 ESP_LOGV(TAG, "Exiting silence mode after %" PRIu32 "ms, have audio data", silence_duration);
381 }
382 this->spdif_silence_start_ = 0;
383 }
384 }
385 }
386
387 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
388
389 // Reset SPDIF encoder state to prevent stale state on next start
390 if (this->spdif_encoder_ != nullptr) {
391 this->spdif_encoder_->set_preload_mode(false);
392 this->spdif_encoder_->reset();
393 }
394
395 audio_source.reset();
396
397 xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED);
398
399 while (true) {
400 // Continuously delay until the loop method deletes the task
401 vTaskDelay(pdMS_TO_TICKS(10));
402 }
403}
404
406 this->current_stream_info_ = audio_stream_info;
407
408 // SPDIF mode validation
409 if (this->sample_rate_ != audio_stream_info.get_sample_rate()) {
410 ESP_LOGE(TAG, "Only supports a single sample rate (configured: %" PRIu32 " Hz, stream: %" PRIu32 " Hz)",
411 this->sample_rate_, audio_stream_info.get_sample_rate());
412 return ESP_ERR_NOT_SUPPORTED;
413 }
414 const uint8_t bits_per_sample = audio_stream_info.get_bits_per_sample();
415 if (bits_per_sample != 16 && bits_per_sample != 24 && bits_per_sample != 32) {
416 ESP_LOGE(TAG, "Only supports 16, 24, or 32 bits per sample (got %u)", (unsigned) bits_per_sample);
417 return ESP_ERR_NOT_SUPPORTED;
418 }
419 if (audio_stream_info.get_channels() != 2) {
420 ESP_LOGE(TAG, "Only supports stereo (2 channels)");
421 return ESP_ERR_NOT_SUPPORTED;
422 }
423
424 // Tell the encoder what input width to expect. 32-bit input is truncated to 24-bit on the wire.
425 this->spdif_encoder_->set_bytes_per_sample(bits_per_sample / 8);
426
427 if (!this->parent_->try_lock()) {
428 ESP_LOGE(TAG, "Parent bus is busy");
429 return ESP_ERR_INVALID_STATE;
430 }
431
432 i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
433
434#if SOC_CLK_APLL_SUPPORTED
435 if (this->use_apll_) {
436 clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL;
437 }
438#endif // SOC_CLK_APLL_SUPPORTED
439
440 // SPDIF mode: fixed configuration for BMC encoding
441 // For new driver, dma_frame_num is in I2S frames (8 bytes each for 32-bit stereo)
442 uint32_t dma_buffer_length = SPDIF_BLOCK_I2S_FRAMES; // One SPDIF block = 384 I2S frames = 3072 bytes
443
444 // Log DMA configuration for debugging
445 ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames = %lu bytes total", (size_t) SPDIF_DMA_BUFFERS_COUNT,
446 (unsigned long) dma_buffer_length,
447 (unsigned long) (SPDIF_DMA_BUFFERS_COUNT * dma_buffer_length * 8)); // 8 bytes per frame for 32-bit stereo
448
449 i2s_chan_config_t chan_cfg = {
450 .id = this->parent_->get_port(),
451 .role = this->i2s_role_,
452 .dma_desc_num = SPDIF_DMA_BUFFERS_COUNT,
453 .dma_frame_num = dma_buffer_length,
454 .auto_clear = true,
455 .intr_priority = 3,
456 };
457
458 // SPDIF: double sample rate for BMC, 32-bit stereo, only data pin needed
459 i2s_std_clk_config_t clk_cfg = {
460 .sample_rate_hz = this->sample_rate_ * 2,
461 .clk_src = clk_src,
462 .mclk_multiple = this->mclk_multiple_,
463 };
464
465 i2s_std_slot_config_t slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO);
466
467 i2s_std_gpio_config_t gpio_cfg = {
468 .mclk = GPIO_NUM_NC,
469 .bclk = GPIO_NUM_NC,
470 .ws = GPIO_NUM_NC,
471 .dout = this->dout_pin_,
472 .din = GPIO_NUM_NC,
473 .invert_flags =
474 {
475 .mclk_inv = false,
476 .bclk_inv = false,
477 .ws_inv = false,
478 },
479 };
480
481 i2s_std_config_t std_cfg = {
482 .clk_cfg = clk_cfg,
483 .slot_cfg = slot_cfg,
484 .gpio_cfg = gpio_cfg,
485 };
486
487 esp_err_t err = this->init_i2s_channel_(chan_cfg, std_cfg, SPDIF_I2S_EVENT_QUEUE_COUNT);
488 if (err != ESP_OK) {
489 return err;
490 }
491
492 // Channel is NOT enabled here. The speaker task will preload DMA buffers
493 // with SPDIF-encoded silence before enabling, ensuring the first data on
494 // the wire is valid SPDIF (not raw zeros from auto_clear) and preventing
495 // phantom DMA events before real audio data is available.
496
497 return ESP_OK;
498}
499
500} // namespace esphome::i2s_audio
501
502#endif // USE_ESP32 && USE_I2S_AUDIO_SPDIF_MODE
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:272
size_t ms_to_bytes(uint32_t ms) const
Converts duration to bytes.
Definition audio.h:72
size_t frames_to_bytes(uint32_t frames) const
Converts frames to bytes.
Definition audio.h:52
uint8_t get_bits_per_sample() const
Definition audio.h:27
uint32_t frames_to_microseconds(uint32_t frames) const
Computes the duration, in microseconds, the given amount of frames represents.
Definition audio.cpp:25
uint32_t bytes_to_frames(size_t bytes) const
Convert bytes to frames.
Definition audio.h:42
uint8_t get_channels() const
Definition audio.h:28
uint32_t get_sample_rate() const
Definition audio.h:29
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.
i2s_mclk_multiple_t mclk_multiple_
Definition i2s_audio.h:32
static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx)
Callback function used to send playback timestamps to the speaker task.
void apply_software_volume_(uint8_t *data, size_t bytes_read)
Apply software volume control using Q15 fixed-point scaling.
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.
esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, size_t event_queue_size)
Shared I2S channel allocation, initialization, and event queue setup.
size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override
esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override
void set_bytes_per_sample(uint8_t bytes_per_sample)
Set input PCM width: 2 = 16-bit, 3 = 24-bit, 4 = 32-bit (truncated to 24-bit on the wire).
esp_err_t write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent=nullptr, size_t *bytes_consumed=nullptr)
Convert PCM audio data to SPDIF BMC encoded data.
void set_sample_rate(uint32_t sample_rate)
Set the sample rate for Channel Status Block encoding.
void reset()
Reset the SPDIF block buffer and position tracking, discarding any partial block.
esp_err_t flush_with_silence(TickType_t ticks_to_wait)
Emit one complete SPDIF block: pad any pending partial block with silence and send,...
bool setup()
Initialize the SPDIF working buffer.
void set_write_callback(SPDIFBlockCallback callback, void *user_ctx)
Set callback for normal writes (used when channel is running)
void set_preload_callback(SPDIFBlockCallback callback, void *user_ctx)
Set callback for preload writes (used when preloading to DMA before enabling channel)
void set_preload_mode(bool preload)
Enable or disable preload mode When in preload mode, completed blocks use the preload callback instea...
static std::unique_ptr< RingBuffer > create(size_t len, MemoryPreference preference=MemoryPreference::EXTERNAL_FIRST)
CallbackManager< void(uint32_t, int64_t)> audio_output_callback_
Definition speaker.h:122
audio::AudioStreamInfo audio_stream_info_
Definition speaker.h:114
uint16_t size
Definition helpers.cpp:25
auto * new_data
Definition helpers.cpp:29
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
uint16_t length
Definition tt21100.cpp:0