1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-14 06:38:17 +00:00

Add support for speaker

This commit is contained in:
raul 2025-02-18 22:53:17 +01:00
parent d200e62202
commit f0014b3666
8 changed files with 173 additions and 21 deletions

View File

@ -61,7 +61,7 @@ I2C_COMM_FMT_OPTIONS = {
"pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG,
}
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32]
def _set_num_channels_from_config(config):
@ -102,7 +102,7 @@ def _validate_esp32_variant(config):
if config[CONF_DAC_TYPE] != "internal":
return config
variant = esp32.get_esp32_variant()
if variant in NO_INTERNAL_DAC_VARIANTS:
if variant not in INTERNAL_DAC_VARIANTS:
raise cv.Invalid(f"{variant} does not have an internal DAC")
return config
@ -144,8 +144,8 @@ CONFIG_SCHEMA = cv.All(
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum(
I2C_COMM_FMT_OPTIONS, lower=True
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.one_of(
*I2C_COMM_FMT_OPTIONS, lower=True
),
}
),
@ -158,9 +158,14 @@ CONFIG_SCHEMA = cv.All(
)
def _final_validate(_):
def _final_validate(config):
if not use_legacy():
raise cv.Invalid("I2S speaker is only compatible with legacy i2s driver.")
if config[CONF_DAC_TYPE] == "internal":
raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver.")
if config[CONF_I2S_COMM_FMT] == "stand_max":
raise cv.Invalid(
"I2S standard max format only implemented with legacy i2s driver."
)
FINAL_VALIDATE_SCHEMA = _final_validate
@ -176,7 +181,17 @@ async def to_code(config):
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT]))
if use_legacy():
cg.add(
var.set_i2s_comm_fmt(I2C_COMM_FMT_OPTIONS[config[CONF_I2S_COMM_FMT]])
)
else:
fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = "msb"
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = "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]))

View File

@ -2,7 +2,11 @@
#ifdef USE_ESP32
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
#include <driver/i2s_std.h>
#endif
#include "esphome/components/audio/audio.h"
@ -294,13 +298,21 @@ void I2SAudioSpeaker::speaker_task(void *params) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings.
break;
}
#ifdef USE_I2S_LEGACY
i2s_event_t i2s_event;
while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) {
if (i2s_event.type == I2S_EVENT_TX_Q_OVF) {
tx_dma_underflow = true;
}
}
#else
bool overflow;
while (xQueueReceive(this_speaker->i2s_event_queue_, &overflow, 0)) {
if (overflow) {
tx_dma_underflow = true;
}
}
#endif
if (this_speaker->pause_state_) {
// Pause state is accessed atomically, so thread safe
@ -327,6 +339,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
size_t bytes_written = 0;
size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read);
#ifdef USE_I2S_LEGACY
if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
@ -336,6 +349,17 @@ void I2SAudioSpeaker::speaker_task(void *params) {
audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written,
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
}
#else
if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->data_bit_width_) {
i2s_channel_write(this_speaker->tx_handle_, this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
} /** else if (audio_stream_info.get_bits_per_sample() < (uint8_t) this_speaker->data_bit_width_) {
i2s_write_expand(this_speaker->parent_->get_port(),
this_speaker->data_buffer_ + i * single_dma_buffer_input_size, bytes_to_write,
audio_stream_info.get_bits_per_sample(), this_speaker->data_bit_width_, &bytes_written,
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
}*/
#endif
uint32_t write_timestamp = micros();
@ -369,8 +393,12 @@ void I2SAudioSpeaker::speaker_task(void *params) {
}
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
#ifdef USE_I2S_LEGACY
i2s_driver_uninstall(this_speaker->parent_->get_port());
#else
i2s_channel_disable(this_speaker->tx_handle_);
i2s_del_channel(this_speaker->tx_handle_);
#endif
this_speaker->parent_->unlock();
}
@ -462,12 +490,20 @@ esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t rin
}
esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
#ifdef USE_I2S_LEGACY
if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#else
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#endif
// Can't reconfigure I2S bus, so the sample rate must match the configured value
return ESP_ERR_NOT_SUPPORTED;
}
#ifdef USE_I2S_LEGACY
if ((i2s_bits_per_sample_t) audio_stream_info.get_bits_per_sample() > this->bits_per_sample_) {
#else
if ((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample() > this->data_bit_width_) {
#endif
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value
return ESP_ERR_NOT_SUPPORTED;
}
@ -476,6 +512,9 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
return ESP_ERR_INVALID_STATE;
}
uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
#ifdef USE_I2S_LEGACY
i2s_channel_fmt_t channel = this->channel_;
if (audio_stream_info.get_channels() == 1) {
@ -488,8 +527,6 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
channel = I2S_CHANNEL_FMT_RIGHT_LEFT;
}
int dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
i2s_driver_config_t config = {
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX),
.sample_rate = audio_stream_info.get_sample_rate(),
@ -498,7 +535,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
.communication_format = this->i2s_comm_fmt_,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = DMA_BUFFERS_COUNT,
.dma_buf_len = dma_buffer_length,
.dma_buf_len = (int) dma_buffer_length,
.use_apll = this->use_apll_,
.tx_desc_auto_clear = true,
.fixed_mclk = I2S_PIN_NO_CHANGE,
@ -545,6 +582,89 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
i2s_driver_uninstall(this->parent_->get_port());
this->parent_->unlock();
}
#else
i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = this->i2s_role_,
.dma_desc_num = DMA_BUFFERS_COUNT,
.dma_frame_num = dma_buffer_length,
.auto_clear = true,
};
/* Allocate a new RX channel and get the handle of this channel */
esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL);
if (err != ESP_OK) {
// ESP_LOGW(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err));
this->parent_->unlock();
return err;
}
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
#ifdef I2S_CLK_SRC_APLL
if (this->use_apll_) {
clk_src = I2S_CLK_SRC_APLL;
}
#endif
i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
i2s_std_clk_config_t clk_cfg = {
.sample_rate_hz = audio_stream_info.get_sample_rate(),
.clk_src = clk_src,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
};
i2s_slot_mode_t slot_mode = this->slot_mode_;
i2s_std_slot_mask_t slot_mask = this->std_slot_mask_;
if (audio_stream_info.get_channels() == 1) {
slot_mode = I2S_SLOT_MODE_MONO;
} else if (audio_stream_info.get_channels() == 2) {
slot_mode = I2S_SLOT_MODE_STEREO;
slot_mask = I2S_STD_SLOT_BOTH;
}
i2s_std_slot_config_t std_slot_cfg;
if (this->i2s_comm_fmt_ == "std") {
std_slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(this->data_bit_width_, slot_mode);
} else if (this->i2s_comm_fmt_ == "pcm") {
std_slot_cfg = I2S_STD_PCM_SLOT_DEFAULT_CONFIG(this->data_bit_width_, slot_mode);
} else {
std_slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(this->data_bit_width_, slot_mode);
}
std_slot_cfg.slot_bit_width = this->slot_bit_width_;
std_slot_cfg.slot_mask = slot_mask;
pin_config.dout = this->dout_pin_;
i2s_std_config_t std_cfg = {
.clk_cfg = clk_cfg,
.slot_cfg = std_slot_cfg,
.gpio_cfg = pin_config,
};
/* Initialize the channel */
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
if (err != ESP_OK) {
// ESP_LOGW(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err));
i2s_del_channel(this->tx_handle_);
this->parent_->unlock();
return err;
}
if (this->i2s_event_queue_ == nullptr) {
this->i2s_event_queue_ = xQueueCreate(1, sizeof(bool));
}
const i2s_event_callbacks_t callbacks = {
.on_send_q_ovf = i2s_overflow_cb,
};
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
/* Before reading data, start the RX channel first */
i2s_channel_enable(this->tx_handle_);
if (err != ESP_OK) {
// ESP_LOGW(TAG, "Error enabling I2S Speaker: %s", esp_err_to_name(err));
i2s_del_channel(this->tx_handle_);
this->parent_->unlock();
}
#endif
return err;
}
@ -564,6 +684,15 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
vTaskDelete(nullptr);
}
#ifndef USE_I2S_LEGACY
bool IRAM_ATTR I2SAudioSpeaker::i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx;
bool overflow = true;
xQueueOverwrite(this_speaker->i2s_event_queue_, &overflow);
return false;
}
#endif
} // namespace i2s_audio
} // namespace esphome

View File

@ -4,8 +4,6 @@
#include "../i2s_audio.h"
#include <driver/i2s.h>
#include <freertos/event_groups.h>
#include <freertos/queue.h>
#include <freertos/FreeRTOS.h>
@ -30,11 +28,16 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
void set_timeout(uint32_t ms) { this->timeout_ = ms; }
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
#endif
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; }
#else
void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; }
void set_i2s_comm_fmt(std::string mode) { this->i2s_comm_fmt_ = std::move(mode); }
#endif
void start() override;
void stop() override;
@ -86,6 +89,10 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
/// @return True if an ERR_ESP bit is set and false if err == ESP_OK
bool send_esp_err_to_event_group_(esp_err_t err);
#ifndef USE_I2S_LEGACY
static bool i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
#endif
/// @brief Allocates the data buffer and ring buffer
/// @param data_buffer_size Number of bytes to allocate for the data buffer.
/// @param ring_buffer_size Number of bytes to allocate for the ring buffer.
@ -121,7 +128,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
uint32_t buffer_duration_ms_;
optional<uint32_t> timeout_;
uint8_t dout_pin_;
bool task_created_{false};
bool pause_state_{false};
@ -130,10 +136,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
size_t bytes_written_{0};
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif
uint8_t dout_pin_;
i2s_comm_format_t i2s_comm_fmt_;
#else
gpio_num_t dout_pin_;
std::string i2s_comm_fmt_;
i2s_chan_handle_t tx_handle_;
#endif
uint32_t accumulated_frames_written_{0};
};

View File

@ -10,7 +10,6 @@ i2s_audio:
i2s_lrclk_pin: ${lrclk_pin}
i2s_bclk_pin: ${bclk_pin}
i2s_mclk_pin: ${mclk_pin}
use_legacy: true
speaker:
- platform: i2s_audio

View File

@ -2,7 +2,6 @@ i2s_audio:
i2s_lrclk_pin: ${lrclk_pin}
i2s_bclk_pin: ${bclk_pin}
i2s_mclk_pin: ${mclk_pin}
use_legacy: true
speaker:
- platform: i2s_audio

View File

@ -23,7 +23,6 @@ i2s_audio:
i2s_lrclk_pin: ${i2s_bclk_pin}
i2s_bclk_pin: ${i2s_lrclk_pin}
i2s_mclk_pin: ${i2s_mclk_pin}
use_legacy: true
audio_dac:
- platform: aic3204

View File

@ -18,7 +18,6 @@ i2s_audio:
i2s_lrclk_pin: ${i2s_bclk_pin}
i2s_bclk_pin: ${i2s_lrclk_pin}
i2s_mclk_pin: ${i2s_mclk_pin}
use_legacy: true
speaker:
- platform: i2s_audio

View File

@ -15,7 +15,6 @@ i2s_audio:
i2s_lrclk_pin: ${i2s_lrclk_pin}
i2s_bclk_pin: ${i2s_bclk_pin}
i2s_mclk_pin: ${i2s_mclk_pin}
use_legacy: true
microphone:
- platform: i2s_audio