diff --git a/esphome/components/aic3204/__init__.py b/esphome/components/aic3204/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/esphome/components/aic3204/aic3204.cpp b/esphome/components/aic3204/aic3204.cpp deleted file mode 100644 index ace17df3..00000000 --- a/esphome/components/aic3204/aic3204.cpp +++ /dev/null @@ -1,177 +0,0 @@ -#include "aic3204.h" - -#include "esphome/core/defines.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace aic3204 { - -static const char *const TAG = "aic3204"; - -#define ERROR_CHECK(err, msg) \ - if (!err) { \ - ESP_LOGE(TAG, msg); \ - this->mark_failed(); \ - return; \ - } - -void AIC3204::setup() { - ESP_LOGCONFIG(TAG, "Setting up AIC3204..."); - - // Set register page to 0 - ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed"); - // Initiate SW reset (PLL is powered off as part of reset) - ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed"); - // *** Program clock settings *** - // Default is CODEC_CLKIN is from MCLK pin. Don't need to change this. - // MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio) - // (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf) - // We do need MDAC*DOSR/32 >= the resource compute level for the processing block - // So here 2*128/32 = 8, which is equal to processing block 1 's resource compute - // See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow - // for determining these settings. - - // Power up NDAC and set to 2 - ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed"); - // Power up MDAC and set to 2 - ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed"); - // Program DOSR = 128 - ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed"); - // Set Audio Interface Config: I2S, 32 bits, slave mode, DOUT always driving. - ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed"); - // For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In - ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed"); - ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed"); - ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed"); - // Program the DAC processing block to be used - PRB_P1 - ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed"); - - // *** Select Page 1 *** - ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed"); - // Enable the internal AVDD_LDO: - ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed"); - // *** Program Analog Blocks *** - // Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO - ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed"); - // Enable Master Analog Power Control - ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed"); - // Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v - // We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123) - // Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware - // We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47) - // (All pages refer to the TLV320AIC3204 Application Reference Guide) - ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed"); - // *** Set PowerTune Modes *** - // Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver. - ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed"); - ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed"); - // Set the REF charging time to 40ms - ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed"); - // HP soft stepping settings for optimal pop performance at power up - // Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling - // capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound. - ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed"); - // Route Left DAC to HPL - ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed"); - // Route Right DAC to HPR - ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed"); - // Route Left DAC to LOL - ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed"); - // Route Right DAC to LOR - ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed"); - - // Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register) - ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed"); - // Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register) - ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed"); - // Unmute LOL and set gain to 0dB - ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed"); - // Unmute LOR and set gain to 0dB - ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed"); - - // Power up HPL and HPR, LOL and LOR drivers - ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed"); - - // Wait for 2.5 sec for soft stepping to take effect before attempting power-up - this->set_timeout(2500, [this]() { - // *** Power Up DAC *** - // Select Page 0 - ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set PAGE_CTRL failed"); - // Power up the Left and Right DAC Channels. Route Left data to Left DAC and Right data to Right DAC. - // DAC Vol control soft step 1 step per DAC word clock. - ERROR_CHECK(this->write_byte(AIC3204_DAC_CH_SET1, 0xd4), "Set DAC_CH_SET1 failed"); - // Set left and right DAC digital volume control - ERROR_CHECK(this->write_volume_(), "Set volume failed"); - // Unmute left and right channels - ERROR_CHECK(this->write_mute_(), "Set mute failed"); - }); -} - -void AIC3204::dump_config() { - ESP_LOGCONFIG(TAG, "AIC3204:"); - LOG_I2C_DEVICE(this); - - if (this->is_failed()) { - ESP_LOGE(TAG, "Communication with AIC3204 failed"); - } -} - -bool AIC3204::set_mute_off() { - this->is_muted_ = false; - return this->write_mute_(); -} - -bool AIC3204::set_mute_on() { - this->is_muted_ = true; - return this->write_mute_(); -} - -bool AIC3204::set_auto_mute_mode(uint8_t auto_mute_mode) { - this->auto_mute_mode_ = auto_mute_mode & 0x07; - ESP_LOGVV(TAG, "Setting auto_mute_mode to 0x%.2x", this->auto_mute_mode_); - return this->write_mute_(); -} - -bool AIC3204::set_volume(float volume) { - this->volume_ = clamp(volume, 0.0, 1.0); - return this->write_volume_(); -} - -bool AIC3204::is_muted() { - return this->is_muted_; -} - -float AIC3204::volume() { - return this->volume_; -} - -bool AIC3204::write_mute_() { - uint8_t mute_mode_byte = this->auto_mute_mode_ << 4; // auto-mute control is bits 4-6 - mute_mode_byte |= this->is_muted_ ? 0x0c : 0x00; // mute bits are 2-3 - if (!this->write_byte(AIC3204_PAGE_CTRL, 0x00) || !this->write_byte(AIC3204_DAC_CH_SET2, mute_mode_byte)) { - ESP_LOGE(TAG, "Writing mute modes failed"); - return false; - } - return true; -} - -bool AIC3204::write_volume_() { - const int8_t dvc_min_byte = -127; - const int8_t dvc_max_byte = 48; - - int8_t volume_byte = dvc_min_byte + (this->volume_ * (dvc_max_byte - dvc_min_byte)); - volume_byte = clamp(volume_byte, dvc_min_byte, dvc_max_byte); - - ESP_LOGVV(TAG, "Setting volume to 0x%.2x", volume_byte & 0xFF); - - if ((!this->write_byte(AIC3204_PAGE_CTRL, 0x00)) || (!this->write_byte(AIC3204_DACL_VOL_D, volume_byte)) || - (!this->write_byte(AIC3204_DACR_VOL_D, volume_byte))) { - ESP_LOGE(TAG, "Writing volume failed"); - return false; - } - return true; -} - -} // namespace aic3204 -} // namespace esphome diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h deleted file mode 100644 index 783a58a2..00000000 --- a/esphome/components/aic3204/aic3204.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include "esphome/components/audio_dac/audio_dac.h" -#include "esphome/components/i2c/i2c.h" -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace aic3204 { - -// TLV320AIC3204 Register Addresses -// Page 0 -static const uint8_t AIC3204_PAGE_CTRL = 0x00; // Register 0 - Page Control -static const uint8_t AIC3204_SW_RST = 0x01; // Register 1 - Software Reset -static const uint8_t AIC3204_CLK_PLL1 = 0x04; // Register 4 - Clock Setting Register 1, Multiplexers -static const uint8_t AIC3204_CLK_PLL2 = 0x05; // Register 5 - Clock Setting Register 2, P and R values -static const uint8_t AIC3204_CLK_PLL3 = 0x06; // Register 6 - Clock Setting Register 3, J values -static const uint8_t AIC3204_NDAC = 0x0B; // Register 11 - NDAC Divider Value -static const uint8_t AIC3204_MDAC = 0x0C; // Register 12 - MDAC Divider Value -static const uint8_t AIC3204_DOSR = 0x0E; // Register 14 - DOSR Divider Value (LS Byte) -static const uint8_t AIC3204_NADC = 0x12; // Register 18 - NADC Divider Value -static const uint8_t AIC3204_MADC = 0x13; // Register 19 - MADC Divider Value -static const uint8_t AIC3204_AOSR = 0x14; // Register 20 - AOSR Divider Value -static const uint8_t AIC3204_CODEC_IF = 0x1B; // Register 27 - CODEC Interface Control -static const uint8_t AIC3204_AUDIO_IF_4 = 0x1F; // Register 31 - Audio Interface Setting Register 4 -static const uint8_t AIC3204_AUDIO_IF_5 = 0x20; // Register 32 - Audio Interface Setting Register 5 -static const uint8_t AIC3204_SCLK_MFP3 = 0x38; // Register 56 - SCLK/MFP3 Function Control -static const uint8_t AIC3204_DAC_SIG_PROC = 0x3C; // Register 60 - DAC Sig Processing Block Control -static const uint8_t AIC3204_ADC_SIG_PROC = 0x3D; // Register 61 - ADC Sig Processing Block Control -static const uint8_t AIC3204_DAC_CH_SET1 = 0x3F; // Register 63 - DAC Channel Setup 1 -static const uint8_t AIC3204_DAC_CH_SET2 = 0x40; // Register 64 - DAC Channel Setup 2 -static const uint8_t AIC3204_DACL_VOL_D = 0x41; // Register 65 - DAC Left Digital Vol Control -static const uint8_t AIC3204_DACR_VOL_D = 0x42; // Register 66 - DAC Right Digital Vol Control -static const uint8_t AIC3204_DRC_ENABLE = 0x44; -static const uint8_t AIC3204_ADC_CH_SET = 0x51; // Register 81 - ADC Channel Setup -static const uint8_t AIC3204_ADC_FGA_MUTE = 0x52; // Register 82 - ADC Fine Gain Adjust/Mute - -// Page 1 -static const uint8_t AIC3204_PWR_CFG = 0x01; // Register 1 - Power Config -static const uint8_t AIC3204_LDO_CTRL = 0x02; // Register 2 - LDO Control -static const uint8_t AIC3204_PLAY_CFG1 = 0x03; // Register 3 - Playback Config 1 -static const uint8_t AIC3204_PLAY_CFG2 = 0x04; // Register 4 - Playback Config 2 -static const uint8_t AIC3204_OP_PWR_CTRL = 0x09; // Register 9 - Output Driver Power Control -static const uint8_t AIC3204_CM_CTRL = 0x0A; // Register 10 - Common Mode Control -static const uint8_t AIC3204_HPL_ROUTE = 0x0C; // Register 12 - HPL Routing Select -static const uint8_t AIC3204_HPR_ROUTE = 0x0D; // Register 13 - HPR Routing Select -static const uint8_t AIC3204_LOL_ROUTE = 0x0E; // Register 14 - LOL Routing Selection -static const uint8_t AIC3204_LOR_ROUTE = 0x0F; // Register 15 - LOR Routing Selection -static const uint8_t AIC3204_HPL_GAIN = 0x10; // Register 16 - HPL Driver Gain -static const uint8_t AIC3204_HPR_GAIN = 0x11; // Register 17 - HPR Driver Gain -static const uint8_t AIC3204_LOL_DRV_GAIN = 0x12; // Register 18 - LOL Driver Gain Setting -static const uint8_t AIC3204_LOR_DRV_GAIN = 0x13; // Register 19 - LOR Driver Gain Setting -static const uint8_t AIC3204_HP_START = 0x14; // Register 20 - Headphone Driver Startup -static const uint8_t AIC3204_LPGA_P_ROUTE = 0x34; // Register 52 - Left PGA Positive Input Route -static const uint8_t AIC3204_LPGA_N_ROUTE = 0x36; // Register 54 - Left PGA Negative Input Route -static const uint8_t AIC3204_RPGA_P_ROUTE = 0x37; // Register 55 - Right PGA Positive Input Route -static const uint8_t AIC3204_RPGA_N_ROUTE = 0x39; // Register 57 - Right PGA Negative Input Route -static const uint8_t AIC3204_LPGA_VOL = 0x3B; // Register 59 - Left PGA Volume -static const uint8_t AIC3204_RPGA_VOL = 0x3C; // Register 60 - Right PGA Volume -static const uint8_t AIC3204_ADC_PTM = 0x3D; // Register 61 - ADC Power Tune Config -static const uint8_t AIC3204_AN_IN_CHRG = 0x47; // Register 71 - Analog Input Quick Charging Config -static const uint8_t AIC3204_REF_STARTUP = 0x7B; // Register 123 - Reference Power Up Config - -class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { - public: - void setup() override; - void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - - bool set_mute_off() override; - bool set_mute_on() override; - bool set_auto_mute_mode(uint8_t auto_mute_mode); - bool set_volume(float volume) override; - - bool is_muted() override; - float volume() override; - - protected: - bool write_mute_(); - bool write_volume_(); - - uint8_t auto_mute_mode_{0}; - float volume_{0}; -}; - -} // namespace aic3204 -} // namespace esphome diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py deleted file mode 100644 index 19deff59..00000000 --- a/esphome/components/aic3204/audio_dac.py +++ /dev/null @@ -1,52 +0,0 @@ -import esphome.codegen as cg -from esphome.components import i2c -import esphome.config_validation as cv -from esphome import automation -from esphome.components.audio_dac import AudioDac, audio_dac_ns -from esphome.const import CONF_ID, CONF_MODE - -CODEOWNERS = ["@kbx81"] -DEPENDENCIES = ["i2c"] - -aic3204_ns = cg.esphome_ns.namespace("aic3204") -AIC3204 = aic3204_ns.class_("AIC3204", AudioDac, cg.Component, i2c.I2CDevice) - -SetAutoMuteAction = aic3204_ns.class_("SetAutoMuteAction", automation.Action) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(AIC3204), - } - ) - .extend(cv.COMPONENT_SCHEMA) - .extend(i2c.i2c_device_schema(0x18)) -) - - -SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( - { - cv.GenerateID(): cv.use_id(AIC3204), - cv.Required(CONF_MODE): cv.templatable(cv.int_range(max=7, min=0)), - }, - key=CONF_MODE, -) - - -@automation.register_action( - "aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA -) -async def aic3204_set_volume_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - - template_ = await cg.templatable(config.get(CONF_MODE), args, int) - cg.add(var.set_auto_mute_mode(template_)) - - return var - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) diff --git a/esphome/components/aic3204/automation.h b/esphome/components/aic3204/automation.h deleted file mode 100644 index 416a88fa..00000000 --- a/esphome/components/aic3204/automation.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "esphome/core/component.h" -#include "aic3204.h" - -namespace esphome { -namespace aic3204 { - -template class SetAutoMuteAction : public Action { - public: - explicit SetAutoMuteAction(AIC3204 *aic3204) : aic3204_(aic3204) {} - - TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) - - void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } - - protected: - AIC3204 *aic3204_; -}; - -} // namespace aic3204 -} // namespace esphome diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py deleted file mode 100644 index 34389f77..00000000 --- a/esphome/components/audio_dac/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome import automation -from esphome.automation import maybe_simple_id -from esphome.core import coroutine_with_priority -from esphome.const import CONF_ID, CONF_VOLUME - -CODEOWNERS = ["@kbx81"] -IS_PLATFORM_COMPONENT = True - -audio_dac_ns = cg.esphome_ns.namespace("audio_dac") -AudioDac = audio_dac_ns.class_("AudioDac") - -MuteOffAction = audio_dac_ns.class_("MuteOffAction", automation.Action) -MuteOnAction = audio_dac_ns.class_("MuteOnAction", automation.Action) -SetVolumeAction = audio_dac_ns.class_("SetVolumeAction", automation.Action) - - -MUTE_ACTION_SCHEMA = maybe_simple_id( - { - cv.GenerateID(): cv.use_id(AudioDac), - } -) - -SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value( - { - cv.GenerateID(): cv.use_id(AudioDac), - cv.Required(CONF_VOLUME): cv.templatable(cv.percentage), - }, - key=CONF_VOLUME, -) - - -@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA) -@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA) -async def audio_dac_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) - - -@automation.register_action( - "audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA -) -async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - - template_ = await cg.templatable(config.get(CONF_VOLUME), args, float) - cg.add(var.set_volume(template_)) - - return var - - -@coroutine_with_priority(100.0) -async def to_code(config): - cg.add_define("USE_AUDIO_DAC") - cg.add_global(audio_dac_ns.using) diff --git a/esphome/components/audio_dac/audio_dac.h b/esphome/components/audio_dac/audio_dac.h deleted file mode 100644 index 1297d9c3..00000000 --- a/esphome/components/audio_dac/audio_dac.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace audio_dac { - -class AudioDac { - public: - virtual bool set_mute_off() = 0; - virtual bool set_mute_on() = 0; - virtual bool set_volume(float volume) = 0; - - virtual bool is_muted() = 0; - virtual float volume() = 0; - - protected: - bool is_muted_{false}; -}; - -} // namespace aic3204 -} // namespace esphome diff --git a/esphome/components/audio_dac/automation.h b/esphome/components/audio_dac/automation.h deleted file mode 100644 index b6cf2aca..00000000 --- a/esphome/components/audio_dac/automation.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "esphome/core/component.h" -#include "audio_dac.h" - -namespace esphome { -namespace audio_dac { - -template class MuteOffAction : public Action { - public: - explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - - void play(Ts... x) override { this->audio_dac_->set_mute_off(); } - - protected: - AudioDac *audio_dac_; -}; - -template class MuteOnAction : public Action { - public: - explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - - void play(Ts... x) override { this->audio_dac_->set_mute_on(); } - - protected: - AudioDac *audio_dac_; -}; - -template class SetVolumeAction : public Action { - public: - explicit SetVolumeAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - - TEMPLATABLE_VALUE(float, volume) - - void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } - - protected: - AudioDac *audio_dac_; -}; - -} // namespace audio_dac -} // namespace esphome diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py deleted file mode 100644 index 157ea27a..00000000 --- a/esphome/components/media_player/__init__.py +++ /dev/null @@ -1,209 +0,0 @@ -from esphome import automation -from esphome.automation import maybe_simple_id -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.const import ( - CONF_ID, - CONF_ON_IDLE, - CONF_ON_STATE, - CONF_TRIGGER_ID, - CONF_VOLUME, -) -from esphome.core import CORE -from esphome.coroutine import coroutine_with_priority -from esphome.cpp_helpers import setup_entity - -CODEOWNERS = ["@jesserockz"] - -IS_PLATFORM_COMPONENT = True - -media_player_ns = cg.esphome_ns.namespace("media_player") - -MediaPlayer = media_player_ns.class_("MediaPlayer") -MediaFile = media_player_ns.struct("MediaFile") -MediaFileType = media_player_ns.enum("MediaFileType", is_class=True) -MEDIA_FILE_TYPE_ENUM = { - "NONE": MediaFileType.NONE, - "WAV": MediaFileType.WAV, - "MP3": MediaFileType.MP3, - "FLAC": MediaFileType.FLAC, -} - - -PlayAction = media_player_ns.class_( - "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PlayMediaAction = media_player_ns.class_( - "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PlayLocalMediaAction = media_player_ns.class_( - "PlayLocalMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -ToggleAction = media_player_ns.class_( - "ToggleAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PauseAction = media_player_ns.class_( - "PauseAction", automation.Action, cg.Parented.template(MediaPlayer) -) -StopAction = media_player_ns.class_( - "StopAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeUpAction = media_player_ns.class_( - "VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeDownAction = media_player_ns.class_( - "VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeSetAction = media_player_ns.class_( - "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) -) - - -CONF_ON_PLAY = "on_play" -CONF_ON_PAUSE = "on_pause" -CONF_ON_ANNOUNCEMENT = "on_announcement" -CONF_MEDIA_URL = "media_url" - -StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template()) -IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template()) -PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template()) -PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template()) -AnnoucementTrigger = media_player_ns.class_( - "AnnouncementTrigger", automation.Trigger.template() -) -IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) -IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition) -IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) - - -async def setup_media_player_core_(var, config): - await setup_entity(var, config) - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_IDLE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PLAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PAUSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ANNOUNCEMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - - -async def register_media_player(var, config): - if not CORE.has_id(config[CONF_ID]): - var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_media_player(var)) - await setup_media_player_core_(var, config) - - -MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( - { - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_IDLE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), - } - ), - cv.Optional(CONF_ON_PLAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlayTrigger), - } - ), - cv.Optional(CONF_ON_PAUSE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Optional(CONF_ON_ANNOUNCEMENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger), - } - ), - } -) - - -MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)}) - - -@automation.register_action( - "media_player.play_media", - PlayMediaAction, - cv.maybe_simple_value( - { - cv.GenerateID(): cv.use_id(MediaPlayer), - cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), - }, - key=CONF_MEDIA_URL, - ), -) -async def media_player_play_media_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string) - cg.add(var.set_media_url(media_url)) - return var - - -@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_condition( - "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_condition( - "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_condition( - "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA -) -async def media_player_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - -@automation.register_action( - "media_player.volume_set", - VolumeSetAction, - cv.maybe_simple_value( - { - cv.GenerateID(): cv.use_id(MediaPlayer), - cv.Required(CONF_VOLUME): cv.templatable(cv.percentage), - }, - key=CONF_VOLUME, - ), -) -async def media_player_volume_set_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - volume = await cg.templatable(config[CONF_VOLUME], args, float) - cg.add(var.set_volume(volume)) - return var - - -@coroutine_with_priority(100.0) -async def to_code(config): - cg.add_global(media_player_ns.using) - cg.add_define("USE_MEDIA_PLAYER") diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h deleted file mode 100644 index 27a49bde..00000000 --- a/esphome/components/media_player/automation.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "media_player.h" - -namespace esphome { - -namespace media_player { - -#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \ - template class ACTION_CLASS : public Action, public Parented { \ - void play(Ts... x) override { \ - this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \ - } \ - }; - -#define MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(TRIGGER_CLASS, TRIGGER_STATE) \ - class TRIGGER_CLASS : public Trigger<> { \ - public: \ - explicit TRIGGER_CLASS(MediaPlayer *player) { \ - player->add_on_state_callback([this, player]() { \ - if (player->state == MediaPlayerState::MEDIA_PLAYER_STATE_##TRIGGER_STATE) \ - this->trigger(); \ - }); \ - } \ - }; - -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN) - -template class PlayMediaAction : public Action, public Parented { - TEMPLATABLE_VALUE(std::string, media_url) - void play(Ts... x) override { this->parent_->make_call().set_media_url(this->media_url_.value(x...)).perform(); } -}; - -template class VolumeSetAction : public Action, public Parented { - TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } -}; - -class StateTrigger : public Trigger<> { - public: - explicit StateTrigger(MediaPlayer *player) { - player->add_on_state_callback([this]() { this->trigger(); }); - } -}; - -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(IdleTrigger, IDLE) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PlayTrigger, PLAYING) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PauseTrigger, PAUSED) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(AnnouncementTrigger, ANNOUNCING) - -template class IsIdleCondition : public Condition, public Parented { - public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } -}; - -template class IsPlayingCondition : public Condition, public Parented { - public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } -}; - -template class IsPausedCondition : public Condition, public Parented { - public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } -}; - -} // namespace media_player -} // namespace esphome diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp deleted file mode 100644 index 46c68d85..00000000 --- a/esphome/components/media_player/media_player.cpp +++ /dev/null @@ -1,152 +0,0 @@ -#include "media_player.h" - -#include "esphome/core/log.h" - -namespace esphome { -namespace media_player { - -static const char *const TAG = "media_player"; - -const char *media_player_state_to_string(MediaPlayerState state) { - switch (state) { - case MEDIA_PLAYER_STATE_IDLE: - return "IDLE"; - case MEDIA_PLAYER_STATE_PLAYING: - return "PLAYING"; - case MEDIA_PLAYER_STATE_PAUSED: - return "PAUSED"; - case MEDIA_PLAYER_STATE_ANNOUNCING: - return "ANNOUNCING"; - case MEDIA_PLAYER_STATE_NONE: - default: - return "UNKNOWN"; - } -} - -const char *media_player_command_to_string(MediaPlayerCommand command) { - switch (command) { - case MEDIA_PLAYER_COMMAND_PLAY: - return "PLAY"; - case MEDIA_PLAYER_COMMAND_PAUSE: - return "PAUSE"; - case MEDIA_PLAYER_COMMAND_STOP: - return "STOP"; - case MEDIA_PLAYER_COMMAND_MUTE: - return "MUTE"; - case MEDIA_PLAYER_COMMAND_UNMUTE: - return "UNMUTE"; - case MEDIA_PLAYER_COMMAND_TOGGLE: - return "TOGGLE"; - case MEDIA_PLAYER_COMMAND_VOLUME_UP: - return "VOLUME_UP"; - case MEDIA_PLAYER_COMMAND_VOLUME_DOWN: - return "VOLUME_DOWN"; - default: - return "UNKNOWN"; - } -} - -const char *media_player_file_type_to_string(MediaFileType file_type) { - switch (file_type) { - // case MediaFileType::NONE: - // return "unknown"; - case MediaFileType::FLAC: - return "FLAC"; - case MediaFileType::MP3: - return "MP3"; - case MediaFileType::WAV: - return "WAV"; - default: - return "unknonw"; - } -} - -void MediaPlayerCall::validate_() { - if (this->media_url_.has_value()) { - if (this->command_.has_value()) { - ESP_LOGW(TAG, "MediaPlayerCall: Setting both command and media_url is not needed."); - this->command_.reset(); - } - } - if (this->volume_.has_value()) { - if (this->volume_.value() < 0.0f || this->volume_.value() > 1.0f) { - ESP_LOGW(TAG, "MediaPlayerCall: Volume must be between 0.0 and 1.0."); - this->volume_.reset(); - } - } -} - -void MediaPlayerCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); - this->validate_(); - if (this->command_.has_value()) { - const char *command_s = media_player_command_to_string(this->command_.value()); - ESP_LOGD(TAG, " Command: %s", command_s); - } - if (this->media_url_.has_value()) { - ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str()); - } - if (this->volume_.has_value()) { - ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value()); - } - if (this->announcement_.has_value()) { - ESP_LOGD(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); - } - this->parent_->control(*this); -} - -MediaPlayerCall &MediaPlayerCall::set_command(MediaPlayerCommand command) { - this->command_ = command; - return *this; -} -MediaPlayerCall &MediaPlayerCall::set_command(optional command) { - this->command_ = command; - return *this; -} -MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) { - if (str_equals_case_insensitive(command, "PLAY")) { - this->set_command(MEDIA_PLAYER_COMMAND_PLAY); - } else if (str_equals_case_insensitive(command, "PAUSE")) { - this->set_command(MEDIA_PLAYER_COMMAND_PAUSE); - } else if (str_equals_case_insensitive(command, "STOP")) { - this->set_command(MEDIA_PLAYER_COMMAND_STOP); - } else if (str_equals_case_insensitive(command, "MUTE")) { - this->set_command(MEDIA_PLAYER_COMMAND_MUTE); - } else if (str_equals_case_insensitive(command, "UNMUTE")) { - this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE); - } else if (str_equals_case_insensitive(command, "TOGGLE")) { - this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str()); - } - return *this; -} - -MediaPlayerCall &MediaPlayerCall::set_media_url(const std::string &media_url) { - this->media_url_ = media_url; - return *this; -} - -MediaPlayerCall &MediaPlayerCall::set_local_media_file(MediaFile *media_file) { - this->media_file_ = media_file; - return *this; -} - -MediaPlayerCall &MediaPlayerCall::set_volume(float volume) { - this->volume_ = volume; - return *this; -} - -MediaPlayerCall &MediaPlayerCall::set_announcement(bool announce) { - this->announcement_ = announce; - return *this; -} - -void MediaPlayer::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void MediaPlayer::publish_state() { this->state_callback_.call(); } - -} // namespace media_player -} // namespace esphome diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h deleted file mode 100644 index ae5d06bc..00000000 --- a/esphome/components/media_player/media_player.h +++ /dev/null @@ -1,130 +0,0 @@ -#pragma once - -#include "esphome/core/entity_base.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace media_player { - -enum MediaPlayerState : uint8_t { - MEDIA_PLAYER_STATE_NONE = 0, - MEDIA_PLAYER_STATE_IDLE = 1, - MEDIA_PLAYER_STATE_PLAYING = 2, - MEDIA_PLAYER_STATE_PAUSED = 3, - MEDIA_PLAYER_STATE_ANNOUNCING = 4 -}; -const char *media_player_state_to_string(MediaPlayerState state); - -enum MediaPlayerCommand : uint8_t { - MEDIA_PLAYER_COMMAND_PLAY = 0, - MEDIA_PLAYER_COMMAND_PAUSE = 1, - MEDIA_PLAYER_COMMAND_STOP = 2, - MEDIA_PLAYER_COMMAND_MUTE = 3, - MEDIA_PLAYER_COMMAND_UNMUTE = 4, - MEDIA_PLAYER_COMMAND_TOGGLE = 5, - MEDIA_PLAYER_COMMAND_VOLUME_UP = 6, - MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7, -}; -const char *media_player_command_to_string(MediaPlayerCommand command); - -enum class MediaFileType : uint8_t { - NONE = 0, - WAV, - MP3, - FLAC, -}; -const char *media_player_file_type_to_string(MediaFileType file_type); - -enum class MediaPlayerFormatPurpose : uint8_t { - PURPOSE_DEFAULT = 0, - PURPOSE_ANNOUNCEMENT = 1, -}; - -struct MediaPlayerSupportedFormat { - std::string format; - uint32_t sample_rate; - uint32_t num_channels; - MediaPlayerFormatPurpose purpose; - uint32_t sample_bytes; -}; - -struct MediaFile { - const uint8_t *data; - size_t length; - MediaFileType file_type; -}; - -class MediaPlayer; - -class MediaPlayerTraits { - public: - MediaPlayerTraits() = default; - - void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; } - - bool get_supports_pause() const { return this->supports_pause_; } - - std::vector &get_supported_formats() { return this->supported_formats_; } - - protected: - bool supports_pause_{false}; - std::vector supported_formats_{}; -}; - -class MediaPlayerCall { - public: - MediaPlayerCall(MediaPlayer *parent) : parent_(parent) {} - - MediaPlayerCall &set_command(MediaPlayerCommand command); - MediaPlayerCall &set_command(optional command); - MediaPlayerCall &set_command(const std::string &command); - - MediaPlayerCall &set_media_url(const std::string &url); - MediaPlayerCall &set_local_media_file(MediaFile *media_file); - - MediaPlayerCall &set_volume(float volume); - MediaPlayerCall &set_announcement(bool announce); - - void perform(); - - const optional &get_command() const { return command_; } - const optional &get_media_url() const { return media_url_; } - const optional &get_volume() const { return volume_; } - const optional &get_announcement() const { return announcement_; } - const optional &get_local_media_file() const { return media_file_; } - - protected: - void validate_(); - MediaPlayer *const parent_; - optional command_; - optional media_url_; - optional volume_; - optional announcement_; - optional media_file_; -}; - -class MediaPlayer : public EntityBase { - public: - MediaPlayerState state{MEDIA_PLAYER_STATE_NONE}; - float volume{1.0f}; - - MediaPlayerCall make_call() { return MediaPlayerCall(this); } - - void publish_state(); - - void add_on_state_callback(std::function &&callback); - - virtual bool is_muted() const { return false; } - - virtual MediaPlayerTraits get_traits() = 0; - - protected: - friend MediaPlayerCall; - - virtual void control(const MediaPlayerCall &call) = 0; - - CallbackManager state_callback_{}; -}; - -} // namespace media_player -} // namespace esphome diff --git a/esphome/components/nabu/__init__.py b/esphome/components/nabu/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/esphome/components/nabu/audio_decoder.cpp b/esphome/components/nabu/audio_decoder.cpp deleted file mode 100644 index 1ac5d364..00000000 --- a/esphome/components/nabu/audio_decoder.cpp +++ /dev/null @@ -1,373 +0,0 @@ -#ifdef USE_ESP_IDF - -#include "audio_decoder.h" - -#include "mp3_decoder.h" - -#include "esphome/core/ring_buffer.h" - -namespace esphome { -namespace nabu { - -static const size_t READ_WRITE_TIMEOUT_MS = 20; - -AudioDecoder::AudioDecoder(RingBuffer *input_ring_buffer, RingBuffer *output_ring_buffer, size_t internal_buffer_size) { - this->input_ring_buffer_ = input_ring_buffer; - this->output_ring_buffer_ = output_ring_buffer; - this->internal_buffer_size_ = internal_buffer_size; -} - -AudioDecoder::~AudioDecoder() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - if (this->input_buffer_ != nullptr) { - allocator.deallocate(this->input_buffer_, this->internal_buffer_size_); - } - if (this->output_buffer_ != nullptr) { - allocator.deallocate(this->output_buffer_, this->internal_buffer_size_); - } - - if (this->flac_decoder_ != nullptr) { - this->flac_decoder_->free_buffers(); - this->flac_decoder_.reset(); // Free the unique_ptr - this->flac_decoder_ = nullptr; - } - - if (this->media_file_type_ == media_player::MediaFileType::MP3) { - MP3FreeDecoder(this->mp3_decoder_); - } - - if (this->wav_decoder_ != nullptr) { - this->wav_decoder_.reset(); // Free the unique_ptr - this->wav_decoder_ = nullptr; - } -} - -esp_err_t AudioDecoder::start(media_player::MediaFileType media_file_type) { - esp_err_t err = this->allocate_buffers_(); - - if (err != ESP_OK) { - return err; - } - - this->media_file_type_ = media_file_type; - - this->input_buffer_current_ = this->input_buffer_; - this->input_buffer_length_ = 0; - this->output_buffer_current_ = this->output_buffer_; - this->output_buffer_length_ = 0; - - this->potentially_failed_count_ = 0; - this->end_of_file_ = false; - - switch (this->media_file_type_) { - case media_player::MediaFileType::FLAC: - this->flac_decoder_ = make_unique(this->input_buffer_); - break; - case media_player::MediaFileType::MP3: - this->mp3_decoder_ = MP3InitDecoder(); - break; - case media_player::MediaFileType::WAV: - this->wav_decoder_ = make_unique(&this->input_buffer_current_); - this->wav_decoder_->reset(); - break; - case media_player::MediaFileType::NONE: - return ESP_ERR_NOT_SUPPORTED; - break; - } - - return ESP_OK; -} - -AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { - if (stop_gracefully) { - if (this->output_buffer_length_ == 0) { - // If the file decoder believes it the end of file - if (this->end_of_file_) { - return AudioDecoderState::FINISHED; - } - // If all the internal buffers are empty, the decoding is done - if ((this->input_ring_buffer_->available() == 0) && (this->input_buffer_length_ == 0)) { - return AudioDecoderState::FINISHED; - } - } - } - - if (this->potentially_failed_count_ > 10) { - return AudioDecoderState::FAILED; - } - - FileDecoderState state = FileDecoderState::MORE_TO_PROCESS; - - while (state == FileDecoderState::MORE_TO_PROCESS) { - if (this->output_buffer_length_ > 0) { - // Have decoded data, write it to the output ring buffer - - size_t bytes_to_write = this->output_buffer_length_; - - if (bytes_to_write > 0) { - size_t bytes_written = this->output_ring_buffer_->write_without_replacement( - (void *) this->output_buffer_current_, bytes_to_write, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - - this->output_buffer_length_ -= bytes_written; - this->output_buffer_current_ += bytes_written; - } - - if (this->output_buffer_length_ > 0) { - // Output buffer still has decoded audio to write - return AudioDecoderState::DECODING; - } - } else { - // Decode more data - - // Shift unread data in input buffer to start - if (this->input_buffer_length_ > 0) { - memmove(this->input_buffer_, this->input_buffer_current_, this->input_buffer_length_); - } - this->input_buffer_current_ = this->input_buffer_; - - // read in new ring buffer data to fill the remaining input buffer - size_t bytes_read = 0; - - size_t bytes_to_read = this->internal_buffer_size_ - this->input_buffer_length_; - - if (bytes_to_read > 0) { - uint8_t *new_audio_data = this->input_buffer_ + this->input_buffer_length_; - bytes_read = this->input_ring_buffer_->read((void *) new_audio_data, bytes_to_read, - pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - - this->input_buffer_length_ += bytes_read; - } - - if ((this->input_buffer_length_ == 0) || ((this->potentially_failed_count_ > 0) && (bytes_read == 0))) { - if ((this->input_buffer_length_ && stop_gracefully) || bytes_to_read == 0) { - // data in buffer won't change, don't try again - state = FileDecoderState::FAILED; - } else { - state = FileDecoderState::IDLE; - } - } else { - switch (this->media_file_type_) { - case media_player::MediaFileType::FLAC: - state = this->decode_flac_(); - break; - case media_player::MediaFileType::MP3: - state = this->decode_mp3_(); - break; - case media_player::MediaFileType::WAV: - state = this->decode_wav_(); - break; - case media_player::MediaFileType::NONE: - state = FileDecoderState::IDLE; - break; - } - } - } - if (state == FileDecoderState::POTENTIALLY_FAILED) { - ++this->potentially_failed_count_; - } else if (state == FileDecoderState::END_OF_FILE) { - this->end_of_file_ = true; - } else if (state == FileDecoderState::FAILED) { - return AudioDecoderState::FAILED; - } else if (state == FileDecoderState::MORE_TO_PROCESS) { - this->potentially_failed_count_ = 0; - } - } - return AudioDecoderState::DECODING; -} - -esp_err_t AudioDecoder::allocate_buffers_() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - - if (this->input_buffer_ == nullptr) - this->input_buffer_ = allocator.allocate(this->internal_buffer_size_); - - if (this->output_buffer_ == nullptr) - this->output_buffer_ = allocator.allocate(this->internal_buffer_size_); - - if ((this->input_buffer_ == nullptr) || (this->output_buffer_ == nullptr)) { - return ESP_ERR_NO_MEM; - } - - return ESP_OK; -} - -FileDecoderState AudioDecoder::decode_flac_() { - if (!this->audio_stream_info_.has_value()) { - // Header hasn't been read - auto result = this->flac_decoder_->read_header(this->input_buffer_length_); - - if (result == flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { - return FileDecoderState::POTENTIALLY_FAILED; - } - - if (result != flac::FLAC_DECODER_SUCCESS) { - // Couldn't read FLAC header - return FileDecoderState::FAILED; - } - - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_buffer_current_ += bytes_consumed; - this->input_buffer_length_ = this->flac_decoder_->get_bytes_left(); - - size_t flac_decoder_output_buffer_min_size = flac_decoder_->get_output_buffer_size(); - if (this->internal_buffer_size_ < flac_decoder_output_buffer_min_size * sizeof(int16_t)) { - // Output buffer is not big enough - return FileDecoderState::FAILED; - } - - audio::AudioStreamInfo audio_stream_info; - audio_stream_info.channels = this->flac_decoder_->get_num_channels(); - audio_stream_info.sample_rate = this->flac_decoder_->get_sample_rate(); - audio_stream_info.bits_per_sample = this->flac_decoder_->get_sample_depth(); - - this->audio_stream_info_ = audio_stream_info; - - return FileDecoderState::MORE_TO_PROCESS; - } - - uint32_t output_samples = 0; - auto result = - this->flac_decoder_->decode_frame(this->input_buffer_length_, (int16_t *) this->output_buffer_, &output_samples); - - if (result == flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { - // Not an issue, just needs more data that we'll get next time. - return FileDecoderState::POTENTIALLY_FAILED; - } else if (result > flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { - // Corrupted frame, don't retry with current buffer content, wait for new sync - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_buffer_current_ += bytes_consumed; - this->input_buffer_length_ = this->flac_decoder_->get_bytes_left(); - - return FileDecoderState::POTENTIALLY_FAILED; - } - - // We have successfully decoded some input data and have new output data - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_buffer_current_ += bytes_consumed; - this->input_buffer_length_ = this->flac_decoder_->get_bytes_left(); - - this->output_buffer_current_ = this->output_buffer_; - this->output_buffer_length_ = output_samples * sizeof(int16_t); - - if (result == flac::FLAC_DECODER_NO_MORE_FRAMES) { - return FileDecoderState::END_OF_FILE; - } - - return FileDecoderState::IDLE; -} - -FileDecoderState AudioDecoder::decode_mp3_() { - // Look for the next sync word - int32_t offset = MP3FindSyncWord(this->input_buffer_current_, this->input_buffer_length_); - if (offset < 0) { - // We may recover if we have more data - return FileDecoderState::POTENTIALLY_FAILED; - } - - // Advance read pointer - this->input_buffer_current_ += offset; - this->input_buffer_length_ -= offset; - - int err = MP3Decode(this->mp3_decoder_, &this->input_buffer_current_, (int *) &this->input_buffer_length_, - (int16_t *) this->output_buffer_, 0); - if (err) { - switch (err) { - case ERR_MP3_MAINDATA_UNDERFLOW: - // Not a problem. Next call to decode will provide more data. - return FileDecoderState::POTENTIALLY_FAILED; - break; - default: - return FileDecoderState::FAILED; - break; - } - } else { - MP3FrameInfo mp3_frame_info; - MP3GetLastFrameInfo(this->mp3_decoder_, &mp3_frame_info); - if (mp3_frame_info.outputSamps > 0) { - int bytes_per_sample = (mp3_frame_info.bitsPerSample / 8); - this->output_buffer_length_ = mp3_frame_info.outputSamps * bytes_per_sample; - this->output_buffer_current_ = this->output_buffer_; - - audio::AudioStreamInfo stream_info; - stream_info.channels = mp3_frame_info.nChans; - stream_info.sample_rate = mp3_frame_info.samprate; - stream_info.bits_per_sample = mp3_frame_info.bitsPerSample; - this->audio_stream_info_ = stream_info; - } - } - - return FileDecoderState::MORE_TO_PROCESS; -} - -FileDecoderState AudioDecoder::decode_wav_() { - if (!this->audio_stream_info_.has_value() && (this->input_buffer_length_ > 44)) { - // Header hasn't been processed - - size_t original_buffer_length = this->input_buffer_length_; - - size_t wav_bytes_to_skip = this->wav_decoder_->bytes_to_skip(); - size_t wav_bytes_to_read = this->wav_decoder_->bytes_needed(); - - bool header_finished = false; - while (!header_finished) { - if (wav_bytes_to_skip > 0) { - // Adjust pointer to skip the appropriate bytes - this->input_buffer_current_ += wav_bytes_to_skip; - this->input_buffer_length_ -= wav_bytes_to_skip; - wav_bytes_to_skip = 0; - } else if (wav_bytes_to_read > 0) { - wav_decoder::WAVDecoderResult result = this->wav_decoder_->next(); - this->input_buffer_current_ += wav_bytes_to_read; - this->input_buffer_length_ -= wav_bytes_to_read; - - if (result == wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { - // Header parsing is complete - - // Assume PCM - audio::AudioStreamInfo audio_stream_info; - audio_stream_info.channels = this->wav_decoder_->num_channels(); - audio_stream_info.sample_rate = this->wav_decoder_->sample_rate(); - audio_stream_info.bits_per_sample = this->wav_decoder_->bits_per_sample(); - this->audio_stream_info_ = audio_stream_info; - this->wav_bytes_left_ = this->wav_decoder_->chunk_bytes_left(); - header_finished = true; - } else if (result == wav_decoder::WAV_DECODER_SUCCESS_NEXT) { - // Continue parsing header - wav_bytes_to_skip = this->wav_decoder_->bytes_to_skip(); - wav_bytes_to_read = this->wav_decoder_->bytes_needed(); - } else { - // Unexpected error parsing the wav header - return FileDecoderState::FAILED; - } - } else { - // Something unexpected has happened - // Reset state and hope we have enough info next time - this->input_buffer_length_ = original_buffer_length; - this->input_buffer_current_ = this->input_buffer_; - return FileDecoderState::POTENTIALLY_FAILED; - } - } - } - - if (this->wav_bytes_left_ > 0) { - size_t bytes_to_write = std::min(this->wav_bytes_left_, this->input_buffer_length_); - bytes_to_write = std::min(bytes_to_write, this->internal_buffer_size_); - if (bytes_to_write > 0) { - std::memcpy(this->output_buffer_, this->input_buffer_current_, bytes_to_write); - this->input_buffer_current_ += bytes_to_write; - this->input_buffer_length_ -= bytes_to_write; - this->output_buffer_current_ = this->output_buffer_; - this->output_buffer_length_ = bytes_to_write; - this->wav_bytes_left_ -= bytes_to_write; - } - - return FileDecoderState::IDLE; - } - - return FileDecoderState::END_OF_FILE; -} - -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/audio_decoder.h b/esphome/components/nabu/audio_decoder.h deleted file mode 100644 index c60a9722..00000000 --- a/esphome/components/nabu/audio_decoder.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#ifdef USE_ESP_IDF - -#include -#include -#include - -#include "esphome/components/audio/audio.h" -#include "esphome/components/media_player/media_player.h" - -#include "esphome/core/ring_buffer.h" - -namespace esphome { -namespace nabu { - -enum class AudioDecoderState : uint8_t { - INITIALIZED = 0, - DECODING, - FINISHED, - FAILED, -}; - -// Only used within the AudioDecoder class; conveys the state of the particular file type decoder -enum class FileDecoderState : uint8_t { - MORE_TO_PROCESS, - IDLE, - POTENTIALLY_FAILED, - FAILED, - END_OF_FILE, -}; - -class AudioDecoder { - public: - AudioDecoder(esphome::RingBuffer *input_ring_buffer, esphome::RingBuffer *output_ring_buffer, - size_t internal_buffer_size); - ~AudioDecoder(); - - esp_err_t start(media_player::MediaFileType media_file_type); - - AudioDecoderState decode(bool stop_gracefully); - - const optional &get_audio_stream_info() const { return this->audio_stream_info_; } - - protected: - esp_err_t allocate_buffers_(); - - FileDecoderState decode_flac_(); - FileDecoderState decode_mp3_(); - FileDecoderState decode_wav_(); - - esphome::RingBuffer *input_ring_buffer_; - esphome::RingBuffer *output_ring_buffer_; - size_t internal_buffer_size_; - - uint8_t *input_buffer_{nullptr}; - uint8_t *input_buffer_current_{nullptr}; - size_t input_buffer_length_; - - uint8_t *output_buffer_{nullptr}; - uint8_t *output_buffer_current_{nullptr}; - size_t output_buffer_length_; - - std::unique_ptr flac_decoder_; - - HMP3Decoder mp3_decoder_; - - std::unique_ptr wav_decoder_; - size_t wav_bytes_left_; - - media_player::MediaFileType media_file_type_{media_player::MediaFileType::NONE}; - optional audio_stream_info_{}; - - size_t potentially_failed_count_{0}; - bool end_of_file_{false}; -}; -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/audio_mixer.cpp b/esphome/components/nabu/audio_mixer.cpp deleted file mode 100644 index 61dcd420..00000000 --- a/esphome/components/nabu/audio_mixer.cpp +++ /dev/null @@ -1,382 +0,0 @@ -#ifdef USE_ESP_IDF - -#include "audio_mixer.h" - -#include - -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace nabu { - -static const size_t INPUT_RING_BUFFER_SAMPLES = 24000; -static const size_t OUTPUT_BUFFER_SAMPLES = 8192; -static const size_t QUEUE_COUNT = 20; - -static const uint32_t TASK_STACK_SIZE = 3072; -static const size_t TASK_DELAY_MS = 25; - -static const int16_t MAX_AUDIO_SAMPLE_VALUE = INT16_MAX; -static const int16_t MIN_AUDIO_SAMPLE_VALUE = INT16_MIN; - -esp_err_t AudioMixer::start(speaker::Speaker *speaker, const std::string &task_name, UBaseType_t priority) { - esp_err_t err = this->allocate_buffers_(); - - if (err != ESP_OK) { - return err; - } - - if (this->task_handle_ == nullptr) { - this->task_handle_ = xTaskCreateStatic(AudioMixer::audio_mixer_task_, task_name.c_str(), TASK_STACK_SIZE, - (void *) this, priority, this->stack_buffer_, &this->task_stack_); - } - - if (this->task_handle_ == nullptr) { - return ESP_FAIL; - } - - this->speaker_ = speaker; - - return ESP_OK; -} - -void AudioMixer::stop() { - vTaskDelete(this->task_handle_); - this->task_handle_ = nullptr; - - xQueueReset(this->event_queue_); - xQueueReset(this->command_queue_); -} - -void AudioMixer::suspend_task() { - if (this->task_handle_ != nullptr) { - vTaskSuspend(this->task_handle_); - } -} - -void AudioMixer::resume_task() { - if (this->task_handle_ != nullptr) { - vTaskResume(task_handle_); - } -} - -void AudioMixer::audio_mixer_task_(void *params) { - AudioMixer *this_mixer = (AudioMixer *) params; - - TaskEvent event; - CommandEvent command_event; - - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - int16_t *media_buffer = allocator.allocate(OUTPUT_BUFFER_SAMPLES); - int16_t *announcement_buffer = allocator.allocate(OUTPUT_BUFFER_SAMPLES); - int16_t *combination_buffer = allocator.allocate(OUTPUT_BUFFER_SAMPLES); - - int16_t *combination_buffer_current = combination_buffer; - size_t combination_buffer_length = 0; - - if ((media_buffer == nullptr) || (announcement_buffer == nullptr)) { - event.type = EventType::WARNING; - event.err = ESP_ERR_NO_MEM; - xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); - - event.type = EventType::STOPPED; - event.err = ESP_OK; - xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); - - while (true) { - delay(TASK_DELAY_MS); - } - - return; - } - - // Handles media stream pausing - bool transfer_media = true; - - // Parameters to control the ducking dB reduction and its transitions - // There is a built in negative sign; e.g., reducing by 5 dB is changing the gain by -5 dB - int8_t target_ducking_db_reduction = 0; - int8_t current_ducking_db_reduction = 0; - - // Each step represents a change in 1 dB. Positive 1 means the dB reduction is increasing. Negative 1 means the dB - // reduction is decreasing. - int8_t db_change_per_ducking_step = 1; - - size_t ducking_transition_samples_remaining = 0; - size_t samples_per_ducking_step = 0; - - event.type = EventType::STARTED; - xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); - - while (true) { - if (xQueueReceive(this_mixer->command_queue_, &command_event, 0) == pdTRUE) { - if (command_event.command == CommandEventType::STOP) { - break; - } else if (command_event.command == CommandEventType::DUCK) { - if (target_ducking_db_reduction != command_event.decibel_reduction) { - current_ducking_db_reduction = target_ducking_db_reduction; - - target_ducking_db_reduction = command_event.decibel_reduction; - - uint8_t total_ducking_steps = 0; - if (target_ducking_db_reduction > current_ducking_db_reduction) { - // The dB reduction level is increasing (which results in quieter audio) - total_ducking_steps = target_ducking_db_reduction - current_ducking_db_reduction - 1; - db_change_per_ducking_step = 1; - } else { - // The dB reduction level is decreasing (which results in louder audio) - total_ducking_steps = current_ducking_db_reduction - target_ducking_db_reduction - 1; - db_change_per_ducking_step = -1; - } - if (total_ducking_steps > 0) { - ducking_transition_samples_remaining = command_event.transition_samples; - - samples_per_ducking_step = ducking_transition_samples_remaining / total_ducking_steps; - } else { - ducking_transition_samples_remaining = 0; - } - } - } else if (command_event.command == CommandEventType::PAUSE_MEDIA) { - transfer_media = false; - } else if (command_event.command == CommandEventType::RESUME_MEDIA) { - transfer_media = true; - } else if (command_event.command == CommandEventType::CLEAR_MEDIA) { - this_mixer->media_ring_buffer_->reset(); - } else if (command_event.command == CommandEventType::CLEAR_ANNOUNCEMENT) { - this_mixer->announcement_ring_buffer_->reset(); - } - } - - if (combination_buffer_length > 0) { - size_t output_bytes_written = this_mixer->speaker_->play((uint8_t *) combination_buffer, - combination_buffer_length, pdMS_TO_TICKS(TASK_DELAY_MS)); - combination_buffer_length -= output_bytes_written; - if ((combination_buffer_length > 0) && (output_bytes_written > 0)) { - memmove(combination_buffer, combination_buffer + output_bytes_written / sizeof(int16_t), - combination_buffer_length); - } - } else { - size_t media_available = this_mixer->media_ring_buffer_->available(); - size_t announcement_available = this_mixer->announcement_ring_buffer_->available(); - - if (media_available * transfer_media + announcement_available > 0) { - size_t bytes_to_read = OUTPUT_BUFFER_SAMPLES * sizeof(int16_t); - - if (media_available * transfer_media > 0) { - bytes_to_read = std::min(bytes_to_read, media_available); - } - - if (announcement_available > 0) { - bytes_to_read = std::min(bytes_to_read, announcement_available); - } - - if (bytes_to_read > 0) { - size_t media_bytes_read = 0; - if (media_available * transfer_media > 0) { - media_bytes_read = this_mixer->media_ring_buffer_->read((void *) media_buffer, bytes_to_read, 0); - if (media_bytes_read > 0) { - size_t samples_read = media_bytes_read / sizeof(int16_t); - if (ducking_transition_samples_remaining > 0) { - // Ducking level is still transitioning - - size_t samples_left = ducking_transition_samples_remaining; - - // There may be more than one step worth of samples to duck in the buffers, so manage positions - int16_t *current_media_buffer = media_buffer; - - size_t samples_left_in_step = samples_left % samples_per_ducking_step; - if (samples_left_in_step == 0) { - // Start of a new ducking step - - current_ducking_db_reduction += db_change_per_ducking_step; - samples_left_in_step = samples_per_ducking_step; - } - size_t samples_left_to_duck = std::min(samples_left_in_step, samples_read); - - size_t total_samples_ducked = 0; - - while (samples_left_to_duck > 0) { - // Ensure we only point to valid index in the Q15 scaling factor table - uint8_t safe_db_reduction_index = - clamp(current_ducking_db_reduction, 0, decibel_reduction_table.size() - 1); - - int16_t q15_scale_factor = decibel_reduction_table[safe_db_reduction_index]; - this_mixer->scale_audio_samples_(current_media_buffer, current_media_buffer, q15_scale_factor, - samples_left_to_duck); - - current_media_buffer += samples_left_to_duck; - - samples_read -= samples_left_to_duck; - samples_left -= samples_left_to_duck; - - total_samples_ducked += samples_left_to_duck; - - samples_left_in_step = samples_left % samples_per_ducking_step; - if (samples_left_in_step == 0) { - // Start of a new step - - current_ducking_db_reduction += db_change_per_ducking_step; - samples_left_in_step = samples_per_ducking_step; - } - samples_left_to_duck = std::min(samples_left_in_step, samples_read); - } - } else if (target_ducking_db_reduction > 0) { - // We still need to apply a ducking scaling, but we are done transitioning - - uint8_t safe_db_reduction_index = - clamp(target_ducking_db_reduction, 0, decibel_reduction_table.size() - 1); - - int16_t q15_scale_factor = decibel_reduction_table[safe_db_reduction_index]; - this_mixer->scale_audio_samples_(media_buffer, media_buffer, q15_scale_factor, samples_read); - } - } - } - - size_t announcement_bytes_read = 0; - if (announcement_available > 0) { - announcement_bytes_read = - this_mixer->announcement_ring_buffer_->read((void *) announcement_buffer, bytes_to_read, 0); - } - - if ((media_bytes_read > 0) && (announcement_bytes_read > 0)) { - // We have both a media and an announcement stream, so mix them together - - size_t samples_read = bytes_to_read / sizeof(int16_t); - - this_mixer->mix_audio_samples_without_clipping_(media_buffer, announcement_buffer, combination_buffer, - samples_read); - - combination_buffer_length = samples_read * sizeof(int16_t); - } else if (media_bytes_read > 0) { - memcpy(combination_buffer, media_buffer, media_bytes_read); - combination_buffer_length = media_bytes_read; - } else if (announcement_bytes_read > 0) { - memcpy(combination_buffer, announcement_buffer, announcement_bytes_read); - combination_buffer_length = announcement_bytes_read; - } - - size_t samples_written = combination_buffer_length / sizeof(int16_t); - if (ducking_transition_samples_remaining > 0) { - ducking_transition_samples_remaining -= std::min(samples_written, ducking_transition_samples_remaining); - } - } - } else { - // No audio data available in either buffer - - delay(TASK_DELAY_MS); - } - } - } - - event.type = EventType::STOPPING; - xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); - - this_mixer->reset_ring_buffers_(); - allocator.deallocate(media_buffer, OUTPUT_BUFFER_SAMPLES); - allocator.deallocate(announcement_buffer, OUTPUT_BUFFER_SAMPLES); - allocator.deallocate(combination_buffer, OUTPUT_BUFFER_SAMPLES); - - event.type = EventType::STOPPED; - xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); - - while (true) { - delay(TASK_DELAY_MS); - } -} - -esp_err_t AudioMixer::allocate_buffers_() { - if (this->media_ring_buffer_ == nullptr) - this->media_ring_buffer_ = RingBuffer::create(INPUT_RING_BUFFER_SAMPLES * sizeof(int16_t)); - - if (this->announcement_ring_buffer_ == nullptr) - this->announcement_ring_buffer_ = RingBuffer::create(INPUT_RING_BUFFER_SAMPLES * sizeof(int16_t)); - - if ((this->announcement_ring_buffer_ == nullptr) || (this->media_ring_buffer_ == nullptr)) { - return ESP_ERR_NO_MEM; - } - - if (this->stack_buffer_ == nullptr) - this->stack_buffer_ = (StackType_t *) malloc(TASK_STACK_SIZE); - - if (this->stack_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->event_queue_ == nullptr) - this->event_queue_ = xQueueCreate(QUEUE_COUNT, sizeof(TaskEvent)); - - if (this->command_queue_ == nullptr) - this->command_queue_ = xQueueCreate(QUEUE_COUNT, sizeof(CommandEvent)); - - if ((this->event_queue_ == nullptr) || (this->command_queue_ == nullptr)) { - return ESP_ERR_NO_MEM; - } - - return ESP_OK; -} - -void AudioMixer::reset_ring_buffers_() { - this->media_ring_buffer_->reset(); - this->announcement_ring_buffer_->reset(); -} - -void AudioMixer::mix_audio_samples_without_clipping_(int16_t *media_buffer, int16_t *announcement_buffer, - int16_t *combination_buffer, size_t samples_to_mix) { - // We first test adding the two clips samples together and check for any clipping - // We want the announcement volume to be consistent, regardless if media is playing or not - // If there is clipping, we determine what factor we need to multiply that media sample by to avoid it - // We take the smallest factor necessary for all the samples so the media volume is consistent on this batch - // of samples - // Note: This may not be the best approach. Adding 2 audio samples together makes both sound louder, even if - // we are not clipping. As a result, the mixed announcement will sound louder (by around 3dB if the audio - // streams are independent?) than if it were by itself. - - int16_t q15_scaling_factor = MAX_AUDIO_SAMPLE_VALUE; - - for (int i = 0; i < samples_to_mix; ++i) { - int32_t added_sample = static_cast(media_buffer[i]) + static_cast(announcement_buffer[i]); - - if ((added_sample > MAX_AUDIO_SAMPLE_VALUE) || (added_sample < MIN_AUDIO_SAMPLE_VALUE)) { - // The largest magnitude the media sample can be to avoid clipping (converted to Q30 fixed point) - int32_t q30_media_sample_safe_max = - static_cast(std::abs(MIN_AUDIO_SAMPLE_VALUE) - std::abs(announcement_buffer[i])) << 15; - - // Actual media sample value (Q15 number stored in an int32 for future division) - int32_t media_sample_value = abs(media_buffer[i]); - - // Calculation to perform the Q15 division for media_sample_safe_max/media_sample_value - // Reference: https://sestevenson.wordpress.com/2010/09/20/fixed-point-division-2/ (accessed August 15, - // 2024) - int16_t necessary_q15_factor = static_cast(q30_media_sample_safe_max / media_sample_value); - // Take the minimum scaling factor (the smaller the factor, the more it needs to be scaled down) - q15_scaling_factor = std::min(necessary_q15_factor, q15_scaling_factor); - } else { - // Store the combined samples in the combination buffer. If we do not need to scale, then the samples are already - // mixed. - combination_buffer[i] = added_sample; - } - } - - if (q15_scaling_factor < MAX_AUDIO_SAMPLE_VALUE) { - // Need to scale to avoid clipping - - this->scale_audio_samples_(media_buffer, media_buffer, q15_scaling_factor, samples_to_mix); - - // Mix both stream by adding them together with no bitshift - // The dsps_add functions have the following inputs: - // (buffer 1, buffer 2, output buffer, number of samples, buffer 1 step, buffer 2 step, output, buffer step, - // bitshift) - dsps_add_s16(media_buffer, announcement_buffer, combination_buffer, samples_to_mix, 1, 1, 1, 0); - } -} - -void AudioMixer::scale_audio_samples_(int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, - size_t samples_to_scale) { - // Scale the audio samples and store them in the output buffer - dsps_mulc_s16(audio_samples, output_buffer, samples_to_scale, scale_factor, 1, 1); -} - -} // namespace nabu -} // namespace esphome -#endif diff --git a/esphome/components/nabu/audio_mixer.h b/esphome/components/nabu/audio_mixer.h deleted file mode 100644 index 7e4cef75..00000000 --- a/esphome/components/nabu/audio_mixer.h +++ /dev/null @@ -1,159 +0,0 @@ -#pragma once - -#ifdef USE_ESP_IDF - -#include "esphome/components/media_player/media_player.h" -#include "esphome/components/speaker/speaker.h" - -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" -#include "esphome/core/ring_buffer.h" - -#include -#include - -namespace esphome { -namespace nabu { - -// Mixes two incoming audio streams together -// - The media stream intended for music playback -// - Able to duck (made quieter) -// - Able to pause -// - The announcement stream is intended for TTS reponses or various beeps/sound effects -// - Unable to duck -// - Unable to pause -// - Each stream has a corresponding input ring buffer. Retrieved via the `get_media_ring_buffer` and -// `get_announcement_ring_buffer` functions -// - The mixed audio is sent to the configured speaker component. -// - The mixer runs as a FreeRTOS task -// - The task reports its state using the TaskEvent queue. Regularly call the `read_event` function to obtain the -// current state -// - Commands are sent to the task using a the CommandEvent queue. Use the `send_command` function to do so. -// - Use the `start` function to initiate. The `stop` function deletes the task, but be sure to send a STOP command -// first to avoid memory leaks. - -enum class EventType : uint8_t { - STARTING = 0, - STARTED, - RUNNING, - IDLE, - STOPPING, - STOPPED, - WARNING = 255, -}; - -// Used for reporting the state of the mixer task -struct TaskEvent { - EventType type; - esp_err_t err; -}; - -enum class CommandEventType : uint8_t { - STOP, // Stop mixing to prepare for stopping the mixing task - DUCK, // Duck the media audio - PAUSE_MEDIA, // Pauses the media stream - RESUME_MEDIA, // Resumes the media stream - CLEAR_MEDIA, // Resets the media ring buffer - CLEAR_ANNOUNCEMENT, // Resets the announcement ring buffer -}; - -// Used to send commands to the mixer task -struct CommandEvent { - CommandEventType command; - uint8_t decibel_reduction; - size_t transition_samples = 0; -}; - -// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB -// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) -// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15) -static const std::vector decibel_reduction_table = { - 32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183, - 4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731, - 651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103}; - -class AudioMixer { - public: - /// @brief Sends a CommandEvent to the command queue - /// @param command Pointer to CommandEvent object to be sent - /// @param ticks_to_wait The number of FreeRTOS ticks to wait for an event to appear on the queue. Defaults to 0. - /// @return pdTRUE if successful, pdFALSE otherwises - BaseType_t send_command(CommandEvent *command, TickType_t ticks_to_wait = portMAX_DELAY) { - return xQueueSend(this->command_queue_, command, ticks_to_wait); - } - - /// @brief Reads a TaskEvent from the event queue indicating its current status - /// @param event Pointer to TaskEvent object to store the event in - /// @param ticks_to_wait The number of FreeRTOS ticks to wait for an event to appear on the queue. Defaults to 0. - /// @return pdTRUE if successful, pdFALSE otherwise - BaseType_t read_event(TaskEvent *event, TickType_t ticks_to_wait = 0) { - return xQueueReceive(this->event_queue_, event, ticks_to_wait); - } - - /// @brief Starts the mixer task - /// @param speaker Pointer to Speaker component - /// @param task_name FreeRTOS task name - /// @param priority FreeRTOS task priority. Defaults to 1 - /// @return ESP_OK if successful, and error otherwise - esp_err_t start(speaker::Speaker *speaker, const std::string &task_name, UBaseType_t priority = 1); - - /// @brief Stops the mixer task and clears the queues - void stop(); - - /// @brief Retrieves the media stream's ring buffer pointer - /// @return pointer to media ring buffer - RingBuffer *get_media_ring_buffer() { return this->media_ring_buffer_.get(); } - - /// @brief Retrieves the announcement stream's ring buffer pointer - /// @return pointer to announcement ring buffer - RingBuffer *get_announcement_ring_buffer() { return this->announcement_ring_buffer_.get(); } - - /// @brief Suspends the mixer task - void suspend_task(); - /// @brief Resumes the mixer task - void resume_task(); - - protected: - /// @brief Allocates the ring buffers, task stack, and queues - /// @return ESP_OK if successful or an error otherwise - esp_err_t allocate_buffers_(); - - /// @brief Resets the media and anouncement ring buffers - void reset_ring_buffers_(); - - /// @brief Mixes the media and announcement samples. If the resulting audio clips, the media samples are first scaled. - /// @param media_buffer buffer for media samples - /// @param announcement_buffer buffer for announcement samples - /// @param combination_buffer buffer for the mixed samples - /// @param samples_to_mix number of samples in the media and annoucnement buffers to mix together - void mix_audio_samples_without_clipping_(int16_t *media_buffer, int16_t *announcement_buffer, - int16_t *combination_buffer, size_t samples_to_mix); - - /// @brief Scales audio samples. Scales in place when audio_samples == output_buffer. - /// @param audio_samples PCM int16 audio samples - /// @param output_buffer Buffer to store the scaled samples - /// @param scale_factor Q15 fixed point scaling factor - /// @param samples_to_scale Number of samples to scale - void scale_audio_samples_(int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, - size_t samples_to_scale); - - static void audio_mixer_task_(void *params); - TaskHandle_t task_handle_{nullptr}; - StaticTask_t task_stack_; - StackType_t *stack_buffer_{nullptr}; - - // Reports events from the mixer task - QueueHandle_t event_queue_; - - // Stores commands to send the mixer task - QueueHandle_t command_queue_; - - speaker::Speaker *speaker_{nullptr}; - - std::unique_ptr media_ring_buffer_; - std::unique_ptr announcement_ring_buffer_; -}; -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/audio_pipeline.cpp b/esphome/components/nabu/audio_pipeline.cpp deleted file mode 100644 index ad665b32..00000000 --- a/esphome/components/nabu/audio_pipeline.cpp +++ /dev/null @@ -1,541 +0,0 @@ -#ifdef USE_ESP_IDF - -#include "audio_pipeline.h" - -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace nabu { - -static const size_t FILE_BUFFER_SIZE = 32 * 1024; -static const size_t FILE_RING_BUFFER_SIZE = 64 * 1024; -static const size_t BUFFER_SIZE_SAMPLES = 32768; -static const size_t BUFFER_SIZE_BYTES = BUFFER_SIZE_SAMPLES * sizeof(int16_t); - -static const uint32_t READER_TASK_STACK_SIZE = 5 * 1024; -static const uint32_t DECODER_TASK_STACK_SIZE = 3 * 1024; -static const uint32_t RESAMPLER_TASK_STACK_SIZE = 3 * 1024; - -static const size_t INFO_ERROR_QUEUE_COUNT = 5; - -static const char *const TAG = "nabu_media_player.pipeline"; - -enum EventGroupBits : uint32_t { - // The stop() function clears all unfinished bits - // MESSAGE_* bits are only set by their respective tasks - - // Stops all activity in the pipeline elements and set by stop() or by each task - PIPELINE_COMMAND_STOP = (1 << 0), - - // Read audio from an HTTP source; cleared by reader task and set by start(uri,...) - READER_COMMAND_INIT_HTTP = (1 << 4), - // Read audio from an audio file from the flash; cleared by reader task and set by start(media_file,...) - READER_COMMAND_INIT_FILE = (1 << 5), - - // Audio file type is read after checking it is supported; cleared by decoder task - READER_MESSAGE_LOADED_MEDIA_TYPE = (1 << 6), - // Reader is done (either through a failure or just end of the stream); cleared by reader task - READER_MESSAGE_FINISHED = (1 << 7), - // Error reading the file; cleared by get_state() - READER_MESSAGE_ERROR = (1 << 8), - - // Decoder has determined the stream information; cleared by resampler - DECODER_MESSAGE_LOADED_STREAM_INFO = (1 << 11), - // Decoder is done (either through a faiilure or the end of the stream); cleared by decoder task - DECODER_MESSAGE_FINISHED = (1 << 12), - // Error decoding the file; cleared by get_state() by decoder task - DECODER_MESSAGE_ERROR = (1 << 13), - - // Resampler is done (either through a failure or the end of the stream); cleared by resampler task - RESAMPLER_MESSAGE_FINISHED = (1 << 17), - // Error resampling the file; cleared by get_state() - RESAMPLER_MESSAGE_ERROR = (1 << 18), - - // Cleared by respective tasks - FINISHED_BITS = READER_MESSAGE_FINISHED | DECODER_MESSAGE_FINISHED | RESAMPLER_MESSAGE_FINISHED, - UNFINISHED_BITS = ~(FINISHED_BITS | 0xff000000), // Only 24 bits are valid for the event group, so make sure first 8 - // bits of uint32 are not set; cleared by stop() -}; - -AudioPipeline::AudioPipeline(AudioMixer *mixer, AudioPipelineType pipeline_type) { - this->mixer_ = mixer; - this->pipeline_type_ = pipeline_type; -} - -esp_err_t AudioPipeline::start(const std::string &uri, uint32_t target_sample_rate, const std::string &task_name, - UBaseType_t priority) { - esp_err_t err = this->common_start_(target_sample_rate, task_name, priority); - - if (err == ESP_OK) { - this->current_uri_ = uri; - xEventGroupSetBits(this->event_group_, READER_COMMAND_INIT_HTTP); - } - - return err; -} - -esp_err_t AudioPipeline::start(media_player::MediaFile *media_file, uint32_t target_sample_rate, - const std::string &task_name, UBaseType_t priority) { - esp_err_t err = this->common_start_(target_sample_rate, task_name, priority); - - if (err == ESP_OK) { - this->current_media_file_ = media_file; - xEventGroupSetBits(this->event_group_, READER_COMMAND_INIT_FILE); - } - - return err; -} - -esp_err_t AudioPipeline::allocate_buffers_() { - if (this->raw_file_ring_buffer_ == nullptr) - this->raw_file_ring_buffer_ = RingBuffer::create(FILE_RING_BUFFER_SIZE); - - if (this->decoded_ring_buffer_ == nullptr) - this->decoded_ring_buffer_ = RingBuffer::create(BUFFER_SIZE_BYTES); - - if ((this->raw_file_ring_buffer_ == nullptr) || (this->decoded_ring_buffer_ == nullptr)) { - return ESP_ERR_NO_MEM; - } - - if (this->read_task_stack_buffer_ == nullptr) - this->read_task_stack_buffer_ = (StackType_t *) malloc(READER_TASK_STACK_SIZE); - - if (this->decode_task_stack_buffer_ == nullptr) - this->decode_task_stack_buffer_ = (StackType_t *) malloc(DECODER_TASK_STACK_SIZE); - - if (this->resample_task_stack_buffer_ == nullptr) - this->resample_task_stack_buffer_ = (StackType_t *) malloc(RESAMPLER_TASK_STACK_SIZE); - - if ((this->read_task_stack_buffer_ == nullptr) || (this->decode_task_stack_buffer_ == nullptr) || - (this->resample_task_stack_buffer_ == nullptr)) { - return ESP_ERR_NO_MEM; - } - - if (this->event_group_ == nullptr) - this->event_group_ = xEventGroupCreate(); - - if (this->event_group_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->info_error_queue_ == nullptr) - this->info_error_queue_ = xQueueCreate(INFO_ERROR_QUEUE_COUNT, sizeof(InfoErrorEvent)); - - if (this->info_error_queue_ == nullptr) - return ESP_ERR_NO_MEM; - - return ESP_OK; -} - -esp_err_t AudioPipeline::common_start_(uint32_t target_sample_rate, const std::string &task_name, - UBaseType_t priority) { - esp_err_t err = this->allocate_buffers_(); - if (err != ESP_OK) { - return err; - } - - if (this->read_task_handle_ == nullptr) { - this->read_task_handle_ = - xTaskCreateStatic(AudioPipeline::read_task_, (task_name + "_read").c_str(), READER_TASK_STACK_SIZE, - (void *) this, priority, this->read_task_stack_buffer_, &this->read_task_stack_); - } - if (this->decode_task_handle_ == nullptr) { - this->decode_task_handle_ = - xTaskCreateStatic(AudioPipeline::decode_task_, (task_name + "_decode").c_str(), DECODER_TASK_STACK_SIZE, - (void *) this, priority, this->decode_task_stack_buffer_, &this->decode_task_stack_); - } - if (this->resample_task_handle_ == nullptr) { - this->resample_task_handle_ = - xTaskCreateStatic(AudioPipeline::resample_task_, (task_name + "_resample").c_str(), RESAMPLER_TASK_STACK_SIZE, - (void *) this, priority, this->resample_task_stack_buffer_, &this->resample_task_stack_); - } - - if ((this->read_task_handle_ == nullptr) || (this->decode_task_handle_ == nullptr) || - (this->resample_task_handle_ == nullptr)) { - return ESP_FAIL; - } - - this->target_sample_rate_ = target_sample_rate; - - return this->stop(); -} - -AudioPipelineState AudioPipeline::get_state() { - InfoErrorEvent event; - if (this->info_error_queue_ != nullptr) { - while (xQueueReceive(this->info_error_queue_, &event, 0)) { - switch (event.source) { - case InfoErrorSource::READER: - if (event.err.has_value()) { - ESP_LOGE(TAG, "Media reader encountered an error: %s", esp_err_to_name(event.err.value())); - } else if (event.file_type.has_value()) { - ESP_LOGD(TAG, "Reading %s file type", - media_player::media_player_file_type_to_string(event.file_type.value())); - } - - break; - case InfoErrorSource::DECODER: - if (event.err.has_value()) { - ESP_LOGE(TAG, "Decoder encountered an error: %s", esp_err_to_name(event.err.value())); - } - - if (event.audio_stream_info.has_value()) { - ESP_LOGD(TAG, "Decoded audio has %d channels, %" PRId32 " Hz sample rate, and %d bits per sample", - event.audio_stream_info.value().channels, event.audio_stream_info.value().sample_rate, - event.audio_stream_info.value().bits_per_sample); - } - - if (event.decoding_err.has_value()) { - switch (event.decoding_err.value()) { - case DecodingError::FAILED_HEADER: - ESP_LOGE(TAG, "Failed to parse the file's header."); - break; - case DecodingError::INCOMPATIBLE_BITS_PER_SAMPLE: - ESP_LOGE(TAG, "Incompatible bits per sample. Only 16 bits per sample is supported"); - break; - case DecodingError::INCOMPATIBLE_CHANNELS: - ESP_LOGE(TAG, "Incompatible number of channels. Only 1 or 2 channel audio is supported."); - break; - } - } - break; - case InfoErrorSource::RESAMPLER: - if (event.err.has_value()) { - ESP_LOGE(TAG, "Resampler encountered an error: %s", esp_err_to_name(event.err.has_value())); - } else if (event.resample_info.has_value()) { - if (event.resample_info.value().resample) { - ESP_LOGD(TAG, "Converting the audio sample rate"); - } - if (event.resample_info.value().mono_to_stereo) { - ESP_LOGD(TAG, "Converting mono channel audio to stereo channel audio"); - } - } - break; - } - } - } - - EventBits_t event_bits = xEventGroupGetBits(this->event_group_); - if (!this->read_task_handle_ && !this->decode_task_handle_ && !this->resample_task_handle_) { - return AudioPipelineState::STOPPED; - } - - if ((event_bits & READER_MESSAGE_ERROR)) { - xEventGroupClearBits(this->event_group_, READER_MESSAGE_ERROR); - return AudioPipelineState::ERROR_READING; - } - - if ((event_bits & DECODER_MESSAGE_ERROR)) { - xEventGroupClearBits(this->event_group_, DECODER_MESSAGE_ERROR); - return AudioPipelineState::ERROR_DECODING; - } - - if ((event_bits & RESAMPLER_MESSAGE_ERROR)) { - xEventGroupClearBits(this->event_group_, RESAMPLER_MESSAGE_ERROR); - return AudioPipelineState::ERROR_RESAMPLING; - } - - if ((event_bits & READER_MESSAGE_FINISHED) && (event_bits & DECODER_MESSAGE_FINISHED) && - (event_bits & RESAMPLER_MESSAGE_FINISHED)) { - return AudioPipelineState::STOPPED; - } - - return AudioPipelineState::PLAYING; -} - -esp_err_t AudioPipeline::stop() { - xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP); - - uint32_t event_group_bits = xEventGroupWaitBits(this->event_group_, - FINISHED_BITS, // Bit message to read - pdFALSE, // Clear the bits on exit - pdTRUE, // Wait for all the bits, - pdMS_TO_TICKS(300)); // Duration to block/wait - - if (!(event_group_bits & READER_MESSAGE_FINISHED)) { - // Reader failed to stop - xEventGroupSetBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR); - } - if (!(event_group_bits & DECODER_MESSAGE_FINISHED)) { - // Ddecoder failed to stop - xEventGroupSetBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR); - } - if (!(event_group_bits & RESAMPLER_MESSAGE_FINISHED)) { - // Resampler failed to stop - xEventGroupSetBits(this->event_group_, EventGroupBits::RESAMPLER_MESSAGE_ERROR); - } - - if ((event_group_bits & FINISHED_BITS) != FINISHED_BITS) { - // Not all bits were set, so it timed out - return ESP_ERR_TIMEOUT; - } - - // Clear the ring buffer in the mixer; avoids playing incorrect audio when starting a new file while paused - CommandEvent command_event; - if (this->pipeline_type_ == AudioPipelineType::MEDIA) { - command_event.command = CommandEventType::CLEAR_MEDIA; - } else { - command_event.command = CommandEventType::CLEAR_ANNOUNCEMENT; - } - this->mixer_->send_command(&command_event); - - xEventGroupClearBits(this->event_group_, UNFINISHED_BITS); - this->reset_ring_buffers(); - - return ESP_OK; -} - -void AudioPipeline::reset_ring_buffers() { - this->raw_file_ring_buffer_->reset(); - this->decoded_ring_buffer_->reset(); -} - -void AudioPipeline::suspend_tasks() { - if (this->read_task_handle_ != nullptr) { - vTaskSuspend(this->read_task_handle_); - } - if (this->decode_task_handle_ != nullptr) { - vTaskSuspend(this->decode_task_handle_); - } - if (this->resample_task_handle_ != nullptr) { - vTaskSuspend(this->resample_task_handle_); - } -} - -void AudioPipeline::resume_tasks() { - if (this->read_task_handle_ != nullptr) { - vTaskResume(this->read_task_handle_); - } - if (this->decode_task_handle_ != nullptr) { - vTaskResume(this->decode_task_handle_); - } - if (this->resample_task_handle_ != nullptr) { - vTaskResume(this->resample_task_handle_); - } -} - -void AudioPipeline::read_task_(void *params) { - AudioPipeline *this_pipeline = (AudioPipeline *) params; - - while (true) { - xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); - - // Wait until the pipeline notifies us the source of the media file - EventBits_t event_bits = - xEventGroupWaitBits(this_pipeline->event_group_, - READER_COMMAND_INIT_FILE | READER_COMMAND_INIT_HTTP, // Bit message to read - pdTRUE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set - - xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); - - { - InfoErrorEvent event; - event.source = InfoErrorSource::READER; - esp_err_t err = ESP_OK; - - AudioReader reader = AudioReader(this_pipeline->raw_file_ring_buffer_.get(), FILE_BUFFER_SIZE); - - if (event_bits & READER_COMMAND_INIT_FILE) { - err = reader.start(this_pipeline->current_media_file_, this_pipeline->current_media_file_type_); - } else { - err = reader.start(this_pipeline->current_uri_, this_pipeline->current_media_file_type_); - } - if (err != ESP_OK) { - // Send specific error message - event.err = err; - xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); - - // Setting up the reader failed, stop the pipeline - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - } else { - // Send the file type to the pipeline - event.file_type = this_pipeline->current_media_file_type_; - xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); - - // Inform the decoder that the media type is available - xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); - } - - while (true) { - event_bits = xEventGroupGetBits(this_pipeline->event_group_); - - if (event_bits & PIPELINE_COMMAND_STOP) { - break; - } - - AudioReaderState reader_state = reader.read(); - - if (reader_state == AudioReaderState::FINISHED) { - break; - } else if (reader_state == AudioReaderState::FAILED) { - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - break; - } - } - } - } -} - -void AudioPipeline::decode_task_(void *params) { - AudioPipeline *this_pipeline = (AudioPipeline *) params; - - while (true) { - xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); - - // Wait until the reader notifies us that the media type is available - EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, - READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read - pdTRUE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set - - xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); - - { - InfoErrorEvent event; - event.source = InfoErrorSource::DECODER; - - std::unique_ptr decoder = make_unique( - this_pipeline->raw_file_ring_buffer_.get(), this_pipeline->decoded_ring_buffer_.get(), FILE_BUFFER_SIZE); - esp_err_t err = decoder->start(this_pipeline->current_media_file_type_); - - if (err != ESP_OK) { - // Send specific error message - event.err = err; - xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); - - // Setting up the decoder failed, stop the pipeline - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - } - - bool has_stream_info = false; - - while (true) { - event_bits = xEventGroupGetBits(this_pipeline->event_group_); - - if (event_bits & PIPELINE_COMMAND_STOP) { - break; - } - - // Stop gracefully if the reader has finished - AudioDecoderState decoder_state = decoder->decode(event_bits & READER_MESSAGE_FINISHED); - - if (decoder_state == AudioDecoderState::FINISHED) { - break; - } else if (decoder_state == AudioDecoderState::FAILED) { - if (!has_stream_info) { - event.decoding_err = DecodingError::FAILED_HEADER; - xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); - } - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - break; - } - - if (!has_stream_info && decoder->get_audio_stream_info().has_value()) { - has_stream_info = true; - - this_pipeline->current_audio_stream_info_ = decoder->get_audio_stream_info().value(); - - // Send the stream information to the pipeline - event.audio_stream_info = this_pipeline->current_audio_stream_info_; - - if (this_pipeline->current_audio_stream_info_.bits_per_sample != 16) { - // Error state, incompatible bits per sample - event.decoding_err = DecodingError::INCOMPATIBLE_BITS_PER_SAMPLE; - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - } else if ((this_pipeline->current_audio_stream_info_.channels > 2)) { - // Error state, incompatible number of channels - event.decoding_err = DecodingError::INCOMPATIBLE_CHANNELS; - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - } else { - // Inform the resampler that the stream information is available - xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_LOADED_STREAM_INFO); - } - - xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); - } - } - } - } -} - -void AudioPipeline::resample_task_(void *params) { - AudioPipeline *this_pipeline = (AudioPipeline *) params; - - while (true) { - xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::RESAMPLER_MESSAGE_FINISHED); - - // Wait until the decoder notifies us that the stream information is available - EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, - DECODER_MESSAGE_LOADED_STREAM_INFO, // Bit message to read - pdTRUE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set - - xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::RESAMPLER_MESSAGE_FINISHED); - - { - InfoErrorEvent event; - event.source = InfoErrorSource::RESAMPLER; - - RingBuffer *output_ring_buffer = nullptr; - - if (this_pipeline->pipeline_type_ == AudioPipelineType::MEDIA) { - output_ring_buffer = this_pipeline->mixer_->get_media_ring_buffer(); - } else { - output_ring_buffer = this_pipeline->mixer_->get_announcement_ring_buffer(); - } - - AudioResampler resampler = - AudioResampler(this_pipeline->decoded_ring_buffer_.get(), output_ring_buffer, BUFFER_SIZE_SAMPLES); - - esp_err_t err = resampler.start(this_pipeline->current_audio_stream_info_, this_pipeline->target_sample_rate_, - this_pipeline->current_resample_info_); - - if (err != ESP_OK) { - // Send specific error message - event.err = err; - xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); - - // Setting up the resampler failed, stop the pipeline - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::RESAMPLER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - } else { - event.resample_info = this_pipeline->current_resample_info_; - xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); - } - - while (true) { - event_bits = xEventGroupGetBits(this_pipeline->event_group_); - - if (event_bits & PIPELINE_COMMAND_STOP) { - break; - } - - // Stop gracefully if the decoder is done - AudioResamplerState resampler_state = resampler.resample(event_bits & DECODER_MESSAGE_FINISHED); - - if (resampler_state == AudioResamplerState::FINISHED) { - break; - } else if (resampler_state == AudioResamplerState::FAILED) { - xEventGroupSetBits(this_pipeline->event_group_, - EventGroupBits::RESAMPLER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); - break; - } - } - } - } -} - -} // namespace nabu -} // namespace esphome -#endif diff --git a/esphome/components/nabu/audio_pipeline.h b/esphome/components/nabu/audio_pipeline.h deleted file mode 100644 index 947c4bc5..00000000 --- a/esphome/components/nabu/audio_pipeline.h +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -#ifdef USE_ESP_IDF - -#include "audio_reader.h" -#include "audio_decoder.h" -#include "audio_resampler.h" -#include "audio_mixer.h" - -#include "esphome/components/audio/audio.h" -#include "esphome/components/media_player/media_player.h" - -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" -#include "esphome/core/ring_buffer.h" - -#include -#include -#include - -namespace esphome { -namespace nabu { - -enum class AudioPipelineType : uint8_t { - MEDIA, - ANNOUNCEMENT, -}; - -enum class AudioPipelineState : uint8_t { - PLAYING, - STOPPED, - ERROR_READING, - ERROR_DECODING, - ERROR_RESAMPLING, -}; - -enum class InfoErrorSource : uint8_t { - READER = 0, - DECODER, - RESAMPLER, -}; - -enum class DecodingError : uint8_t { - FAILED_HEADER = 0, - INCOMPATIBLE_BITS_PER_SAMPLE, - INCOMPATIBLE_CHANNELS, -}; - -// Used to pass information from each task. -struct InfoErrorEvent { - InfoErrorSource source; - optional err; - optional file_type; - optional audio_stream_info; - optional resample_info; - optional decoding_err; -}; - -class AudioPipeline { - public: - AudioPipeline(AudioMixer *mixer, AudioPipelineType pipeline_type); - - /// @brief Starts an audio pipeline given a media url - /// @param uri media file url - /// @param target_sample_rate the desired sample rate of the audio stream - /// @param task_name FreeRTOS task name - /// @param priority FreeRTOS task priority - /// @return ESP_OK if successful or an appropriate error if not - esp_err_t start(const std::string &uri, uint32_t target_sample_rate, const std::string &task_name, - UBaseType_t priority = 1); - - /// @brief Starts an audio pipeline given a MediaFile pointer - /// @param media_file pointer to a MediaFile object - /// @param target_sample_rate the desired sample rate of the audio stream - /// @param task_name FreeRTOS task name - /// @param priority FreeRTOS task priority - /// @return ESP_OK if successful or an appropriate error if not - esp_err_t start(media_player::MediaFile *media_file, uint32_t target_sample_rate, const std::string &task_name, - UBaseType_t priority = 1); - - /// @brief Stops the pipeline. Sends a stop signal to each task (if running) and clears the ring buffers. - /// @return ESP_OK if successful or ESP_ERR_TIMEOUT if the tasks did not indicate they stopped - esp_err_t stop(); - - /// @brief Gets the state of the audio pipeline based on the info_error_queue_ and event_group_ - /// @return AudioPipelineState - AudioPipelineState get_state(); - - /// @brief Resets the ring buffers, discarding any existing data - void reset_ring_buffers(); - - /// @brief Suspends any running tasks - void suspend_tasks(); - /// @brief Resumes any running tasks - void resume_tasks(); - - protected: - /// @brief Allocates the ring buffers, event group, and info error queue. - /// @return ESP_OK if successful or ESP_ERR_NO_MEM if it is unable to allocate all parts - esp_err_t allocate_buffers_(); - - /// @brief Common start code for the pipeline, regardless if the source is a file or url. - /// @param target_sample_rate the desired sample rate of the audio stream - /// @param task_name FreeRTOS task name - /// @param priority FreeRTOS task priority - /// @return ESP_OK if successful or an appropriate error if not - esp_err_t common_start_(uint32_t target_sample_rate, const std::string &task_name, UBaseType_t priority); - - // Pointer to the media player's mixer object. The resample task feeds the appropriate ring buffer directly - AudioMixer *mixer_; - - std::string current_uri_{}; - media_player::MediaFile *current_media_file_{nullptr}; - - media_player::MediaFileType current_media_file_type_; - audio::AudioStreamInfo current_audio_stream_info_; - ResampleInfo current_resample_info_; - uint32_t target_sample_rate_; - - AudioPipelineType pipeline_type_; - - std::unique_ptr raw_file_ring_buffer_; - std::unique_ptr decoded_ring_buffer_; - - // Handles basic control/state of the three tasks - EventGroupHandle_t event_group_{nullptr}; - - // Receives detailed info (file type, stream info, resampling info) or specific errors from the three tasks - QueueHandle_t info_error_queue_{nullptr}; - - // Handles reading the media file from flash or a url - static void read_task_(void *params); - TaskHandle_t read_task_handle_{nullptr}; - StaticTask_t read_task_stack_; - StackType_t *read_task_stack_buffer_{nullptr}; - - // Decodes the media file into PCM audio - static void decode_task_(void *params); - TaskHandle_t decode_task_handle_{nullptr}; - StaticTask_t decode_task_stack_; - StackType_t *decode_task_stack_buffer_{nullptr}; - - // Resamples the audio to match the specified target sample rate. Converts mono audio to stereo audio if necessary. - static void resample_task_(void *params); - TaskHandle_t resample_task_handle_{nullptr}; - StaticTask_t resample_task_stack_; - StackType_t *resample_task_stack_buffer_{nullptr}; -}; - -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/audio_reader.cpp b/esphome/components/nabu/audio_reader.cpp deleted file mode 100644 index d8f1c0e7..00000000 --- a/esphome/components/nabu/audio_reader.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#ifdef USE_ESP_IDF - -#include "audio_reader.h" - -#include "esphome/core/ring_buffer.h" - -#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE -#include "esp_crt_bundle.h" -#endif - -namespace esphome { -namespace nabu { - -static const size_t READ_WRITE_TIMEOUT_MS = 20; - -// The number of times the http read times out with no data before throwing an error -static const size_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 50; - -AudioReader::AudioReader(esphome::RingBuffer *output_ring_buffer, size_t transfer_buffer_size) { - this->output_ring_buffer_ = output_ring_buffer; - this->transfer_buffer_size_ = transfer_buffer_size; -} - -AudioReader::~AudioReader() { - if (this->transfer_buffer_ != nullptr) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); - } - - this->cleanup_connection_(); -} - -esp_err_t AudioReader::allocate_buffers_() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - if (this->transfer_buffer_ == nullptr) - this->transfer_buffer_ = allocator.allocate(this->transfer_buffer_size_); - - if (this->transfer_buffer_ == nullptr) - return ESP_ERR_NO_MEM; - - return ESP_OK; -} - -esp_err_t AudioReader::start(media_player::MediaFile *media_file, media_player::MediaFileType &file_type) { - file_type = media_player::MediaFileType::NONE; - - esp_err_t err = this->allocate_buffers_(); - if (err != ESP_OK) { - return err; - } - - this->current_media_file_ = media_file; - - this->transfer_buffer_current_ = media_file->data; - this->transfer_buffer_length_ = media_file->length; - file_type = media_file->file_type; - - return ESP_OK; -} - -esp_err_t AudioReader::start(const std::string &uri, media_player::MediaFileType &file_type) { - file_type = media_player::MediaFileType::NONE; - - esp_err_t err = this->allocate_buffers_(); - if (err != ESP_OK) { - return err; - } - - this->cleanup_connection_(); - - if (uri.empty()) { - return ESP_ERR_INVALID_ARG; - } - - esp_http_client_config_t client_config = {}; - - client_config.url = uri.c_str(); - client_config.cert_pem = nullptr; - client_config.disable_auto_redirect = false; - client_config.max_redirection_count = 10; - client_config.buffer_size = 512; - client_config.keep_alive_enable = true; - client_config.timeout_ms = 5000; // Doesn't raise an error if exceeded in esp-idf v4.4, it just prevents the - // http_client_read command from blocking for too long - -#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE - if (uri.find("https:") != std::string::npos) { - client_config.crt_bundle_attach = esp_crt_bundle_attach; - } -#endif - - this->client_ = esp_http_client_init(&client_config); - - if (this->client_ == nullptr) { - return ESP_FAIL; - } - - if ((err = esp_http_client_open(this->client_, 0)) != ESP_OK) { - this->cleanup_connection_(); - return err; - } - - int content_length = esp_http_client_fetch_headers(this->client_); - - char url[500]; - err = esp_http_client_get_url(this->client_, url, 500); - if (err != ESP_OK) { - this->cleanup_connection_(); - return err; - } - - std::string url_string = url; - - if (str_endswith(url_string, ".wav")) { - file_type = media_player::MediaFileType::WAV; - } else if (str_endswith(url_string, ".mp3")) { - file_type = media_player::MediaFileType::MP3; - } else if (str_endswith(url_string, ".flac")) { - file_type = media_player::MediaFileType::FLAC; - } else { - file_type = media_player::MediaFileType::NONE; - this->cleanup_connection_(); - return ESP_ERR_NOT_SUPPORTED; - } - - this->transfer_buffer_current_ = this->transfer_buffer_; - this->transfer_buffer_length_ = 0; - this->no_data_read_count_ = 0; - - return ESP_OK; -} - -AudioReaderState AudioReader::read() { - if (this->client_ != nullptr) { - return this->http_read_(); - } else if (this->current_media_file_ != nullptr) { - return this->file_read_(); - } - - return AudioReaderState::FAILED; -} - -AudioReaderState AudioReader::file_read_() { - if (this->transfer_buffer_length_ > 0) { - size_t bytes_written = this->output_ring_buffer_->write_without_replacement( - (void *) this->transfer_buffer_current_, this->transfer_buffer_length_, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - this->transfer_buffer_length_ -= bytes_written; - this->transfer_buffer_current_ += bytes_written; - - return AudioReaderState::READING; - } - return AudioReaderState::FINISHED; -} - -AudioReaderState AudioReader::http_read_() { - if (this->transfer_buffer_length_ > 0) { - size_t bytes_written = this->output_ring_buffer_->write_without_replacement( - (void *) this->transfer_buffer_, this->transfer_buffer_length_, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - this->transfer_buffer_length_ -= bytes_written; - - // Shift remaining data to the start of the transfer buffer - memmove(this->transfer_buffer_, this->transfer_buffer_ + bytes_written, this->transfer_buffer_length_); - } - - if (esp_http_client_is_complete_data_received(this->client_)) { - if (this->transfer_buffer_length_ == 0) { - this->cleanup_connection_(); - return AudioReaderState::FINISHED; - } - } else { - size_t bytes_to_read = this->transfer_buffer_size_ - this->transfer_buffer_length_; - int received_len = esp_http_client_read( - this->client_, (char *) this->transfer_buffer_ + this->transfer_buffer_length_, bytes_to_read); - - if (received_len > 0) { - this->transfer_buffer_length_ += received_len; - this->no_data_read_count_ = 0; - } else if (received_len < 0) { - // HTTP read error - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } else { - if (bytes_to_read > 0) { - // Read timed out - ++this->no_data_read_count_; - if (this->no_data_read_count_ >= ERROR_COUNT_NO_DATA_READ_TIMEOUT) { - // Timed out with no data read too many times, so the http read has failed - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } - vTaskDelay(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - } - } - } - - return AudioReaderState::READING; -} - -void AudioReader::cleanup_connection_() { - if (this->client_ != nullptr) { - esp_http_client_close(this->client_); - esp_http_client_cleanup(this->client_); - this->client_ = nullptr; - } -} - -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/audio_reader.h b/esphome/components/nabu/audio_reader.h deleted file mode 100644 index 2080bd38..00000000 --- a/esphome/components/nabu/audio_reader.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#ifdef USE_ESP_IDF - -#include "esphome/components/media_player/media_player.h" -#include "esphome/core/ring_buffer.h" - -#include - -namespace esphome { -namespace nabu { - -enum class AudioReaderState : uint8_t { - READING = 0, - FINISHED, - FAILED, -}; - -class AudioReader { - public: - AudioReader(esphome::RingBuffer *output_ring_buffer, size_t transfer_buffer_size); - ~AudioReader(); - - esp_err_t start(const std::string &uri, media_player::MediaFileType &file_type); - esp_err_t start(media_player::MediaFile *media_file, media_player::MediaFileType &file_type); - - AudioReaderState read(); - - protected: - esp_err_t allocate_buffers_(); - - AudioReaderState file_read_(); - AudioReaderState http_read_(); - - void cleanup_connection_(); - - esphome::RingBuffer *output_ring_buffer_; - - size_t transfer_buffer_length_; // Amount of data currently stored in transfer buffer (in bytes) - size_t transfer_buffer_size_; // Capacity of transfer buffer (in bytes) - - ssize_t no_data_read_count_; - - uint8_t *transfer_buffer_{nullptr}; - const uint8_t *transfer_buffer_current_{nullptr}; - - esp_http_client_handle_t client_{nullptr}; - - media_player::MediaFile *current_media_file_{nullptr}; -}; -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/audio_resampler.cpp b/esphome/components/nabu/audio_resampler.cpp deleted file mode 100644 index 8e29edf9..00000000 --- a/esphome/components/nabu/audio_resampler.cpp +++ /dev/null @@ -1,317 +0,0 @@ -#ifdef USE_ESP_IDF - -#include "audio_resampler.h" - -#include "esphome/core/ring_buffer.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace nabu { - -static const size_t NUM_TAPS = 32; -static const size_t NUM_FILTERS = 32; -static const bool USE_PRE_POST_FILTER = true; - -// These output parameters are currently hardcoded in the elements further down the pipeline (mixer and speaker) -static const uint8_t OUTPUT_CHANNELS = 2; -static const uint8_t OUTPUT_BITS_PER_SAMPLE = 16; - -static const size_t READ_WRITE_TIMEOUT_MS = 20; - -AudioResampler::AudioResampler(RingBuffer *input_ring_buffer, RingBuffer *output_ring_buffer, - size_t internal_buffer_samples) { - this->input_ring_buffer_ = input_ring_buffer; - this->output_ring_buffer_ = output_ring_buffer; - this->internal_buffer_samples_ = internal_buffer_samples; -} - -AudioResampler::~AudioResampler() { - ExternalRAMAllocator int16_allocator(ExternalRAMAllocator::ALLOW_FAILURE); - ExternalRAMAllocator float_allocator(ExternalRAMAllocator::ALLOW_FAILURE); - - if (this->input_buffer_ != nullptr) { - int16_allocator.deallocate(this->input_buffer_, this->internal_buffer_samples_); - } - if (this->output_buffer_ != nullptr) { - int16_allocator.deallocate(this->output_buffer_, this->internal_buffer_samples_); - } - if (this->float_input_buffer_ != nullptr) { - float_allocator.deallocate(this->float_input_buffer_, this->internal_buffer_samples_); - } - if (this->float_output_buffer_ != nullptr) { - float_allocator.deallocate(this->float_output_buffer_, this->internal_buffer_samples_); - } - if (this->resampler_ != nullptr) { - resampleFree(this->resampler_); - this->resampler_ = nullptr; - } -} - -esp_err_t AudioResampler::allocate_buffers_() { - ExternalRAMAllocator int16_allocator(ExternalRAMAllocator::ALLOW_FAILURE); - ExternalRAMAllocator float_allocator(ExternalRAMAllocator::ALLOW_FAILURE); - - if (this->input_buffer_ == nullptr) - this->input_buffer_ = int16_allocator.allocate(this->internal_buffer_samples_); - if (this->output_buffer_ == nullptr) - this->output_buffer_ = int16_allocator.allocate(this->internal_buffer_samples_); - - if (this->float_input_buffer_ == nullptr) - this->float_input_buffer_ = float_allocator.allocate(this->internal_buffer_samples_); - - if (this->float_output_buffer_ == nullptr) - this->float_output_buffer_ = float_allocator.allocate(this->internal_buffer_samples_); - - if ((this->input_buffer_ == nullptr) || (this->output_buffer_ == nullptr) || (this->float_input_buffer_ == nullptr) || - (this->float_output_buffer_ == nullptr)) { - return ESP_ERR_NO_MEM; - } - - return ESP_OK; -} - -esp_err_t AudioResampler::start(audio::AudioStreamInfo &stream_info, uint32_t target_sample_rate, - ResampleInfo &resample_info) { - esp_err_t err = this->allocate_buffers_(); - if (err != ESP_OK) { - return err; - } - - this->stream_info_ = stream_info; - - this->input_buffer_current_ = this->input_buffer_; - this->input_buffer_length_ = 0; - this->float_input_buffer_current_ = this->float_input_buffer_; - this->float_input_buffer_length_ = 0; - - this->output_buffer_current_ = this->output_buffer_; - this->output_buffer_length_ = 0; - this->float_output_buffer_current_ = this->float_output_buffer_; - this->float_output_buffer_length_ = 0; - - resample_info.mono_to_stereo = (stream_info.channels != 2); - - if ((stream_info.channels > OUTPUT_CHANNELS) || (stream_info_.bits_per_sample != OUTPUT_BITS_PER_SAMPLE)) { - return ESP_ERR_NOT_SUPPORTED; - } - - if (stream_info.channels > 0) { - this->channel_factor_ = 2 / stream_info.channels; - } - - if (stream_info.sample_rate != target_sample_rate) { - int flags = 0; - - resample_info.resample = true; - - this->sample_ratio_ = static_cast(target_sample_rate) / static_cast(stream_info.sample_rate); - - if (this->sample_ratio_ < 1.0) { - this->lowpass_ratio_ -= (10.24 / 16); - - if (this->lowpass_ratio_ < 0.84) { - this->lowpass_ratio_ = 0.84; - } - - if (this->lowpass_ratio_ < this->sample_ratio_) { - // avoid discontinuities near unity sample ratios - this->lowpass_ratio_ = this->sample_ratio_; - } - } - if (this->lowpass_ratio_ * this->sample_ratio_ < 0.98 && USE_PRE_POST_FILTER) { - float cutoff = this->lowpass_ratio_ * this->sample_ratio_ / 2.0; - biquad_lowpass(&this->lowpass_coeff_, cutoff); - this->pre_filter_ = true; - } - - if (this->lowpass_ratio_ / this->sample_ratio_ < 0.98 && USE_PRE_POST_FILTER && !this->pre_filter_) { - float cutoff = this->lowpass_ratio_ / this->sample_ratio_ / 2.0; - biquad_lowpass(&this->lowpass_coeff_, cutoff); - this->post_filter_ = true; - } - - if (this->pre_filter_ || this->post_filter_) { - for (int i = 0; i < stream_info.channels; ++i) { - biquad_init(&this->lowpass_[i][0], &this->lowpass_coeff_, 1.0); - biquad_init(&this->lowpass_[i][1], &this->lowpass_coeff_, 1.0); - } - } - - if (this->sample_ratio_ < 1.0) { - this->resampler_ = resampleInit(stream_info.channels, NUM_TAPS, NUM_FILTERS, - this->sample_ratio_ * this->lowpass_ratio_, flags | INCLUDE_LOWPASS); - } else if (this->lowpass_ratio_ < 1.0) { - this->resampler_ = - resampleInit(stream_info.channels, NUM_TAPS, NUM_FILTERS, this->lowpass_ratio_, flags | INCLUDE_LOWPASS); - } else { - this->resampler_ = resampleInit(stream_info.channels, NUM_TAPS, NUM_FILTERS, 1.0, flags); - } - - resampleAdvancePosition(this->resampler_, NUM_TAPS / 2.0); - - } else { - resample_info.resample = false; - } - - this->resample_info_ = resample_info; - return ESP_OK; -} - -AudioResamplerState AudioResampler::resample(bool stop_gracefully) { - if (stop_gracefully) { - if ((this->input_ring_buffer_->available() == 0) && (this->output_ring_buffer_->available() == 0) && - (this->input_buffer_length_ == 0) && (this->output_buffer_length_ == 0)) { - return AudioResamplerState::FINISHED; - } - } - - if (this->output_buffer_length_ > 0) { - size_t bytes_to_write = this->output_buffer_length_; - - if (bytes_to_write > 0) { - size_t bytes_written = this->output_ring_buffer_->write_without_replacement( - (void *) this->output_buffer_current_, bytes_to_write, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - - this->output_buffer_current_ += bytes_written / sizeof(int16_t); - this->output_buffer_length_ -= bytes_written; - } - - return AudioResamplerState::RESAMPLING; - } - - // Copy audio data directly to output_buffer if resampling isn't required - if (!this->resample_info_.resample && !this->resample_info_.mono_to_stereo) { - size_t bytes_read = - this->input_ring_buffer_->read((void *) this->output_buffer_, this->internal_buffer_samples_ * sizeof(int16_t), - pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - - this->output_buffer_current_ = this->output_buffer_; - this->output_buffer_length_ += bytes_read; - - return AudioResamplerState::RESAMPLING; - } - - ////// - // Refill input buffer - ////// - - // Depending on if we are converting mono to stereo or if we are upsampling, we may need to restrict how many input - // samples we transfer - size_t max_input_samples = this->internal_buffer_samples_; - - // Mono to stereo -> cut in half - max_input_samples /= (2 / this->stream_info_.channels); - - if (this->sample_ratio_ > 1.0) { - // Upsampling -> reduce by a factor of the ceiling of sample_ratio_ - uint32_t upsampling_factor = std::ceil(this->sample_ratio_); - max_input_samples /= upsampling_factor; - } - - // Move old data to the start of the buffer - if (this->input_buffer_length_ > 0) { - memmove((void *) this->input_buffer_, (void *) this->input_buffer_current_, this->input_buffer_length_); - } - this->input_buffer_current_ = this->input_buffer_; - - // Copy new data to the end of the of the buffer - size_t bytes_to_read = max_input_samples * sizeof(int16_t) - this->input_buffer_length_; - - if (bytes_to_read > 0) { - int16_t *new_input_buffer_data = this->input_buffer_ + this->input_buffer_length_ / sizeof(int16_t); - size_t bytes_read = this->input_ring_buffer_->read((void *) new_input_buffer_data, bytes_to_read, - pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); - - this->input_buffer_length_ += bytes_read; - } - - if (this->input_buffer_length_ == 0) { - return AudioResamplerState::RESAMPLING; - } - - if (this->resample_info_.resample) { - if (this->input_buffer_length_ > 0) { - // Samples are indiviudal int16 values. Frames include 1 sample for mono and 2 samples for stereo - // Be careful converting between bytes, samples, and frames! - // 1 sample = 2 bytes = sizeof(int16_t) - // if mono: - // 1 frame = 1 sample - // if stereo: - // 1 frame = 2 samples (left and right) - - size_t samples_read = this->input_buffer_length_ / sizeof(int16_t); - - for (int i = 0; i < samples_read; ++i) { - this->float_input_buffer_[i] = static_cast(this->input_buffer_[i]) / 32768.0f; - } - - size_t frames_read = samples_read / this->stream_info_.channels; - - if (this->pre_filter_) { - for (int i = 0; i < this->stream_info_.channels; ++i) { - biquad_apply_buffer(&this->lowpass_[i][0], this->float_input_buffer_ + i, frames_read, - this->stream_info_.channels); - biquad_apply_buffer(&this->lowpass_[i][1], this->float_input_buffer_ + i, frames_read, - this->stream_info_.channels); - } - } - - ResampleResult res; - - res = resampleProcessInterleaved(this->resampler_, this->float_input_buffer_, frames_read, - this->float_output_buffer_, - this->internal_buffer_samples_ / this->channel_factor_, this->sample_ratio_); - - size_t frames_used = res.input_used; - size_t samples_used = frames_used * this->stream_info_.channels; - - size_t frames_generated = res.output_generated; - if (this->post_filter_) { - for (int i = 0; i < this->stream_info_.channels; ++i) { - biquad_apply_buffer(&this->lowpass_[i][0], this->float_output_buffer_ + i, frames_generated, - this->stream_info_.channels); - biquad_apply_buffer(&this->lowpass_[i][1], this->float_output_buffer_ + i, frames_generated, - this->stream_info_.channels); - } - } - - size_t samples_generated = frames_generated * this->stream_info_.channels; - - for (int i = 0; i < samples_generated; ++i) { - this->output_buffer_[i] = static_cast(this->float_output_buffer_[i] * 32767); - } - - this->input_buffer_current_ += samples_used; - this->input_buffer_length_ -= samples_used * sizeof(int16_t); - - this->output_buffer_current_ = this->output_buffer_; - this->output_buffer_length_ += samples_generated * sizeof(int16_t); - } - } else { - size_t bytes_to_transfer = - std::min(this->internal_buffer_samples_ / this->channel_factor_ * sizeof(int16_t), this->input_buffer_length_); - std::memcpy((void *) this->output_buffer_, (void *) this->input_buffer_current_, bytes_to_transfer); - - this->input_buffer_current_ += bytes_to_transfer / sizeof(int16_t); - this->input_buffer_length_ -= bytes_to_transfer; - - this->output_buffer_current_ = this->output_buffer_; - this->output_buffer_length_ += bytes_to_transfer; - } - - if (this->resample_info_.mono_to_stereo) { - // Convert mono to stereo - for (int i = this->output_buffer_length_ / (sizeof(int16_t)) - 1; i >= 0; --i) { - this->output_buffer_[2 * i] = this->output_buffer_[i]; - this->output_buffer_[2 * i + 1] = this->output_buffer_[i]; - } - - this->output_buffer_length_ *= 2; // double the bytes for stereo samples - } - return AudioResamplerState::RESAMPLING; -} - -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/audio_resampler.h b/esphome/components/nabu/audio_resampler.h deleted file mode 100644 index 46521e41..00000000 --- a/esphome/components/nabu/audio_resampler.h +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once - -#ifdef USE_ESP_IDF - -#include "biquad.h" -#include "resampler.h" - -#include "esphome/components/audio/audio.h" -#include "esphome/core/ring_buffer.h" - -namespace esphome { -namespace nabu { - -enum class AudioResamplerState : uint8_t { - INITIALIZED = 0, - RESAMPLING, - FINISHED, - FAILED, -}; - -struct ResampleInfo { - bool resample; - bool mono_to_stereo; -}; - -class AudioResampler { - public: - AudioResampler(esphome::RingBuffer *input_ring_buffer, esphome::RingBuffer *output_ring_buffer, - size_t internal_buffer_samples); - ~AudioResampler(); - - /// @brief Sets up the various bits necessary to resample - /// @param stream_info the incoming sample rate, bits per sample, and number of channels - /// @param target_sample_rate the necessary sample rate to convert to - /// @return ESP_OK if it is able to convert the incoming stream or an error otherwise - esp_err_t start(audio::AudioStreamInfo &stream_info, uint32_t target_sample_rate, ResampleInfo &resample_info); - - AudioResamplerState resample(bool stop_gracefully); - - protected: - esp_err_t allocate_buffers_(); - - esphome::RingBuffer *input_ring_buffer_; - esphome::RingBuffer *output_ring_buffer_; - size_t internal_buffer_samples_; - - int16_t *input_buffer_{nullptr}; - int16_t *input_buffer_current_{nullptr}; - size_t input_buffer_length_; - - int16_t *output_buffer_{nullptr}; - int16_t *output_buffer_current_{nullptr}; - size_t output_buffer_length_; - - float *float_input_buffer_{nullptr}; - float *float_input_buffer_current_{nullptr}; - size_t float_input_buffer_length_; - - float *float_output_buffer_{nullptr}; - float *float_output_buffer_current_{nullptr}; - size_t float_output_buffer_length_; - - audio::AudioStreamInfo stream_info_; - ResampleInfo resample_info_; - - Resample *resampler_{nullptr}; - - Biquad lowpass_[2][2]; - BiquadCoefficients lowpass_coeff_; - - float sample_ratio_{1.0}; - float lowpass_ratio_{1.0}; - uint8_t channel_factor_{1}; - - bool pre_filter_{false}; - bool post_filter_{false}; -}; - -} // namespace nabu -} // namespace esphome - -#endif diff --git a/esphome/components/nabu/media_player.py b/esphome/components/nabu/media_player.py deleted file mode 100644 index 949c5966..00000000 --- a/esphome/components/nabu/media_player.py +++ /dev/null @@ -1,330 +0,0 @@ -"""Nabu Media Player Setup.""" - -import hashlib -import logging -from pathlib import Path - -from esphome import automation, external_files -import esphome.codegen as cg -from esphome.components import audio_dac, media_player, speaker -from esphome.components.media_player import MEDIA_FILE_TYPE_ENUM, MediaFile -import esphome.config_validation as cv -from esphome.const import ( - CONF_DURATION, - CONF_FILE, - CONF_FILES, - CONF_ID, - CONF_PATH, - CONF_RAW_DATA_ID, - CONF_SAMPLE_RATE, - CONF_SPEAKER, - CONF_TYPE, - CONF_URL, -) -from esphome.core import CORE, HexInt -from esphome.external_files import download_content - -_LOGGER = logging.getLogger(__name__) - -AUTO_LOAD = ["audio"] - -CODEOWNERS = ["@synesthesiam", "@kahrendt"] -DEPENDENCIES = ["media_player"] -DOMAIN = "file" - -TYPE_LOCAL = "local" -TYPE_WEB = "web" - -CONF_DECIBEL_REDUCTION = "decibel_reduction" - -CONF_AUDIO_DAC = "audio_dac" -CONF_ANNOUNCEMENT = "announcement" -CONF_MEDIA_FILE = "media_file" -CONF_VOLUME_INCREMENT = "volume_increment" -CONF_VOLUME_MIN = "volume_min" -CONF_VOLUME_MAX = "volume_max" - -CONF_ON_MUTE = "on_mute" -CONF_ON_UNMUTE = "on_unmute" -CONF_ON_VOLUME = "on_volume" - -nabu_ns = cg.esphome_ns.namespace("nabu") -NabuMediaPlayer = nabu_ns.class_("NabuMediaPlayer") -NabuMediaPlayer = nabu_ns.class_( - "NabuMediaPlayer", - NabuMediaPlayer, - media_player.MediaPlayer, - cg.Component, -) - -DuckingSetAction = nabu_ns.class_( - "DuckingSetAction", automation.Action, cg.Parented.template(NabuMediaPlayer) -) -PlayLocalMediaAction = nabu_ns.class_( - "PlayLocalMediaAction", automation.Action, cg.Parented.template(NabuMediaPlayer) -) - - -def _compute_local_file_path(value: dict) -> Path: - url = value[CONF_URL] - h = hashlib.new("sha256") - h.update(url.encode()) - key = h.hexdigest()[:8] - base_dir = external_files.compute_local_file_dir(DOMAIN) - _LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key) - return base_dir / key - - -def _download_web_file(value): - url = value[CONF_URL] - path = _compute_local_file_path(value) - - download_content(url, path) - _LOGGER.debug("download_web_file: path=%s", path) - return value - - -LOCAL_SCHEMA = cv.Schema( - { - cv.Required(CONF_PATH): cv.file_, - } -) - -WEB_SCHEMA = cv.All( - { - cv.Required(CONF_URL): cv.url, - }, - _download_web_file, -) - - -def _validate_file_shorthand(value): - value = cv.string_strict(value) - if value.startswith("http://") or value.startswith("https://"): - return _file_schema( - { - CONF_TYPE: TYPE_WEB, - CONF_URL: value, - } - ) - return _file_schema( - { - CONF_TYPE: TYPE_LOCAL, - CONF_PATH: value, - } - ) - - -TYPED_FILE_SCHEMA = cv.typed_schema( - { - TYPE_LOCAL: LOCAL_SCHEMA, - TYPE_WEB: WEB_SCHEMA, - }, -) - - -def _file_schema(value): - if isinstance(value, str): - return _validate_file_shorthand(value) - return TYPED_FILE_SCHEMA(value) - - -TYPED_FILE_SCHEMA = cv.typed_schema( - { - TYPE_LOCAL: LOCAL_SCHEMA, - TYPE_WEB: WEB_SCHEMA, - }, -) - - -MEDIA_FILE_TYPE_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(MediaFile), - cv.Required(CONF_FILE): _file_schema, - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - } -) - - -CONFIG_SCHEMA = media_player.MEDIA_PLAYER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(NabuMediaPlayer), - cv.Required(CONF_SPEAKER): cv.use_id(speaker.Speaker), - cv.Optional(CONF_AUDIO_DAC): cv.use_id(audio_dac.AudioDac), - cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), - cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, - cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, - cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage, - cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), - cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True), - cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True), - cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), - } -) - - -def _read_audio_file_and_type(file_config): - conf_file = file_config[CONF_FILE] - file_source = conf_file[CONF_TYPE] - if file_source == TYPE_LOCAL: - path = CORE.relative_config_path(conf_file[CONF_PATH]) - elif file_source == TYPE_WEB: - path = _compute_local_file_path(conf_file) - else: - raise cv.Invalid("Unsupported file source.") - - with open(path, "rb") as f: - data = f.read() - - try: - import puremagic - - file_type: str = puremagic.from_string(data) - except ImportError: - try: - from magic import Magic - - magic = Magic(mime=True) - file_type: str = magic.from_buffer(data) - except ImportError as exc: - raise cv.Invalid("Please install puremagic") from exc - if file_type.startswith("."): - file_type = file_type[1:] - - media_file_type = MEDIA_FILE_TYPE_ENUM["NONE"] - if file_type in ("wav"): - media_file_type = MEDIA_FILE_TYPE_ENUM["WAV"] - elif file_type in ("mp3", "mpeg", "mpga"): - media_file_type = MEDIA_FILE_TYPE_ENUM["MP3"] - elif file_type in ("flac"): - media_file_type = MEDIA_FILE_TYPE_ENUM["FLAC"] - - return data, media_file_type - - -def _supported_local_file_validate(config): - if files_list := config.get(CONF_FILES): - for file_config in files_list: - _, media_file_type = _read_audio_file_and_type(file_config) - if str(media_file_type) == str(MEDIA_FILE_TYPE_ENUM["NONE"]): - raise cv.Invalid("Unsupported local media file.") - - -FINAL_VALIDATE_SCHEMA = _supported_local_file_validate - - -async def to_code(config): - cg.add_library("esphome/esp-audio-libs", "1.0.0") - - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await media_player.register_media_player(var, config) - - cg.add_define("USE_OTA_STATE_CALLBACK") - - cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) - - cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT])) - cg.add(var.set_volume_max(config[CONF_VOLUME_MAX])) - cg.add(var.set_volume_min(config[CONF_VOLUME_MIN])) - - spkr = await cg.get_variable(config[CONF_SPEAKER]) - cg.add(var.set_speaker(spkr)) - - if on_mute := config.get(CONF_ON_MUTE): - await automation.build_automation( - var.get_mute_trigger(), - [], - on_mute, - ) - if on_unmute := config.get(CONF_ON_UNMUTE): - await automation.build_automation( - var.get_unmute_trigger(), - [], - on_unmute, - ) - if on_volume := config.get(CONF_ON_VOLUME): - await automation.build_automation( - var.get_volume_trigger(), - [(cg.float_, "x")], - on_volume, - ) - - 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)) - - if files_list := config.get(CONF_FILES): - for file_config in files_list: - data, media_file_type = _read_audio_file_and_type(file_config) - - rhs = [HexInt(x) for x in data] - prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) - - media_files_struct = cg.StructInitializer( - MediaFile, - ( - "data", - prog_arr, - ), - ( - "length", - len(rhs), - ), - ( - "file_type", - media_file_type, - ), - ) - - cg.new_Pvariable( - file_config[CONF_ID], - media_files_struct, - ) - - -DUCKING_SET_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(NabuMediaPlayer), - cv.Required(CONF_DECIBEL_REDUCTION): cv.templatable( - cv.int_range(min=0, max=51) - ), - cv.Optional(CONF_DURATION, default="0.0s"): cv.templatable( - cv.positive_time_period_seconds - ), - } -) - - -@automation.register_action( - "nabu.play_local_media_file", - PlayLocalMediaAction, - cv.maybe_simple_value( - { - cv.GenerateID(): cv.use_id(NabuMediaPlayer), - cv.Required(CONF_MEDIA_FILE): cv.use_id(MediaFile), - cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.boolean, - }, - key=CONF_MEDIA_FILE, - ), -) -async def media_player_play_media_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - media_file = await cg.get_variable(config[CONF_MEDIA_FILE]) - cg.add(var.set_media_file(media_file)) - cg.add(var.set_announcement(config[CONF_ANNOUNCEMENT])) - return var - - -@automation.register_action("nabu.set_ducking", DuckingSetAction, DUCKING_SET_SCHEMA) -async def ducking_set_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - decibel_reduction = await cg.templatable( - config[CONF_DECIBEL_REDUCTION], args, cg.uint8 - ) - cg.add(var.set_decibel_reduction(decibel_reduction)) - duration = await cg.templatable(config[CONF_DURATION], args, cg.float_) - cg.add(var.set_duration(duration)) - return var diff --git a/esphome/components/nabu/nabu_media_player.cpp b/esphome/components/nabu/nabu_media_player.cpp deleted file mode 100644 index b870f885..00000000 --- a/esphome/components/nabu/nabu_media_player.cpp +++ /dev/null @@ -1,484 +0,0 @@ -#ifdef USE_ESP_IDF - -#include "nabu_media_player.h" - -#include "esphome/components/audio/audio.h" - -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -#ifdef USE_OTA -#include "esphome/components/ota/ota_backend.h" -#endif - -namespace esphome { -namespace nabu { - -// Framework: -// - Media player that can handle two streams; one for media and one for announcements -// - If played together, they are mixed with the announcement stream staying at full volume -// - The media audio is scaled, if necessary, to avoid clipping when mixing an announcement stream -// - The media audio can be further ducked via the ``set_ducking_reduction`` function -// - Each stream is handled by an ``AudioPipeline`` object with three parts/tasks -// - ``AudioReader`` handles reading from an HTTP source or from a PROGMEM flash set at compile time -// - ``AudioDecoder`` handles decoding the audio file. All formats are limited to two channels and 16 bits per sample -// - FLAC -// - WAV -// - MP3 (based on the libhelix decoder - a random mp3 file may be incompatible) -// - ``AudioResampler`` handles converting the sample rate to the configured output sample rate and converting mono -// to stereo -// - The quality is not good, and it is slow! Please use audio at the configured sample rate to avoid these issues -// - Each task will always run once started, but they will not doing anything until they are needed -// - FreeRTOS Event Groups make up the inter-task communication -// - The ``AudioPipeline`` sets up an output ring buffer for the Reader and Decoder parts. The next part/task -// automatically pulls from the previous ring buffer -// - The streams are mixed together in the ``AudioMixer`` task -// - Each stream has a corresponding input buffer that the ``AudioResampler`` feeds directly -// - Pausing the media stream is done here -// - Media stream ducking is done here -// - The output ring buffer feeds the configured speaker the audio directly -// - Media player commands are received by the ``control`` function. The commands are added to the -// ``media_control_command_queue_`` to be processed in the component's loop -// - Starting a stream intializes the appropriate pipeline or stops it if it is already running -// - Volume and mute commands are achieved by the ``mute``, ``unmute``, ``set_volume`` functions. Volume changes use -// an ``audio_dac`` component if configured. If one isn't, software volume control is used. -// - Volume commands are ignored if the media control queue is full to avoid crashing when the track wheel is spun -// fast -// - Pausing is sent to the ``AudioMixer`` task. It only effects the media stream. -// - The components main loop performs housekeeping: -// - It reads the media control queue and processes it directly -// - It watches the state of speaker and mixer tasks -// - It determines the overall state of the media player by considering the state of each pipeline -// - announcement playback takes highest priority - -static const size_t QUEUE_LENGTH = 20; - -static const uint8_t NUMBER_OF_CHANNELS = 2; // Hard-coded expectation of stereo (2 channel) audio - -static const UBaseType_t MEDIA_PIPELINE_TASK_PRIORITY = 1; -static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1; -static const UBaseType_t MIXER_TASK_PRIORITY = 10; - -static const size_t TASK_DELAY_MS = 10; - -static const float FIRST_BOOT_DEFAULT_VOLUME = 0.5f; - -static const char *const TAG = "nabu_media_player"; - -void NabuMediaPlayer::setup() { - state = media_player::MEDIA_PLAYER_STATE_IDLE; - - this->media_control_command_queue_ = xQueueCreate(QUEUE_LENGTH, sizeof(MediaCallCommand)); - - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); - - VolumeRestoreState volume_restore_state; - if (this->pref_.load(&volume_restore_state)) { - this->set_volume_(volume_restore_state.volume); - this->set_mute_state_(volume_restore_state.is_muted); - } else { - this->set_volume_(FIRST_BOOT_DEFAULT_VOLUME); - this->set_mute_state_(false); - } - -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - if (this->audio_mixer_ != nullptr) { - this->audio_mixer_->suspend_task(); - } - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->suspend_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->suspend_tasks(); - } - } else if (state == ota::OTA_ERROR) { - if (this->audio_mixer_ != nullptr) { - this->audio_mixer_->resume_task(); - } - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->resume_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->resume_tasks(); - } - } - }); -#endif - - ESP_LOGI(TAG, "Set up nabu media player"); -} - -esp_err_t NabuMediaPlayer::start_pipeline_(AudioPipelineType type, bool url) { - esp_err_t err = ESP_OK; - - if (this->speaker_ != nullptr) { - audio::AudioStreamInfo audio_stream_info; - audio_stream_info.channels = 2; - audio_stream_info.bits_per_sample = 16; - audio_stream_info.sample_rate = this->sample_rate_; - - this->speaker_->set_audio_stream_info(audio_stream_info); - } - - if (this->audio_mixer_ == nullptr) { - this->audio_mixer_ = make_unique(); - err = this->audio_mixer_->start(this->speaker_, "mixer", MIXER_TASK_PRIORITY); - if (err != ESP_OK) { - return err; - } - } - - if (type == AudioPipelineType::MEDIA) { - if (this->media_pipeline_ == nullptr) { - this->media_pipeline_ = make_unique(this->audio_mixer_.get(), type); - } - - if (url) { - err = this->media_pipeline_->start(this->media_url_.value(), this->sample_rate_, "media", - MEDIA_PIPELINE_TASK_PRIORITY); - } else { - err = this->media_pipeline_->start(this->media_file_.value(), this->sample_rate_, "media", - MEDIA_PIPELINE_TASK_PRIORITY); - } - - if (this->is_paused_) { - CommandEvent command_event; - command_event.command = CommandEventType::RESUME_MEDIA; - this->audio_mixer_->send_command(&command_event); - } - this->is_paused_ = false; - } else if (type == AudioPipelineType::ANNOUNCEMENT) { - if (this->announcement_pipeline_ == nullptr) { - this->announcement_pipeline_ = make_unique(this->audio_mixer_.get(), type); - } - - if (url) { - err = this->announcement_pipeline_->start(this->announcement_url_.value(), this->sample_rate_, "ann", - ANNOUNCEMENT_PIPELINE_TASK_PRIORITY); - } else { - err = this->announcement_pipeline_->start(this->announcement_file_.value(), this->sample_rate_, "ann", - ANNOUNCEMENT_PIPELINE_TASK_PRIORITY); - } - } - - return err; -} - -void NabuMediaPlayer::watch_media_commands_() { - if (!this->is_ready()) { - return; - } - - MediaCallCommand media_command; - CommandEvent command_event; - esp_err_t err = ESP_OK; - - if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) { - if (media_command.new_url.has_value() && media_command.new_url.value()) { - if (media_command.announce.has_value() && media_command.announce.value()) { - err = this->start_pipeline_(AudioPipelineType::ANNOUNCEMENT, true); - } else { - err = this->start_pipeline_(AudioPipelineType::MEDIA, true); - } - } - - if (media_command.new_file.has_value() && media_command.new_file.value()) { - if (media_command.announce.has_value() && media_command.announce.value()) { - err = this->start_pipeline_(AudioPipelineType::ANNOUNCEMENT, false); - } else { - err = this->start_pipeline_(AudioPipelineType::MEDIA, false); - } - } - - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error starting the audio pipeline: %s", esp_err_to_name(err)); - this->status_set_error(); - } else { - this->status_clear_error(); - } - - if (media_command.volume.has_value()) { - this->set_volume_(media_command.volume.value()); - this->publish_state(); - } - - if (media_command.command.has_value()) { - switch (media_command.command.value()) { - case media_player::MEDIA_PLAYER_COMMAND_PLAY: - if ((this->audio_mixer_ != nullptr) && this->is_paused_) { - command_event.command = CommandEventType::RESUME_MEDIA; - this->audio_mixer_->send_command(&command_event); - } - this->is_paused_ = false; - break; - case media_player::MEDIA_PLAYER_COMMAND_PAUSE: - if ((this->audio_mixer_ != nullptr) && !this->is_paused_) { - command_event.command = CommandEventType::PAUSE_MEDIA; - this->audio_mixer_->send_command(&command_event); - } - this->is_paused_ = true; - break; - case media_player::MEDIA_PLAYER_COMMAND_STOP: - command_event.command = CommandEventType::STOP; - if (media_command.announce.has_value() && media_command.announce.value()) { - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->stop(); - } - } else { - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->stop(); - } - } - break; - case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: - if ((this->audio_mixer_ != nullptr) && this->is_paused_) { - command_event.command = CommandEventType::RESUME_MEDIA; - this->audio_mixer_->send_command(&command_event); - this->is_paused_ = false; - } else if (this->audio_mixer_ != nullptr) { - command_event.command = CommandEventType::PAUSE_MEDIA; - this->audio_mixer_->send_command(&command_event); - this->is_paused_ = true; - } - break; - case media_player::MEDIA_PLAYER_COMMAND_MUTE: { - this->set_mute_state_(true); - - this->publish_state(); - break; - } - case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: - this->set_mute_state_(false); - this->publish_state(); - break; - case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: - this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_)); - this->publish_state(); - break; - case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: - this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_)); - this->publish_state(); - break; - default: - break; - } - } - } -} - -void NabuMediaPlayer::watch_mixer_() { - TaskEvent event; - if (this->audio_mixer_ != nullptr) { - while (this->audio_mixer_->read_event(&event)) - if (event.type == EventType::WARNING) { - ESP_LOGD(TAG, "Mixer encountered an error: %s", esp_err_to_name(event.err)); - this->status_set_error(); - } - } -} - -void NabuMediaPlayer::loop() { - this->watch_media_commands_(); - this->watch_mixer_(); - - // Determine state of the media player - media_player::MediaPlayerState old_state = this->state; - - if (this->announcement_pipeline_ != nullptr) - this->announcement_pipeline_state_ = this->announcement_pipeline_->get_state(); - - if (this->media_pipeline_ != nullptr) - this->media_pipeline_state_ = this->media_pipeline_->get_state(); - - if (this->media_pipeline_state_ == AudioPipelineState::ERROR_READING) { - ESP_LOGE(TAG, "The media pipeline's file reader encountered an error."); - } else if (this->media_pipeline_state_ == AudioPipelineState::ERROR_DECODING) { - ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error."); - } else if (this->media_pipeline_state_ == AudioPipelineState::ERROR_RESAMPLING) { - ESP_LOGE(TAG, "The media pipeline's audio resampler encountered an error."); - } - - if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_READING) { - ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error."); - } else if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_DECODING) { - ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error."); - } else if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_RESAMPLING) { - ESP_LOGE(TAG, "The announcement pipeline's audio resampler encountered an error."); - } - - if (this->announcement_pipeline_state_ != AudioPipelineState::STOPPED) { - this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; - } else { - if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { - this->state = media_player::MEDIA_PLAYER_STATE_IDLE; - } else if (this->is_paused_) { - this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; - } else { - this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; - } - } - - if (this->state != old_state) { - this->publish_state(); - } -} - -void NabuMediaPlayer::set_ducking_reduction(uint8_t decibel_reduction, float duration) { - if (this->audio_mixer_ != nullptr) { - CommandEvent command_event; - command_event.command = CommandEventType::DUCK; - command_event.decibel_reduction = decibel_reduction; - - // Convert the duration in seconds to number of samples, accounting for the sample rate and number of channels - command_event.transition_samples = static_cast(duration * this->sample_rate_ * NUMBER_OF_CHANNELS); - this->audio_mixer_->send_command(&command_event); - } -} - -void NabuMediaPlayer::control(const media_player::MediaPlayerCall &call) { - if (!this->is_ready()) { - return; - } - - MediaCallCommand media_command; - - if (call.get_announcement().has_value() && call.get_announcement().value()) { - media_command.announce = true; - } else { - media_command.announce = false; - } - - if (call.get_media_url().has_value()) { - std::string new_uri = call.get_media_url().value(); - - media_command.new_url = true; - if (call.get_announcement().has_value() && call.get_announcement().value()) { - this->announcement_url_ = new_uri; - } else { - this->media_url_ = new_uri; - } - xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY); - return; - } - - if (call.get_local_media_file().has_value()) { - if (call.get_announcement().has_value() && call.get_announcement().value()) { - this->announcement_file_ = call.get_local_media_file().value(); - } else { - this->media_file_ = call.get_local_media_file().value(); - } - media_command.new_file = true; - xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY); - return; - } - - if (call.get_volume().has_value()) { - media_command.volume = call.get_volume().value(); - // Wait 0 ticks for queue to be free, volume sets aren't that important! - xQueueSend(this->media_control_command_queue_, &media_command, 0); - return; - } - - if (call.get_command().has_value()) { - media_command.command = call.get_command().value(); - TickType_t ticks_to_wait = portMAX_DELAY; - if ((call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) || - (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) { - ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important! - } - xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait); - return; - } -} - -media_player::MediaPlayerTraits NabuMediaPlayer::get_traits() { - auto traits = media_player::MediaPlayerTraits(); - traits.set_supports_pause(true); - traits.get_supported_formats().push_back( - media_player::MediaPlayerSupportedFormat{.format = "flac", - .sample_rate = this->sample_rate_, - .num_channels = 2, - .purpose = media_player::MediaPlayerFormatPurpose::PURPOSE_DEFAULT, - .sample_bytes = 2}); - traits.get_supported_formats().push_back( - media_player::MediaPlayerSupportedFormat{.format = "flac", - .sample_rate = this->sample_rate_, - .num_channels = 1, - .purpose = media_player::MediaPlayerFormatPurpose::PURPOSE_ANNOUNCEMENT, - .sample_bytes = 2}); - return traits; -}; - -void NabuMediaPlayer::save_volume_restore_state_() { - VolumeRestoreState volume_restore_state; - volume_restore_state.volume = this->volume; - volume_restore_state.is_muted = this->is_muted_; - this->pref_.save(&volume_restore_state); -} - -void NabuMediaPlayer::set_mute_state_(bool mute_state) { -#ifdef USE_AUDIO_DAC - if (this->audio_dac_ != nullptr) { - if (mute_state) { - this->audio_dac_->set_mute_on(); - } else { - this->audio_dac_->set_mute_off(); - } - } else -#endif - { // Fall back to software mute control if there is no audio_dac or if it isn't configured - if (mute_state) { - this->speaker_->set_volume(0.0f); - } else if (this->speaker_->get_volume() == 0.0f) { - this->set_volume_(this->volume, false); // restore previous volume - } - } - - bool old_mute_state = this->is_muted_; - this->is_muted_ = mute_state; - - this->save_volume_restore_state_(); - - if (old_mute_state != mute_state) { - if (mute_state) { - this->defer([this]() { this->mute_trigger_->trigger(); }); - } else { - this->defer([this]() { this->unmute_trigger_->trigger(); }); - } - } -} - -void NabuMediaPlayer::set_volume_(float volume, bool publish) { - // Remap the volume to fit with in the configured limits - float bounded_volume = remap(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); - -#ifdef USE_AUDIO_DAC - if (this->audio_dac_ != nullptr) { - this->audio_dac_->set_volume(bounded_volume); - } else -#endif - { // Fall back to the speaker's volume control if there is no audio_dac or if it isn't configured - this->speaker_->set_volume(bounded_volume); - } - - if (publish) { - this->volume = volume; - this->save_volume_restore_state_(); - } - - // Turn on the mute state if the volume is effectively zero, off otherwise - if (volume < 0.001) { - this->set_mute_state_(true); - } else { - this->set_mute_state_(false); - } - - this->defer([this, volume]() { this->volume_trigger_->trigger(volume); }); -} - -} // namespace nabu -} // namespace esphome -#endif diff --git a/esphome/components/nabu/nabu_media_player.h b/esphome/components/nabu/nabu_media_player.h deleted file mode 100644 index 7fd71662..00000000 --- a/esphome/components/nabu/nabu_media_player.h +++ /dev/null @@ -1,159 +0,0 @@ -#pragma once - -#ifdef USE_ESP_IDF - -#include "audio_mixer.h" -#include "audio_pipeline.h" - -#ifdef USE_AUDIO_DAC -#include "esphome/components/audio_dac/audio_dac.h" -#endif -#include "esphome/components/media_player/media_player.h" -#include "esphome/components/speaker/speaker.h" - -#include "esphome/core/automation.h" -#include "esphome/core/component.h" -#include "esphome/core/preferences.h" - -#include -#include - -#include - -namespace esphome { -namespace nabu { - -struct MediaCallCommand { - optional command; - optional volume; - optional announce; - optional new_url; - optional new_file; -}; - -struct VolumeRestoreState { - float volume; - bool is_muted; -}; - -class NabuMediaPlayer : public Component, public media_player::MediaPlayer { - public: - float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } - void setup() override; - void loop() override; - - // MediaPlayer implementations - media_player::MediaPlayerTraits get_traits() override; - bool is_muted() const override { return this->is_muted_; } - - /// @brief Sets the ducking level for the media stream in the mixer - /// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB - /// @param duration (float) The duration (in seconds) for transitioning to the new ducking level - void set_ducking_reduction(uint8_t decibel_reduction, float duration); - - void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } - - // Percentage to increase or decrease the volume for volume up or volume down commands - void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; } - - void set_volume_max(float volume_max) { this->volume_max_ = volume_max; } - void set_volume_min(float volume_min) { this->volume_min_ = volume_min; } - -#ifdef USE_AUDIO_DAC - void set_audio_dac(audio_dac::AudioDac *audio_dac) { this->audio_dac_ = audio_dac; } -#endif - - void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } - - Trigger<> *get_mute_trigger() const { return this->mute_trigger_; } - Trigger<> *get_unmute_trigger() const { return this->unmute_trigger_; } - Trigger *get_volume_trigger() const { return this->volume_trigger_; } - - protected: - // Receives commands from HA or from the voice assistant component - // Sends commands to the media_control_commanda_queue_ - void control(const media_player::MediaPlayerCall &call) override; - - /// @brief Updates this->volume and saves volume/mute state to flash for restortation if publish is true. - void set_volume_(float volume, bool publish = true); - - /// @brief Sets the mute state. Restores previous volume if unmuting. Always saves volume/mute state to flash for - /// restoration. - /// @param mute_state If true, audio will be muted. If false, audio will be unmuted - void set_mute_state_(bool mute_state); - - /// @brief Saves the current volume and mute state to the flash for restoration. - void save_volume_restore_state_(); - - // Reads commands from media_control_command_queue_. Starts pipelines and mixer if necessary. - void watch_media_commands_(); - - std::unique_ptr media_pipeline_; - std::unique_ptr announcement_pipeline_; - std::unique_ptr audio_mixer_; - - speaker::Speaker *speaker_{nullptr}; - - // Monitors the mixer task - void watch_mixer_(); - - // Starts the ``type`` pipeline with a ``url`` or file. Starts the mixer, pipeline, and speaker tasks if necessary. - // Unpauses if starting media in paused state - esp_err_t start_pipeline_(AudioPipelineType type, bool url); - - AudioPipelineState media_pipeline_state_{AudioPipelineState::STOPPED}; - AudioPipelineState announcement_pipeline_state_{AudioPipelineState::STOPPED}; - - optional media_url_{}; // only modified by control function - optional announcement_url_{}; // only modified by control function - optional media_file_{}; // only modified by control fucntion - optional announcement_file_{}; // only modified by control fucntion - - QueueHandle_t media_control_command_queue_; - - uint32_t sample_rate_; - - bool is_paused_{false}; - bool is_muted_{false}; - - // The amount to change the volume on volume up/down commands - float volume_increment_; - - float volume_max_; - float volume_min_; - -#ifdef USE_AUDIO_DAC - audio_dac::AudioDac *audio_dac_{nullptr}; -#endif - - // Used to save volume/mute state for restoration on reboot - ESPPreferenceObject pref_; - - Trigger<> *mute_trigger_ = new Trigger<>(); - Trigger<> *unmute_trigger_ = new Trigger<>(); - Trigger *volume_trigger_ = new Trigger(); -}; - -template class DuckingSetAction : public Action, public Parented { - TEMPLATABLE_VALUE(uint8_t, decibel_reduction) - TEMPLATABLE_VALUE(float, duration) - void play(Ts... x) override { - this->parent_->set_ducking_reduction(this->decibel_reduction_.value(x...), this->duration_.value(x...)); - } -}; - -template class PlayLocalMediaAction : public Action, public Parented { - TEMPLATABLE_VALUE(media_player::MediaFile *, media_file) - TEMPLATABLE_VALUE(bool, announcement) - void play(Ts... x) override { - this->parent_->make_call() - .set_announcement(this->announcement_.value(x...)) - .set_local_media_file(this->media_file_.value(x...)) - .perform(); - } -}; - -} // namespace nabu -} // namespace esphome - -#endif