[speaker] Add SPDIF output support (#8065)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org>
This commit is contained in:
John Boiles
2026-05-08 14:43:55 -04:00
committed by GitHub
co-authored by Keith Burzinski Kevin Ahrendt
parent 3d8fffbea9
commit 88c2a1c096
10 changed files with 1291 additions and 12 deletions
+3 -2
View File
@@ -201,7 +201,7 @@ async def register_i2s_audio_component(var, config):
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioComponent),
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number,
},
@@ -290,7 +290,8 @@ async def to_code(config):
# Helps avoid callbacks being skipped due to processor load
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_LRCLK_PIN in config:
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config:
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
if CONF_I2S_MCLK_PIN in config:
@@ -18,10 +18,12 @@ from .. import (
CONF_I2S_DOUT_PIN,
CONF_I2S_MODE,
CONF_LEFT,
CONF_MCLK_MULTIPLE,
CONF_MONO,
CONF_PRIMARY,
CONF_RIGHT,
CONF_STEREO,
CONF_USE_APLL,
I2SAudioOut,
i2s_audio_component_schema,
i2s_audio_ns,
@@ -40,6 +42,15 @@ I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase)
CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
CONF_SPDIF_MODE = "spdif_mode"
I2SAudioSpeakerBase = i2s_audio_ns.class_(
"I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut
)
I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase)
I2SAudioSpeakerSPDIF = i2s_audio_ns.class_("I2SAudioSpeakerSPDIF", I2SAudioSpeakerBase)
I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True)
I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True)
@@ -77,7 +88,17 @@ def _set_num_channels_from_config(config):
def _set_stream_limits(config):
if config[CONF_I2S_MODE] == CONF_PRIMARY:
if config.get(CONF_SPDIF_MODE, False):
# SPDIF mode: fixed to 16-bit stereo at configured sample rate
audio.set_stream_limits(
min_bits_per_sample=16,
max_bits_per_sample=16,
min_channels=2,
max_channels=2,
min_sample_rate=config.get(CONF_SAMPLE_RATE),
max_sample_rate=config.get(CONF_SAMPLE_RATE),
)(config)
elif config[CONF_I2S_MODE] == CONF_PRIMARY:
# Primary mode has modifiable stream settings
audio.set_stream_limits(
min_bits_per_sample=8,
@@ -101,6 +122,13 @@ def _set_stream_limits(config):
return config
def _select_speaker_class(config):
"""Override ID type when SPDIF mode is enabled."""
if config.get(CONF_SPDIF_MODE, False):
config[CONF_ID].type = I2SAudioSpeakerSPDIF
return config
def _validate_esp32_variant(config):
variant = esp32.get_esp32_variant()
if config[CONF_DAC_TYPE] == "internal":
@@ -155,6 +183,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.one_of(
*I2C_COMM_FMT_OPTIONS, lower=True
),
cv.Optional(CONF_SPDIF_MODE, default=False): cv.boolean,
}
),
},
@@ -163,6 +192,7 @@ CONFIG_SCHEMA = cv.All(
_validate_esp32_variant,
_set_num_channels_from_config,
_set_stream_limits,
_select_speaker_class,
validate_mclk_divisible_by_3,
)
@@ -175,6 +205,28 @@ def _final_validate(config):
if config[CONF_I2S_COMM_FMT] == "stand_max":
raise cv.Invalid("I2S standard max format is no longer supported.")
if config.get(CONF_SPDIF_MODE, False):
# SPDIF mode specific validations
if config[CONF_SAMPLE_RATE] not in [44100, 48000]:
raise cv.Invalid(
"SPDIF mode only supports 44100 Hz or 48000 Hz sample rates"
)
if config[CONF_CHANNEL] != CONF_STEREO:
raise cv.Invalid("SPDIF mode only supports stereo channel configuration")
# bits_per_sample is converted to float by the schema
if config[CONF_BITS_PER_SAMPLE] != 16:
raise cv.Invalid("SPDIF mode only supports 16 bits per sample")
if not config[CONF_USE_APLL]:
raise cv.Invalid(
"SPDIF mode requires 'use_apll: true' for accurate clock generation"
)
if config[CONF_I2S_MODE] != CONF_PRIMARY:
raise cv.Invalid("SPDIF mode requires 'i2s_mode: primary'")
if config[CONF_I2S_COMM_FMT] != "stand_i2s":
raise cv.Invalid("SPDIF mode requires 'i2s_comm_fmt: stand_i2s'")
if config[CONF_MCLK_MULTIPLE] != 256:
raise cv.Invalid("SPDIF mode requires 'mclk_multiple: 256'")
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -186,12 +238,18 @@ async def to_code(config):
await speaker.register_speaker(var, config)
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = I2SCommFmt.MSB
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = I2SCommFmt.PCM
cg.add(var.set_i2s_comm_fmt(fmt))
is_spdif = config.get(CONF_SPDIF_MODE, False)
if is_spdif:
cg.add_define("USE_I2S_AUDIO_SPDIF_MODE")
else:
fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = I2SCommFmt.MSB
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = I2SCommFmt.PCM
cg.add(var.set_i2s_comm_fmt(fmt))
if config[CONF_TIMEOUT] != CONF_NEVER:
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
@@ -0,0 +1,629 @@
#include "i2s_audio_spdif.h"
#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE)
#include <driver/i2s_std.h>
#include "esphome/components/audio/audio.h"
#include "esphome/components/audio/audio_transfer_buffer.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esp_timer.h"
namespace esphome::i2s_audio {
static const char *const TAG = "i2s_audio.spdif";
// SPDIF mode adds overhead as each sample is encapsulated in a subframe;
// each DMA buffer can hold only 192 samples (~4ms each vs. ~15ms for standard I2S).
// To match the standard I2S buffering duration, we use more buffers to minimize
// the impact of the overhead, such as stuttering or audio/silence oscillation.
// 15 buffers x 4ms = 60ms of DMA buffering (same as 4 x 15ms for standard)
static constexpr size_t SPDIF_DMA_BUFFERS_COUNT = 15;
// Timeout for flushing pending frames if no callback received.
static constexpr uint32_t SPDIF_FLUSH_TIMEOUT_MS = 20;
// Number of DMA events between upstream callbacks (~16ms = 4 events x 4ms each).
// Matches non-SPDIF timing to prevent overwhelming upstream sync algorithms.
static constexpr uint32_t SPDIF_DMA_EVENTS_PER_CALLBACK = 4;
// Consider TX stalled only if no DMA callbacks have arrived for this long.
// Zero-block non-blocking writes alone are not sufficient (they can happen when DMA is simply full).
static constexpr uint32_t SPDIF_STALL_NO_DMA_MS = 80;
// Fallback stall detector: force recovery if silence writes make no forward progress for too long,
// even if occasional DMA callbacks are still observed.
static constexpr uint32_t SPDIF_STALL_ZERO_PROGRESS_MS = 1000;
// Minimum spacing between re-prime attempts to avoid churn.
static constexpr uint32_t SPDIF_REPRIME_COOLDOWN_MS = 500;
// Small waits used in SPDIF mode to keep DMA fed during rapid pipeline churn.
static constexpr uint32_t SPDIF_EMPTY_READ_DELAY_MS = 1;
static constexpr uint32_t SPDIF_SILENCE_LOOP_DELAY_MS = 1;
static constexpr uint32_t SPDIF_PLAY_RETRY_WAIT_MS = 5;
static constexpr size_t SPDIF_I2S_EVENT_QUEUE_COUNT = SPDIF_DMA_BUFFERS_COUNT + 1;
// Static silence buffer for SPDIF continuous mode
// 192 samples * 2 channels * 2 bytes per sample = 768 bytes
// Stored in flash (.rodata section) to avoid stack/heap usage
static const int16_t SPDIF_SILENCE_BUFFER[SPDIF_BLOCK_SAMPLES * 2] = {0};
// Static callback functions for SPDIF encoder (avoids std::function overhead)
static esp_err_t spdif_preload_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) {
auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx);
size_t bytes_written = 0;
esp_err_t err = i2s_channel_preload_data(speaker->get_tx_handle(), data, size, &bytes_written);
if (err != ESP_OK || bytes_written != size) {
ESP_LOGW(TAG, "Preload failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size);
return (err != ESP_OK) ? err : ESP_ERR_NO_MEM;
}
return ESP_OK;
}
static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) {
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);
// ESP_ERR_TIMEOUT is expected under DMA backpressure in SPDIF mode.
if (err != ESP_OK && err != ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size);
}
return err;
}
void I2SAudioSpeakerSPDIF::setup() {
I2SAudioSpeakerBase::setup();
if (this->is_failed()) {
return;
}
this->spdif_encoder_ = new SPDIFEncoder();
if (!this->spdif_encoder_->setup()) {
ESP_LOGE(TAG, "Encoder setup failed");
this->mark_failed();
return;
}
// Configure channel status block with the sample rate
this->spdif_encoder_->set_sample_rate(this->sample_rate_);
// Separate callbacks for preload (during underflow recovery) and normal writes
this->spdif_encoder_->set_preload_callback(spdif_preload_cb, this);
this->spdif_encoder_->set_write_callback(spdif_write_cb, this);
}
void I2SAudioSpeakerSPDIF::dump_config() {
I2SAudioSpeakerBase::dump_config();
ESP_LOGCONFIG(TAG,
" SPDIF Mode: YES\n"
" Sample Rate: %" PRIu32 " Hz",
this->sample_rate_);
}
void I2SAudioSpeakerSPDIF::on_task_stopped() { this->spdif_silence_start_ = 0; }
size_t I2SAudioSpeakerSPDIF::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
if (this->is_failed()) {
ESP_LOGE(TAG, "Setup failed; cannot play audio");
return 0;
}
// In SPDIF mode, keep accepting upstream audio while the speaker task is active.
// This avoids transient drops during stop/start transitions.
const bool task_active = (this->speaker_task_handle_ != nullptr);
if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) {
this->start();
}
if (!task_active && this->state_ != speaker::STATE_RUNNING) {
// Unable to write data to a running speaker, so delay the max amount of time so it can get ready
vTaskDelay(ticks_to_wait);
ticks_to_wait = 0;
}
size_t bytes_written = 0;
if (this->state_ == speaker::STATE_RUNNING || task_active) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
// In SPDIF mode, a tiny wait helps avoid transient 0-byte writes during short backpressure windows.
TickType_t effective_ticks_to_wait = ticks_to_wait;
if (effective_ticks_to_wait == 0) {
effective_ticks_to_wait = pdMS_TO_TICKS(1);
}
bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, effective_ticks_to_wait);
if (bytes_written == 0 && length > 0) {
// Retry once to catch short free-space windows during rapid seek/track transitions.
bytes_written =
temp_ring_buffer->write_without_replacement((void *) data, length, pdMS_TO_TICKS(SPDIF_PLAY_RETRY_WAIT_MS));
}
}
}
return bytes_written;
}
void I2SAudioSpeakerSPDIF::run_speaker_task() {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
// Reset SPDIF encoder at task start to ensure clean state
// (previous task may have left stale data in encoder buffer)
if (this->spdif_encoder_ != nullptr) {
this->spdif_encoder_->reset();
}
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * SPDIF_DMA_BUFFERS_COUNT;
// Ensure ring buffer duration is at least the duration of all DMA buffers
const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_);
// The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info
const size_t ring_buffer_size = this->current_stream_info_.ms_to_bytes(ring_buffer_duration);
// For SPDIF mode, one DMA buffer = one SPDIF block = 192 PCM frames
const uint32_t frames_to_fill_single_dma_buffer = SPDIF_BLOCK_SAMPLES;
const size_t bytes_to_fill_single_dma_buffer =
this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer);
bool successful_setup = false;
std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer =
audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer);
if (transfer_buffer != nullptr) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
if (temp_ring_buffer.use_count() == 1) {
transfer_buffer->set_source(temp_ring_buffer);
this->audio_ring_buffer_ = temp_ring_buffer;
successful_setup = true;
}
}
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.
// Track how many buffers were preloaded so the DMA event loop can skip
// frame accounting until the preloaded silence has fully drained.
uint32_t preload_buffers_remaining = 0;
this->spdif_encoder_->set_preload_mode(true);
for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) {
uint32_t preload_blocks = 0;
esp_err_t preload_err = this->spdif_encoder_->write(reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER),
sizeof(SPDIF_SILENCE_BUFFER),
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &preload_blocks);
if (preload_err != ESP_OK || preload_blocks == 0) {
break; // DMA buffers full or error
}
preload_buffers_remaining += preload_blocks;
}
this->spdif_encoder_->set_preload_mode(false);
this->spdif_encoder_->reset(); // Clean encoder state for the main loop
// Now register the callback and enable the channel
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_);
bool stop_gracefully = false;
bool tx_dma_underflow = true;
uint32_t frames_written = 0;
// SPDIF Continuous Silence Mode + Callback Decimation
//
// Key principles:
// 1. NEVER stop the I2S channel - always output a valid SPDIF stream
// 2. When no audio data, output silence-encoded SPDIF blocks (not zeros!)
// 3. Fire callbacks every 4 DMA events (~16ms), matching non-SPDIF timing
//
// This eliminates gaps that cause SPDIF receivers to re-sync, and reduces
// callback rate to prevent overwhelming upstream sync algorithms.
const uint32_t spdif_callback_threshold = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS);
uint32_t spdif_pending_frames = 0;
int64_t spdif_pending_timestamp = 0;
uint32_t spdif_last_callback_time = millis();
// Count DMA events for decimation
uint32_t spdif_dma_event_count = 0;
uint32_t spdif_last_dma_event_time = millis();
// Detect a stalled DMA path (many silence write attempts with zero accepted blocks).
uint32_t spdif_zero_block_streak = 0;
uint32_t spdif_last_block_progress_time = millis();
uint32_t spdif_last_reprime_time = 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 or silence timeout).
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);
// In SPDIF continuous mode, don't tear down or expose STOPPED here.
// Keep the task alive and transition to silence output.
this->spdif_silence_start_ = millis();
ESP_LOGV(TAG, "COMMAND_STOP received, continuing in silence mode");
}
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY);
stop_gracefully = true;
}
if (this->audio_stream_info_ != this->current_stream_info_) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings.
ESP_LOGV(TAG, "Exiting: stream info changed");
break;
}
int64_t write_timestamp;
while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) {
spdif_last_dma_event_time = millis();
// Skip frame accounting for preloaded silence buffers still draining.
// These DMA events correspond to silence that was preloaded before the
// channel was enabled, not real audio written by the task.
if (preload_buffers_remaining > 0) {
preload_buffers_remaining--;
continue;
}
// Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes
// on the timing info via the audio_output_callback.
uint32_t frames_sent = frames_to_fill_single_dma_buffer;
if (frames_to_fill_single_dma_buffer > frames_written) {
tx_dma_underflow = true;
frames_sent = frames_written;
const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written;
write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed);
} else {
tx_dma_underflow = false;
}
frames_written -= frames_sent;
// SPDIF Callback Decimation: fire every 4th DMA event (~16ms)
// This matches non-SPDIF timing and prevents overwhelming upstream.
if (spdif_callback_threshold > 0) {
spdif_dma_event_count++;
// Accumulate frames; always keep the latest timestamp so the
// callback reports when the last sample left the wire, not the first.
if (frames_sent > 0) {
spdif_pending_timestamp = write_timestamp;
spdif_pending_frames += frames_sent;
}
// Fire callback every 4 DMA events, or on timeout if we have pending frames
bool decimation_reached = (spdif_dma_event_count >= SPDIF_DMA_EVENTS_PER_CALLBACK);
bool timeout_flush =
(spdif_pending_frames > 0) && ((millis() - spdif_last_callback_time) >= SPDIF_FLUSH_TIMEOUT_MS);
if (decimation_reached || timeout_flush) {
if (spdif_pending_frames > 0) {
this->audio_output_callback_(spdif_pending_frames, spdif_pending_timestamp);
spdif_pending_frames = 0;
spdif_last_callback_time = millis();
}
spdif_dma_event_count = 0; // Reset decimation counter
}
}
}
if (this->pause_state_) {
// Pause state is accessed atomically, so thread safe
// Delay so the task yields, then skip transferring audio data
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
continue;
}
// Wait half the duration of the data already written to the DMA buffers for new audio data
// The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000
uint32_t read_delay = (this->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
// In SPDIF mode, if transfer buffer is empty (we're pumping silence), use a very short timeout.
// This ensures we can pump silence fast enough to keep the DMA fed (~250 blocks/sec needed).
// Otherwise the long timeout based on frames_written causes DMA to run dry.
if (transfer_buffer->available() == 0) {
read_delay = SPDIF_EMPTY_READ_DELAY_MS;
}
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read;
if (bytes_read > 0) {
this->apply_software_volume_(new_data, bytes_read);
this->swap_esp32_mono_samples_(new_data, bytes_read);
}
if (transfer_buffer->available() == 0) {
// SPDIF Continuous Silence Mode: always output valid SPDIF stream
// When no audio data, write silence-encoded blocks to keep receiver happy
if (this->spdif_encoder_ != nullptr) {
// "Graceful stop" means "drain buffered audio, then stop." In SPDIF
// continuous mode we never actually stop, so once audio is drained
// (we're here), reset the flag to re-enable silence writing and stall
// recovery. Without this, stop_gracefully stays true forever and
// blocks silence output, causing DMA to degrade on auto_clear zeros.
stop_gracefully = false;
// Track when we entered silence mode
if (this->spdif_silence_start_ == 0) {
this->spdif_silence_start_ = millis();
}
// If silence persists past the configured timeout, stop the task
// so components expecting timeout semantics can recover.
if (this->timeout_.has_value()) {
const uint32_t silence_duration = millis() - this->spdif_silence_start_;
if (silence_duration >= this->timeout_.value()) {
ESP_LOGV(TAG, "Silence timeout reached (%" PRIu32 "ms) - stopping speaker", silence_duration);
break;
}
}
// First flush any partial block with silence padding (non-blocking to avoid getting stuck).
// IMPORTANT: Credit any partial block frames to frames_written so the audio_output_callback_
// fires for them. Without this, pending_playback_frames_ in the mixer's SourceSpeaker never
// reaches 0 when a stream ends on a non-192-frame boundary, permanently blocking teardown.
if (this->spdif_encoder_->has_pending_data()) {
uint32_t partial_frames = this->spdif_encoder_->get_pending_frames();
// Use a tiny timeout to allow DMA queue progress without stalling the task.
esp_err_t flush_err = this->spdif_encoder_->flush_with_silence(pdMS_TO_TICKS(1));
if (flush_err == ESP_OK && partial_frames > 0) {
frames_written += partial_frames;
}
}
// CRITICAL: In SPDIF continuous mode, ALWAYS write silence when no audio data.
// We don't check tx_dma_underflow because:
// 1. When DMA runs empty, callbacks stop, so tx_dma_underflow doesn't update
// 2. The non-blocking write handles "DMA full" gracefully (just doesn't write)
// 3. We need continuous output to prevent receiver from losing sync
if (!stop_gracefully) {
uint32_t silence_blocks = 0;
esp_err_t write_err = this->spdif_encoder_->write(
reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), sizeof(SPDIF_SILENCE_BUFFER), pdMS_TO_TICKS(1),
&silence_blocks); // Non-blocking
// Don't count silence as frames_written - it's not real audio
// Recovery path for a stalled SPDIF TX channel:
// if silence writes repeatedly produce zero blocks AND DMA callbacks have stopped,
// re-prime DMA using preload mode.
const uint32_t ms_since_dma = millis() - spdif_last_dma_event_time;
const bool dma_events_stalled = ms_since_dma >= SPDIF_STALL_NO_DMA_MS;
if (silence_blocks > 0) {
spdif_last_block_progress_time = millis();
}
const bool long_zero_progress = (millis() - spdif_last_block_progress_time) >= SPDIF_STALL_ZERO_PROGRESS_MS;
if (dma_events_stalled && silence_blocks == 0 && (write_err == ESP_OK || write_err == ESP_ERR_TIMEOUT)) {
spdif_zero_block_streak++;
} else {
spdif_zero_block_streak = 0;
}
const uint32_t now_ms = millis();
const bool reprime_cooldown_elapsed =
(spdif_last_reprime_time == 0) || ((now_ms - spdif_last_reprime_time) >= SPDIF_REPRIME_COOLDOWN_MS);
if ((spdif_zero_block_streak >= 100 || long_zero_progress) && reprime_cooldown_elapsed) {
ESP_LOGV(TAG, "TX appears stalled, attempting DMA re-prime");
i2s_channel_disable(this->tx_handle_);
const i2s_event_callbacks_t null_callbacks = {.on_sent = nullptr};
i2s_channel_register_event_callback(this->tx_handle_, &null_callbacks, this);
this->spdif_encoder_->set_preload_mode(true);
uint32_t preload_blocks = 0;
esp_err_t preload_err = this->spdif_encoder_->write(
reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), sizeof(SPDIF_SILENCE_BUFFER),
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &preload_blocks);
this->spdif_encoder_->set_preload_mode(false);
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_);
if (preload_err == ESP_OK && preload_blocks > 0) {
tx_dma_underflow = false;
preload_buffers_remaining = preload_blocks;
frames_written = 0; // Stale after channel disable/enable cycle
ESP_LOGV(TAG, "DMA re-prime successful (%" PRIu32 " preload blocks)", preload_blocks);
spdif_last_block_progress_time = now_ms;
} else {
ESP_LOGW(TAG, "DMA re-prime failed (%s, blocks=%" PRIu32 ")", esp_err_to_name(preload_err),
preload_blocks);
}
spdif_last_reprime_time = now_ms;
spdif_zero_block_streak = 0;
}
}
}
if (stop_gracefully && tx_dma_underflow) {
// In SPDIF continuous mode, don't break on graceful stop during silence
// Keep outputting silence until new audio arrives or explicit COMMAND_STOP
// (handled above which transitions to silence mode rather than breaking)
}
// In SPDIF mode, use a shorter delay to pump silence faster
// We need ~250 blocks/sec to keep DMA fed, so max 4ms per iteration
vTaskDelay(pdMS_TO_TICKS(SPDIF_SILENCE_LOOP_DELAY_MS));
} else {
// Have audio data to write
size_t bytes_written = 0;
// Clear silence timer since we have audio data now
if (this->spdif_silence_start_ != 0) {
uint32_t silence_duration = millis() - this->spdif_silence_start_;
if (silence_duration > 100) {
ESP_LOGV(TAG, "Exiting silence mode after %" PRIu32 "ms, have audio data", silence_duration);
}
this->spdif_silence_start_ = 0;
}
{
uint32_t blocks_sent = 0;
size_t pcm_bytes_consumed = 0;
// Write audio data to encoder (which writes to DMA)
esp_err_t err =
this->spdif_encoder_->write(transfer_buffer->get_buffer_start(), transfer_buffer->available(),
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &blocks_sent, &pcm_bytes_consumed);
if (err != ESP_OK && err != ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Write failed: %s", esp_err_to_name(err));
}
// Only consume source bytes that were actually accepted by the encoder.
bytes_written = pcm_bytes_consumed;
// Update frame accounting based on complete blocks sent (192 frames per block)
if (bytes_written > 0) {
frames_written += blocks_sent * SPDIF_BLOCK_SAMPLES;
transfer_buffer->decrease_buffer_length(bytes_written);
// Audio blocks count as DMA progress for the stall detector.
// Without this, a long uninterrupted audio stream makes the
// progress timer stale, triggering a spurious re-prime the
// instant we transition to silence.
spdif_last_block_progress_time = millis();
}
}
}
}
// If we reach here, the while loop exited - either via break or condition became false
// In SPDIF mode, loop exit is expected when:
// 1. Timeout reached (user configured timeout)
// 2. Stream info changed
// Only warn if timeout is "never" since that should never exit
if (!this->timeout_.has_value()) {
ESP_LOGW(TAG, "Unexpected loop exit; set 'timeout: never' to prevent this");
}
}
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
// Reset SPDIF encoder state to prevent stale state on next start
if (this->spdif_encoder_ != nullptr) {
this->spdif_encoder_->set_preload_mode(false);
this->spdif_encoder_->reset();
}
if (transfer_buffer != nullptr) {
transfer_buffer.reset();
}
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED);
while (true) {
// Continuously delay until the loop method deletes the task
vTaskDelay(pdMS_TO_TICKS(10));
}
}
esp_err_t I2SAudioSpeakerSPDIF::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) {
this->current_stream_info_ = audio_stream_info;
// SPDIF mode validation
if (this->sample_rate_ != audio_stream_info.get_sample_rate()) {
ESP_LOGE(TAG, "Only supports a single sample rate (configured: %" PRIu32 " Hz, stream: %" PRIu32 " Hz)",
this->sample_rate_, audio_stream_info.get_sample_rate());
return ESP_ERR_NOT_SUPPORTED;
}
if (audio_stream_info.get_bits_per_sample() != 16) {
ESP_LOGE(TAG, "Only supports 16 bits per sample");
return ESP_ERR_NOT_SUPPORTED;
}
if (audio_stream_info.get_channels() != 2) {
ESP_LOGE(TAG, "Only supports stereo (2 channels)");
return ESP_ERR_NOT_SUPPORTED;
}
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO &&
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
ESP_LOGE(TAG, "Stream bits per sample must be less than or equal to the speaker's configuration");
return ESP_ERR_NOT_SUPPORTED;
}
if (!this->parent_->try_lock()) {
ESP_LOGE(TAG, "Parent bus is busy");
return ESP_ERR_INVALID_STATE;
}
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
#if SOC_CLK_APLL_SUPPORTED
if (this->use_apll_) {
clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL;
}
#endif // SOC_CLK_APLL_SUPPORTED
// SPDIF mode: fixed configuration for BMC encoding
// For new driver, dma_frame_num is in I2S frames (8 bytes each for 32-bit stereo)
uint32_t dma_buffer_length = SPDIF_BLOCK_I2S_FRAMES; // One SPDIF block = 384 I2S frames = 3072 bytes
// Log DMA configuration for debugging
ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames = %lu bytes total", (size_t) SPDIF_DMA_BUFFERS_COUNT,
(unsigned long) dma_buffer_length,
(unsigned long) (SPDIF_DMA_BUFFERS_COUNT * dma_buffer_length * 8)); // 8 bytes per frame for 32-bit stereo
i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = this->i2s_role_,
.dma_desc_num = SPDIF_DMA_BUFFERS_COUNT,
.dma_frame_num = dma_buffer_length,
.auto_clear = true,
.intr_priority = 3,
};
// SPDIF: double sample rate for BMC, 32-bit stereo, only data pin needed
i2s_std_clk_config_t clk_cfg = {
.sample_rate_hz = this->sample_rate_ * 2,
.clk_src = clk_src,
.mclk_multiple = this->mclk_multiple_,
};
i2s_std_slot_config_t slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO);
i2s_std_gpio_config_t gpio_cfg = {
.mclk = GPIO_NUM_NC,
.bclk = GPIO_NUM_NC,
.ws = GPIO_NUM_NC,
.dout = this->dout_pin_,
.din = GPIO_NUM_NC,
.invert_flags =
{
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
};
i2s_std_config_t std_cfg = {
.clk_cfg = clk_cfg,
.slot_cfg = slot_cfg,
.gpio_cfg = gpio_cfg,
};
esp_err_t err = this->init_i2s_channel_(chan_cfg, std_cfg, SPDIF_I2S_EVENT_QUEUE_COUNT);
if (err != ESP_OK) {
return err;
}
// Channel is NOT enabled here. The speaker task will preload DMA buffers
// with SPDIF-encoded silence before enabling, ensuring the first data on
// the wire is valid SPDIF (not raw zeros from auto_clear) and preventing
// phantom DMA events before real audio data is available.
return ESP_OK;
}
} // namespace esphome::i2s_audio
#endif // USE_ESP32 && USE_I2S_AUDIO_SPDIF_MODE
@@ -0,0 +1,34 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE)
#include "i2s_audio_speaker.h"
#include "spdif_encoder.h"
namespace esphome::i2s_audio {
/// @brief SPDIF speaker implementation.
/// Encodes PCM audio into IEC 60958-3 S/PDIF bitstream using BMC encoding,
/// outputting through a single I2S data pin. Maintains continuous output
/// (silence when no audio) to keep SPDIF receivers synchronized.
class I2SAudioSpeakerSPDIF : public I2SAudioSpeakerBase {
public:
void setup() override;
void dump_config() override;
size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override;
protected:
void run_speaker_task() override;
esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override;
void on_task_stopped() override;
SPDIFEncoder *spdif_encoder_{nullptr};
uint32_t spdif_silence_start_{0}; // Timestamp when silence mode started (0 = not in silence)
};
} // namespace esphome::i2s_audio
#endif // USE_ESP32 && USE_I2S_AUDIO_SPDIF_MODE
@@ -257,7 +257,7 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize channel");
ESP_LOGE(TAG, "Failed to initialize I2S channel");
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
this->parent_->unlock();
@@ -18,7 +18,7 @@
namespace esphome::i2s_audio {
// Shared constants for I2S audio speaker implementations
// Shared constants used by both standard and SPDIF speaker implementations
static constexpr uint32_t DMA_BUFFER_DURATION_MS = 15;
static constexpr size_t TASK_STACK_SIZE = 4096;
static constexpr ssize_t TASK_PRIORITY = 19;
@@ -42,7 +42,7 @@ enum SpeakerEventGroupBits : uint32_t {
/// @brief Abstract base class for I2S audio speaker implementations.
/// Provides shared infrastructure (event groups, ring buffer, volume control, task lifecycle)
/// for derived I2S speaker classes.
/// for derived standard I2S and SPDIF speaker classes.
class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public Component {
public:
float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
@@ -0,0 +1,385 @@
#include "spdif_encoder.h"
#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE)
#include "esphome/core/log.h"
namespace esphome::i2s_audio {
static const char *const TAG = "i2s_audio.spdif_encoder";
// S/PDIF preamble patterns (8 BMC bits each)
// These are the BMC-encoded sync patterns that violate normal BMC rules for easy detection.
// All preambles end at phase HIGH (last bit = 1), enabling consistent data encoding.
// Preamble is placed at bits 24-31 of word[0] for MSB-first transmission.
static constexpr uint8_t PREAMBLE_B = 0x17; // Block start (left channel, frame 0)
static constexpr uint8_t PREAMBLE_M = 0x1d; // Left channel (not block start)
static constexpr uint8_t PREAMBLE_W = 0x1b; // Right channel
// BMC encoding of 4 zero bits starting at phase HIGH: 00_11_00_11 = 0x33
// Since both aux nibbles (bits 4-7, 8-11) are zero for 16-bit audio and phase is preserved, both are 0x33.
static constexpr uint32_t BMC_ZERO_NIBBLE = 0x33;
// Constexpr BMC encoder for compile-time LUT generation.
// Encodes with start phase=true (HIGH). The complement property allows phase=false
// via XOR: bmc_encode(v, N, false) == bmc_encode(v, N, true) ^ mask
static constexpr uint16_t bmc_lut_encode(uint32_t data, uint8_t num_bits) {
uint16_t bmc = 0;
bool phase = true;
for (uint8_t i = 0; i < num_bits; i++) {
bool bit = (data >> i) & 1;
uint8_t bmc_pair = phase ? (bit ? 0b01 : 0b00) : (bit ? 0b10 : 0b11);
bmc |= static_cast<uint16_t>(bmc_pair) << ((num_bits - 1 - i) * 2);
if (!bit)
phase = !phase;
}
return bmc;
}
// 4-bit BMC lookup table: 16 entries (16 bytes in flash)
// Index: 4-bit data value (0-15), always phase=true start
static constexpr auto BMC_LUT_4 = [] {
std::array<uint8_t, 16> t{};
for (uint32_t i = 0; i < 16; i++)
t[i] = static_cast<uint8_t>(bmc_lut_encode(i, 4));
return t;
}();
// 8-bit BMC lookup table: 256 entries (512 bytes in flash)
// Index: 8-bit data value (0-255), always phase=true start
static constexpr auto BMC_LUT_8 = [] {
std::array<uint16_t, 256> t{};
for (uint32_t i = 0; i < 256; i++)
t[i] = bmc_lut_encode(i, 8);
return t;
}();
// Initialize S/PDIF buffer
bool SPDIFEncoder::setup() {
this->spdif_block_buf_ = std::make_unique<uint32_t[]>(SPDIF_BLOCK_SIZE_U32);
if (!this->spdif_block_buf_) {
ESP_LOGE(TAG, "Buffer allocation failed (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES);
return false;
}
ESP_LOGV(TAG, "Buffer allocated (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES);
// Build initial channel status block with default sample rate
this->build_channel_status_();
this->reset();
return true;
}
void SPDIFEncoder::reset() {
this->spdif_block_ptr_ = this->spdif_block_buf_.get();
this->frame_in_block_ = 0;
this->is_left_channel_ = true;
}
void SPDIFEncoder::set_sample_rate(uint32_t sample_rate) {
if (this->sample_rate_ != sample_rate) {
this->sample_rate_ = sample_rate;
this->build_channel_status_();
ESP_LOGD(TAG, "Sample rate set to %lu Hz", (unsigned long) sample_rate);
}
}
void SPDIFEncoder::build_channel_status_() {
// IEC 60958-3 Consumer Channel Status Block (192 bits = 24 bytes)
// Transmitted LSB-first within each byte, one bit per frame via C bit
//
// Byte 0: Control bits
// Bit 0: 0 = Consumer format (not professional AES3)
// Bit 1: 0 = PCM audio (not non-audio data like AC3)
// Bit 2: 0 = No copyright assertion
// Bits 3-5: 000 = No pre-emphasis
// Bits 6-7: 00 = Mode 0 (basic consumer format)
//
// Byte 1: Category code (0x00 = general, 0x01 = CD, etc.)
//
// Byte 2: Source/channel numbers
// Bits 0-3: Source number (0 = unspecified)
// Bits 4-7: Channel number (0 = unspecified)
//
// Byte 3: Sample frequency and clock accuracy
// Bits 0-3: Sample frequency code
// Bits 4-5: Clock accuracy (00 = Level II, ±1000 ppm, appropriate for ESP32)
// Bits 6-7: Reserved (0)
//
// Bytes 4-23: Reserved (zeros for basic compliance)
// Clear all bytes first
this->channel_status_.fill(0);
// Byte 0: Consumer, PCM audio, no copyright, no pre-emphasis, Mode 0
// All bits are 0, which is already set
// Byte 1: Category code = 0x00 (general)
// Already 0
// Byte 2: Source/channel unspecified
// Already 0
// Byte 3: Sample frequency code (bits 0-3) + clock accuracy (bits 4-5)
// Clock accuracy = 00 (Level II, ±1000 ppm) - appropriate for ESP32
uint8_t freq_code;
switch (this->sample_rate_) {
case 44100:
freq_code = 0x0; // 0000
break;
case 48000:
freq_code = 0x2; // 0010
break;
default:
// Other values are possible but they're not supported by ESPHome
freq_code = 0x1; // 0001 = not indicated
ESP_LOGW(TAG, "Unsupported sample rate %lu Hz, channel status will indicate 'not specified'",
(unsigned long) this->sample_rate_);
break;
}
// Byte 3: freq_code in bits 0-3, clock accuracy (00) in bits 4-5
this->channel_status_[3] = freq_code; // Clock accuracy bits 4-5 are already 0
// Bytes 4-23 remain zero (word length not specified, no original sample freq, etc.)
}
HOT void SPDIFEncoder::encode_sample_(const uint8_t *pcm_sample) {
// ============================================================================
// Build raw 32-bit subframe (IEC 60958 format)
// ============================================================================
// Bit layout:
// Bits 0-3: Preamble (handled separately, not in raw_subframe)
// Bits 4-7: Auxiliary audio data (zeros for 16-bit audio)
// Bits 8-11: Audio LSB extension (zeros for 16-bit audio)
// Bits 12-27: 16-bit audio sample (MSB-aligned in 20-bit audio field)
// Bit 28: V (Validity) - 0 = valid audio
// Bit 29: U (User data) - 0
// Bit 30: C (Channel status) - from channel status block
// Bit 31: P (Parity) - even parity over bits 4-31
// ============================================================================
// Place 16-bit audio sample at bits 12-27 (little-endian input: [0]=LSB, [1]=MSB)
uint32_t raw_subframe = (static_cast<uint32_t>(pcm_sample[1]) << 20) | (static_cast<uint32_t>(pcm_sample[0]) << 12);
// V = 0 (valid audio), U = 0 (no user data)
// C = channel status bit for current frame (same bit used for both L and R subframes)
bool c_bit = this->get_channel_status_bit_(this->frame_in_block_);
if (c_bit) {
raw_subframe |= (1U << 30);
}
// Calculate even parity over bits 4-30
// This ensures consistent BMC ending phase regardless of audio content
uint32_t bits_4_30 = (raw_subframe >> 4) & 0x07FFFFFF; // 27 bits (4-30)
uint32_t ones_count = __builtin_popcount(bits_4_30);
uint32_t parity = ones_count & 1; // 1 if odd count, 0 if even
raw_subframe |= parity << 31; // Set P bit to make total even
// ============================================================================
// Select preamble based on position in block and channel
// ============================================================================
// B = block start (left channel, frame 0 of 192-frame block)
// M = left channel (frames 1-191)
// W = right channel (all frames)
uint8_t preamble;
if (this->is_left_channel_) {
preamble = (this->frame_in_block_ == 0) ? PREAMBLE_B : PREAMBLE_M;
} else {
preamble = PREAMBLE_W;
}
// ============================================================================
// BMC encode the data portion (bits 4-31) using lookup tables
// ============================================================================
// The I2S uses 16-bit halfword swap: bits 16-31 transmit before bits 0-15.
// This applies to BOTH word[0] and word[1].
//
// word[0] transmission order: [16-23] → [24-31] → [0-7] → [8-15]
// For correct S/PDIF subframe order (preamble → aux → audio):
// - bits 16-23: preamble (8 BMC bits)
// - bits 24-31: BMC(subframe bits 4-7) - first aux nibble
// - bits 0-7: BMC(subframe bits 8-11) - second aux nibble
// - bits 8-15: BMC(subframe bits 12-15) - audio low nibble
//
// word[1] transmission order: [16-31] → [0-15]
// For correct S/PDIF subframe order:
// - bits 16-31: BMC(subframe bits 16-23) - audio mid byte
// - bits 0-15: BMC(subframe bits 24-31) - audio high nibble + VUCP
// ============================================================================
// All preambles end at phase HIGH. Bits 4-11 are always zero for 16-bit audio;
// two zero nibbles flip phase 8 times total → back to HIGH.
// So bits 12-15 always start encoding at phase=true.
// Bits 12-15: 4-bit LUT lookup (always phase=true start)
uint32_t nibble = (raw_subframe >> 12) & 0xF;
uint32_t bmc_12_15 = BMC_LUT_4[nibble];
// Phase tracking via branchless XOR mask:
// - 0x0000 means phase=true (use LUT value directly)
// - 0xFFFF means phase=false (complement LUT value)
// End phase = start XOR (popcount & 1) since zero-bits flip phase,
// and for even bit widths: #zeros parity == popcount parity.
uint32_t phase_mask = -(__builtin_popcount(nibble) & 1u) & 0xFFFF;
// Bits 16-23: 8-bit LUT lookup with phase correction
uint32_t byte_mid = (raw_subframe >> 16) & 0xFF;
uint32_t bmc_16_23 = BMC_LUT_8[byte_mid] ^ phase_mask;
phase_mask ^= -(__builtin_popcount(byte_mid) & 1u) & 0xFFFF;
// Bits 24-31: 8-bit LUT lookup with phase correction
uint32_t byte_hi = (raw_subframe >> 24) & 0xFF;
uint32_t bmc_24_31 = BMC_LUT_8[byte_hi] ^ phase_mask;
// ============================================================================
// Combine with correct positioning for I2S transmission
// ============================================================================
// I2S with halfword swap: transmits bits 16-31, then bits 0-15.
// Within each halfword, MSB (highest bit) is transmitted first.
//
// For upper halfword (bits 16-31): bit 31 → bit 16
// For lower halfword (bits 0-15): bit 15 → bit 0
//
// Desired S/PDIF order: preamble → bmc_4_7 → bmc_8_11 → bmc_12_15
//
// word[0] layout for correct transmission:
// bits 24-31: preamble (transmitted 1st, as MSB of upper halfword)
// bits 16-23: BMC_ZERO_NIBBLE (transmitted 2nd, aux bits 4-7)
// bits 8-15: BMC_ZERO_NIBBLE (transmitted 3rd, aux bits 8-11)
// bits 0-7: bmc_12_15 (transmitted 4th, audio low nibble)
//
// word[1] layout:
// bits 16-31: bmc_16_23 (transmitted 5th)
// bits 0-15: bmc_24_31 (transmitted 6th)
this->spdif_block_ptr_[0] =
bmc_12_15 | (BMC_ZERO_NIBBLE << 8) | (BMC_ZERO_NIBBLE << 16) | (static_cast<uint32_t>(preamble) << 24);
this->spdif_block_ptr_[1] = bmc_24_31 | (bmc_16_23 << 16);
this->spdif_block_ptr_ += 2;
// ============================================================================
// Update position tracking
// ============================================================================
if (!this->is_left_channel_) {
// Completed a stereo frame, advance frame counter
if (++this->frame_in_block_ >= SPDIF_BLOCK_SAMPLES) {
this->frame_in_block_ = 0;
}
}
this->is_left_channel_ = !this->is_left_channel_;
}
esp_err_t SPDIFEncoder::send_block_(TickType_t ticks_to_wait) {
// Use the appropriate callback and context based on preload mode
SPDIFBlockCallback callback;
void *ctx;
if (this->preload_mode_) {
callback = this->preload_callback_;
ctx = this->preload_callback_ctx_;
} else {
callback = this->write_callback_;
ctx = this->write_callback_ctx_;
}
if (callback == nullptr) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = callback(ctx, this->spdif_block_buf_.get(), SPDIF_BLOCK_SIZE_BYTES, ticks_to_wait);
if (err == ESP_OK) {
// Reset pointer for next block; position tracking continues from where it left off
this->spdif_block_ptr_ = this->spdif_block_buf_.get();
}
return err;
}
size_t SPDIFEncoder::get_pending_pcm_bytes() const {
if (this->spdif_block_ptr_ == nullptr || this->spdif_block_buf_ == nullptr) {
return 0;
}
// Each PCM sample (2 bytes) produces 2 uint32_t values in the SPDIF buffer
// So pending uint32s / 2 = pending samples, and each sample is 2 bytes
size_t pending_uint32s = this->spdif_block_ptr_ - this->spdif_block_buf_.get();
size_t pending_samples = pending_uint32s / 2;
return pending_samples * 2; // 2 bytes per sample
}
HOT esp_err_t SPDIFEncoder::write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent,
size_t *bytes_consumed) {
const uint8_t *pcm_data = src;
const uint8_t *pcm_end = src + size;
uint32_t block_count = 0;
while (pcm_data < pcm_end) {
// Check if there's a 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) {
if (blocks_sent != nullptr) {
*blocks_sent = block_count;
}
if (bytes_consumed != nullptr) {
*bytes_consumed = pcm_data - src;
}
return err;
}
++block_count;
}
// Encode one 16-bit sample
this->encode_sample_(pcm_data);
pcm_data += 2;
}
// Send any complete block that was just finished
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) {
if (blocks_sent != nullptr) {
*blocks_sent = block_count;
}
if (bytes_consumed != nullptr) {
*bytes_consumed = pcm_data - src;
}
return err;
}
++block_count;
}
if (blocks_sent != nullptr) {
*blocks_sent = block_count;
}
if (bytes_consumed != nullptr) {
*bytes_consumed = size;
}
return ESP_OK;
}
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 (!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);
}
} // namespace esphome::i2s_audio
#endif // USE_I2S_AUDIO_SPDIF_MODE
@@ -0,0 +1,146 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE)
#include <array>
#include <cstdint>
#include <memory>
#include <freertos/FreeRTOS.h>
#include "esp_err.h"
#include "esphome/core/helpers.h"
namespace esphome::i2s_audio {
// A SPDIF sample is 64-bits
static constexpr uint8_t SPDIF_BITS_PER_SAMPLE = 64;
// Number of samples in a SPDIF block
static constexpr uint16_t SPDIF_BLOCK_SAMPLES = 192;
// To emulate bi-phase mark code (BMC) (aka differential Manchester encoding) we send twice
// as many bits per sample so that we can generate the transitions this encoding requires.
static constexpr uint8_t EMULATED_BMC_BITS_PER_SAMPLE = SPDIF_BITS_PER_SAMPLE * 2;
static constexpr uint16_t SPDIF_BLOCK_SIZE_BYTES = SPDIF_BLOCK_SAMPLES * (EMULATED_BMC_BITS_PER_SAMPLE / 8);
static constexpr uint32_t SPDIF_BLOCK_SIZE_U32 = SPDIF_BLOCK_SIZE_BYTES / sizeof(uint32_t); // 3072 bytes / 4 = 768
// I2S frame count for one SPDIF block (for new driver where frame = 8 bytes for 32-bit stereo)
static constexpr uint32_t SPDIF_BLOCK_I2S_FRAMES = SPDIF_BLOCK_SIZE_BYTES / 8; // 3072 / 8 = 384 frames
// PCM bytes needed for one complete SPDIF block (192 stereo frames * 2 bytes per sample * 2 channels)
static constexpr uint16_t SPDIF_PCM_BYTES_PER_BLOCK = SPDIF_BLOCK_SAMPLES * 2 * 2; // = 768 bytes
/// Callback signature for block completion (raw function pointer for minimal overhead)
/// @param user_ctx User context pointer passed during callback registration
/// @param data Pointer to SPDIF encoded block data
/// @param size Size of the block in bytes (always SPDIF_BLOCK_SIZE_BYTES)
/// @param ticks_to_wait FreeRTOS ticks to wait for write completion
/// @return ESP_OK on success, or an error code
using SPDIFBlockCallback = esp_err_t (*)(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait);
class SPDIFEncoder {
public:
/// @brief Initialize the SPDIF working buffer
/// @return true if setup was successful, false if allocation failed
bool setup();
/// @brief Set callback for normal writes (used when channel is running)
/// @param callback Function pointer to call when a block is ready
/// @param user_ctx Context pointer passed to callback (typically 'this' pointer of speaker)
void set_write_callback(SPDIFBlockCallback callback, void *user_ctx) {
this->write_callback_ = callback;
this->write_callback_ctx_ = user_ctx;
}
/// @brief Set callback for preload writes (used when preloading to DMA before enabling channel)
/// @param callback Function pointer to call when a block is ready for preload
/// @param user_ctx Context pointer passed to callback (typically 'this' pointer of speaker)
void set_preload_callback(SPDIFBlockCallback callback, void *user_ctx) {
this->preload_callback_ = callback;
this->preload_callback_ctx_ = user_ctx;
}
/// @brief Enable or disable preload mode
/// When in preload mode, completed blocks use the preload callback instead of write callback
void set_preload_mode(bool preload) { this->preload_mode_ = preload; }
/// @brief Check if currently in preload mode
bool is_preload_mode() const { return this->preload_mode_; }
/// @brief Convert PCM audio data to SPDIF BMC encoded data
/// @param src Source PCM audio data (16-bit stereo)
/// @param size Size of source data in bytes
/// @param ticks_to_wait Timeout for blocking writes
/// @param blocks_sent Optional pointer to receive the number of complete SPDIF blocks sent
/// @param bytes_consumed Optional pointer to receive the number of PCM bytes consumed from src
/// @return esp_err_t as returned from the callback
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);
/// @brief Get the number of PCM bytes currently pending in the partial block buffer
/// @return Number of pending PCM bytes (0 to SPDIF_PCM_BYTES_PER_BLOCK - 1)
size_t get_pending_pcm_bytes() const;
/// @brief Get the number of PCM frames currently pending in the partial block buffer
/// @return Number of pending PCM frames (0 to SPDIF_BLOCK_SAMPLES - 1)
uint32_t get_pending_frames() const { return this->get_pending_pcm_bytes() / 4; }
/// @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
/// @param ticks_to_wait Timeout for blocking writes
/// @return esp_err_t as returned from the callback, or ESP_OK if nothing to flush
esp_err_t flush_with_silence(TickType_t ticks_to_wait);
/// @brief Reset the SPDIF block buffer and position tracking, discarding any partial block
void reset();
/// @brief Set the sample rate for Channel Status Block encoding
/// @param sample_rate Sample rate in Hz (e.g., 44100, 48000, 96000)
/// Call this before writing audio data to ensure correct channel status.
void set_sample_rate(uint32_t sample_rate);
/// @brief Get the currently configured sample rate
uint32_t get_sample_rate() const { return this->sample_rate_; }
protected:
/// @brief Encode a single 16-bit PCM sample into the current block position
HOT void encode_sample_(const uint8_t *pcm_sample);
/// @brief Send the completed block via the appropriate callback
esp_err_t send_block_(TickType_t ticks_to_wait);
/// @brief Build the channel status block from current configuration
void build_channel_status_();
/// @brief Get the channel status bit for a specific frame
/// @param frame Frame number (0-191)
/// @return The C bit value for this frame
ESPHOME_ALWAYS_INLINE inline bool get_channel_status_bit_(uint8_t frame) const {
// Channel status is 192 bits transmitted over 192 frames
// Bit N is transmitted in frame N, LSB-first within each byte
return (this->channel_status_[frame >> 3] >> (frame & 7)) & 1;
}
// Member ordering optimized to minimize padding (largest alignment first)
// 4-byte aligned members (pointers and uint32_t)
SPDIFBlockCallback write_callback_{nullptr};
SPDIFBlockCallback preload_callback_{nullptr};
void *write_callback_ctx_{nullptr};
void *preload_callback_ctx_{nullptr};
std::unique_ptr<uint32_t[]> spdif_block_buf_; // Working buffer for SPDIF block (heap allocated)
uint32_t *spdif_block_ptr_{nullptr}; // Current position in block buffer
uint32_t sample_rate_{48000}; // Sample rate for Channel Status Block encoding
// 1-byte aligned members (grouped together to avoid internal padding)
uint8_t frame_in_block_{0}; // 0-191, tracks stereo frame position within block
bool is_left_channel_{true}; // Alternates L/R for stereo samples
bool preload_mode_{false}; // Whether to use preload callback vs write callback
// Channel Status Block (192 bits = 24 bytes, transmitted over 192 frames)
// Placed last since std::array<uint8_t> has 1-byte alignment
std::array<uint8_t, 24> channel_status_{};
};
} // namespace esphome::i2s_audio
#endif // USE_I2S_AUDIO_SPDIF_MODE
+1
View File
@@ -72,6 +72,7 @@
#define USE_GRAPHICAL_DISPLAY_MENU
#define USE_HOMEASSISTANT_TIME
#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT
#define USE_I2S_AUDIO_SPDIF_MODE
#define USE_IMAGE
#define USE_IMPROV_SERIAL
#define USE_IMPROV_SERIAL_NEXT_URL
@@ -0,0 +1,25 @@
substitutions:
i2s_bclk_pin: GPIO27
i2s_lrclk_pin: GPIO26
i2s_mclk_pin: GPIO25
i2s_dout_pin: GPIO12
spdif_data_pin: GPIO4
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
i2s_audio:
- id: i2s_output
speaker:
- platform: i2s_audio
id: speaker_id
dac_type: external
i2s_dout_pin: ${spdif_data_pin}
spdif_mode: true
use_apll: true
timeout: 2s
sample_rate: 48000
bits_per_sample: 16bit
channel: stereo
i2s_mode: primary