mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11bcffced2 | ||
|
|
8e10a7188a | ||
|
|
6817960365 | ||
|
|
cfdaf958e9 |
@@ -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), false);
|
||||
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
|
||||
|
||||
const size_t available_before_decode = this->input_buffer_->available();
|
||||
|
||||
|
||||
@@ -129,9 +129,8 @@ 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. 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);
|
||||
// Expose a chunk of the ring buffer's internal storage.
|
||||
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
|
||||
|
||||
if (this->audio_source_->available() == 0) {
|
||||
// No samples available to process
|
||||
|
||||
@@ -20,16 +20,6 @@ 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;
|
||||
@@ -132,27 +122,6 @@ 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()) {
|
||||
@@ -192,9 +161,44 @@ 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;
|
||||
@@ -268,6 +272,23 @@ 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.
|
||||
@@ -276,7 +297,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, bool /*pre_shift*/) {
|
||||
size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait) {
|
||||
if (this->current_available_ > 0) {
|
||||
// Caller has not finished consuming the current exposure
|
||||
return 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#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"
|
||||
@@ -152,13 +153,20 @@ 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, bool pre_shift) { return 0; }
|
||||
size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); }
|
||||
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); }
|
||||
};
|
||||
|
||||
class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer {
|
||||
// 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 {
|
||||
/*
|
||||
* @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.
|
||||
@@ -189,10 +197,12 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadab
|
||||
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.
|
||||
@@ -246,14 +256,21 @@ 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;
|
||||
/// 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;
|
||||
/// 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;
|
||||
|
||||
/// @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
|
||||
|
||||
@@ -33,10 +33,7 @@ CONF_REQUEST_VOLTAGE = "request_voltage"
|
||||
fusb302b_ns = cg.esphome_ns.namespace("fusb302b")
|
||||
FUSB302B = fusb302b_ns.class_("FUSB302B", cg.Component, i2c.I2CDevice)
|
||||
|
||||
ConnectForwarder = fusb302b_ns.class_("ConnectForwarder")
|
||||
DisconnectForwarder = fusb302b_ns.class_("DisconnectForwarder")
|
||||
PowerReadyForwarder = fusb302b_ns.class_("PowerReadyForwarder")
|
||||
ErrorForwarder = fusb302b_ns.class_("ErrorForwarder")
|
||||
PdState = fusb302b_ns.enum("PdState", is_class=True)
|
||||
|
||||
# USB PD fixed supply voltages are 5, 9, 12, 15 and 20 V; the highest one that does not exceed the
|
||||
# requested voltage is chosen.
|
||||
@@ -78,18 +75,43 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.only_on_esp32,
|
||||
)
|
||||
|
||||
_STATE_PARAMS = [(PdState, "state"), (PdState, "previous")]
|
||||
_CONNECTED = "fusb302b::is_connected_state"
|
||||
|
||||
# Each state trigger fires on the change described by its filter.
|
||||
_STATE_TRIGGERS = (
|
||||
# A source is attached, including after recovering from a communication error.
|
||||
(CONF_ON_CONNECT, f"{_CONNECTED}(state) && !{_CONNECTED}(previous)"),
|
||||
# The source is removed, or is no longer known to be attached because communication failed.
|
||||
(CONF_ON_DISCONNECT, f"!{_CONNECTED}(state) && {_CONNECTED}(previous)"),
|
||||
# Negotiation is finished: a new PD contract is in place, or the source does not speak PD.
|
||||
(
|
||||
CONF_ON_POWER_READY,
|
||||
(
|
||||
"state == fusb302b::PdState::PD_STATE_EXPLICIT_CONTRACT"
|
||||
" || state == fusb302b::PdState::PD_STATE_PD_TIMEOUT"
|
||||
),
|
||||
),
|
||||
# Communication with the chip fails.
|
||||
(
|
||||
CONF_ON_ERROR,
|
||||
(
|
||||
"state == fusb302b::PdState::PD_STATE_ERROR"
|
||||
" && previous != fusb302b::PdState::PD_STATE_ERROR"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CONNECT, "add_on_state_callback", forwarder=ConnectForwarder
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DISCONNECT, "add_on_state_callback", forwarder=DisconnectForwarder
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_POWER_READY, "add_on_state_callback", forwarder=PowerReadyForwarder
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ERROR, "add_on_state_callback", forwarder=ErrorForwarder
|
||||
*(
|
||||
automation.CallbackAutomation(
|
||||
key,
|
||||
"add_on_state_callback",
|
||||
params=_STATE_PARAMS,
|
||||
forward=[],
|
||||
when=when,
|
||||
)
|
||||
for key, when in _STATE_TRIGGERS
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_REQUEST_FAILED, "add_on_request_failed_callback"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "pd.h"
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
/// Triggers when a source is attached, including after recovering from a communication error.
|
||||
struct ConnectForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(PdState state, PdState previous) const {
|
||||
if (is_connected_state(state) && !is_connected_state(previous))
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
/// Triggers when the source is removed, or is no longer known to be attached because communication failed.
|
||||
struct DisconnectForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(PdState state, PdState previous) const {
|
||||
if (!is_connected_state(state) && is_connected_state(previous))
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
/// Triggers when negotiation is finished: a new PD contract is in place, or the source does not speak PD.
|
||||
struct PowerReadyForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(PdState state, PdState /*previous*/) const {
|
||||
if (state == PdState::PD_STATE_EXPLICIT_CONTRACT || state == PdState::PD_STATE_PD_TIMEOUT)
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
/// Triggers when communication with the chip fails.
|
||||
struct ErrorForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(PdState state, PdState previous) const {
|
||||
if (state == PdState::PD_STATE_ERROR && previous != PdState::PD_STATE_ERROR)
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(sizeof(ConnectForwarder) <= sizeof(void *));
|
||||
static_assert(std::is_trivially_copyable_v<ConnectForwarder>);
|
||||
static_assert(sizeof(DisconnectForwarder) <= sizeof(void *));
|
||||
static_assert(std::is_trivially_copyable_v<DisconnectForwarder>);
|
||||
static_assert(sizeof(PowerReadyForwarder) <= sizeof(void *));
|
||||
static_assert(std::is_trivially_copyable_v<PowerReadyForwarder>);
|
||||
static_assert(sizeof(ErrorForwarder) <= sizeof(void *));
|
||||
static_assert(std::is_trivially_copyable_v<ErrorForwarder>);
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
@@ -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, false);
|
||||
size_t bytes_read = audio_source->fill(read_timeout_ticks);
|
||||
if (bytes_read == 0) {
|
||||
break; // No upstream data within the read budget; silence-pad the remainder.
|
||||
}
|
||||
|
||||
@@ -372,10 +372,18 @@ 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->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16) {
|
||||
if (this->esp32_mono_swap_needed_()) {
|
||||
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,9 +160,11 @@ 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 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.
|
||||
/// Only applies when esp32_mono_swap_needed_() is true. `data` must start on a DMA word and hold whole pairs.
|
||||
/// @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) {
|
||||
static uint32_t dma_buffer_frames(const audio::AudioStreamInfo &stream_info, uint32_t frame_multiple) {
|
||||
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);
|
||||
return (std::min(frames_from_duration, max_frames) / frame_multiple) * frame_multiple;
|
||||
}
|
||||
|
||||
void I2SAudioSpeaker::dump_config() {
|
||||
@@ -85,10 +85,12 @@ 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);
|
||||
// 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.
|
||||
// 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.
|
||||
const size_t ring_buffer_size =
|
||||
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame;
|
||||
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / source_alignment) * source_alignment;
|
||||
|
||||
// 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();
|
||||
@@ -108,7 +110,8 @@ 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_));
|
||||
dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(
|
||||
dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_()));
|
||||
}
|
||||
// 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);
|
||||
@@ -128,7 +131,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>(bytes_per_frame));
|
||||
static_cast<uint8_t>(source_alignment));
|
||||
|
||||
if (audio_source != nullptr) {
|
||||
// audio_source is nullptr if the ring buffer fails to allocate
|
||||
@@ -286,7 +289,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, false);
|
||||
size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS) / 2);
|
||||
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;
|
||||
@@ -295,9 +298,20 @@ 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.
|
||||
const uint32_t frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available());
|
||||
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
|
||||
@@ -326,13 +340,17 @@ 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;
|
||||
real_frames_total += frames_to_write - padding_frames;
|
||||
partial_write_failure = true;
|
||||
break;
|
||||
}
|
||||
audio_source->consume(input_bytes);
|
||||
bytes_written_total += output_bytes;
|
||||
real_frames_total += frames_to_write;
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (real_frames_total > 0) {
|
||||
last_data_received_time = millis();
|
||||
@@ -440,7 +458,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_);
|
||||
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_());
|
||||
|
||||
i2s_role_t i2s_role = this->i2s_role_;
|
||||
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
|
||||
|
||||
@@ -24,6 +24,9 @@ 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), false);
|
||||
audio_source->fill(pdMS_TO_TICKS(DATA_TIMEOUT_MS));
|
||||
|
||||
// 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, false);
|
||||
size_t bytes_read = audio_source->fill(ticks_to_wait);
|
||||
|
||||
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
|
||||
if (samples_to_duck > 0) {
|
||||
|
||||
@@ -81,8 +81,7 @@ void SoundLevelComponent::loop() {
|
||||
}
|
||||
|
||||
// Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop.
|
||||
// pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
|
||||
this->audio_source_->fill(0, false);
|
||||
this->audio_source_->fill(0);
|
||||
|
||||
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, false);
|
||||
this->audio_source_->fill(0);
|
||||
size_t available = this->audio_source_->available();
|
||||
size_t available2 = 0;
|
||||
if (this->audio_source2_ != nullptr) {
|
||||
this->audio_source2_->fill(0, false);
|
||||
this->audio_source2_->fill(0);
|
||||
available2 = this->audio_source2_->available();
|
||||
}
|
||||
|
||||
@@ -383,14 +383,13 @@ 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, false);
|
||||
this->audio_source_->fill(0);
|
||||
size_t available = this->audio_source_->available();
|
||||
if (available == 0) {
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user