mirror of
https://github.com/esphome/esphome.git
synced 2026-09-22 18:05:00 +08:00
[i2s_audio] Refactor SPDIF output, fixing synchronization problems (#16319)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -69,6 +69,17 @@ 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;
|
||||
}
|
||||
@@ -87,18 +98,11 @@ void I2SAudioSpeakerBase::loop() {
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
}
|
||||
|
||||
// Log any errors encountered by the task
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) {
|
||||
ESP_LOGE(TAG, "Not enough memory");
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
}
|
||||
|
||||
// Warn if any playback timestamp events are dropped, which drastically reduces synced playback accuracy
|
||||
if (event_group_bits & SpeakerEventGroupBits::WARN_DROPPED_EVENT) {
|
||||
ESP_LOGW(TAG, "Event dropped, synchronized playback accuracy is reduced");
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT);
|
||||
}
|
||||
|
||||
// Handle the speaker's state
|
||||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING:
|
||||
@@ -271,6 +275,22 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
|
||||
xQueueReset(this->i2s_event_queue_);
|
||||
}
|
||||
|
||||
// Lockstep records queue. One record per in-flight DMA buffer; sized to match the I2S event queue
|
||||
// so a fully-saturated DMA pipeline cannot overflow either side before drain.
|
||||
if (this->write_records_queue_ == nullptr) {
|
||||
this->write_records_queue_ = xQueueCreate(event_queue_size, sizeof(uint32_t));
|
||||
} else {
|
||||
xQueueReset(this->write_records_queue_);
|
||||
}
|
||||
|
||||
if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)");
|
||||
i2s_del_channel(this->tx_handle_);
|
||||
this->tx_handle_ = nullptr;
|
||||
this->parent_->unlock();
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -293,10 +313,16 @@ 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 and set the warning flag to inform the user
|
||||
// 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).
|
||||
int64_t dummy;
|
||||
xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
|
||||
xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT, &need_yield2);
|
||||
xEventGroupSetBitsFromISR(this_speaker->event_group_,
|
||||
SpeakerEventGroupBits::ERR_DROPPED_EVENT | SpeakerEventGroupBits::COMMAND_STOP,
|
||||
&need_yield2);
|
||||
}
|
||||
|
||||
xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
|
||||
|
||||
@@ -35,7 +35,11 @@ enum SpeakerEventGroupBits : uint32_t {
|
||||
|
||||
ERR_ESP_NO_MEM = (1 << 19),
|
||||
|
||||
WARN_DROPPED_EVENT = (1 << 20),
|
||||
ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event
|
||||
ERR_PARTIAL_WRITE = (1 << 21), // a DMA write returned fewer bytes than requested (or the encoder
|
||||
// failed to commit a complete block), which breaks the lockstep
|
||||
// invariant for every subsequent event
|
||||
ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync
|
||||
|
||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
@@ -141,7 +145,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
TaskHandle_t speaker_task_handle_{nullptr};
|
||||
EventGroupHandle_t event_group_{nullptr};
|
||||
|
||||
// Lockstepped DMA buffer queues: i2s_event is outgoing, write_records is incoming
|
||||
QueueHandle_t i2s_event_queue_{nullptr};
|
||||
QueueHandle_t write_records_queue_{nullptr};
|
||||
|
||||
std::weak_ptr<ring_buffer::RingBuffer> audio_ring_buffer_;
|
||||
|
||||
|
||||
@@ -358,25 +358,15 @@ HOT esp_err_t SPDIFEncoder::write(const uint8_t *src, size_t size, TickType_t ti
|
||||
}
|
||||
|
||||
esp_err_t SPDIFEncoder::flush_with_silence(TickType_t ticks_to_wait) {
|
||||
// First, send any pending complete block from a previous failed send
|
||||
if (this->spdif_block_ptr_ >= &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
esp_err_t err = this->send_block_(ticks_to_wait);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
// If a complete block is already pending (from a previous failed send), emit just that block.
|
||||
// Otherwise pad the partial block with silence (or generate a full silence block if empty)
|
||||
// and send. Always emits exactly one block on success.
|
||||
if (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
static const uint8_t SILENCE[2] = {0, 0};
|
||||
while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
this->encode_sample_(SILENCE);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->has_pending_data()) {
|
||||
return ESP_OK; // Nothing to flush
|
||||
}
|
||||
|
||||
// Encode silence (zeros) until the block is complete
|
||||
static const uint8_t SILENCE[2] = {0, 0};
|
||||
|
||||
while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
this->encode_sample_(SILENCE);
|
||||
}
|
||||
|
||||
return this->send_block_(ticks_to_wait);
|
||||
}
|
||||
|
||||
|
||||
@@ -85,9 +85,10 @@ class SPDIFEncoder {
|
||||
/// @brief Check if there is a partial block pending
|
||||
bool has_pending_data() const { return this->spdif_block_ptr_ != this->spdif_block_buf_.get(); }
|
||||
|
||||
/// @brief Flush any pending partial block by padding with silence and sending
|
||||
/// @brief Emit one complete SPDIF block: pad any pending partial block with silence and send,
|
||||
/// or send a full silence block if nothing is pending. Always produces exactly one block on success.
|
||||
/// @param ticks_to_wait Timeout for blocking writes
|
||||
/// @return esp_err_t as returned from the callback, or ESP_OK if nothing to flush
|
||||
/// @return esp_err_t as returned from the callback
|
||||
esp_err_t flush_with_silence(TickType_t ticks_to_wait);
|
||||
|
||||
/// @brief Reset the SPDIF block buffer and position tracking, discarding any partial block
|
||||
|
||||
Reference in New Issue
Block a user