mirror of
https://github.com/esphome/esphome.git
synced 2026-09-18 02:28:42 +00:00
[i2s_audio] Resync DMA lockstep in place instead of restarting the speaker task (#19319)
This commit is contained in:
@@ -48,10 +48,11 @@ static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, Tic
|
||||
auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx);
|
||||
size_t bytes_written = 0;
|
||||
esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait);
|
||||
if (err != ESP_OK) {
|
||||
if (err != ESP_OK || bytes_written != size) {
|
||||
ESP_LOGV(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size);
|
||||
return (err != ESP_OK) ? err : ESP_FAIL;
|
||||
}
|
||||
return err;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerSPDIF::setup() {
|
||||
@@ -167,33 +168,44 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!successful_setup) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
} else {
|
||||
// Preload DMA buffers with SPDIF-encoded silence before enabling the channel.
|
||||
// This ensures the first data transmitted is valid SPDIF (not raw zeros from
|
||||
// auto_clear) and prevents phantom DMA events before real audio is available.
|
||||
// Each preloaded block pushes a 0-real-frame record so that the corresponding
|
||||
// on_sent events drain in lockstep without crediting any audio frames.
|
||||
// Preload DMA buffers with SPDIF-encoded silence before enabling the channel.
|
||||
// This ensures the first data transmitted is valid SPDIF (not raw zeros from
|
||||
// auto_clear) and prevents phantom DMA events before real audio is available.
|
||||
// Each preloaded block pushes a 0-real-frame record so that the corresponding
|
||||
// on_sent events drain in lockstep without crediting any audio frames. Runs with
|
||||
// the channel disabled: at startup and after a resync.
|
||||
auto preload_silence = [&]() -> bool {
|
||||
bool ok = true;
|
||||
this->spdif_encoder_->set_preload_mode(true);
|
||||
for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) {
|
||||
// i2s_channel_preload_data is non-blocking (returns immediately when the preload buffer fills), so no wait.
|
||||
esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(0);
|
||||
if (preload_err != ESP_OK) {
|
||||
break; // DMA preload buffer full or error
|
||||
}
|
||||
const uint32_t silence_record = 0;
|
||||
xQueueSendToBack(this->write_records_queue_, &silence_record, 0);
|
||||
if ((this->spdif_encoder_->flush_with_silence(0) != ESP_OK) ||
|
||||
(xQueueSendToBack(this->write_records_queue_, &silence_record, 0) != pdTRUE)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->spdif_encoder_->set_preload_mode(false);
|
||||
this->spdif_encoder_->reset(); // Clean encoder state for the main loop
|
||||
return ok;
|
||||
};
|
||||
|
||||
// Now register the callback and enable the channel
|
||||
if (successful_setup) {
|
||||
successful_setup = preload_silence();
|
||||
}
|
||||
|
||||
if (successful_setup) {
|
||||
// Register the callback before enabling so the first transmitted block generates a queued event.
|
||||
xQueueReset(this->i2s_event_queue_);
|
||||
const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb};
|
||||
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
|
||||
i2s_channel_enable(this->tx_handle_);
|
||||
successful_setup = i2s_channel_enable(this->tx_handle_) == ESP_OK;
|
||||
}
|
||||
|
||||
if (!successful_setup) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
} else {
|
||||
// Always-fill model: each iteration produces exactly one SPDIF block (= one DMA buffer).
|
||||
// We drain real PCM up to one block from the ring buffer and silence-pad any remainder.
|
||||
// Blocking writes pace the loop at the DMA consumption rate. This mirrors the standard
|
||||
@@ -210,24 +222,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
uint32_t spdif_pending_frames = 0;
|
||||
int64_t spdif_pending_timestamp = 0;
|
||||
uint32_t spdif_dma_event_count = 0;
|
||||
bool resync_needed = false;
|
||||
// Real frames consumed from the ring buffer that never reached a write record
|
||||
uint32_t unrecorded_frames = 0;
|
||||
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
|
||||
|
||||
// SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data
|
||||
// to keep the receiver synced. Exits only via break (stream info change, silence timeout,
|
||||
// lockstep desync, dropped event, or partial-write failure).
|
||||
// or a failed lockstep resync).
|
||||
while (true) {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
|
||||
// The ISR pairs COMMAND_STOP with ERR_DROPPED_EVENT when it has to discard a completion
|
||||
// event; that desyncs the lockstep queues permanently and the only safe recovery is a full
|
||||
// task restart.
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGV(TAG, "Exiting: ISR dropped event, restarting to recover lockstep");
|
||||
break;
|
||||
}
|
||||
// User-initiated stop. In SPDIF continuous mode, transition to silence output rather
|
||||
// than tearing the task down.
|
||||
this->spdif_silence_start_ = millis();
|
||||
@@ -244,6 +252,30 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep");
|
||||
resync_needed = true;
|
||||
}
|
||||
if (resync_needed) {
|
||||
// Rebuild the lockstep in place. Frames held back by decimation are credited too, since their
|
||||
// blocks are discarded with the rest of the DMA contents.
|
||||
this->spdif_encoder_->reset();
|
||||
const uint32_t credited_frames = unrecorded_frames + spdif_pending_frames;
|
||||
const bool resynced = this->resync_lockstep_(credited_frames, preload_silence);
|
||||
unrecorded_frames = 0;
|
||||
spdif_pending_frames = 0;
|
||||
spdif_dma_event_count = 0;
|
||||
resync_needed = false;
|
||||
if (credited_frames > 0) {
|
||||
// Real audio was dropped, so the silence timer's start no longer reflects the stream
|
||||
this->spdif_silence_start_ = 0;
|
||||
}
|
||||
if (!resynced) {
|
||||
ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Drain ISR completion events, popping a matching record for each.
|
||||
int64_t write_timestamp;
|
||||
bool lockstep_broken = false;
|
||||
@@ -253,8 +285,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
// order matches DMA completion order. Empty records queue here means lockstep broke.
|
||||
uint32_t real_frames = 0;
|
||||
if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) {
|
||||
ESP_LOGV(TAG, "Event without matching write record");
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep");
|
||||
lockstep_broken = true;
|
||||
break;
|
||||
}
|
||||
@@ -290,8 +321,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
if (lockstep_broken) {
|
||||
ESP_LOGV(TAG, "Exiting: lockstep desync, restarting task");
|
||||
break;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always-fill: produce exactly one SPDIF block this iteration. The blocking encoder write
|
||||
@@ -322,9 +353,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
&blocks_sent, &pcm_consumed);
|
||||
if (err != ESP_OK) {
|
||||
// A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer;
|
||||
// resuming would credit the next iteration's bytes against an old block. Bail and
|
||||
// let loop() restart the task with a clean encoder.
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
// resuming would credit the next iteration's bytes against an old block.
|
||||
ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep");
|
||||
partial_write_failure = true;
|
||||
break;
|
||||
}
|
||||
@@ -341,7 +371,9 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
}
|
||||
|
||||
if (partial_write_failure) {
|
||||
break;
|
||||
unrecorded_frames += real_frames_in_block;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!block_committed) {
|
||||
@@ -349,16 +381,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
// or emit a full silence block if the encoder is empty.
|
||||
esp_err_t err = this->spdif_encoder_->flush_with_silence(write_timeout_ticks);
|
||||
if (err != ESP_OK) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
break;
|
||||
ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep");
|
||||
unrecorded_frames += real_frames_in_block;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// One block committed to DMA; push exactly one record carrying its real-audio frame count.
|
||||
// Failure here means the records queue is full, which violates the lockstep invariant.
|
||||
if (xQueueSendToBack(this->write_records_queue_, &real_frames_in_block, 0) != pdTRUE) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
break;
|
||||
ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep");
|
||||
unrecorded_frames += real_frames_in_block;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Silence-timeout tracking and graceful-stop reset.
|
||||
|
||||
@@ -80,17 +80,6 @@ void I2SAudioSpeakerBase::loop() {
|
||||
}
|
||||
if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
|
||||
ESP_LOGV(TAG, "Stopping");
|
||||
// Lockstep-breaking error bits are latched by the task and cleared along with all other bits
|
||||
// when TASK_STOPPED is processed; log them here, exactly once, as the task winds down.
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGE(TAG, "ISR event queue overflow, restarting speaker task to recover timestamp sync");
|
||||
}
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_PARTIAL_WRITE) {
|
||||
ESP_LOGE(TAG, "Partial DMA write broke buffer alignment, restarting speaker task");
|
||||
}
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC) {
|
||||
ESP_LOGE(TAG, "Event/record queues desynced, restarting speaker task");
|
||||
}
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
@@ -325,16 +314,10 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
|
||||
I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx;
|
||||
|
||||
if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
|
||||
// Queue is full, so discard the oldest event. Once we drop a completion event, ``i2s_event_queue_``
|
||||
// and any per-buffer record queue maintained by the task are permanently desynced, so the task
|
||||
// must restart to recover. Set both ERR_DROPPED_EVENT (so loop() can log it) and COMMAND_STOP
|
||||
// (so the task bails immediately, closing the race where loop() could clear the error bit
|
||||
// before the task observes it).
|
||||
// Queue is full, so discard the oldest event. The lockstep queues are now desynced; the task resyncs them.
|
||||
int64_t dummy;
|
||||
xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
|
||||
xEventGroupSetBitsFromISR(this_speaker->event_group_,
|
||||
SpeakerEventGroupBits::ERR_DROPPED_EVENT | SpeakerEventGroupBits::COMMAND_STOP,
|
||||
&need_yield2);
|
||||
xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT, &need_yield2);
|
||||
}
|
||||
|
||||
xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
|
||||
@@ -342,6 +325,24 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
|
||||
return need_yield1 | need_yield2 | need_yield3;
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::drain_lockstep_(uint32_t extra_frames) {
|
||||
// Stop DMA so no more completion events arrive while the queues are rebuilt
|
||||
i2s_channel_disable(this->tx_handle_);
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT);
|
||||
|
||||
uint32_t frames = extra_frames;
|
||||
uint32_t record_frames = 0;
|
||||
while (xQueueReceive(this->write_records_queue_, &record_frames, 0) == pdTRUE) {
|
||||
frames += record_frames;
|
||||
}
|
||||
xQueueReset(this->i2s_event_queue_);
|
||||
|
||||
if (frames > 0) {
|
||||
ESP_LOGV(TAG, "Crediting %" PRIu32 " dropped frames as played", frames);
|
||||
this->audio_output_callback_(frames, esp_timer_get_time());
|
||||
}
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
|
||||
#ifdef USE_AUDIO_DAC
|
||||
if (this->audio_dac_ != nullptr) {
|
||||
|
||||
@@ -36,9 +36,7 @@ enum SpeakerEventGroupBits : uint32_t {
|
||||
|
||||
ERR_ESP_NO_MEM = (1 << 19),
|
||||
|
||||
ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event
|
||||
ERR_PARTIAL_WRITE = (1 << 21), // i2s_channel_write returned fewer bytes than requested
|
||||
ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync
|
||||
ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event
|
||||
|
||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
@@ -134,6 +132,21 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
|
||||
virtual void on_task_stopped() {}
|
||||
|
||||
/// @brief Rebuilds the lockstep queues in place: disables the channel, credits every in-flight real frame as
|
||||
/// played now, empties both queues, preloads silence through ``preload`` and re-enables the channel. Speaker
|
||||
/// task only.
|
||||
/// @param extra_frames Real frames the caller consumed that never reached a write record
|
||||
/// @param preload Callable returning true once every DMA descriptor holds silence with a matching record
|
||||
/// @return false if the preload or the channel enable failed; the caller should restart the task
|
||||
template<typename F> bool resync_lockstep_(uint32_t extra_frames, F &&preload) {
|
||||
this->drain_lockstep_(extra_frames);
|
||||
return preload() && (i2s_channel_enable(this->tx_handle_) == ESP_OK);
|
||||
}
|
||||
|
||||
/// @brief Disables the channel, credits ``extra_frames`` plus every real frame still recorded as in flight,
|
||||
/// and empties both lockstep queues.
|
||||
void drain_lockstep_(uint32_t extra_frames);
|
||||
|
||||
/// @brief Apply software volume control by running the samples through the gain ramp. Called from the
|
||||
/// speaker task only.
|
||||
/// @param data Pointer to audio sample data (modified in place)
|
||||
|
||||
@@ -134,27 +134,29 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
|
||||
if (successful_setup) {
|
||||
// Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer.
|
||||
// This guarantees that every on_sent event has a corresponding write record from the start, so
|
||||
// ``i2s_event_queue_`` and ``write_records_queue_`` stay in lockstep for the entire task lifetime.
|
||||
// Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer, so every
|
||||
// on_sent event has a write record from the start. Runs with the channel disabled: at startup and after a resync.
|
||||
auto preload_silence = [&]() -> bool {
|
||||
for (size_t i = 0; i < DMA_BUFFERS_COUNT; i++) {
|
||||
size_t bytes_loaded = 0;
|
||||
esp_err_t err = i2s_channel_preload_data(this->tx_handle_, silence_buffer, dma_buffer_bytes, &bytes_loaded);
|
||||
if (err != ESP_OK || bytes_loaded != dma_buffer_bytes) {
|
||||
ESP_LOGV(TAG, "Failed to preload silence into DMA buffer %u (err=%d, loaded=%u)", (unsigned) i, (int) err,
|
||||
(unsigned) bytes_loaded);
|
||||
successful_setup = false;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
uint32_t zero_real_frames = 0;
|
||||
if (xQueueSend(this->write_records_queue_, &zero_real_frames, 0) != pdTRUE) {
|
||||
// Should never happen: the queue was just reset and is sized for DMA_BUFFERS_COUNT * 2 entries.
|
||||
ESP_LOGV(TAG, "Failed to push preload write record");
|
||||
successful_setup = false;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (successful_setup) {
|
||||
successful_setup = preload_silence();
|
||||
}
|
||||
|
||||
if (successful_setup) {
|
||||
@@ -177,6 +179,9 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
// stop to wait until every real-audio buffer has been confirmed played by an ISR event.
|
||||
uint32_t pending_real_buffers = 0;
|
||||
uint32_t last_data_received_time = millis();
|
||||
bool resync_needed = false;
|
||||
// Real frames consumed from the ring buffer that never reached a write record
|
||||
uint32_t unrecorded_frames = 0;
|
||||
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
|
||||
|
||||
@@ -197,8 +202,6 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
|
||||
// COMMAND_STOP is set both by user-initiated stop() and by the ISR when it drops a completion
|
||||
// event (paired with ERR_DROPPED_EVENT so loop() can distinguish the two cases).
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
|
||||
ESP_LOGV(TAG, "Exiting: COMMAND_STOP received");
|
||||
break;
|
||||
@@ -214,6 +217,22 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep");
|
||||
resync_needed = true;
|
||||
}
|
||||
if (resync_needed) {
|
||||
// Rebuild the lockstep in place; the ring buffer keeps accepting audio throughout
|
||||
const bool resynced = this->resync_lockstep_(unrecorded_frames, preload_silence);
|
||||
unrecorded_frames = 0;
|
||||
pending_real_buffers = 0;
|
||||
resync_needed = false;
|
||||
if (!resynced) {
|
||||
ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Drain ISR-stamped completion events. Each event corresponds 1:1 with a write_records_queue_
|
||||
// entry by construction (preloaded records at startup, plus exactly one record pushed per
|
||||
// iteration alongside exactly one DMA-buffer-sized write).
|
||||
@@ -223,8 +242,7 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
uint32_t real_frames = 0;
|
||||
if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) {
|
||||
// Should never happen: would indicate the lockstep invariant is broken.
|
||||
ESP_LOGV(TAG, "Event without matching write record");
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep");
|
||||
lockstep_broken = true;
|
||||
break;
|
||||
}
|
||||
@@ -240,7 +258,8 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
if (lockstep_broken) {
|
||||
break;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Graceful stop: exit only after the source's exposed chunk is drained, the underlying ring
|
||||
@@ -299,10 +318,12 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
size_t bw = 0;
|
||||
i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS);
|
||||
if (bw != output_bytes) {
|
||||
// A short real-audio write breaks DMA descriptor alignment for every subsequent event;
|
||||
// the only safe recovery is to restart the task.
|
||||
ESP_LOGV(TAG, "Partial real audio write: %u of %u bytes", (unsigned) bw, (unsigned) output_bytes);
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
// A short write breaks DMA descriptor alignment for every subsequent event. Drop the chunk rather
|
||||
// than retry it: it was already narrowed in place.
|
||||
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
|
||||
(unsigned) output_bytes);
|
||||
audio_source->consume(input_bytes);
|
||||
real_frames_total += frames_to_write;
|
||||
partial_write_failure = true;
|
||||
break;
|
||||
}
|
||||
@@ -316,7 +337,9 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
}
|
||||
|
||||
if (partial_write_failure) {
|
||||
break;
|
||||
unrecorded_frames += real_frames_total;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t silence_bytes = dma_buffer_bytes - bytes_written_total;
|
||||
@@ -325,19 +348,22 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
i2s_channel_write(this->tx_handle_, silence_buffer, silence_bytes, &bw, WRITE_TIMEOUT_TICKS);
|
||||
if (bw != silence_bytes) {
|
||||
// Same descriptor-alignment hazard as a partial real-audio write.
|
||||
ESP_LOGV(TAG, "Partial silence write: %u of %u bytes", (unsigned) bw, (unsigned) silence_bytes);
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
break;
|
||||
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
|
||||
(unsigned) silence_bytes);
|
||||
unrecorded_frames += real_frames_total;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Push the matching write record. Capacity headroom in I2S_EVENT_QUEUE_COUNT guarantees this
|
||||
// succeeds even with a transient backlog of unprocessed events; if it ever fails the lockstep
|
||||
// invariant is broken and every subsequent timestamp would be silently wrong, so bail.
|
||||
// invariant is broken and every subsequent timestamp would be silently wrong, so rebuild it.
|
||||
if (xQueueSend(this->write_records_queue_, &real_frames_total, 0) != pdTRUE) {
|
||||
ESP_LOGV(TAG, "Exiting: write records queue full");
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
break;
|
||||
ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep");
|
||||
unrecorded_frames += real_frames_total;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
if (real_frames_total > 0) {
|
||||
pending_real_buffers++;
|
||||
|
||||
Reference in New Issue
Block a user