|
|
|
@@ -38,6 +38,11 @@ static constexpr size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT * 2;
|
|
|
|
|
// DMA_BUFFER_DURATION_MS, so a multiple of that gives plenty of slack against scheduling jitter
|
|
|
|
|
// without masking real failures.
|
|
|
|
|
static constexpr TickType_t WRITE_TIMEOUT_TICKS = pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * (DMA_BUFFERS_COUNT + 1));
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
// Stack buffer that widened audio passes through on the original ESP32. Small enough for the speaker task's stack,
|
|
|
|
|
// large enough that the extra i2s_channel_write calls cost little.
|
|
|
|
|
static constexpr size_t WIDEN_BUFFER_BYTES = 512;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// Requested frames per DMA buffer for the given stream, clamped so the byte size stays within the ESP-IDF
|
|
|
|
|
// maximum DMA descriptor size. This is only the value handed to the channel config: ESP-IDF may still adjust
|
|
|
|
@@ -66,9 +71,8 @@ void I2SAudioSpeaker::dump_config() {
|
|
|
|
|
}
|
|
|
|
|
ESP_LOGCONFIG(TAG, " Communication format: %s", fmt_str);
|
|
|
|
|
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
|
|
|
|
// The width of each I2S slot. It is also the narrowing ceiling: streams wider than this are narrowed to
|
|
|
|
|
// it. A stream narrower than the slot is left at its own width and clocked into the wider slot, so this
|
|
|
|
|
// is not necessarily the sample data width (which depends on the incoming stream).
|
|
|
|
|
// The width of each I2S slot. Wider streams are narrowed to it. Narrower streams are padded into the slot by
|
|
|
|
|
// the peripheral, except on the original ESP32, where they are widened to it.
|
|
|
|
|
ESP_LOGCONFIG(TAG, " Slot bit width: %u", (unsigned) static_cast<uint32_t>(this->slot_bit_width_));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -92,11 +96,14 @@ void I2SAudioSpeaker::run_speaker_task() {
|
|
|
|
|
const size_t ring_buffer_size =
|
|
|
|
|
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / source_alignment) * source_alignment;
|
|
|
|
|
|
|
|
|
|
// Per-frame byte widths and whether the task must narrow the bit depth before writing to the I2S peripheral.
|
|
|
|
|
// Per-frame byte widths and whether the task must convert the bit depth before writing to the I2S peripheral.
|
|
|
|
|
const uint8_t channels = this->current_stream_info_.get_channels();
|
|
|
|
|
const uint8_t input_bytes_per_sample = this->current_stream_info_.get_bits_per_sample() / 8;
|
|
|
|
|
const uint8_t output_bytes_per_sample = this->output_stream_info_.get_bits_per_sample() / 8;
|
|
|
|
|
const bool narrowing = input_bytes_per_sample != output_bytes_per_sample;
|
|
|
|
|
const bool narrowing = input_bytes_per_sample > output_bytes_per_sample;
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
const bool widening = input_bytes_per_sample < output_bytes_per_sample;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// ESP-IDF may allocate smaller (or cache-line-rounded) DMA buffers than dma_buffer_frames() requested: it
|
|
|
|
|
// clamps each descriptor to the max DMA descriptor size and, on targets that route internal memory through
|
|
|
|
@@ -323,20 +330,26 @@ void I2SAudioSpeaker::run_speaker_task() {
|
|
|
|
|
const size_t output_bytes = this->output_stream_info_.frames_to_bytes(frames_to_write);
|
|
|
|
|
|
|
|
|
|
uint8_t *chunk = audio_source->mutable_data();
|
|
|
|
|
if (narrowing) {
|
|
|
|
|
// Narrow the bit depth in place: output exactly aliases input with the same channel count and a
|
|
|
|
|
// smaller width, which copy_frames handles as a single forward pass. Only the frames about to be
|
|
|
|
|
// consumed are overwritten, so any unprocessed tail stays intact for the next iteration.
|
|
|
|
|
esp_audio_libs::pcm_convert::copy_frames(chunk, chunk, input_bytes_per_sample, channels,
|
|
|
|
|
output_bytes_per_sample, channels, frames_to_write);
|
|
|
|
|
}
|
|
|
|
|
this->swap_esp32_mono_samples_(chunk, output_bytes);
|
|
|
|
|
|
|
|
|
|
size_t bw = 0;
|
|
|
|
|
i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS);
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
if (widening) {
|
|
|
|
|
bw = this->write_widened_(chunk, frames_to_write);
|
|
|
|
|
} else
|
|
|
|
|
#endif
|
|
|
|
|
{
|
|
|
|
|
if (narrowing) {
|
|
|
|
|
// Narrow the bit depth in place: output exactly aliases input with the same channel count and a
|
|
|
|
|
// smaller width, which copy_frames handles as a single forward pass. Only the frames about to be
|
|
|
|
|
// consumed are overwritten, so any unprocessed tail stays intact for the next iteration.
|
|
|
|
|
esp_audio_libs::pcm_convert::copy_frames(chunk, chunk, input_bytes_per_sample, channels,
|
|
|
|
|
output_bytes_per_sample, channels, frames_to_write);
|
|
|
|
|
}
|
|
|
|
|
this->swap_esp32_mono_samples_(chunk, output_bytes);
|
|
|
|
|
i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS);
|
|
|
|
|
}
|
|
|
|
|
if (bw != output_bytes) {
|
|
|
|
|
// A short write breaks DMA descriptor alignment for every subsequent event. Drop the chunk rather
|
|
|
|
|
// than retry it: it was already narrowed in place.
|
|
|
|
|
// than retry it: it may already have been converted 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);
|
|
|
|
@@ -416,26 +429,26 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
|
|
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When the stream is wider than the configured slot bit width, the speaker task narrows each frame in place
|
|
|
|
|
// before handing it to the I2S peripheral. Compute the output format here so the driver, DMA buffers, and
|
|
|
|
|
// the task's conversion all agree on the clocked-out width. A stream no wider than the slot width is passed
|
|
|
|
|
// through unchanged (the slot may still be wider than the data, the existing behavior).
|
|
|
|
|
// Compute the output width so the driver, DMA buffers, and the task's conversion all agree. A stream wider than
|
|
|
|
|
// the configured slot is narrowed to it, and a narrower stream keeps its width for the peripheral to pad into the
|
|
|
|
|
// slot. The original ESP32 cannot clock a slot wider than its data, so there every stream is converted to the
|
|
|
|
|
// slot's memory width instead.
|
|
|
|
|
uint8_t output_bits_per_sample = audio_stream_info.get_bits_per_sample();
|
|
|
|
|
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
output_bits_per_sample = this->slot_memory_bits_();
|
|
|
|
|
#else
|
|
|
|
|
const uint8_t configured_bits = static_cast<uint8_t>(this->slot_bit_width_);
|
|
|
|
|
if (output_bits_per_sample > configured_bits) {
|
|
|
|
|
output_bits_per_sample = configured_bits;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
this->output_stream_info_ = audio::AudioStreamInfo(output_bits_per_sample, audio_stream_info.get_channels(),
|
|
|
|
|
audio_stream_info.get_sample_rate());
|
|
|
|
|
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
// The original ESP32 I2S peripheral stores each sample in a whole number of 16-bit words (a 24-bit sample
|
|
|
|
|
// occupies 4 bytes in the DMA buffer, an 8-bit sample 2 bytes), but ESPHome's audio pipeline packs samples
|
|
|
|
|
// tightly (3 bytes for 24-bit, 1 for 8-bit). The two layouts only line up when the bit depth is a multiple
|
|
|
|
|
// of 16. The check is on the output width since that is what reaches the peripheral; a wider input is fine
|
|
|
|
|
// as long as it narrows to a 16- or 32-bit slot.
|
|
|
|
|
// Only a slot left at AUTO can reach this; every configured slot is converted to 16- or 32-bit samples above
|
|
|
|
|
if (output_bits_per_sample % 16 != 0) {
|
|
|
|
|
ESP_LOGE(TAG, "ESP32 supports only 16- or 32-bit output, got %u-bit", (unsigned) output_bits_per_sample);
|
|
|
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
|
|
@@ -487,8 +500,8 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
|
|
|
|
|
|
|
|
|
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
|
|
|
|
audio::AudioStreamInfo I2SAudioSpeaker::full_duplex_stream_info_() const {
|
|
|
|
|
return audio::AudioStreamInfo(static_cast<uint8_t>(this->slot_bit_width_),
|
|
|
|
|
this->slot_mode_ == I2S_SLOT_MODE_STEREO ? 2 : 1, this->sample_rate_);
|
|
|
|
|
return audio::AudioStreamInfo(this->slot_memory_bits_(), this->slot_mode_ == I2S_SLOT_MODE_STEREO ? 2 : 1,
|
|
|
|
|
this->sample_rate_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void I2SAudioSpeaker::build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) {
|
|
|
|
@@ -530,8 +543,14 @@ void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_str
|
|
|
|
|
slot_mask = I2S_STD_SLOT_BOTH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure the data bit width from the output (post-narrowing) format, which is what is clocked out.
|
|
|
|
|
const i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) output_stream_info.get_bits_per_sample();
|
|
|
|
|
// Configure the data bit width from the output (post-conversion) format, which is what is clocked out.
|
|
|
|
|
i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) output_stream_info.get_bits_per_sample();
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
// The original ESP32 clocks data at the slot width; a 24-bit slot sends the top 24 bits of each 32-bit sample
|
|
|
|
|
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
|
|
|
|
data_bit_width = (i2s_data_bit_width_t) this->slot_bit_width_;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
i2s_std_slot_config_t slot_cfg;
|
|
|
|
|
switch (this->i2s_comm_fmt_) {
|
|
|
|
|
case I2SCommFmt::PCM:
|
|
|
|
@@ -545,23 +564,10 @@ void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_str
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
// There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher than the
|
|
|
|
|
// bits per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems
|
|
|
|
|
// to make it play at the correct speed while sending more bits per slot.
|
|
|
|
|
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
|
|
|
|
uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_);
|
|
|
|
|
slot_cfg.ws_width = configured_bit_width;
|
|
|
|
|
if (configured_bit_width > 16) {
|
|
|
|
|
slot_cfg.msb_right = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
slot_cfg.slot_bit_width = this->slot_bit_width_;
|
|
|
|
|
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
|
|
|
|
slot_cfg.ws_width = static_cast<uint32_t>(this->slot_bit_width_);
|
|
|
|
|
}
|
|
|
|
|
#endif // USE_ESP32_VARIANT_ESP32
|
|
|
|
|
slot_cfg.slot_mask = slot_mask;
|
|
|
|
|
|
|
|
|
|
i2s_std_gpio_config_t gpio_cfg = this->parent_->get_pin_config();
|
|
|
|
@@ -574,6 +580,45 @@ void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_str
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t I2SAudioSpeaker::slot_memory_bits_() const {
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
// The original ESP32 stores a 24-bit slot's samples in 32-bit words
|
|
|
|
|
if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_24BIT) {
|
|
|
|
|
return 32;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
return static_cast<uint8_t>(this->slot_bit_width_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef USE_ESP32_VARIANT_ESP32
|
|
|
|
|
size_t I2SAudioSpeaker::write_widened_(const uint8_t *input, uint32_t frames) {
|
|
|
|
|
alignas(4) uint8_t widened[WIDEN_BUFFER_BYTES];
|
|
|
|
|
const uint8_t channels = this->current_stream_info_.get_channels();
|
|
|
|
|
const uint8_t input_bytes_per_sample = this->current_stream_info_.get_bits_per_sample() / 8;
|
|
|
|
|
const uint8_t output_bytes_per_sample = this->output_stream_info_.get_bits_per_sample() / 8;
|
|
|
|
|
const uint32_t frames_per_pass = WIDEN_BUFFER_BYTES / this->output_stream_info_.frames_to_bytes(1);
|
|
|
|
|
|
|
|
|
|
size_t bytes_written = 0;
|
|
|
|
|
while (frames > 0) {
|
|
|
|
|
const uint32_t pass_frames = std::min(frames, frames_per_pass);
|
|
|
|
|
esp_audio_libs::pcm_convert::copy_frames(input, widened, input_bytes_per_sample, channels, output_bytes_per_sample,
|
|
|
|
|
channels, pass_frames);
|
|
|
|
|
const size_t pass_bytes = this->output_stream_info_.frames_to_bytes(pass_frames);
|
|
|
|
|
// 8-bit mono widened to 16-bit mono needs the pair swap; passes hold whole pairs since writes are pair-aligned
|
|
|
|
|
this->swap_esp32_mono_samples_(widened, pass_bytes);
|
|
|
|
|
size_t bw = 0;
|
|
|
|
|
i2s_channel_write(this->tx_handle_, widened, pass_bytes, &bw, WRITE_TIMEOUT_TICKS);
|
|
|
|
|
bytes_written += bw;
|
|
|
|
|
if (bw != pass_bytes) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
input += this->current_stream_info_.frames_to_bytes(pass_frames);
|
|
|
|
|
frames -= pass_frames;
|
|
|
|
|
}
|
|
|
|
|
return bytes_written;
|
|
|
|
|
}
|
|
|
|
|
#endif // USE_ESP32_VARIANT_ESP32
|
|
|
|
|
|
|
|
|
|
} // namespace esphome::i2s_audio
|
|
|
|
|
|
|
|
|
|
#endif // USE_ESP32
|
|
|
|
|