mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 03:32:50 +00:00
[i2s_audio] Add full duplex support (#19959)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
Claude Opus 5.5
J. Nick Koston
J. Nick Koston
parent
6555da2906
commit
51b1fdcc8b
@@ -19,8 +19,16 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.microphone import DOMAIN as MICROPHONE_DOMAIN
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
|
||||
from esphome.const import (
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
CONF_CHANNEL,
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_SPEAKER,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
import esphome.final_validate as fv
|
||||
@@ -42,6 +50,9 @@ CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
|
||||
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
|
||||
|
||||
CONF_I2S_AUDIO_ID = "i2s_audio_id"
|
||||
CONF_FULL_DUPLEX = "full_duplex"
|
||||
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
|
||||
CONF_SPDIF_MODE = "spdif_mode"
|
||||
|
||||
CONF_I2S_MODE = "i2s_mode"
|
||||
CONF_PRIMARY = "primary"
|
||||
@@ -185,7 +196,8 @@ def i2s_audio_component_schema(
|
||||
|
||||
|
||||
async def register_i2s_audio_component(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
|
||||
parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID])
|
||||
await cg.register_parented(var, parent)
|
||||
cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]]))
|
||||
slot_mode = config[CONF_CHANNEL]
|
||||
if slot_mode != CONF_STEREO:
|
||||
@@ -200,6 +212,12 @@ async def register_i2s_audio_component(var: MockObj, config: ConfigType) -> None
|
||||
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
|
||||
cg.add(var.set_mclk_multiple(I2S_MCLK_MULTIPLE[config[CONF_MCLK_MULTIPLE]]))
|
||||
|
||||
if str(config[CONF_I2S_AUDIO_ID]) in _get_data().full_duplex_buses:
|
||||
if config[CONF_ID].type.inherits_from(I2SAudioIn):
|
||||
cg.add(parent.set_audio_in(var))
|
||||
else:
|
||||
cg.add(parent.set_audio_out(var))
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -207,6 +225,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
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,
|
||||
cv.Optional(CONF_FULL_DUPLEX, default=False): cv.boolean,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -216,6 +235,7 @@ class I2SAudioData:
|
||||
"""I2S audio component state stored in CORE.data."""
|
||||
|
||||
port_map: dict[str, int] = field(default_factory=dict)
|
||||
full_duplex_buses: set[str] = field(default_factory=set)
|
||||
|
||||
|
||||
def _get_data() -> I2SAudioData:
|
||||
@@ -262,8 +282,56 @@ def _assign_ports() -> None:
|
||||
next_port += 1
|
||||
|
||||
|
||||
def _bus_devices(full_config: ConfigType, domain: str, bus_id: str) -> list[ConfigType]:
|
||||
return [
|
||||
device
|
||||
for device in full_config.get(domain, [])
|
||||
if device.get(CONF_PLATFORM) == DOMAIN
|
||||
and str(device[CONF_I2S_AUDIO_ID]) == bus_id
|
||||
]
|
||||
|
||||
|
||||
def _validate_full_duplex(full_config: ConfigType, bus_id: str) -> None:
|
||||
"""Check that a full duplex bus has one microphone and speakers that can share its clocks and TX channel."""
|
||||
microphones = _bus_devices(full_config, MICROPHONE_DOMAIN, bus_id)
|
||||
speakers = _bus_devices(full_config, CONF_SPEAKER, bus_id)
|
||||
if len(microphones) != 1 or not speakers:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_FULL_DUPLEX}' requires exactly one i2s_audio microphone and at least one "
|
||||
f"i2s_audio speaker on bus '{bus_id}'"
|
||||
)
|
||||
microphone = microphones[0]
|
||||
if microphone.get(CONF_PDM):
|
||||
raise cv.Invalid(f"A PDM microphone cannot use a '{CONF_FULL_DUPLEX}' bus")
|
||||
for speaker in speakers:
|
||||
if speaker.get(CONF_SPDIF_MODE):
|
||||
raise cv.Invalid(f"An SPDIF speaker cannot use a '{CONF_FULL_DUPLEX}' bus")
|
||||
# Both directions run from the same bit and word clocks
|
||||
for key in (
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
CONF_I2S_MODE,
|
||||
CONF_USE_APLL,
|
||||
CONF_MCLK_MULTIPLE,
|
||||
):
|
||||
if microphone[key] != speaker[key]:
|
||||
raise cv.Invalid(
|
||||
f"The microphone and speaker '{speaker[CONF_ID]}' on '{CONF_FULL_DUPLEX}' bus "
|
||||
f"'{bus_id}' must use the same '{key}'"
|
||||
)
|
||||
# The TX channel is set up once from one speaker's configuration, and the speakers take turns using it
|
||||
first = speakers[0]
|
||||
for speaker in speakers[1:]:
|
||||
for key in (CONF_I2S_DOUT_PIN, CONF_CHANNEL, CONF_I2S_COMM_FMT):
|
||||
if speaker.get(key) != first.get(key):
|
||||
raise cv.Invalid(
|
||||
f"The speakers on '{CONF_FULL_DUPLEX}' bus '{bus_id}' must use the same '{key}'"
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(_: ConfigType) -> None:
|
||||
i2s_audio_configs = fv.full_config.get()[DOMAIN]
|
||||
full_config = fv.full_config.get()
|
||||
i2s_audio_configs = full_config[DOMAIN]
|
||||
variant = get_esp32_variant()
|
||||
if variant not in I2S_PORTS:
|
||||
raise cv.Invalid(f"Unsupported variant {variant}")
|
||||
@@ -273,6 +341,13 @@ def _final_validate(_: ConfigType) -> None:
|
||||
)
|
||||
_assign_ports()
|
||||
|
||||
data = _get_data()
|
||||
for config in i2s_audio_configs:
|
||||
if config[CONF_FULL_DUPLEX]:
|
||||
bus_id = str(config[CONF_ID])
|
||||
_validate_full_duplex(full_config, bus_id)
|
||||
data.full_duplex_buses.add(bus_id)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
@@ -299,3 +374,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
|
||||
if CONF_I2S_MCLK_PIN in config:
|
||||
cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN]))
|
||||
if config[CONF_FULL_DUPLEX]:
|
||||
cg.add_define("USE_I2S_AUDIO_FULL_DUPLEX")
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{"i2s_audio.cpp": "USE_I2S_AUDIO_FULL_DUPLEX"}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "i2s_audio.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_FULL_DUPLEX)
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::i2s_audio {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "i2s_audio");
|
||||
|
||||
void I2SAudioComponent::setup() {
|
||||
if (!this->is_full_duplex())
|
||||
return;
|
||||
|
||||
// ESP-IDF only shares the bit and word clocks between channels allocated in the same call, and deleting either
|
||||
// channel ends the pairing. Allocate both once here and keep them for the lifetime of the device.
|
||||
i2s_chan_config_t chan_cfg{};
|
||||
i2s_std_config_t tx_cfg{};
|
||||
i2s_std_config_t rx_cfg{};
|
||||
this->audio_out_->build_full_duplex_config(chan_cfg, tx_cfg);
|
||||
this->audio_in_->build_full_duplex_config(rx_cfg);
|
||||
|
||||
esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, &this->rx_handle_);
|
||||
if (err == ESP_OK) {
|
||||
// The first initialized channel drives the shared clocks. Use RX so the microphone runs on its own.
|
||||
err = i2s_channel_init_std_mode(this->rx_handle_, &rx_cfg);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = i2s_channel_init_std_mode(this->tx_handle_, &tx_cfg);
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Full duplex channel setup failed: %s", esp_err_to_name(err));
|
||||
if (this->tx_handle_ != nullptr) {
|
||||
i2s_del_channel(this->tx_handle_);
|
||||
this->tx_handle_ = nullptr;
|
||||
}
|
||||
if (this->rx_handle_ != nullptr) {
|
||||
i2s_del_channel(this->rx_handle_);
|
||||
this->rx_handle_ = nullptr;
|
||||
}
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
i2s_chan_handle_t I2SAudioComponent::acquire_(bool &in_use, i2s_chan_handle_t handle) {
|
||||
if (handle == nullptr)
|
||||
return nullptr;
|
||||
in_use = true;
|
||||
if (!this->update_rx_channel_()) {
|
||||
in_use = false;
|
||||
return nullptr;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
i2s_chan_handle_t I2SAudioComponent::acquire_rx_channel() { return this->acquire_(this->rx_in_use_, this->rx_handle_); }
|
||||
|
||||
void I2SAudioComponent::release_rx_channel() {
|
||||
this->rx_in_use_ = false;
|
||||
this->update_rx_channel_();
|
||||
}
|
||||
|
||||
i2s_chan_handle_t I2SAudioComponent::acquire_tx_channel() {
|
||||
// Speakers sharing the bus take turns; a second one must not get the channel while the first is playing
|
||||
if (this->tx_in_use_)
|
||||
return nullptr;
|
||||
return this->acquire_(this->tx_in_use_, this->tx_handle_);
|
||||
}
|
||||
|
||||
void I2SAudioComponent::release_tx_channel() {
|
||||
// The speaker task may have left the channel disabled already, so an invalid state error is expected here
|
||||
i2s_channel_disable(this->tx_handle_);
|
||||
this->tx_in_use_ = false;
|
||||
this->update_rx_channel_();
|
||||
}
|
||||
|
||||
bool I2SAudioComponent::update_rx_channel_() {
|
||||
const bool needed = this->rx_in_use_ || this->tx_in_use_;
|
||||
if (needed == this->rx_enabled_)
|
||||
return true;
|
||||
|
||||
esp_err_t err = needed ? i2s_channel_enable(this->rx_handle_) : i2s_channel_disable(this->rx_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to %s RX channel: %s", needed ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"),
|
||||
esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
this->rx_enabled_ = needed;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::i2s_audio
|
||||
|
||||
#endif // USE_ESP32 && USE_I2S_AUDIO_FULL_DUPLEX
|
||||
@@ -23,6 +23,15 @@ class I2SAudioBase : public Parented<I2SAudioComponent> {
|
||||
void set_mclk_multiple(i2s_mclk_multiple_t mclk_multiple) { this->mclk_multiple_ = mclk_multiple; }
|
||||
|
||||
protected:
|
||||
i2s_clock_src_t get_clock_source_() const {
|
||||
#ifdef I2S_CLK_SRC_APLL
|
||||
if (this->use_apll_) {
|
||||
return I2S_CLK_SRC_APLL;
|
||||
}
|
||||
#endif
|
||||
return I2S_CLK_SRC_DEFAULT;
|
||||
}
|
||||
|
||||
i2s_role_t i2s_role_{};
|
||||
i2s_slot_mode_t slot_mode_;
|
||||
i2s_std_slot_mask_t std_slot_mask_;
|
||||
@@ -32,12 +41,51 @@ class I2SAudioBase : public Parented<I2SAudioComponent> {
|
||||
i2s_mclk_multiple_t mclk_multiple_;
|
||||
};
|
||||
|
||||
class I2SAudioIn : public I2SAudioBase {};
|
||||
class I2SAudioIn : public I2SAudioBase {
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
public:
|
||||
/// @brief Builds the RX configuration the parent uses to set up a full duplex channel pair.
|
||||
virtual void build_full_duplex_config(i2s_std_config_t &std_cfg) = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
class I2SAudioOut : public I2SAudioBase {};
|
||||
class I2SAudioOut : public I2SAudioBase {
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
public:
|
||||
/// @brief Builds the TX configuration the parent uses to set up a full duplex channel pair. The channel
|
||||
/// configuration (DMA layout, role, interrupt priority) is shared by both channels. Validation only puts
|
||||
/// standard speakers on a full duplex bus, so the SPDIF speaker never gets here.
|
||||
virtual void build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) {}
|
||||
#endif
|
||||
};
|
||||
|
||||
class I2SAudioComponent final : public Component {
|
||||
public:
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
void setup() override;
|
||||
|
||||
void set_audio_in(I2SAudioIn *audio_in) { this->audio_in_ = audio_in; }
|
||||
void set_audio_out(I2SAudioOut *audio_out) { this->audio_out_ = audio_out; }
|
||||
|
||||
/// @brief True when a microphone and a speaker share this bus at the same time.
|
||||
bool is_full_duplex() const { return this->audio_in_ != nullptr && this->audio_out_ != nullptr; }
|
||||
|
||||
/// @brief Enables the full duplex RX channel. Main loop only.
|
||||
/// @return The enabled RX handle, or nullptr if the channel pair is unavailable
|
||||
i2s_chan_handle_t acquire_rx_channel();
|
||||
/// @brief Releases the RX channel; it keeps running while the TX side needs its clocks. Main loop only.
|
||||
void release_rx_channel();
|
||||
|
||||
/// @brief Starts the shared clocks and hands over the full duplex TX channel, still disabled, so the caller
|
||||
/// can register callbacks and preload data before enabling it. Main loop only.
|
||||
/// @return The TX handle, or nullptr if the channel pair is unavailable or another speaker holds it
|
||||
i2s_chan_handle_t acquire_tx_channel();
|
||||
/// @brief Disables the TX channel and stops the shared clocks if the RX side is idle. Only the speaker that
|
||||
/// acquired the channel may call this. Main loop only.
|
||||
void release_tx_channel();
|
||||
/// @brief True while another speaker holds the full duplex TX channel
|
||||
bool is_tx_in_use() const { return this->tx_in_use_; }
|
||||
#endif
|
||||
i2s_std_gpio_config_t get_pin_config() const {
|
||||
return {.mclk = (gpio_num_t) this->mclk_pin_,
|
||||
.bclk = (gpio_num_t) this->bclk_pin_,
|
||||
@@ -68,8 +116,20 @@ class I2SAudioComponent final : public Component {
|
||||
protected:
|
||||
Mutex lock_;
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
/// @brief Marks one side active and brings the shared clocks up; undoes the mark if that fails.
|
||||
i2s_chan_handle_t acquire_(bool &in_use, i2s_chan_handle_t handle);
|
||||
/// @brief Enables the RX channel while either side is active, since it drives the shared clocks.
|
||||
bool update_rx_channel_();
|
||||
|
||||
I2SAudioIn *audio_in_{nullptr};
|
||||
I2SAudioOut *audio_out_{nullptr};
|
||||
i2s_chan_handle_t rx_handle_{nullptr};
|
||||
i2s_chan_handle_t tx_handle_{nullptr};
|
||||
bool rx_in_use_{false};
|
||||
bool tx_in_use_{false};
|
||||
bool rx_enabled_{false};
|
||||
#endif
|
||||
int mclk_pin_{I2S_GPIO_UNUSED};
|
||||
int bclk_pin_{I2S_GPIO_UNUSED};
|
||||
int lrclk_pin_;
|
||||
|
||||
@@ -46,6 +46,13 @@ void I2SAudioMicrophone::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
if (this->parent_->is_full_duplex() && this->parent_->is_failed()) {
|
||||
this->mark_failed(LOG_STR("Full duplex bus setup failed"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->configure_stream_settings_();
|
||||
}
|
||||
|
||||
@@ -94,7 +101,44 @@ void I2SAudioMicrophone::start() {
|
||||
xSemaphoreTake(this->active_listeners_semaphore_, 0);
|
||||
}
|
||||
|
||||
void I2SAudioMicrophone::build_std_config_(i2s_std_config_t &std_cfg) const {
|
||||
i2s_std_slot_config_t std_slot_cfg =
|
||||
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_);
|
||||
std_slot_cfg.slot_bit_width = this->slot_bit_width_;
|
||||
std_slot_cfg.slot_mask = this->std_slot_mask_;
|
||||
|
||||
i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
|
||||
pin_config.din = this->din_pin_;
|
||||
|
||||
std_cfg = {
|
||||
.clk_cfg =
|
||||
{
|
||||
.sample_rate_hz = this->sample_rate_,
|
||||
.clk_src = this->get_clock_source_(),
|
||||
.mclk_multiple = this->mclk_multiple_,
|
||||
},
|
||||
.slot_cfg = std_slot_cfg,
|
||||
.gpio_cfg = pin_config,
|
||||
};
|
||||
}
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
void I2SAudioMicrophone::build_full_duplex_config(i2s_std_config_t &std_cfg) { this->build_std_config_(std_cfg); }
|
||||
#endif
|
||||
|
||||
bool I2SAudioMicrophone::start_driver_() {
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
if (this->parent_->is_full_duplex()) {
|
||||
this->rx_handle_ = this->parent_->acquire_rx_channel();
|
||||
if (this->rx_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Full duplex channel unavailable");
|
||||
return false;
|
||||
}
|
||||
this->configure_stream_settings_();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!this->parent_->try_lock()) {
|
||||
return false; // Waiting for another i2s to return lock
|
||||
}
|
||||
@@ -115,18 +159,12 @@ bool I2SAudioMicrophone::start_driver_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
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();
|
||||
#if SOC_I2S_SUPPORTS_PDM_RX
|
||||
if (this->pdm_) {
|
||||
i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
|
||||
i2s_pdm_rx_clk_config_t clk_cfg = {
|
||||
.sample_rate_hz = this->sample_rate_,
|
||||
.clk_src = clk_src,
|
||||
.clk_src = this->get_clock_source_(),
|
||||
.mclk_multiple = this->mclk_multiple_,
|
||||
.dn_sample_mode = this->pdm_dsr_,
|
||||
};
|
||||
@@ -162,23 +200,8 @@ bool I2SAudioMicrophone::start_driver_() {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
i2s_std_clk_config_t clk_cfg = {
|
||||
.sample_rate_hz = this->sample_rate_,
|
||||
.clk_src = clk_src,
|
||||
.mclk_multiple = this->mclk_multiple_,
|
||||
};
|
||||
i2s_std_slot_config_t std_slot_cfg =
|
||||
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_);
|
||||
std_slot_cfg.slot_bit_width = this->slot_bit_width_;
|
||||
std_slot_cfg.slot_mask = this->std_slot_mask_;
|
||||
|
||||
pin_config.din = this->din_pin_;
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = clk_cfg,
|
||||
.slot_cfg = std_slot_cfg,
|
||||
.gpio_cfg = pin_config,
|
||||
};
|
||||
i2s_std_config_t std_cfg;
|
||||
this->build_std_config_(std_cfg);
|
||||
/* Initialize the channel */
|
||||
err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg);
|
||||
}
|
||||
@@ -210,6 +233,17 @@ void I2SAudioMicrophone::stop_driver_() {
|
||||
// There is no harm continuing to unload the driver if an error is ever returned by the various functions. This
|
||||
// ensures that we stop/unload the driver when it only partially starts.
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
if (this->parent_->is_full_duplex()) {
|
||||
// The parent owns the channel pair, so only hand the RX channel back
|
||||
if (this->rx_handle_ != nullptr) {
|
||||
this->parent_->release_rx_channel();
|
||||
this->rx_handle_ = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
esp_err_t err;
|
||||
if (this->rx_handle_ != nullptr) {
|
||||
/* Have to stop the channel before deleting it */
|
||||
|
||||
@@ -33,7 +33,14 @@ class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphon
|
||||
void set_pdm_dsr(i2s_pdm_dsr_t pdm_dsr) { this->pdm_dsr_ = pdm_dsr; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
void build_full_duplex_config(i2s_std_config_t &std_cfg) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/// @brief Builds the standard (non-PDM) mode configuration from the configured settings.
|
||||
void build_std_config_(i2s_std_config_t &std_cfg) const;
|
||||
|
||||
/// @brief Starts the I2S driver. Updates the ``audio_stream_info_`` member variable with the current setttings.
|
||||
/// @return True if succesful, false otherwise
|
||||
bool start_driver_();
|
||||
@@ -59,7 +66,7 @@ class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphon
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
|
||||
gpio_num_t din_pin_{I2S_GPIO_UNUSED};
|
||||
i2s_chan_handle_t rx_handle_;
|
||||
i2s_chan_handle_t rx_handle_{nullptr};
|
||||
#if SOC_I2S_SUPPORTS_PDM_RX
|
||||
i2s_pdm_dsr_t pdm_dsr_{I2S_PDM_DSR_8S};
|
||||
#endif
|
||||
|
||||
@@ -20,6 +20,7 @@ from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_I2S_AUDIO_ID,
|
||||
CONF_I2S_COMM_FMT,
|
||||
CONF_I2S_DOUT_PIN,
|
||||
CONF_I2S_MODE,
|
||||
CONF_LEFT,
|
||||
@@ -27,6 +28,7 @@ from .. import (
|
||||
CONF_MONO,
|
||||
CONF_PRIMARY,
|
||||
CONF_RIGHT,
|
||||
CONF_SPDIF_MODE,
|
||||
CONF_STEREO,
|
||||
CONF_USE_APLL,
|
||||
I2SAudioOut,
|
||||
@@ -46,9 +48,7 @@ I2SAudioSpeakerBase = i2s_audio_ns.class_(
|
||||
I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase)
|
||||
|
||||
CONF_DAC_TYPE = "dac_type"
|
||||
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
CONF_SPDIF_MODE = "spdif_mode"
|
||||
|
||||
I2SAudioSpeakerBase = i2s_audio_ns.class_(
|
||||
"I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut
|
||||
|
||||
@@ -467,14 +467,6 @@ esp_err_t I2SAudioSpeakerSPDIF::start_i2s_driver(audio::AudioStreamInfo &audio_s
|
||||
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
|
||||
@@ -496,7 +488,7 @@ esp_err_t I2SAudioSpeakerSPDIF::start_i2s_driver(audio::AudioStreamInfo &audio_s
|
||||
// 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,
|
||||
.clk_src = this->get_clock_source_(),
|
||||
.mclk_multiple = this->mclk_multiple_,
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +41,17 @@ void I2SAudioSpeakerBase::setup() {
|
||||
this->set_volume(this->volume_);
|
||||
this->set_mute_state(this->mute_state_);
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
// The parent set up before us and already routed dout to the shared TX channel, which is never re-initialized.
|
||||
// Parking would detach the pin for good. SPDIF speakers cannot use a full duplex bus.
|
||||
if (this->parent_->is_full_duplex()) {
|
||||
if (this->parent_->is_failed()) {
|
||||
this->mark_failed(LOG_STR("Full duplex bus setup failed"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Until the I2S driver first starts, dout sits in its reset state (often pulled high, or a JTAG
|
||||
// function on the ESP32-S3), which keeps a SPDIF optical transmitter lit. Park it low now.
|
||||
this->park_dout_pin_();
|
||||
@@ -130,7 +141,7 @@ void I2SAudioSpeakerBase::loop() {
|
||||
|
||||
// Handle the speaker's state
|
||||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING:
|
||||
case speaker::STATE_STARTING: {
|
||||
if (this->status_has_error()) {
|
||||
break;
|
||||
}
|
||||
@@ -140,7 +151,21 @@ void I2SAudioSpeakerBase::loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) {
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
if (this->parent_->is_tx_in_use()) {
|
||||
// Another speaker on the bus is playing; wait for it to release the channel
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
const esp_err_t err = this->start_i2s_driver(this->audio_stream_info_);
|
||||
if (err == ESP_ERR_NOT_SUPPORTED) {
|
||||
// Retrying cannot help; stay stopped so the audio source gives up instead of waiting forever
|
||||
ESP_LOGE(TAG, "Stream format not supported; not starting");
|
||||
this->rejected_stream_info_ = this->audio_stream_info_;
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
break;
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
|
||||
this->status_momentary_error(1000);
|
||||
break;
|
||||
@@ -159,6 +184,7 @@ void I2SAudioSpeakerBase::loop() {
|
||||
this->stop_i2s_driver_(false); // Stops the driver to return the lock; will be reloaded in next attempt
|
||||
}
|
||||
break;
|
||||
}
|
||||
case speaker::STATE_RUNNING: // Intentional fallthrough
|
||||
case speaker::STATE_STOPPING: // Intentional fallthrough
|
||||
case speaker::STATE_STOPPED:
|
||||
@@ -240,6 +266,9 @@ void I2SAudioSpeakerBase::start() {
|
||||
return;
|
||||
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
|
||||
return;
|
||||
// play() calls start() on every write, so without this a rejected stream would be retried and logged nonstop
|
||||
if (this->rejected_stream_info_.has_value() && this->rejected_stream_info_.value() == this->audio_stream_info_)
|
||||
return;
|
||||
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
|
||||
}
|
||||
@@ -277,6 +306,15 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
|
||||
return err;
|
||||
}
|
||||
|
||||
err = this->prepare_event_queues_(event_queue_size);
|
||||
if (err != ESP_OK) {
|
||||
this->delete_channel_();
|
||||
this->parent_->unlock();
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t I2SAudioSpeakerBase::prepare_event_queues_(size_t event_queue_size) {
|
||||
// Both queues are drained by the speaker task before it preloads, whether the channel is new or kept
|
||||
if (this->i2s_event_queue_ == nullptr) {
|
||||
this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t));
|
||||
@@ -290,15 +328,41 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
|
||||
|
||||
if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)");
|
||||
this->delete_channel_();
|
||||
this->parent_->unlock();
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
esp_err_t I2SAudioSpeakerBase::acquire_full_duplex_channel_(size_t event_queue_size) {
|
||||
this->tx_handle_ = this->parent_->acquire_tx_channel();
|
||||
if (this->tx_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Full duplex channel unavailable");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t err = this->prepare_event_queues_(event_queue_size);
|
||||
if (err != ESP_OK) {
|
||||
this->parent_->release_tx_channel();
|
||||
this->tx_handle_ = nullptr;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
#endif
|
||||
|
||||
void I2SAudioSpeakerBase::stop_i2s_driver_(bool keep_channel) {
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
if (this->parent_->is_full_duplex()) {
|
||||
// The parent owns the channel pair and its pins, so only hand the TX channel back
|
||||
if (this->tx_handle_ != nullptr) {
|
||||
this->parent_->release_tx_channel();
|
||||
this->tx_handle_ = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->tx_handle_ != nullptr) {
|
||||
i2s_channel_disable(this->tx_handle_);
|
||||
// A kept channel lets the next start skip i2s_new_channel and i2s_channel_init_std_mode, whose GPIO and
|
||||
@@ -372,9 +436,9 @@ void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_rea
|
||||
this->current_stream_info_.bytes_to_samples(bytes_read));
|
||||
}
|
||||
|
||||
bool I2SAudioSpeakerBase::esp32_mono_swap_needed_() const {
|
||||
bool I2SAudioSpeakerBase::esp32_mono_swap_needed(const audio::AudioStreamInfo &stream_info) {
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
return this->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16;
|
||||
return stream_info.get_channels() == 1 && stream_info.get_bits_per_sample() == 16;
|
||||
#else
|
||||
return false;
|
||||
#endif // USE_ESP32_VARIANT_ESP32
|
||||
|
||||
@@ -127,6 +127,18 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg,
|
||||
size_t event_queue_size);
|
||||
|
||||
/// @brief Creates the lockstep queues unless they exist already.
|
||||
/// @param event_queue_size Size of each queue
|
||||
/// @return ESP_OK if successful, or ESP_ERR_NO_MEM
|
||||
esp_err_t prepare_event_queues_(size_t event_queue_size);
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
/// @brief Takes the TX channel of the parent's full duplex pair and prepares the event queues.
|
||||
/// @param event_queue_size Size of the event queue
|
||||
/// @return ESP_OK if successful, or an error code. On failure, hands the channel back to the parent.
|
||||
esp_err_t acquire_full_duplex_channel_(size_t event_queue_size);
|
||||
#endif
|
||||
|
||||
/// @brief Stops the I2S driver and unlocks the I2S port; keep_channel leaves the disabled channel allocated
|
||||
void stop_i2s_driver_(bool keep_channel);
|
||||
|
||||
@@ -160,8 +172,10 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
/// @param bytes_read Number of bytes of audio data
|
||||
void apply_software_volume_(uint8_t *data, size_t bytes_read);
|
||||
|
||||
/// @brief Whether 16-bit mono samples in this format need the ESP32 (non-variant) pair swap.
|
||||
static bool esp32_mono_swap_needed(const audio::AudioStreamInfo &stream_info);
|
||||
/// @brief Whether the output is 16-bit mono on the original ESP32, which sends each pair of samples swapped.
|
||||
bool esp32_mono_swap_needed_() const;
|
||||
bool esp32_mono_swap_needed_() const { return esp32_mono_swap_needed(this->output_stream_info_); }
|
||||
|
||||
/// @brief Swap adjacent 16-bit mono samples for ESP32 (non-variant) hardware quirk.
|
||||
/// Only applies when esp32_mono_swap_needed_() is true. `data` must start on a DMA word and hold whole pairs.
|
||||
@@ -182,6 +196,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
|
||||
optional<uint32_t> timeout_;
|
||||
|
||||
// A format start_i2s_driver() refused. Those checks compare against fixed settings, so it can never start.
|
||||
optional<audio::AudioStreamInfo> rejected_stream_info_;
|
||||
|
||||
bool pause_state_{false};
|
||||
bool keep_alive_{false};
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
// The ring buffer holds input-format audio (what play() receives), so size it from the input stream info.
|
||||
const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1);
|
||||
// Hand over whole ESP32 mono sample pairs so writes stay word-aligned
|
||||
const size_t source_alignment = bytes_per_frame * this->dma_frame_multiple_();
|
||||
const size_t source_alignment = bytes_per_frame * dma_frame_multiple(this->output_stream_info_);
|
||||
const bool pair_aligned = source_alignment > bytes_per_frame;
|
||||
// Round the ring buffer size down to a multiple of source_alignment so the wrap boundary avoids splices.
|
||||
const size_t ring_buffer_size =
|
||||
@@ -111,7 +111,7 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
} else {
|
||||
// Should not happen for a READY channel; fall back to the requested size.
|
||||
dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(
|
||||
dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_()));
|
||||
dma_buffer_frames(this->output_stream_info_, dma_frame_multiple(this->output_stream_info_)));
|
||||
}
|
||||
// dma_buffer_bytes counts output-format bytes; convert with the output stream info.
|
||||
const uint32_t frames_per_dma_buffer = this->output_stream_info_.bytes_to_frames(dma_buffer_bytes);
|
||||
@@ -442,6 +442,25 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
||||
}
|
||||
#endif // USE_ESP32_VARIANT_ESP32
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
if (this->parent_->is_full_duplex()) {
|
||||
// The parent set up the channel once from the configured format, so the stream must produce exactly that
|
||||
const audio::AudioStreamInfo required = this->full_duplex_stream_info_();
|
||||
if (this->output_stream_info_ != required) {
|
||||
ESP_LOGE(TAG, "Full duplex requires %u-bit, %u channel audio at %" PRIu32 " Hz",
|
||||
(unsigned) required.get_bits_per_sample(), (unsigned) required.get_channels(),
|
||||
required.get_sample_rate());
|
||||
if (this->output_stream_info_.get_channels() != required.get_channels()) {
|
||||
ESP_LOGE(TAG, "A resampler does not change the channel count; add a mixer speaker with num_channels: %u",
|
||||
(unsigned) required.get_channels());
|
||||
}
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
// The speaker task will enable the channel after preloading.
|
||||
return this->acquire_full_duplex_channel_(I2S_EVENT_QUEUE_COUNT);
|
||||
}
|
||||
#endif // USE_I2S_AUDIO_FULL_DUPLEX
|
||||
|
||||
if (!this->parent_->try_lock()) {
|
||||
ESP_LOGE(TAG, "Parent bus is busy");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
@@ -457,25 +476,37 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
||||
|
||||
this->current_stream_info_ = audio_stream_info;
|
||||
|
||||
i2s_chan_config_t chan_cfg;
|
||||
i2s_std_config_t std_cfg;
|
||||
this->build_i2s_config_(this->output_stream_info_, chan_cfg, std_cfg);
|
||||
|
||||
// The speaker task will enable the channel after preloading.
|
||||
return this->init_i2s_channel_(chan_cfg, std_cfg, I2S_EVENT_QUEUE_COUNT);
|
||||
}
|
||||
|
||||
#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_);
|
||||
}
|
||||
|
||||
void I2SAudioSpeaker::build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) {
|
||||
this->build_i2s_config_(this->full_duplex_stream_info_(), chan_cfg, std_cfg);
|
||||
}
|
||||
#endif // USE_I2S_AUDIO_FULL_DUPLEX
|
||||
|
||||
void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_stream_info, i2s_chan_config_t &chan_cfg,
|
||||
i2s_std_config_t &std_cfg) const {
|
||||
// The DMA buffers hold output-format (post-narrowing) samples, so size them from the output stream info.
|
||||
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_());
|
||||
|
||||
i2s_role_t i2s_role = this->i2s_role_;
|
||||
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
|
||||
uint32_t dma_buffer_length = dma_buffer_frames(output_stream_info, dma_frame_multiple(output_stream_info));
|
||||
|
||||
// Log DMA configuration for debugging
|
||||
ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames", (size_t) DMA_BUFFERS_COUNT,
|
||||
(unsigned long) dma_buffer_length);
|
||||
|
||||
i2s_chan_config_t chan_cfg = {
|
||||
chan_cfg = {
|
||||
.id = this->parent_->get_port(),
|
||||
.role = i2s_role,
|
||||
.role = this->i2s_role_,
|
||||
.dma_desc_num = DMA_BUFFERS_COUNT,
|
||||
.dma_frame_num = dma_buffer_length,
|
||||
.auto_clear = true,
|
||||
@@ -484,22 +515,22 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
||||
|
||||
// Build standard I2S clock/slot/gpio configuration
|
||||
i2s_std_clk_config_t clk_cfg = {
|
||||
.sample_rate_hz = audio_stream_info.get_sample_rate(),
|
||||
.clk_src = clk_src,
|
||||
.sample_rate_hz = output_stream_info.get_sample_rate(),
|
||||
.clk_src = this->get_clock_source_(),
|
||||
.mclk_multiple = this->mclk_multiple_,
|
||||
};
|
||||
|
||||
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) {
|
||||
if (output_stream_info.get_channels() == 1) {
|
||||
slot_mode = I2S_SLOT_MODE_MONO;
|
||||
} else if (audio_stream_info.get_channels() == 2) {
|
||||
} else if (output_stream_info.get_channels() == 2) {
|
||||
slot_mode = I2S_SLOT_MODE_STEREO;
|
||||
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) this->output_stream_info_.get_bits_per_sample();
|
||||
const i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) output_stream_info.get_bits_per_sample();
|
||||
i2s_std_slot_config_t slot_cfg;
|
||||
switch (this->i2s_comm_fmt_) {
|
||||
case I2SCommFmt::PCM:
|
||||
@@ -535,20 +566,11 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
||||
i2s_std_gpio_config_t gpio_cfg = this->parent_->get_pin_config();
|
||||
gpio_cfg.dout = this->dout_pin_;
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
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, I2S_EVENT_QUEUE_COUNT);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// The speaker task will enable the channel after preloading.
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
} // namespace esphome::i2s_audio
|
||||
|
||||
@@ -20,12 +20,28 @@ class I2SAudioSpeaker final : public I2SAudioSpeakerBase {
|
||||
|
||||
void set_i2s_comm_fmt(I2SCommFmt fmt) { this->i2s_comm_fmt_ = fmt; }
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
void build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void run_speaker_task() override;
|
||||
esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override;
|
||||
|
||||
/// @brief Builds the channel and standard mode configuration for the given output format.
|
||||
/// @param output_stream_info Format clocked out of the I2S peripheral
|
||||
void build_i2s_config_(const audio::AudioStreamInfo &output_stream_info, i2s_chan_config_t &chan_cfg,
|
||||
i2s_std_config_t &std_cfg) const;
|
||||
|
||||
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
|
||||
/// @brief The output format of a full duplex bus, fixed by the configuration since the channel is set up once.
|
||||
audio::AudioStreamInfo full_duplex_stream_info_() const;
|
||||
#endif
|
||||
|
||||
/// @brief Frame multiple for DMA buffers and writes; 2 keeps ESP32 mono sample pairs word-aligned.
|
||||
uint32_t dma_frame_multiple_() const { return this->esp32_mono_swap_needed_() ? 2 : 1; }
|
||||
static uint32_t dma_frame_multiple(const audio::AudioStreamInfo &stream_info) {
|
||||
return esp32_mono_swap_needed(stream_info) ? 2 : 1;
|
||||
}
|
||||
|
||||
I2SCommFmt i2s_comm_fmt_{I2SCommFmt::STANDARD};
|
||||
};
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
#define USE_HOMEASSISTANT_TIME
|
||||
#define USE_HOMEASSISTANT_TIMEZONE
|
||||
#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT
|
||||
#define USE_I2S_AUDIO_FULL_DUPLEX
|
||||
#define USE_I2S_AUDIO_SPDIF_MODE
|
||||
#define USE_IMAGE
|
||||
#define USE_IMPROV_BLE_STATE_CALLBACK
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
i2s_audio:
|
||||
- id: duplex_bus
|
||||
i2s_bclk_pin: GPIO5
|
||||
i2s_lrclk_pin: GPIO4
|
||||
full_duplex: true
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: duplex_mic
|
||||
i2s_audio_id: duplex_bus
|
||||
i2s_din_pin: GPIO16
|
||||
adc_type: external
|
||||
sample_rate: 48000
|
||||
bits_per_sample: 32bit
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: duplex_speaker_a
|
||||
i2s_audio_id: duplex_bus
|
||||
i2s_dout_pin:
|
||||
number: GPIO17
|
||||
allow_other_uses: true
|
||||
dac_type: external
|
||||
sample_rate: 48000
|
||||
bits_per_sample: 32bit
|
||||
- platform: i2s_audio
|
||||
id: duplex_speaker_b
|
||||
i2s_audio_id: duplex_bus
|
||||
i2s_dout_pin:
|
||||
number: GPIO17
|
||||
allow_other_uses: true
|
||||
dac_type: external
|
||||
sample_rate: 48000
|
||||
bits_per_sample: 32bit
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Tests for the i2s_audio full duplex bus."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.i2s_audio import _validate_full_duplex
|
||||
from esphome.core import CORE, ID
|
||||
|
||||
BUS_ID = "duplex_bus"
|
||||
|
||||
|
||||
def _device(**overrides: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"platform": "i2s_audio",
|
||||
"id": ID("device", is_declaration=True),
|
||||
"i2s_audio_id": ID(BUS_ID, is_declaration=False),
|
||||
"i2s_dout_pin": 17,
|
||||
"channel": "stereo",
|
||||
"i2s_comm_fmt": "stand_i2s",
|
||||
"sample_rate": 48000,
|
||||
"bits_per_sample": 32,
|
||||
"i2s_mode": "primary",
|
||||
"use_apll": False,
|
||||
"mclk_multiple": 256,
|
||||
} | overrides
|
||||
|
||||
|
||||
def _full_config(
|
||||
microphones: list[dict[str, Any]], speakers: list[dict[str, Any]]
|
||||
) -> dict[str, Any]:
|
||||
return {"microphone": microphones, "speaker": speakers}
|
||||
|
||||
|
||||
def test_full_duplex_generates_code(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""The microphone and every speaker are registered with the bus and the full duplex code is enabled."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/i2s_audio/test_full_duplex_multiple_speakers.yaml"
|
||||
)
|
||||
|
||||
assert "duplex_bus->set_audio_in(duplex_mic);" in main_cpp
|
||||
assert "duplex_bus->set_audio_out(duplex_speaker_a);" in main_cpp
|
||||
assert "duplex_bus->set_audio_out(duplex_speaker_b);" in main_cpp
|
||||
assert "USE_I2S_AUDIO_FULL_DUPLEX" in {define.name for define in CORE.defines}
|
||||
|
||||
|
||||
_OTHER_BUS = _device(i2s_audio_id=ID("other_bus", is_declaration=False))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("microphones", "speakers"),
|
||||
[
|
||||
pytest.param([_device(pdm=False)], [_device(spdif_mode=False)], id="matching"),
|
||||
pytest.param(
|
||||
[_device(), _OTHER_BUS], [_device(), _OTHER_BUS], id="other-bus-ignored"
|
||||
),
|
||||
pytest.param(
|
||||
[_device()], [_device(), _device(), _device()], id="several-speakers"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_full_duplex_accepts(
|
||||
microphones: list[dict[str, Any]], speakers: list[dict[str, Any]]
|
||||
) -> None:
|
||||
"""Devices with the same clock and TX settings share the bus; other buses do not count."""
|
||||
_validate_full_duplex(_full_config(microphones, speakers), BUS_ID)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("microphones", "speakers"),
|
||||
[
|
||||
([], [_device()]),
|
||||
([_device()], []),
|
||||
([_device(), _device()], [_device()]),
|
||||
],
|
||||
)
|
||||
def test_full_duplex_requires_one_microphone_and_a_speaker(
|
||||
microphones: list[dict[str, Any]], speakers: list[dict[str, Any]]
|
||||
) -> None:
|
||||
"""Exactly one microphone and at least one speaker must use the bus."""
|
||||
with pytest.raises(cv.Invalid, match="exactly one i2s_audio microphone"):
|
||||
_validate_full_duplex(_full_config(microphones, speakers), BUS_ID)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("microphone", "speaker", "error_match"),
|
||||
[
|
||||
pytest.param(_device(pdm=True), _device(), "PDM microphone", id="pdm"),
|
||||
pytest.param(_device(), _device(spdif_mode=True), "SPDIF speaker", id="spdif"),
|
||||
],
|
||||
)
|
||||
def test_full_duplex_rejects_unsupported_devices(
|
||||
microphone: dict[str, Any], speaker: dict[str, Any], error_match: str
|
||||
) -> None:
|
||||
"""PDM receive and SPDIF output cannot share the standard mode clocks."""
|
||||
with pytest.raises(cv.Invalid, match=error_match):
|
||||
_validate_full_duplex(_full_config([microphone], [speaker]), BUS_ID)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("key", "value"),
|
||||
[
|
||||
("sample_rate", 16000),
|
||||
("bits_per_sample", 16),
|
||||
("i2s_mode", "secondary"),
|
||||
("use_apll", True),
|
||||
("mclk_multiple", 384),
|
||||
],
|
||||
)
|
||||
def test_full_duplex_rejects_mismatched_clock_settings(key: str, value: Any) -> None:
|
||||
"""Settings that shape the shared clocks must match."""
|
||||
with pytest.raises(cv.Invalid, match=f"must use the same '{key}'"):
|
||||
_validate_full_duplex(
|
||||
_full_config([_device()], [_device(**{key: value})]), BUS_ID
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("key", "value"),
|
||||
[
|
||||
("i2s_dout_pin", 18),
|
||||
("channel", "mono"),
|
||||
("i2s_comm_fmt", "stand_msb"),
|
||||
],
|
||||
)
|
||||
def test_full_duplex_rejects_mismatched_speakers(key: str, value: Any) -> None:
|
||||
"""Settings that shape the TX channel must match across speakers."""
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=f"speakers on 'full_duplex' bus '{BUS_ID}' must use the same '{key}'",
|
||||
):
|
||||
_validate_full_duplex(
|
||||
_full_config([_device()], [_device(), _device(**{key: value})]), BUS_ID
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
i2s_audio:
|
||||
- id: i2s_duplex_bus
|
||||
i2s_bclk_pin: ${i2s_bclk_pin}
|
||||
i2s_lrclk_pin: ${i2s_lrclk_pin}
|
||||
i2s_mclk_pin: ${i2s_mclk_pin}
|
||||
full_duplex: true
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: duplex_mic
|
||||
i2s_audio_id: i2s_duplex_bus
|
||||
i2s_din_pin: ${i2s_din_pin}
|
||||
adc_type: external
|
||||
sample_rate: 48000
|
||||
bits_per_sample: 32bit
|
||||
channel: stereo
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: duplex_speaker
|
||||
i2s_audio_id: i2s_duplex_bus
|
||||
i2s_dout_pin: ${i2s_dout_pin}
|
||||
dac_type: external
|
||||
sample_rate: 48000
|
||||
bits_per_sample: 32bit
|
||||
channel: stereo
|
||||
@@ -0,0 +1,9 @@
|
||||
substitutions:
|
||||
i2s_bclk_pin: GPIO5
|
||||
i2s_lrclk_pin: GPIO4
|
||||
i2s_mclk_pin: GPIO15
|
||||
i2s_din_pin: GPIO16
|
||||
i2s_dout_pin: GPIO17
|
||||
|
||||
packages:
|
||||
i2s_audio: !include common-full_duplex.yaml
|
||||
@@ -0,0 +1,9 @@
|
||||
substitutions:
|
||||
i2s_bclk_pin: GPIO5
|
||||
i2s_lrclk_pin: GPIO4
|
||||
i2s_mclk_pin: GPIO15
|
||||
i2s_din_pin: GPIO16
|
||||
i2s_dout_pin: GPIO17
|
||||
|
||||
packages:
|
||||
i2s_audio: !include common-full_duplex.yaml
|
||||
Reference in New Issue
Block a user