ESPHome 2026.5.3
Loading...
Searching...
No Matches
audio_transfer_buffer.cpp
Go to the documentation of this file.
2
3#ifdef USE_ESP32
4
5#include <cstring>
6
8
9namespace esphome::audio {
10
12
13std::unique_ptr<AudioSinkTransferBuffer> AudioSinkTransferBuffer::create(size_t buffer_size) {
14 std::unique_ptr<AudioSinkTransferBuffer> sink_buffer = make_unique<AudioSinkTransferBuffer>();
15
16 if (!sink_buffer->allocate_buffer_(buffer_size)) {
17 return nullptr;
18 }
19
20 return sink_buffer;
21}
22
23std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
24 std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
25
26 if (!source_buffer->allocate_buffer_(buffer_size)) {
27 return nullptr;
28 }
29
30 return source_buffer;
31}
32
34 if (this->buffer_size_ == 0) {
35 return 0;
36 }
37 return this->buffer_size_ - (this->buffer_length_ + (this->data_start_ - this->buffer_));
38}
39
41 this->buffer_length_ -= bytes;
42 if (this->buffer_length_ > 0) {
43 this->data_start_ += bytes;
44 } else {
45 // All the data in the buffer has been consumed, reset the start pointer
46 this->data_start_ = this->buffer_;
47 }
48}
49
50void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_length_ += bytes; }
51
53 this->buffer_length_ = 0;
54 if (this->ring_buffer_.use_count() > 0) {
55 this->ring_buffer_->reset();
56 }
57}
58
60 this->buffer_length_ = 0;
61 if (this->ring_buffer_.use_count() > 0) {
62 this->ring_buffer_->reset();
63 }
64#ifdef USE_SPEAKER
65 if (this->speaker_ != nullptr) {
66 this->speaker_->stop();
67 }
68#endif
69}
70
72 if (this->ring_buffer_.use_count() > 0) {
73 return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
74 }
75 return (this->available() > 0);
76}
77
78bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
79 if (this->buffer_ == nullptr) {
80 return this->allocate_buffer_(new_buffer_size);
81 }
82
83 if (new_buffer_size < this->buffer_length_) {
84 // New size is too small to hold existing data
85 return false;
86 }
87
88 // Shift existing data to the start of the buffer so realloc preserves it
89 if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) {
90 std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
91 this->data_start_ = this->buffer_;
92 }
93
94 RAMAllocator<uint8_t> allocator;
95 uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size);
96 if (new_buffer == nullptr) {
97 // Reallocation failed, but the original buffer is still valid
98 return false;
99 }
100
101 this->buffer_ = new_buffer;
102 this->data_start_ = this->buffer_;
103 this->buffer_size_ = new_buffer_size;
104 return true;
105}
106
108 this->buffer_size_ = buffer_size;
109
110 RAMAllocator<uint8_t> allocator;
111
112 this->buffer_ = allocator.allocate(this->buffer_size_);
113 if (this->buffer_ == nullptr) {
114 return false;
115 }
116
117 this->data_start_ = this->buffer_;
118 this->buffer_length_ = 0;
119
120 return true;
121}
122
124 if (this->buffer_ != nullptr) {
125 RAMAllocator<uint8_t> allocator;
126 allocator.deallocate(this->buffer_, this->buffer_size_);
127 this->buffer_ = nullptr;
128 this->data_start_ = nullptr;
129 }
130
131 this->buffer_size_ = 0;
132 this->buffer_length_ = 0;
133}
134
135size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
136 if (pre_shift) {
137 // Shift data in buffer to start
138 if (this->buffer_length_ > 0) {
139 std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
140 }
141 this->data_start_ = this->buffer_;
142 }
143
144 size_t bytes_to_read = AudioTransferBuffer::free();
145 size_t bytes_read = 0;
146 if (bytes_to_read > 0) {
147 if (this->ring_buffer_.use_count() > 0) {
148 bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
149 }
150
151 this->increase_buffer_length(bytes_read);
152 }
153 return bytes_read;
154}
155
156size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift) {
157 size_t bytes_written = 0;
158 if (this->available()) {
159#ifdef USE_SPEAKER
160 if (this->speaker_ != nullptr) {
161 bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
162 } else
163#endif
164 if (this->ring_buffer_.use_count() > 0) {
165 bytes_written =
166 this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
167 } else if (this->sink_callback_ != nullptr) {
168 bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait);
169 }
170
171 this->decrease_buffer_length(bytes_written);
172 }
173
174 if (post_shift) {
175 // Shift unwritten data to the start of the buffer
176 std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
177 this->data_start_ = this->buffer_;
178 }
179
180 return bytes_written;
181}
182
184#ifdef USE_SPEAKER
185 if (this->speaker_ != nullptr) {
186 return (this->speaker_->has_buffered_data() || (this->available() > 0));
187 }
188#endif
189 if (this->ring_buffer_.use_count() > 0) {
190 return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
191 }
192 return (this->available() > 0);
193}
194
196
198
199void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) {
200 this->data_start_ = data;
201 this->length_ = length;
202}
203
205 bytes = std::min(bytes, this->length_);
206 this->length_ -= bytes;
207 this->data_start_ += bytes;
208}
209
210std::unique_ptr<RingBufferAudioSource> RingBufferAudioSource::create(
211 std::shared_ptr<ring_buffer::RingBuffer> ring_buffer, size_t max_fill_bytes, uint8_t alignment_bytes) {
212 if (ring_buffer == nullptr || max_fill_bytes == 0 || alignment_bytes == 0 || alignment_bytes > MAX_ALIGNMENT_BYTES) {
213 return nullptr;
214 }
215 return std::unique_ptr<RingBufferAudioSource>(
216 new RingBufferAudioSource(std::move(ring_buffer), max_fill_bytes, alignment_bytes));
217}
218
220 if (this->acquired_item_ != nullptr) {
221 this->ring_buffer_->receive_release(this->acquired_item_);
222 this->acquired_item_ = nullptr;
223 }
224}
225
227 if (this->acquired_item_ == nullptr) {
228 return;
229 }
230 if (this->item_trailing_length_ > 0) {
231 // Copy the trailing sub-frame bytes into the splice buffer before returning the item; the next
232 // fill() will complete the frame from the head of the next chunk.
233 std::memcpy(this->splice_buffer_, this->item_trailing_ptr_, this->item_trailing_length_);
235 this->item_trailing_ptr_ = nullptr;
236 this->item_trailing_length_ = 0;
237 }
238 this->ring_buffer_->receive_release(this->acquired_item_);
239 this->acquired_item_ = nullptr;
240}
241
243 bytes = std::min(bytes, this->current_available_);
244 this->current_data_ += bytes;
245 this->current_available_ -= bytes;
246 // Promotion of queued data is deferred to fill() so callers see new data as a fresh return value
247 // rather than appearing silently after consume(). When the held item has nothing left depending
248 // on it (no exposed bytes and no queued region), release it now so the ring buffer can be
249 // reclaimed by writers even if fill() is never called again.
250 if (this->current_available_ == 0 && this->queued_length_ == 0) {
251 this->release_item_();
252 }
253}
254
256 // splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion
257 // bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports.
258 // Counting it separately would strand a drain loop when a stream ends mid-frame and those completion
259 // bytes never come.
260 return (this->current_available_ > 0) || (this->queued_length_ > 0) || (this->ring_buffer_->available() > 0);
261}
262
263size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait, bool /*pre_shift*/) {
264 if (this->current_available_ > 0) {
265 // Caller has not finished consuming the current exposure
266 return 0;
267 }
268
269 // If a queued region (the aligned remainder of the new chunk after a splice frame) is waiting,
270 // promote it to the exposed region and report its size as fresh data.
271 if (this->queued_length_ > 0) {
272 this->current_data_ = this->queued_data_;
274 this->queued_data_ = nullptr;
275 this->queued_length_ = 0;
276 return this->current_available_;
277 }
278
279 // Nothing exposed and nothing queued: release the previously held item (saving any sub-frame tail
280 // to splice_buffer_) and acquire a new chunk.
281 this->release_item_();
282
283 size_t chunk_length = 0;
284 void *item = this->ring_buffer_->receive_acquire(chunk_length, this->max_fill_bytes_, ticks_to_wait);
285 if (item == nullptr) {
286 return 0;
287 }
288
289 uint8_t *chunk_data = static_cast<uint8_t *>(item);
290 bool exposing_splice_frame = false;
291
292 // Complete any pending splice frame from the head of the new chunk.
293 if (this->splice_length_ > 0) {
294 const size_t needed = static_cast<size_t>(this->alignment_bytes_) - this->splice_length_;
295 if (chunk_length < needed) {
296 // Not enough data to complete the spliced frame yet; absorb everything and wait for more.
297 std::memcpy(this->splice_buffer_ + this->splice_length_, chunk_data, chunk_length);
298 this->splice_length_ += chunk_length;
299 this->ring_buffer_->receive_release(item);
300 return 0;
301 }
302 std::memcpy(this->splice_buffer_ + this->splice_length_, chunk_data, needed);
303 chunk_data += needed;
304 chunk_length -= needed;
305 this->splice_length_ = 0;
306 exposing_splice_frame = true;
307 }
308
309 this->acquired_item_ = item;
310
311 // Split the remaining chunk into its aligned region and a (possibly zero) sub-frame trailing tail.
312 const size_t trailing = (this->alignment_bytes_ > 1) ? (chunk_length % this->alignment_bytes_) : 0;
313 const size_t aligned_bytes = chunk_length - trailing;
314 if (trailing > 0) {
315 this->item_trailing_ptr_ = chunk_data + aligned_bytes;
316 this->item_trailing_length_ = trailing;
317 }
318
319 if (exposing_splice_frame) {
320 // Expose the spliced frame from splice_buffer_, queuing the chunk's aligned region for the next
321 // fill() call.
322 this->current_data_ = this->splice_buffer_;
324 this->queued_data_ = chunk_data;
325 this->queued_length_ = aligned_bytes;
326 return this->alignment_bytes_;
327 }
328
329 if (aligned_bytes == 0) {
330 // The entire chunk is a sub-frame tail (only possible when alignment exceeds chunk size). Save it
331 // to the splice buffer and release the item so the next fill() can complete the frame.
332 this->release_item_();
333 return 0;
334 }
335
336 this->current_data_ = chunk_data;
337 this->current_available_ = aligned_bytes;
338 return aligned_bytes;
339}
340
341} // namespace esphome::audio
342
343#endif
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2053
T * reallocate(T *p, size_t n)
Definition helpers.h:2095
void deallocate(T *p, size_t n)
Definition helpers.h:2110
T * allocate(size_t n)
Definition helpers.h:2080
virtual size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait)=0
size_t transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift=true)
Writes any available data in the transfer buffer to the sink.
static std::unique_ptr< AudioSinkTransferBuffer > create(size_t buffer_size)
Creates a new sink transfer buffer.
size_t transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift=true)
Reads any available data from the source into the transfer buffer.
static std::unique_ptr< AudioSourceTransferBuffer > create(size_t buffer_size)
Creates a new source transfer buffer.
size_t free() const
Returns the transfer buffer's currrently free bytes available to write.
virtual bool has_buffered_data() const
Tests if there is any data in the tranfer buffer or the source/sink.
virtual void clear_buffered_data()
Clears data in the transfer buffer and, if possible, the source/sink.
~AudioTransferBuffer()
Destructor that deallocates the transfer buffer.
uint8_t * get_buffer_end() const
Returns a pointer to the end of the transfer buffer where free() bytes of new data can be written.
void increase_buffer_length(size_t bytes)
Updates the internal state of the transfer buffer.
bool reallocate(size_t new_buffer_size)
Reallocates the transfer buffer, preserving any existing data.
bool allocate_buffer_(size_t buffer_size)
Allocates the transfer buffer in external memory, if available.
void decrease_buffer_length(size_t bytes)
Updates the internal state of the transfer buffer.
size_t available() const
Returns the transfer buffer's currently available bytes to read.
std::shared_ptr< ring_buffer::RingBuffer > ring_buffer_
void deallocate_buffer_()
Deallocates the buffer and resets the class variables.
void set_data(const uint8_t *data, size_t length)
Sets the data pointer and length for the buffer.
const uint8_t * data() const override
uint8_t splice_buffer_[MAX_ALIGNMENT_BYTES]
void release_item_()
Releases the currently held ring buffer item, first copying any trailing sub-frame bytes into the spl...
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override
pre_shift is ignored: there is no intermediate transfer buffer to compact, so an unconsumed exposure ...
static constexpr size_t MAX_ALIGNMENT_BYTES
Maximum supported alignment. Sized to cover 32-bit samples across up to 2 channels (8 bytes).
static std::unique_ptr< RingBufferAudioSource > create(std::shared_ptr< ring_buffer::RingBuffer > ring_buffer, size_t max_fill_bytes, uint8_t alignment_bytes=1)
Creates a new ring-buffer-backed audio source after validating its parameters.
RingBufferAudioSource(std::shared_ptr< ring_buffer::RingBuffer > ring_buffer, size_t max_fill_bytes, uint8_t alignment_bytes)
Constructs a new ring-buffer-backed audio source.
std::shared_ptr< ring_buffer::RingBuffer > ring_buffer_
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
virtual bool has_buffered_data() const =0
virtual void stop()=0
uint16_t length
Definition tt21100.cpp:0