mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 08:22:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c01aac386 | ||
|
|
e869cdeeb9 |
No files matched your search
@@ -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`.
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,9 +3,8 @@ import esphome.codegen as cg
|
||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||
from esphome.core import ID, coroutine_with_priority
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns
|
||||
@@ -21,9 +20,6 @@ CONF_MD5_URL = "md5_url"
|
||||
OtaHttpRequestComponent = http_request_ns.class_(
|
||||
"OtaHttpRequestComponent", OTAComponent
|
||||
)
|
||||
OtaHttpRequestComponentFlashAction = http_request_ns.class_(
|
||||
"OtaHttpRequestComponentFlashAction", automation.Action
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
@@ -68,38 +64,13 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"ota.http_request.flash",
|
||||
OtaHttpRequestComponentFlashAction,
|
||||
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_MD5_URL, "set_md5_url", cg.std_string),
|
||||
automation.ApplyField(CONF_MD5, "set_md5", cg.std_string),
|
||||
automation.ApplyField(CONF_PASSWORD, "set_password", cg.std_string),
|
||||
automation.ApplyField(CONF_USERNAME, "set_username", cg.std_string),
|
||||
automation.ApplyField(CONF_URL, "set_url", cg.std_string),
|
||||
automation.ApplyCall("flash()"),
|
||||
)
|
||||
async def ota_http_request_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
if md5_url := config.get(CONF_MD5_URL):
|
||||
template_ = await cg.templatable(md5_url, args, cg.std_string)
|
||||
cg.add(var.set_md5_url(template_))
|
||||
|
||||
if md5_str := config.get(CONF_MD5):
|
||||
template_ = await cg.templatable(md5_str, args, cg.std_string)
|
||||
cg.add(var.set_md5(template_))
|
||||
|
||||
if password_str := config.get(CONF_PASSWORD):
|
||||
template_ = await cg.templatable(password_str, args, cg.std_string)
|
||||
cg.add(var.set_password(template_))
|
||||
|
||||
if username_str := config.get(CONF_USERNAME):
|
||||
template_ = await cg.templatable(username_str, args, cg.std_string)
|
||||
cg.add(var.set_username(template_))
|
||||
|
||||
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
|
||||
cg.add(var.set_url(template_))
|
||||
|
||||
return var
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
#include "ota_http_request.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::http_request {
|
||||
|
||||
template<typename... Ts> class OtaHttpRequestComponentFlashAction final : public Action<Ts...> {
|
||||
public:
|
||||
OtaHttpRequestComponentFlashAction(OtaHttpRequestComponent *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(std::string, md5_url)
|
||||
TEMPLATABLE_VALUE(std::string, md5)
|
||||
TEMPLATABLE_VALUE(std::string, password)
|
||||
TEMPLATABLE_VALUE(std::string, url)
|
||||
TEMPLATABLE_VALUE(std::string, username)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->md5_url_.has_value()) {
|
||||
this->parent_->set_md5_url(this->md5_url_.value(x...));
|
||||
}
|
||||
if (this->md5_.has_value()) {
|
||||
this->parent_->set_md5(this->md5_.value(x...));
|
||||
}
|
||||
if (this->password_.has_value()) {
|
||||
this->parent_->set_password(this->password_.value(x...));
|
||||
}
|
||||
if (this->username_.has_value()) {
|
||||
this->parent_->set_username(this->username_.value(x...));
|
||||
}
|
||||
this->parent_->set_url(this->url_.value(x...));
|
||||
|
||||
this->parent_->flash();
|
||||
// Normally never reached due to reboot
|
||||
}
|
||||
|
||||
protected:
|
||||
OtaHttpRequestComponent *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::http_request
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user