Merge branch 'no_gen_empty_messages' into integration

# Conflicts:
#	esphome/components/esp32_hosted/update/esp32_hosted_update.cpp
#	esphome/core/component.cpp
This commit is contained in:
J. Nick Koston
2026-02-09 12:12:28 -06:00
13 changed files with 481 additions and 294 deletions
-91
View File
@@ -440,19 +440,6 @@ class PingResponse final : public ProtoMessage {
protected:
};
class DeviceInfoRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#ifdef USE_AREAS
class AreaInfo final : public ProtoMessage {
public:
@@ -546,19 +533,6 @@ class DeviceInfoResponse final : public ProtoMessage {
protected:
};
class ListEntitiesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 11;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class ListEntitiesDoneResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 19;
@@ -572,19 +546,6 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
protected:
};
class SubscribeStatesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 20;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_states_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#ifdef USE_BINARY_SENSOR
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
public:
@@ -1037,19 +998,6 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
};
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 34;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_homeassistant_services_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{};
@@ -1117,19 +1065,6 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_home_assistant_states_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 39;
@@ -2160,19 +2095,6 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
protected:
};
class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 80;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class BluetoothConnectionsFreeResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 81;
@@ -2279,19 +2201,6 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
protected:
};
class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 87;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 88;
-28
View File
@@ -764,10 +764,6 @@ const char *PingResponse::dump_to(DumpBuffer &out) const {
out.append("PingResponse {}");
return out.c_str();
}
const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const {
out.append("DeviceInfoRequest {}");
return out.c_str();
}
#ifdef USE_AREAS
const char *AreaInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "AreaInfo");
@@ -848,18 +844,10 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif
return out.c_str();
}
const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const {
out.append("ListEntitiesRequest {}");
return out.c_str();
}
const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
out.append("ListEntitiesDoneResponse {}");
return out.c_str();
}
const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeStatesRequest {}");
return out.c_str();
}
#ifdef USE_BINARY_SENSOR
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
@@ -1191,10 +1179,6 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeHomeassistantServicesRequest {}");
return out.c_str();
}
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceMap");
dump_field(out, "key", this->key);
@@ -1245,10 +1229,6 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const {
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeHomeAssistantStatesRequest {}");
return out.c_str();
}
const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id);
@@ -1924,10 +1904,6 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const {
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
return out.c_str();
}
const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeBluetoothConnectionsFreeRequest {}");
return out.c_str();
}
const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
dump_field(out, "free", this->free);
@@ -1970,10 +1946,6 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const {
dump_field(out, "error", this->error);
return out.c_str();
}
const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const {
out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}");
return out.c_str();
}
const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
dump_field(out, "address", this->address);
+8 -8
View File
@@ -27,7 +27,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break;
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
case 9 /* DeviceInfoRequest is empty */: // Connection setup only
if (!this->check_connection_setup_()) {
return;
}
@@ -76,21 +76,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_ping_response();
break;
}
case DeviceInfoRequest::MESSAGE_TYPE: {
case 9 /* DeviceInfoRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_device_info_request"));
#endif
this->on_device_info_request();
break;
}
case ListEntitiesRequest::MESSAGE_TYPE: {
case 11 /* ListEntitiesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_list_entities_request"));
#endif
this->on_list_entities_request();
break;
}
case SubscribeStatesRequest::MESSAGE_TYPE: {
case 20 /* SubscribeStatesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
#endif
@@ -151,7 +151,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
case 34 /* SubscribeHomeassistantServicesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
#endif
@@ -169,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break;
}
#ifdef USE_API_HOMEASSISTANT_STATES
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
case 38 /* SubscribeHomeAssistantStatesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
#endif
@@ -376,7 +376,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
#endif
@@ -385,7 +385,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
#endif
+4
View File
@@ -1435,6 +1435,10 @@ async def to_code(config):
CORE.relative_internal_path(".espressif")
)
# Set the uv cache inside the data dir so "Clean All" clears it.
# Avoids persistent corrupted cache from mid-stream download failures.
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
@@ -27,6 +27,11 @@ static const char *const TAG = "esp32_hosted.update";
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// Interval/timeout IDs (uint32_t to avoid string comparison)
constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
#endif
// Compile-time version string from esp_hosted_host_fw_ver.h macros
#define STRINGIFY_(x) #x
#define STRINGIFY(x) STRINGIFY_(x)
@@ -131,10 +136,10 @@ void Esp32HostedUpdate::setup() {
// Only if update interval is > 1 minute to avoid redundant checks
if (this->get_update_interval() > 60000) {
this->initial_check_remaining_ = 6;
this->set_interval("initial_check", 10000, [this]() {
this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() {
bool connected = network::is_connected();
if (--this->initial_check_remaining_ == 0 || connected) {
this->cancel_interval("initial_check");
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
if (connected) {
this->check();
}
+8 -2
View File
@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import audio, esp32, speaker
from esphome.components import audio, esp32, socket, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
@@ -61,7 +61,7 @@ def _set_stream_limits(config):
def _validate_source_speaker(config):
fconf = fv.full_config.get()
# Get ID for the output speaker and add it to the source speakrs config to easily inherit properties
# Get ID for the output speaker and add it to the source speakers config to easily inherit properties
path = fconf.get_path_for_id(config[CONF_ID])[:-3]
path.append(CONF_OUTPUT_SPEAKER)
output_speaker_id = fconf.get_config_for_path(path)
@@ -111,6 +111,9 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config):
# Enable wake_loop_threadsafe for immediate command processing from other tasks
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -127,6 +130,9 @@ async def to_code(config):
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
# Initialize FixedVector with exact count of source speakers
cg.add(var.init_source_speakers(len(config[CONF_SOURCE_SPEAKERS])))
for speaker_config in config[CONF_SOURCE_SPEAKERS]:
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])
@@ -8,8 +8,8 @@
namespace esphome {
namespace mixer_speaker {
template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> {
TEMPLATABLE_VALUE(uint8_t, decibel_reduction)
TEMPLATABLE_VALUE(uint32_t, duration)
TEMPLATABLE_VALUE(uint8_t, decibel_reduction);
TEMPLATABLE_VALUE(uint32_t, duration);
void play(const Ts &...x) override {
this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...));
}
+320 -118
View File
@@ -2,11 +2,13 @@
#ifdef USE_ESP32
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <array>
#include <cstring>
namespace esphome {
@@ -14,6 +16,7 @@ namespace mixer_speaker {
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
static const uint32_t STOPPING_TIMEOUT_MS = 5000;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 25;
@@ -27,21 +30,53 @@ static const char *const TAG = "speaker_mixer";
// 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<int16_t> DECIBEL_REDUCTION_TABLE = {
static const std::array<int16_t, 51> 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};
enum MixerEventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // stops the mixer task
STATE_STARTING = (1 << 10),
STATE_RUNNING = (1 << 11),
STATE_STOPPING = (1 << 12),
STATE_STOPPED = (1 << 13),
ERR_ESP_NO_MEM = (1 << 19),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
// Event bits for SourceSpeaker command processing
enum SourceSpeakerEventBits : uint32_t {
SOURCE_SPEAKER_COMMAND_START = (1 << 0),
SOURCE_SPEAKER_COMMAND_STOP = (1 << 1),
SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2),
};
// Event bits for mixer task control and state
enum MixerTaskEventBits : uint32_t {
MIXER_TASK_COMMAND_START = (1 << 0),
MIXER_TASK_COMMAND_STOP = (1 << 1),
MIXER_TASK_STATE_STARTING = (1 << 10),
MIXER_TASK_STATE_RUNNING = (1 << 11),
MIXER_TASK_STATE_STOPPING = (1 << 12),
MIXER_TASK_STATE_STOPPED = (1 << 13),
MIXER_TASK_ERR_ESP_NO_MEM = (1 << 19),
MIXER_TASK_ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
uint32_t current = var.load(std::memory_order_acquire);
uint32_t subtracted = 0;
if (current > 0) {
uint32_t new_value;
do {
subtracted = std::min(amount, current);
new_value = current - subtracted;
} while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire));
}
return subtracted;
}
static bool create_event_group(EventGroupHandle_t &event_group, Component *component) {
event_group = xEventGroupCreate();
if (event_group == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
component->mark_failed();
return false;
}
return true;
}
void SourceSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Mixer Source Speaker\n"
@@ -55,22 +90,70 @@ void SourceSpeaker::dump_config() {
}
void SourceSpeaker::setup() {
this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
// The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames
uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_);
this->pending_playback_frames_ -= speakers_playback_frames;
if (!create_event_group(this->event_group_, this)) {
return;
}
if (speakers_playback_frames > 0) {
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
// Start with loop disabled since we begin in STATE_STOPPED with no pending commands
this->disable_loop();
this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
// First, drain the playback delay (frames in pipeline before this source started contributing)
uint32_t delay_to_drain = atomic_subtract_clamped(this->playback_delay_frames_, new_frames);
uint32_t remaining_frames = new_frames - delay_to_drain;
// Then, count towards this source's pending playback frames
if (remaining_frames > 0) {
uint32_t speakers_playback_frames = atomic_subtract_clamped(this->pending_playback_frames_, remaining_frames);
if (speakers_playback_frames > 0) {
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
}
}
});
}
void SourceSpeaker::loop() {
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
// Process commands with priority: STOP > FINISH > START
// This ensures stop commands take precedence over conflicting start commands
if (event_bits & SOURCE_SPEAKER_COMMAND_STOP) {
if (this->state_ == speaker::STATE_RUNNING) {
// Clear both STOP and START bits - stop takes precedence
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
this->enter_stopping_state_();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bits
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
}
// Leave bits set if transitioning states (STARTING/STOPPING) - will be processed once state allows
} else if (event_bits & SOURCE_SPEAKER_COMMAND_FINISH) {
if (this->state_ == speaker::STATE_RUNNING) {
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
this->stop_gracefully_ = true;
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bit
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
}
// Leave bit set if transitioning states - will be processed once state allows
} else if (event_bits & SOURCE_SPEAKER_COMMAND_START) {
if (this->state_ == speaker::STATE_STOPPED) {
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
this->state_ = speaker::STATE_STARTING;
} else if (this->state_ == speaker::STATE_RUNNING) {
// Already running, just clear the command bit
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
}
// Leave bit set if transitioning states - will be processed once state allows
}
// Process state machine
switch (this->state_) {
case speaker::STATE_STARTING: {
esp_err_t err = this->start_();
if (err == ESP_OK) {
this->pending_playback_frames_.store(0, std::memory_order_release); // reset pending playback frames
this->playback_delay_frames_.store(0, std::memory_order_release); // reset playback delay
this->has_contributed_.store(false, std::memory_order_release); // reset contribution tracking
this->state_ = speaker::STATE_RUNNING;
this->stop_gracefully_ = false;
this->last_seen_data_ms_ = millis();
@@ -78,41 +161,62 @@ void SourceSpeaker::loop() {
} else {
switch (err) {
case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory"));
this->status_set_error(LOG_STR("Not enough memory"));
break;
case ESP_ERR_NOT_SUPPORTED:
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample"));
this->status_set_error(LOG_STR("Unsupported bit depth"));
break;
case ESP_ERR_INVALID_ARG:
this->status_set_error(
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
this->status_set_error(LOG_STR("Incompatible audio streams"));
break;
case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start"));
this->status_set_error(LOG_STR("Task failed"));
break;
default:
this->status_set_error(LOG_STR("Failed to start mixer"));
this->status_set_error(LOG_STR("Failed"));
break;
}
this->state_ = speaker::STATE_STOPPING;
this->enter_stopping_state_();
}
break;
}
case speaker::STATE_RUNNING:
if (!this->transfer_buffer_->has_buffered_data()) {
if (!this->transfer_buffer_->has_buffered_data() &&
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
// No audio data in buffer waiting to get mixed and no frames are pending playback
if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
this->stop_gracefully_) {
this->state_ = speaker::STATE_STOPPING;
// Timeout exceeded or graceful stop requested
this->enter_stopping_state_();
}
}
break;
case speaker::STATE_STOPPING:
this->stop_();
this->stop_gracefully_ = false;
this->state_ = speaker::STATE_STOPPED;
case speaker::STATE_STOPPING: {
if ((this->parent_->get_output_speaker()->get_pause_state()) ||
((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
// If parent speaker is paused or if the stopping timeout is exceeded, force stop the output speaker
this->parent_->get_output_speaker()->stop();
}
if (this->parent_->get_output_speaker()->is_stopped() ||
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
// Output speaker is stopped OR all pending playback frames have played
this->pending_playback_frames_.store(0, std::memory_order_release);
this->stop_gracefully_ = false;
this->state_ = speaker::STATE_STOPPED;
}
break;
}
case speaker::STATE_STOPPED:
// Re-check event bits for any new commands that may have arrived
event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits &
(SOURCE_SPEAKER_COMMAND_START | SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_FINISH))) {
// No pending commands, disable loop to save CPU cycles
this->disable_loop();
}
break;
}
}
@@ -122,17 +226,34 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
this->start();
}
size_t bytes_written = 0;
if (this->ring_buffer_.use_count() == 1) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer.use_count() > 0) {
// Only write to the ring buffer if the reference is valid
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
if (bytes_written > 0) {
this->last_seen_data_ms_ = millis();
}
} else {
// Delay to avoid repeatedly hammering while waiting for the speaker to start
vTaskDelay(ticks_to_wait);
}
return bytes_written;
}
void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
this->enable_loop_soon_any_context();
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits & command_bit)) {
xEventGroupSetBits(this->event_group_, command_bit);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
if (wake_loop) {
App.wake_loop_threadsafe();
}
#endif
}
}
void SourceSpeaker::start() { this->send_command_(SOURCE_SPEAKER_COMMAND_START, true); }
esp_err_t SourceSpeaker::start_() {
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
@@ -143,35 +264,26 @@ esp_err_t SourceSpeaker::start_() {
if (this->transfer_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
std::shared_ptr<RingBuffer> temp_ring_buffer;
if (!this->ring_buffer_.use_count()) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (!temp_ring_buffer) {
temp_ring_buffer = RingBuffer::create(ring_buffer_size);
this->ring_buffer_ = temp_ring_buffer;
}
if (!this->ring_buffer_.use_count()) {
if (!temp_ring_buffer) {
return ESP_ERR_NO_MEM;
} else {
this->transfer_buffer_->set_source(temp_ring_buffer);
}
}
this->pending_playback_frames_ = 0; // reset
return this->parent_->start(this->audio_stream_info_);
}
void SourceSpeaker::stop() {
if (this->state_ != speaker::STATE_STOPPED) {
this->state_ = speaker::STATE_STOPPING;
}
}
void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
void SourceSpeaker::stop_() {
this->transfer_buffer_.reset(); // deallocates the transfer buffer
}
void SourceSpeaker::finish() { this->stop_gracefully_ = true; }
void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
bool SourceSpeaker::has_buffered_data() const {
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
@@ -191,19 +303,16 @@ void SourceSpeaker::set_volume(float volume) {
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
if (!this->transfer_buffer_.use_count()) {
return 0;
}
size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
TickType_t ticks_to_wait) {
// Store current offset, as these samples are already ducked
const size_t current_length = this->transfer_buffer_->available();
const size_t current_length = transfer_buffer->available();
size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait);
size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait);
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
if (samples_to_duck > 0) {
int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length);
int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length);
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
@@ -215,10 +324,13 @@ size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
if (this->target_ducking_db_reduction_ != decibel_reduction) {
// Start transition from the previous target (which becomes the new current level)
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
this->target_ducking_db_reduction_ = decibel_reduction;
// Calculate the number of intermediate dB steps for the transition timing.
// Subtract 1 because the first step is taken immediately after this calculation.
uint8_t total_ducking_steps = 0;
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
// The dB reduction level is increasing (which results in quieter audio)
@@ -234,7 +346,7 @@ void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration)
this->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
this->ducking_transition_samples_remaining_ =
this->samples_per_ducking_step_ * total_ducking_steps; // Adjust for integer division rounding
this->samples_per_ducking_step_ * total_ducking_steps; // adjust for integer division rounding
this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
} else {
@@ -293,6 +405,12 @@ void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_t
}
}
void SourceSpeaker::enter_stopping_state_() {
this->state_ = speaker::STATE_STOPPING;
this->stopping_start_ms_ = millis();
this->transfer_buffer_.reset();
}
void MixerSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Speaker Mixer:\n"
@@ -301,42 +419,74 @@ void MixerSpeaker::dump_config() {
}
void MixerSpeaker::setup() {
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
if (!create_event_group(this->event_group_, this)) {
return;
}
// Register callback to track frames in the output pipeline
this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
atomic_subtract_clamped(this->frames_in_pipeline_, new_frames);
});
// Start with loop disabled since no task is running and no commands are pending
this->disable_loop();
}
void MixerSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & MixerEventGroupBits::STATE_STARTING) {
ESP_LOGD(TAG, "Starting speaker mixer");
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
// Handle pending start request
if (event_group_bits & MIXER_TASK_COMMAND_START) {
// Only start the task if it's fully stopped and cleaned up
if (!this->status_has_error() && (this->task_handle_ == nullptr) && (this->task_stack_buffer_ == nullptr)) {
esp_err_t err = this->start_task_();
switch (err) {
case ESP_OK:
xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START);
break;
case ESP_ERR_NO_MEM:
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
this->status_momentary_error("memory-failure", 1000);
return;
case ESP_ERR_INVALID_STATE:
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
this->status_momentary_error("task-failure", 1000);
return;
default:
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
this->status_momentary_error("failure", 1000);
return;
}
}
}
if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer"));
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
if (event_group_bits & MIXER_TASK_STATE_STARTING) {
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
}
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
ESP_LOGD(TAG, "Started speaker mixer");
if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Not enough memory"));
xEventGroupClearBits(this->event_group_, MIXER_TASK_ERR_ESP_NO_MEM);
}
if (event_group_bits & MIXER_TASK_STATE_RUNNING) {
ESP_LOGV(TAG, "Started");
this->status_clear_error();
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING);
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
}
if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) {
ESP_LOGD(TAG, "Stopping speaker mixer");
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING);
if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
}
if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) {
if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
if (this->delete_task_() == ESP_OK) {
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS);
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
}
}
if (this->task_handle_ != nullptr) {
// If the mixer task is running, check if all source speakers are stopped
bool all_stopped = true;
for (auto &speaker : this->source_speakers_) {
@@ -344,7 +494,15 @@ void MixerSpeaker::loop() {
}
if (all_stopped) {
this->stop();
// Send stop command signal to the mixer task since no source speakers are active
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
}
} else if (this->task_stack_buffer_ == nullptr) {
// Task is fully stopped and cleaned up, check if we can disable loop
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits == 0) {
// No pending events, disable loop to save CPU cycles
this->disable_loop();
}
}
}
@@ -366,7 +524,18 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
}
}
return this->start_task_();
this->enable_loop_soon_any_context(); // ensure loop processes command
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits & MIXER_TASK_COMMAND_START)) {
// Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
}
return ESP_OK;
}
esp_err_t MixerSpeaker::start_task_() {
@@ -397,28 +566,31 @@ esp_err_t MixerSpeaker::start_task_() {
}
esp_err_t MixerSpeaker::delete_task_() {
if (!this->task_created_) {
if (this->task_handle_ != nullptr) {
// Delete the task
vTaskDelete(this->task_handle_);
this->task_handle_ = nullptr;
if (this->task_stack_buffer_ != nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
}
this->task_stack_buffer_ = nullptr;
}
return ESP_OK;
}
return ESP_ERR_INVALID_STATE;
}
if ((this->task_handle_ == nullptr) && (this->task_stack_buffer_ != nullptr)) {
// Deallocate the task stack buffer
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
}
void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); }
this->task_stack_buffer_ = nullptr;
}
if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) {
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
@@ -472,32 +644,34 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio
}
void MixerSpeaker::audio_mixer_task(void *params) {
MixerSpeaker *this_mixer = (MixerSpeaker *) params;
MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING);
this_mixer->task_created_ = true;
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
if (output_transfer_buffer == nullptr) {
xEventGroupSetBits(this_mixer->event_group_,
MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM);
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
this_mixer->task_created_ = false;
vTaskDelete(nullptr);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING);
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
bool sent_finished = false;
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
FixedVector<SourceSpeaker *> speakers_with_data;
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
speakers_with_data.init(this_mixer->source_speakers_.size());
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
while (true) {
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) {
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
break;
}
@@ -507,15 +681,20 @@ void MixerSpeaker::audio_mixer_task(void *params) {
const uint32_t output_frames_free =
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
std::vector<SourceSpeaker *> speakers_with_data;
std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
speakers_with_data.clear();
transfer_buffers_with_data.clear();
for (auto &speaker : this_mixer->source_speakers_) {
if (speaker->get_transfer_buffer().use_count() > 0) {
if (speaker->is_running() && !speaker->get_pause_state()) {
// Speaker is running and not paused, so it possibly can provide audio data
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers
if (transfer_buffer.use_count() == 0) {
// No transfer buffer allocated, so skip processing this speaker
continue;
}
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) {
if (transfer_buffer->available() > 0) {
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
transfer_buffers_with_data.push_back(transfer_buffer);
speakers_with_data.push_back(speaker);
@@ -547,13 +726,21 @@ void MixerSpeaker::audio_mixer_task(void *params) {
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
// Update source speaker buffer length
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
speakers_with_data[0]->pending_playback_frames_ += frames_to_mix;
// Set playback delay for newly contributing source
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[0]->playback_delay_frames_.store(
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
}
// Update output transfer buffer length
// Update source speaker pending frames
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
// Update output transfer buffer length and pipeline frame count
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
} else {
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
if (!this_mixer->output_speaker_->is_stopped()) {
@@ -568,6 +755,8 @@ void MixerSpeaker::audio_mixer_task(void *params) {
active_stream_info.get_sample_rate());
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
this_mixer->output_speaker_->start();
// Reset pipeline frame count since we're starting fresh with a new sample rate
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
sent_finished = false;
}
}
@@ -596,26 +785,39 @@ void MixerSpeaker::audio_mixer_task(void *params) {
}
}
// Get current pipeline depth for delay calculation (before incrementing)
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
// Set playback delay for newly contributing sources
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
}
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[i]->decrease_buffer_length(
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
speakers_with_data[i]->pending_playback_frames_ += frames_to_mix;
}
// Update output transfer buffer length
// Update output transfer buffer length and pipeline frame count (once, not per source)
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
}
}
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPING);
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
// Reset pipeline frame count since the task is stopping
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
output_transfer_buffer.reset();
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED);
this_mixer->task_created_ = false;
vTaskDelete(nullptr);
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
} // namespace mixer_speaker
@@ -7,26 +7,31 @@
#include "esphome/components/speaker/speaker.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <freertos/event_groups.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <atomic>
namespace esphome {
namespace mixer_speaker {
/* Classes for mixing several source speaker audio streams and writing it to another speaker component.
* - Volume controls are passed through to the output speaker
* - Source speaker commands are signaled via event group bits and processed in its loop function to ensure thread
* safety
* - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker.
* - Audio sent to the SourceSpeaker's must have 16 bits per sample.
* - Audio sent to the SourceSpeaker must have 16 bits per sample.
* - Audio sent to the SourceSpeaker can have any number of channels. They are duplicated or ignored as needed to match
* the number of channels required for the output speaker.
* - In queue mode, the audio sent to the SoureSpeakers can have different sample rates.
* - In queue mode, the audio sent to the SourceSpeakers can have different sample rates.
* - In non-queue mode, the audio sent to the SourceSpeakers must have the same sample rates.
* - SourceSpeaker has an internal ring buffer. It also allocates a shared_ptr for an AudioTranserBuffer object.
* - Audio Data Flow:
* - Audio data played on a SourceSpeaker first writes to its internal ring buffer.
* - MixerSpeaker task temporarily takes shared ownership of each SourceSpeaker's AudioTransferBuffer.
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which tranfers audio from the SourceSpeaker's
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which transfers audio from the SourceSpeaker's
* ring buffer to its AudioTransferBuffer. Audio ducking is applied at this step.
* - In queue mode, MixerSpeaker prioritizes the earliest configured SourceSpeaker with audio data. Audio data is
* sent to the output speaker.
@@ -63,13 +68,15 @@ class SourceSpeaker : public speaker::Speaker, public Component {
bool get_pause_state() const override { return this->pause_state_; }
/// @brief Transfers audio from the ring buffer into the transfer buffer. Ducks audio while transferring.
/// @param transfer_buffer Locked shared_ptr to the transfer buffer (must be valid, not null)
/// @param ticks_to_wait FreeRTOS ticks to wait while waiting to read from the ring buffer.
/// @return Number of bytes transferred from the ring buffer.
size_t process_data_from_source(TickType_t ticks_to_wait);
size_t process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
TickType_t ticks_to_wait);
/// @brief Sets the ducking level for the source speaker.
/// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
/// @param duration (uint32_t) The number of milliseconds to transition from the current level to the new level
/// @param decibel_reduction The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
/// @param duration The number of milliseconds to transition from the current level to the new level
void apply_ducking(uint8_t decibel_reduction, uint32_t duration);
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
@@ -81,14 +88,15 @@ class SourceSpeaker : public speaker::Speaker, public Component {
protected:
friend class MixerSpeaker;
esp_err_t start_();
void stop_();
void enter_stopping_state_();
void send_command_(uint32_t command_bit, bool wake_loop = false);
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
/// over a specified amount of samples.
/// @param input_buffer buffer with audio samples to be ducked in place
/// @param input_samples_to_duck number of samples to process in ``input_buffer``
/// @param current_ducking_db_reduction pointer to the current dB reduction
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the the
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the
/// transition is finished
/// @param samples_per_ducking_step total number of samples per ducking step for the transition
/// @param db_change_per_ducking_step the change in dB reduction per step
@@ -114,7 +122,12 @@ class SourceSpeaker : public speaker::Speaker, public Component {
uint32_t ducking_transition_samples_remaining_{0};
uint32_t samples_per_ducking_step_{0};
uint32_t pending_playback_frames_{0};
std::atomic<uint32_t> pending_playback_frames_{0};
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
EventGroupHandle_t event_group_{nullptr};
uint32_t stopping_start_ms_{0};
};
class MixerSpeaker : public Component {
@@ -123,10 +136,11 @@ class MixerSpeaker : public Component {
void setup() override;
void loop() override;
void init_source_speakers(size_t count) { this->source_speakers_.init(count); }
void add_source_speaker(SourceSpeaker *source_speaker) { this->source_speakers_.push_back(source_speaker); }
/// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information
/// @param stream_info The calling source speakers audio stream information
/// @param stream_info The calling source speaker's audio stream information
/// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample
/// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream
/// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack
@@ -134,8 +148,6 @@ class MixerSpeaker : public Component {
/// ESP_OK if the incoming stream is compatible and the mixer task starts
esp_err_t start(audio::AudioStreamInfo &stream_info);
void stop();
void set_output_channels(uint8_t output_channels) { this->output_channels_ = output_channels; }
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
@@ -143,6 +155,9 @@ class MixerSpeaker : public Component {
speaker::Speaker *get_output_speaker() const { return this->output_speaker_; }
/// @brief Returns the current number of frames in the output pipeline (written but not yet played)
uint32_t get_frames_in_pipeline() const { return this->frames_in_pipeline_.load(std::memory_order_acquire); }
protected:
/// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels
/// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has
@@ -159,11 +174,11 @@ class MixerSpeaker : public Component {
/// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number
/// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample
/// overflows.
/// @param primary_buffer (int16_t *) samples buffer for the primary stream
/// @param primary_buffer samples buffer for the primary stream
/// @param primary_stream_info stream info for the primary stream
/// @param secondary_buffer (int16_t *) samples buffer for secondary stream
/// @param secondary_buffer samples buffer for secondary stream
/// @param secondary_stream_info stream info for the secondary stream
/// @param output_buffer (int16_t *) buffer for the mixed samples
/// @param output_buffer buffer for the mixed samples
/// @param output_stream_info stream info for the output buffer
/// @param frames_to_mix number of frames in the primary and secondary buffers to mix together
static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
@@ -185,20 +200,20 @@ class MixerSpeaker : public Component {
EventGroupHandle_t event_group_{nullptr};
std::vector<SourceSpeaker *> source_speakers_;
FixedVector<SourceSpeaker *> source_speakers_;
speaker::Speaker *output_speaker_{nullptr};
uint8_t output_channels_;
bool queue_mode_;
bool task_stack_in_psram_{false};
bool task_created_{false};
TaskHandle_t task_handle_{nullptr};
StaticTask_t task_stack_;
StackType_t *task_stack_buffer_{nullptr};
optional<audio::AudioStreamInfo> audio_stream_info_;
std::atomic<uint32_t> frames_in_pipeline_{0}; // Frames written to output but not yet played
};
} // namespace mixer_speaker
+25 -14
View File
@@ -228,39 +228,50 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
return;
}
std::vector<uint8_t> data;
data.push_back(address);
data.push_back(function_code);
static constexpr size_t ADDR_SIZE = 1;
static constexpr size_t FC_SIZE = 1;
static constexpr size_t START_ADDR_SIZE = 2;
static constexpr size_t NUM_ENTITIES_SIZE = 2;
static constexpr size_t BYTE_COUNT_SIZE = 1;
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
static constexpr size_t CRC_SIZE = 2;
static constexpr size_t MAX_FRAME_SIZE =
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
uint8_t data[MAX_FRAME_SIZE];
size_t pos = 0;
data[pos++] = address;
data[pos++] = function_code;
if (this->role == ModbusRole::CLIENT) {
data.push_back(start_address >> 8);
data.push_back(start_address >> 0);
data[pos++] = start_address >> 8;
data[pos++] = start_address >> 0;
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0);
data[pos++] = number_of_entities >> 8;
data[pos++] = number_of_entities >> 0;
}
}
if (payload != nullptr) {
if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
data.push_back(payload_len); // Byte count is required for write
data[pos++] = payload_len; // Byte count is required for write
} else {
payload_len = 2; // Write single register or coil
}
for (int i = 0; i < payload_len; i++) {
data.push_back(payload[i]);
data[pos++] = payload[i];
}
}
auto crc = crc16(data.data(), data.size());
data.push_back(crc >> 0);
data.push_back(crc >> 8);
auto crc = crc16(data, pos);
data[pos++] = crc >> 0;
data[pos++] = crc >> 8;
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(true);
this->write_array(data);
this->write_array(data, pos);
this->flush();
if (this->flow_control_pin_ != nullptr)
@@ -270,7 +281,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
}
// Helper function for lambdas
@@ -1,8 +1,11 @@
from pathlib import Path
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.helpers import copy_file_if_changed
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
@@ -49,5 +52,15 @@ async def to_code(config):
CORE.add_platformio_option(
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
)
# ESPAsyncWebServer uses Hash library for sha1() on RP2040
cg.add_library("Hash", None)
# Fix Hash.h include conflict: Crypto-no-arduino (used by dsmr)
# provides a Hash.h that shadows the framework's Hash library.
# Prepend the framework Hash path so it's found first.
copy_file_if_changed(
Path(__file__).parent / "fix_rp2040_hash.py.script",
CORE.relative_build_path("fix_rp2040_hash.py"),
)
cg.add_platformio_option("extra_scripts", ["pre:fix_rp2040_hash.py"])
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
@@ -0,0 +1,11 @@
# ESPAsyncWebServer includes <Hash.h> expecting the Arduino-Pico framework's Hash
# library (which provides sha1() functions). However, the Crypto-no-arduino library
# (used by dsmr) also provides a Hash.h that can shadow the framework version when
# PlatformIO's chain+ LDF mode auto-discovers it as a dependency.
# Prepend the framework Hash path to CXXFLAGS so it is found first.
import os
Import("env")
framework_dir = env.PioPlatform().get_package_dir("framework-arduinopico")
hash_src = os.path.join(framework_dir, "libraries", "Hash", "src")
env.Prepend(CXXFLAGS=["-I" + hash_src])
+49 -10
View File
@@ -2277,6 +2277,12 @@ ifdefs: dict[str, str] = {}
# Track messages with no fields (empty messages) for parameter elision
EMPTY_MESSAGES: set[str] = set()
# Track empty SOURCE_CLIENT messages that don't need class generation
# These messages have no fields and are only received (never sent), so the
# class definition (vtable, dump_to, message_name, ESTIMATED_SIZE) is dead code
# that the compiler compiles but the linker strips away.
SKIP_CLASS_GENERATION: set[str] = set()
def get_opt(
desc: descriptor.DescriptorProto,
@@ -2527,7 +2533,11 @@ def build_service_message_type(
case += "#endif\n"
case += f"this->{func}({'msg' if not is_empty else ''});\n"
case += "break;"
RECEIVE_CASES[id_] = (case, ifdef, mt.name)
if mt.name in SKIP_CLASS_GENERATION:
case_label = f"{id_} /* {mt.name} is empty */"
else:
case_label = f"{mt.name}::MESSAGE_TYPE"
RECEIVE_CASES[id_] = (case, ifdef, case_label)
# Only close ifdef if we opened it
if ifdef is not None:
@@ -2723,6 +2733,19 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
mt = file.message_type
# Identify empty SOURCE_CLIENT messages that don't need class generation
for m in mt:
if m.options.deprecated:
continue
if not m.options.HasExtension(pb.id):
continue
source = message_source_map.get(m.name)
if source != SOURCE_CLIENT:
continue
has_fields = any(not field.options.deprecated for field in m.field)
if not has_fields:
SKIP_CLASS_GENERATION.add(m.name)
# Collect messages by base class
base_class_groups = collect_messages_by_base_class(mt)
@@ -2755,6 +2778,10 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
if m.name not in used_messages and not m.options.HasExtension(pb.id):
continue
# Skip class generation for empty SOURCE_CLIENT messages
if m.name in SKIP_CLASS_GENERATION:
continue
s, c, dc = build_message_type(m, base_class_fields, message_source_map)
msg_ifdef = message_ifdef_map.get(m.name)
@@ -2901,10 +2928,18 @@ static const char *const TAG = "api.service";
no_conn_ids: set[int] = set()
conn_only_ids: set[int] = set()
for id_, (_, _, case_msg_name) in cases:
if case_msg_name in message_auth_map:
needs_auth = message_auth_map[case_msg_name]
needs_conn = message_conn_map[case_msg_name]
# Build a reverse lookup from case_label to message name for auth lookups
case_label_to_msg: dict[int, str] = {}
for mt in file.message_type:
id_ = get_opt(mt, pb.id)
if id_ is not None and not mt.options.deprecated:
case_label_to_msg[id_] = mt.name
for id_, (_, _, case_label) in cases:
msg_name = case_label_to_msg.get(id_, "")
if msg_name in message_auth_map:
needs_auth = message_auth_map[msg_name]
needs_conn = message_conn_map[msg_name]
if not needs_conn:
no_conn_ids.add(id_)
@@ -2915,10 +2950,10 @@ static const char *const TAG = "api.service";
def generate_cases(ids: set[int], comment: str) -> str:
result = ""
for id_ in sorted(ids):
_, ifdef, msg_name = RECEIVE_CASES[id_]
_, ifdef, case_label = RECEIVE_CASES[id_]
if ifdef:
result += f"#ifdef {ifdef}\n"
result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n"
result += f" case {case_label}: {comment}\n"
if ifdef:
result += "#endif\n"
return result
@@ -2930,8 +2965,8 @@ static const char *const TAG = "api.service";
out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n"
# Auth check block before dispatch switch
out += " // Check authentication/connection requirements\n"
if no_conn_ids or conn_only_ids:
out += " // Check authentication/connection requirements\n"
out += " switch (msg_type) {\n"
if no_conn_ids:
@@ -2951,14 +2986,18 @@ static const char *const TAG = "api.service";
out += " }\n"
out += " break;\n"
out += " }\n"
else:
out += " if (!this->check_authenticated_()) {\n"
out += " return;\n"
out += " }\n"
# Dispatch switch
out += " switch (msg_type) {\n"
for i, (case, ifdef, message_name) in cases:
for i, (case, ifdef, case_label) in cases:
if ifdef is not None:
out += f"#ifdef {ifdef}\n"
c = f" case {message_name}::MESSAGE_TYPE: {{\n"
c = f" case {case_label}: {{\n"
c += indent(case, " ") + "\n"
c += " }"
out += c + "\n"