diff --git a/CODEOWNERS b/CODEOWNERS index 616b18293d..f96e43d5b7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -202,7 +202,7 @@ esphome/components/i2c_device/* @gabest11 esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz -esphome/components/i2s_audio/speaker/* @jesserockz +esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core @@ -377,7 +377,7 @@ esphome/components/smt100/* @piechade esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov -esphome/components/speaker/* @jesserockz +esphome/components/speaker/* @jesserockz @kahrendt esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 9fdaced64c..dd43d6cb39 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -17,7 +17,7 @@ from .. import ( ) AUTO_LOAD = ["audio"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] DEPENDENCIES = ["i2s_audio"] I2SAudioSpeaker = i2s_audio_ns.class_( diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 4fc489d0a3..cf6c3bbbba 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -32,6 +32,7 @@ enum SpeakerEventGroupBits : uint32_t { STATE_RUNNING = (1 << 11), STATE_STOPPING = (1 << 12), STATE_STOPPED = (1 << 13), + ERR_INVALID_FORMAT = (1 << 14), ERR_TASK_FAILED_TO_START = (1 << 15), ERR_ESP_INVALID_STATE = (1 << 16), ERR_ESP_INVALID_ARG = (1 << 17), @@ -104,16 +105,6 @@ void I2SAudioSpeaker::setup() { void I2SAudioSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { - this->status_set_error("Failed to start speaker task"); - } - - if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { - uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; - ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); - this->status_set_warning(); - } - if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { ESP_LOGD(TAG, "Starting Speaker"); this->state_ = speaker::STATE_STARTING; @@ -139,12 +130,64 @@ void I2SAudioSpeaker::loop() { this->speaker_task_handle_ = nullptr; } } + + if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { + this->status_set_error("Failed to start speaker task"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); + } + + if (event_group_bits & SpeakerEventGroupBits::ERR_INVALID_FORMAT) { + this->status_set_error("Failed to adjust I2S bus to match the incoming audio"); + ESP_LOGE(TAG, + "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8, + this->audio_stream_info_.sample_rate, this->audio_stream_info_.channels, + this->audio_stream_info_.bits_per_sample); + } + + if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { + uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; + ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); + this->status_set_warning(); + } } void I2SAudioSpeaker::set_volume(float volume) { this->volume_ = volume; - ssize_t decibel_index = remap(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); - this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + if (volume > 0.0) { + this->audio_dac_->set_mute_off(); + } + this->audio_dac_->set_volume(volume); + } else +#endif + { + // Fallback to software volume control by using a Q15 fixed point scaling factor + ssize_t decibel_index = remap(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); + this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; + } +} + +void I2SAudioSpeaker::set_mute_state(bool mute_state) { + this->mute_state_ = mute_state; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_) { + if (mute_state) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } else +#endif + { + if (mute_state) { + // Fallback to software volume control and scale by 0 + this->q15_volume_factor_ = 0; + } else { + // Revert to previous volume when unmuting + this->set_volume(this->volume_); + } + } } size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { @@ -275,6 +318,9 @@ void I2SAudioSpeaker::speaker_task(void *params) { i2s_zero_dma_buffer(this_speaker->parent_->get_port()); } } + } else { + // Couldn't configure the I2S port to be compatible with the incoming audio + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_INVALID_FORMAT); } i2s_zero_dma_buffer(this_speaker->parent_->get_port()); @@ -288,7 +334,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { } void I2SAudioSpeaker::start() { - if (this->is_failed()) + if (this->is_failed() || this->status_has_error()) return; if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 245f97d1e7..3c512d4d4d 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -49,11 +49,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp bool has_buffered_data() const override; - /// @brief Sets the volume of the speaker. It is implemented as a software volume control. - /// Overrides the default setter to convert the floating point volume to a Q15 fixed-point factor. - /// @param volume + /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is + /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a + /// Q15 fixed-point factor. + /// @param volume between 0.0 and 1.0 void set_volume(float volume) override; - float get_volume() override { return this->volume_; } + + /// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is + /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a + /// Q15 fixed-point factor. + /// @param mute_state true for muting, false for unmuting + void set_mute_state(bool mute_state) override; protected: /// @brief Function for the FreeRTOS task handling audio output. diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 1bbc0b02ef..7a668dc2f3 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -1,15 +1,18 @@ from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg +from esphome.components import audio_dac import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME from esphome.core import CORE from esphome.coroutine import coroutine_with_priority -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] IS_PLATFORM_COMPONENT = True +CONF_AUDIO_DAC = "audio_dac" + speaker_ns = cg.esphome_ns.namespace("speaker") Speaker = speaker_ns.class_("Speaker") @@ -26,6 +29,12 @@ FinishAction = speaker_ns.class_( VolumeSetAction = speaker_ns.class_( "VolumeSetAction", automation.Action, cg.Parented.template(Speaker) ) +MuteOnAction = speaker_ns.class_( + "MuteOnAction", automation.Action, cg.Parented.template(Speaker) +) +MuteOffAction = speaker_ns.class_( + "MuteOffAction", automation.Action, cg.Parented.template(Speaker) +) IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition) @@ -33,7 +42,9 @@ IsStoppedCondition = speaker_ns.class_("IsStoppedCondition", automation.Conditio async def setup_speaker_core_(var, config): - pass + if audio_dac_config := config.get(CONF_AUDIO_DAC): + aud_dac = await cg.get_variable(audio_dac_config) + cg.add(var.set_audio_dac(aud_dac)) async def register_speaker(var, config): @@ -42,8 +53,11 @@ async def register_speaker(var, config): await setup_speaker_core_(var, config) -SPEAKER_SCHEMA = cv.Schema({}) - +SPEAKER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_AUDIO_DAC): cv.use_id(audio_dac.AudioDac), + } +) SPEAKER_AUTOMATION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Speaker)}) @@ -113,6 +127,15 @@ async def speaker_volume_set_action(config, action_id, template_arg, args): return var +@automation.register_action( + "speaker.mute_off", MuteOffAction, SPEAKER_AUTOMATION_SCHEMA +) +@automation.register_action("speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA) +async def speaker_mute_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(speaker_ns.using) diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index 9efda011f2..c083796eea 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -39,6 +39,26 @@ template class VolumeSetAction : public Action, public Pa void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); } }; +template class MuteOnAction : public Action { + public: + explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {} + + void play(Ts... x) override { this->speaker_->set_mute_state(true); } + + protected: + Speaker *speaker_; +}; + +template class MuteOffAction : public Action { + public: + explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {} + + void play(Ts... x) override { this->speaker_->set_mute_state(false); } + + protected: + Speaker *speaker_; +}; + template class StopAction : public Action, public Parented { public: void play(Ts... x) override { this->parent_->stop(); } diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 9390e4edb7..96843e2d5a 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -8,7 +8,12 @@ #include #endif +#include "esphome/core/defines.h" + #include "esphome/components/audio/audio.h" +#ifdef USE_AUDIO_DAC +#include "esphome/components/audio_dac/audio_dac.h" +#endif namespace esphome { namespace speaker { @@ -56,9 +61,35 @@ class Speaker { bool is_running() const { return this->state_ == STATE_RUNNING; } bool is_stopped() const { return this->state_ == STATE_STOPPED; } - // Volume control must be implemented by each speaker component, otherwise it will have no effect. - virtual void set_volume(float volume) { this->volume_ = volume; }; - virtual float get_volume() { return this->volume_; } + // Volume control is handled by a configured audio dac component. Individual speaker components can + // override and implement in software if an audio dac isn't available. + virtual void set_volume(float volume) { + this->volume_ = volume; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + this->audio_dac_->set_volume(volume); + } +#endif + }; + float get_volume() { return this->volume_; } + + virtual void set_mute_state(bool mute_state) { + this->mute_state_ = mute_state; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_) { + if (mute_state) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } +#endif + } + bool get_mute_state() { return this->mute_state_; } + +#ifdef USE_AUDIO_DAC + void set_audio_dac(audio_dac::AudioDac *audio_dac) { this->audio_dac_ = audio_dac; } +#endif void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info) { this->audio_stream_info_ = audio_stream_info; @@ -68,6 +99,11 @@ class Speaker { State state_{STATE_STOPPED}; audio::AudioStreamInfo audio_stream_info_; float volume_{1.0f}; + bool mute_state_{false}; + +#ifdef USE_AUDIO_DAC + audio_dac::AudioDac *audio_dac_{nullptr}; +#endif }; } // namespace speaker diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml index 9a24d00f68..396b4d95ea 100644 --- a/tests/components/speaker/test.esp32-ard.yaml +++ b/tests/components/speaker/test.esp32-ard.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: diff --git a/tests/components/speaker/test.esp32-c3-ard.yaml b/tests/components/speaker/test.esp32-c3-ard.yaml index f28014337c..636aeba766 100644 --- a/tests/components/speaker/test.esp32-c3-ard.yaml +++ b/tests/components/speaker/test.esp32-c3-ard.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml index f28014337c..636aeba766 100644 --- a/tests/components/speaker/test.esp32-c3-idf.yaml +++ b/tests/components/speaker/test.esp32-c3-idf.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: diff --git a/tests/components/speaker/test.esp32-idf.yaml b/tests/components/speaker/test.esp32-idf.yaml index 9a24d00f68..b69440b133 100644 --- a/tests/components/speaker/test.esp32-idf.yaml +++ b/tests/components/speaker/test.esp32-idf.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: @@ -17,8 +19,17 @@ i2s_audio: i2s_bclk_pin: 17 i2s_mclk_pin: 15 +i2c: + scl: 12 + sda: 10 + +audio_dac: + - platform: aic3204 + id: internal_dac + speaker: - platform: i2s_audio - id: speaker_id + id: speaker_with_audio_dac_id + audio_dac: internal_dac dac_type: external - i2s_dout_pin: 13 + i2s_dout_pin: 14