Compare commits

..
15 changed files with 75 additions and 152 deletions

No files matched your search

+1 -19
View File
@@ -395,7 +395,7 @@ file does, and it is the authority when they disagree. The most useful starting
* **Triggers -- Callback method (preferred):**
Use `build_callback_automation()` for simple triggers. The automation is registered directly as a callback on the parent, so there is no C++ Trigger class and nothing is allocated. No `CONF_TRIGGER_ID` in the schema.
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema.
**Python:**
```python
@@ -448,24 +448,6 @@ file does, and it is the authority when they disagree. The most useful starting
forward=[cg.StringRef(parent.option_at(index)), index],
)
```
**Choosing the filter:** a filter that many automations share, such as entering one value of a state enum,
is a small forwarder struct templated on that value. Every automation for that value then shares one
function and passes only its `Automation *`. `when` generates a separate lambda per automation, which cost
16 to 64 bytes more flash when lock, media_player, alarm_control_panel and fusb302b were tried with it.
```cpp
template<LockState State> struct LockStateForwarder {
Automation<> *automation;
void operator()(LockState state) const {
if (state == State)
this->automation->trigger();
}
};
```
Use `when` for a test that is not a fixed value shared across automations, e.g. one that reads the parent:
`when="{parent}->is_fully_open()"` (valve). Only a trigger that must remember something between calls
(the last position, a click count) keeps a Trigger class.
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
+1 -1
View File
@@ -188,7 +188,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
// Expose the next chunk of file data. Every decoder buffers internally and consumes only what it
// processed, so the source does not need to accumulate or stitch chunks across fill() calls.
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
const size_t available_before_decode = this->input_buffer_->available();
+3 -2
View File
@@ -129,8 +129,9 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
delay(READ_WRITE_TIMEOUT_MS);
}
// Expose a chunk of the ring buffer's internal storage.
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Expose a chunk of the ring buffer's internal storage. pre_shift is ignored by RingBufferAudioSource
// (there is no intermediate transfer buffer to compact).
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (this->audio_source_->available() == 0) {
// No samples available to process
@@ -20,6 +20,16 @@ std::unique_ptr<AudioSinkTransferBuffer> AudioSinkTransferBuffer::create(size_t
return sink_buffer;
}
std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
if (!source_buffer->allocate_buffer_(buffer_size)) {
return nullptr;
}
return source_buffer;
}
size_t AudioTransferBuffer::free() const {
if (this->buffer_size_ == 0) {
return 0;
@@ -122,6 +132,27 @@ void AudioTransferBuffer::deallocate_buffer_() {
this->buffer_length_ = 0;
}
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
if (pre_shift) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
}
size_t bytes_to_read = AudioTransferBuffer::free();
size_t bytes_read = 0;
if (bytes_to_read > 0) {
if (this->ring_buffer_ != nullptr) {
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
}
this->increase_buffer_length(bytes_read);
}
return bytes_read;
}
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift) {
size_t bytes_written = 0;
if (this->available()) {
@@ -161,44 +192,9 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
return (this->available() > 0);
}
// AudioSourceTransferBuffer is deprecated. Remove before 2027.5.0.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
if (!source_buffer->allocate_buffer_(buffer_size)) {
return nullptr;
}
return source_buffer;
}
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
if (pre_shift) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
}
size_t bytes_to_read = AudioTransferBuffer::free();
size_t bytes_read = 0;
if (bytes_to_read > 0) {
if (this->ring_buffer_ != nullptr) {
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
}
this->increase_buffer_length(bytes_read);
}
return bytes_read;
}
size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); }
bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); }
#pragma GCC diagnostic pop
void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) {
this->data_start_ = data;
@@ -272,23 +268,6 @@ void RingBufferAudioSource::clear_buffered_data() {
this->ring_buffer_->reset();
}
size_t RingBufferAudioSource::expose_padded_partial(size_t frame_bytes) {
if (this->current_available_ > 0 || this->queued_length_ > 0) {
return 0;
}
// Moves any trailing bytes still inside a held item into the splice buffer
this->release_item_();
const size_t real_bytes = this->splice_length_;
if (real_bytes == 0 || frame_bytes == 0 || real_bytes % frame_bytes != 0) {
return 0;
}
std::memset(this->splice_buffer_ + real_bytes, 0, this->alignment_bytes_ - real_bytes);
this->splice_length_ = 0;
this->current_data_ = this->splice_buffer_;
this->current_available_ = this->alignment_bytes_;
return real_bytes;
}
bool RingBufferAudioSource::has_buffered_data() const {
// splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion
// bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports.
@@ -297,7 +276,7 @@ bool RingBufferAudioSource::has_buffered_data() const {
return (this->current_available_ > 0) || (this->queued_length_ > 0) || (this->ring_buffer_->available() > 0);
}
size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait) {
size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait, bool /*pre_shift*/) {
if (this->current_available_ > 0) {
// Caller has not finished consuming the current exposure
return 0;
@@ -3,7 +3,6 @@
#ifdef USE_ESP32
#include "esphome/components/ring_buffer/ring_buffer.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" // for ESPDEPRECATED
#ifdef USE_SPEAKER
#include "esphome/components/speaker/speaker.h"
@@ -153,20 +152,13 @@ class AudioReadableBuffer {
/// @brief Refills the buffer from its source. No-op by default for read-only buffers.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for data
/// @param pre_shift If true, shifts existing data to the start of the buffer before reading
/// @return Number of bytes read
virtual size_t fill(TickType_t ticks_to_wait) { return 0; }
// Remove before 2027.5.0
ESPDEPRECATED("Use fill(ticks_to_wait) instead. Removed in 2027.5.0.", "2026.11.0")
virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return this->fill(ticks_to_wait); }
virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; }
size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); }
};
// Remove before 2027.5.0
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
class ESPDEPRECATED("Use RingBufferAudioSource instead. Removed in 2027.5.0.", "2026.11.0") AudioSourceTransferBuffer
: public AudioTransferBuffer,
public AudioReadableBuffer {
class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer {
/*
* @brief A class that implements a transfer buffer for audio sources.
* Supports reading audio data from a ring buffer into the transfer buffer for processing.
@@ -197,12 +189,10 @@ class ESPDEPRECATED("Use RingBufferAudioSource instead. Removed in 2027.5.0.", "
size_t free() const override;
void consume(size_t bytes) override { this->decrease_buffer_length(bytes); }
bool has_buffered_data() const override;
size_t fill(TickType_t ticks_to_wait) override { return this->transfer_data_from_source(ticks_to_wait); }
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override {
return this->transfer_data_from_source(ticks_to_wait, pre_shift);
}
};
#pragma GCC diagnostic pop
/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory).
/// Does not allocate memory or transfer data from external sources.
@@ -256,21 +246,14 @@ class RingBufferAudioSource : public AudioReadableBuffer {
size_t available() const override { return this->current_available_; }
void consume(size_t bytes) override;
bool has_buffered_data() const override;
/// An unconsumed exposure stays in place and fill() returns 0 until it is fully consumed.
size_t fill(TickType_t ticks_to_wait) override;
// Keeps the deprecated fill(ticks_to_wait, pre_shift) overload visible. Remove before 2027.5.0.
using AudioReadableBuffer::fill;
/// pre_shift is ignored: there is no intermediate transfer buffer to compact, so an unconsumed
/// exposure stays in place and fill() returns 0 until it is fully consumed.
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override;
/// @brief Discards all buffered audio: releases any held ring buffer item, clears the source's in-flight
/// state, and resets the underlying ring buffer. Must be invoked from the ring buffer's consumer thread.
void clear_buffered_data();
/// @brief Zero-pads held whole frames short of an alignment unit and exposes them, so a stream's tail is not
/// stranded. A held partial frame is left to wait for its remaining bytes.
/// @param frame_bytes Bytes per audio frame
/// @return Real (unpadded) bytes exposed, or 0 if nothing is exposable
size_t expose_padded_partial(size_t frame_bytes);
/// @brief Returns a mutable pointer to the currently exposed audio data.
/// The pointer may reference the ring buffer's internal storage or, when exposing a stitched frame
/// across a wrap boundary, an internal splice buffer. In either case mutations are safe but data
@@ -334,7 +334,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
if (!this->pause_state_) {
while (real_frames_in_block < SPDIF_BLOCK_SAMPLES) {
if (audio_source->available() == 0) {
size_t bytes_read = audio_source->fill(read_timeout_ticks);
size_t bytes_read = audio_source->fill(read_timeout_ticks, false);
if (bytes_read == 0) {
break; // No upstream data within the read budget; silence-pad the remainder.
}
@@ -372,18 +372,10 @@ void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_rea
this->current_stream_info_.bytes_to_samples(bytes_read));
}
bool I2SAudioSpeakerBase::esp32_mono_swap_needed_() const {
#ifdef USE_ESP32_VARIANT_ESP32
return this->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16;
#else
return false;
#endif // USE_ESP32_VARIANT_ESP32
}
void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
#ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 16-bit mono mode, adjacent samples need to be swapped.
if (this->esp32_mono_swap_needed_()) {
if (this->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16) {
int16_t *samples = reinterpret_cast<int16_t *>(data);
size_t sample_count = bytes_read / sizeof(int16_t);
for (size_t i = 0; i + 1 < sample_count; i += 2) {
@@ -160,11 +160,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
/// @param bytes_read Number of bytes of audio data
void apply_software_volume_(uint8_t *data, size_t bytes_read);
/// @brief Whether the output is 16-bit mono on the original ESP32, which sends each pair of samples swapped.
bool esp32_mono_swap_needed_() const;
/// @brief Swap adjacent 16-bit mono samples for ESP32 (non-variant) hardware quirk.
/// Only applies when esp32_mono_swap_needed_() is true. `data` must start on a DMA word and hold whole pairs.
/// Only applies when running on original ESP32 with 16-bit mono output. Operates on the data that is
/// handed to the I2S peripheral, so the check uses the output (post-narrowing) stream info.
/// @param data Pointer to audio sample data (modified in place)
/// @param bytes_read Number of bytes of audio data
void swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read);
@@ -44,10 +44,10 @@ static constexpr TickType_t WRITE_TIMEOUT_TICKS = pdMS_TO_TICKS(DMA_BUFFER_DURAT
// it (e.g. cache-line rounding on some targets), so the speaker task reads the size actually allocated back
// from the driver instead of assuming this value. Clamping here keeps the request in range and avoids a
// noisy ESP-IDF "dma frame num is out of dma buffer size" warning at high sample rates or bit depths.
static uint32_t dma_buffer_frames(const audio::AudioStreamInfo &stream_info, uint32_t frame_multiple) {
static uint32_t dma_buffer_frames(const audio::AudioStreamInfo &stream_info) {
const uint32_t frames_from_duration = stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
const uint32_t max_frames = I2S_DMA_BUFFER_MAX_SIZE / stream_info.frames_to_bytes(1);
return (std::min(frames_from_duration, max_frames) / frame_multiple) * frame_multiple;
return std::min(frames_from_duration, max_frames);
}
void I2SAudioSpeaker::dump_config() {
@@ -85,12 +85,10 @@ void I2SAudioSpeaker::run_speaker_task() {
// The ring buffer holds input-format audio (what play() receives), so size it from the input stream info.
const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1);
// Hand over whole ESP32 mono sample pairs so writes stay word-aligned
const size_t source_alignment = bytes_per_frame * this->dma_frame_multiple_();
const bool pair_aligned = source_alignment > bytes_per_frame;
// Round the ring buffer size down to a multiple of source_alignment so the wrap boundary avoids splices.
// Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and
// avoids unnecessary single-frame splices.
const size_t ring_buffer_size =
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / source_alignment) * source_alignment;
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame;
// Per-frame byte widths and whether the task must narrow the bit depth before writing to the I2S peripheral.
const uint8_t channels = this->current_stream_info_.get_channels();
@@ -110,8 +108,7 @@ void I2SAudioSpeaker::run_speaker_task() {
dma_buffer_bytes = chan_info.total_dma_buf_size / DMA_BUFFERS_COUNT;
} else {
// Should not happen for a READY channel; fall back to the requested size.
dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(
dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_()));
dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(dma_buffer_frames(this->output_stream_info_));
}
// dma_buffer_bytes counts output-format bytes; convert with the output stream info.
const uint32_t frames_per_dma_buffer = this->output_stream_info_.bytes_to_frames(dma_buffer_bytes);
@@ -131,7 +128,7 @@ void I2SAudioSpeaker::run_speaker_task() {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, dma_buffer_input_bytes,
static_cast<uint8_t>(source_alignment));
static_cast<uint8_t>(bytes_per_frame));
if (audio_source != nullptr) {
// audio_source is nullptr if the ring buffer fails to allocate
@@ -289,7 +286,7 @@ void I2SAudioSpeaker::run_speaker_task() {
if (!this->pause_state_) {
while (bytes_written_total < dma_buffer_bytes) {
size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS) / 2);
size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS) / 2, false);
if (bytes_read > 0) {
// Apply volume at the input bit depth, before any narrowing, so the full precision is scaled.
uint8_t *new_data = audio_source->mutable_data() + audio_source->available() - bytes_read;
@@ -298,20 +295,9 @@ void I2SAudioSpeaker::run_speaker_task() {
// Convert as many whole frames as fit in the remaining DMA space, bounded by what the source
// currently exposes. Frame counts are shared between input and output; only the byte widths differ.
uint32_t frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available());
const uint32_t frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available());
const uint32_t frames_room =
this->output_stream_info_.bytes_to_frames(dma_buffer_bytes - bytes_written_total);
uint32_t padding_frames = 0;
if (pair_aligned && frames_available == 0 && frames_room >= 2 && !audio_source->has_buffered_data()) {
// Silence padding follows, so write a held half pair now with a silent partner rather than strand it.
// Only the real frame is reported as played.
const size_t real_bytes = audio_source->expose_padded_partial(bytes_per_frame);
if (real_bytes > 0) {
this->apply_software_volume_(audio_source->mutable_data(), real_bytes);
frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available());
padding_frames = frames_available - this->current_stream_info_.bytes_to_frames(real_bytes);
}
}
const uint32_t frames_to_write = std::min(frames_available, frames_room);
if (frames_to_write == 0) {
// Ring buffer has nothing more to hand over right now; pad the rest of this DMA buffer
@@ -340,17 +326,13 @@ void I2SAudioSpeaker::run_speaker_task() {
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
(unsigned) output_bytes);
audio_source->consume(input_bytes);
real_frames_total += frames_to_write - padding_frames;
real_frames_total += frames_to_write;
partial_write_failure = true;
break;
}
audio_source->consume(input_bytes);
bytes_written_total += output_bytes;
real_frames_total += frames_to_write - padding_frames;
if (padding_frames > 0) {
// The source was already empty; skip waiting on another fill() before the silence padding
break;
}
real_frames_total += frames_to_write;
}
if (real_frames_total > 0) {
last_data_received_time = millis();
@@ -458,7 +440,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
this->current_stream_info_ = audio_stream_info;
// The DMA buffers hold output-format (post-narrowing) samples, so size them from the output stream info.
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_());
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_);
i2s_role_t i2s_role = this->i2s_role_;
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
@@ -24,9 +24,6 @@ class I2SAudioSpeaker final : public I2SAudioSpeakerBase {
void run_speaker_task() override;
esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override;
/// @brief Frame multiple for DMA buffers and writes; 2 keeps ESP32 mono sample pairs word-aligned.
uint32_t dma_frame_multiple_() const { return this->esp32_mono_swap_needed_() ? 2 : 1; }
I2SCommFmt i2s_comm_fmt_{I2SCommFmt::STANDARD};
};
@@ -221,7 +221,7 @@ void MicroWakeWord::inference_task(void *params) {
xEventGroupClearBits(this_mww->event_group_, EventGroupBits::COMMAND_RESET_RING_BUFFER);
}
audio_source->fill(pdMS_TO_TICKS(DATA_TIMEOUT_MS));
audio_source->fill(pdMS_TO_TICKS(DATA_TIMEOUT_MS), false);
// The frontend buffers samples internally and only emits a feature once it has a full window, so we can
// hand it whatever the source exposes. The frontend consumes at least one sample per call, so available()
@@ -302,7 +302,7 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBuffer
return 0;
}
size_t bytes_read = audio_source->fill(ticks_to_wait);
size_t bytes_read = audio_source->fill(ticks_to_wait, false);
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
if (samples_to_duck > 0) {
@@ -81,7 +81,8 @@ void SoundLevelComponent::loop() {
}
// Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop.
this->audio_source_->fill(0);
// pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
this->audio_source_->fill(0, false);
if (this->audio_source_->available() == 0) {
// No new audio available for processing
@@ -224,11 +224,11 @@ void VoiceAssistant::stream_api_audio_() {
while (true) {
// fill() exposes a new chunk, or returns 0 if a previous chunk is still exposed; available()
// reports the currently exposed bytes either way.
this->audio_source_->fill(0);
this->audio_source_->fill(0, false);
size_t available = this->audio_source_->available();
size_t available2 = 0;
if (this->audio_source2_ != nullptr) {
this->audio_source2_->fill(0);
this->audio_source2_->fill(0, false);
available2 = this->audio_source2_->available();
}
@@ -383,13 +383,14 @@ void VoiceAssistant::loop() {
break; // State changed when udp server port received
}
case State::STREAMING_MICROPHONE: {
// pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
if (this->audio_mode_ == AUDIO_MODE_API) {
this->stream_api_audio_();
} else {
// UDP (will eventually be deprecated)
// Only the primary microphone channel is used
while (true) {
this->audio_source_->fill(0);
this->audio_source_->fill(0, false);
size_t available = this->audio_source_->available();
if (available == 0) {
break;
@@ -42,6 +42,13 @@ button:
md5: 0123456789abcdef0123456789abcdef
url: http://my.ha.net:8123/local/esphome/firmware.bin
- ota.http_request.flash:
id: http_request_ota
md5_url: !lambda 'return std::string("http://my.ha.net:8123/local/esphome/firmware.md5");'
username: user
password: !lambda 'return std::string("pass");'
url: http://my.ha.net:8123/local/esphome/firmware.bin
- logger.log: "This message should be not displayed (reboot)"
update: