ESPHome 2026.11.0-dev
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
24 if (this->buffer_size_ == 0) {
25 return 0;
26 }
27 return this->buffer_size_ - (this->buffer_length_ + (this->data_start_ - this->buffer_));
28}
29
31 this->buffer_length_ -= bytes;
32 if (this->buffer_length_ > 0) {
33 this->data_start_ += bytes;
34 } else {
35 // All the data in the buffer has been consumed, reset the start pointer
36 this->data_start_ = this->buffer_;
37 }
38}
39
40void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_length_ += bytes; }
41
43 this->buffer_length_ = 0;
44 if (this->ring_buffer_ != nullptr) {
45 this->ring_buffer_->reset();
46 }
47}
48
50 this->buffer_length_ = 0;
51 if (this->ring_buffer_ != nullptr) {
52 this->ring_buffer_->reset();
53 }
54#ifdef USE_SPEAKER
55 if (this->speaker_ != nullptr) {
56 this->speaker_->stop();
57 }
58#endif
59}
60
62 if (this->ring_buffer_ != nullptr) {
63 return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
64 }
65 return (this->available() > 0);
66}
67
68bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
69 if (this->buffer_ == nullptr) {
70 return this->allocate_buffer_(new_buffer_size);
71 }
72
73 if (new_buffer_size < this->buffer_length_) {
74 // New size is too small to hold existing data
75 return false;
76 }
77
78 // Shift existing data to the start of the buffer so realloc preserves it
79 if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) {
80 std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
81 this->data_start_ = this->buffer_;
82 }
83
84 RAMAllocator<uint8_t> allocator;
85 uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size);
86 if (new_buffer == nullptr) {
87 // Reallocation failed, but the original buffer is still valid
88 return false;
89 }
90
91 this->buffer_ = new_buffer;
92 this->data_start_ = this->buffer_;
93 this->buffer_size_ = new_buffer_size;
94 return true;
95}
96
97bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
98 this->buffer_size_ = buffer_size;
99
100 RAMAllocator<uint8_t> allocator;
101
102 this->buffer_ = allocator.allocate(this->buffer_size_);
103 if (this->buffer_ == nullptr) {
104 return false;
105 }
106
107 this->data_start_ = this->buffer_;
108 this->buffer_length_ = 0;
109
110 return true;
111}
112
114 if (this->buffer_ != nullptr) {
115 RAMAllocator<uint8_t> allocator;
116 allocator.deallocate(this->buffer_, this->buffer_size_);
117 this->buffer_ = nullptr;
118 this->data_start_ = nullptr;
119 }
120
121 this->buffer_size_ = 0;
122 this->buffer_length_ = 0;
123}
124
125size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift) {
126 size_t bytes_written = 0;
127 if (this->available()) {
128#ifdef USE_SPEAKER
129 if (this->speaker_ != nullptr) {
130 bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
131 } else
132#endif
133 if (this->ring_buffer_ != nullptr) {
134 bytes_written =
135 this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
136 } else if (this->sink_callback_ != nullptr) {
137 bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait);
138 }
139
140 this->decrease_buffer_length(bytes_written);
141 }
142
143 if (post_shift) {
144 // Shift unwritten data to the start of the buffer
145 std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
146 this->data_start_ = this->buffer_;
147 }
148
149 return bytes_written;
150}
151
153#ifdef USE_SPEAKER
154 if (this->speaker_ != nullptr) {
155 return (this->speaker_->has_buffered_data() || (this->available() > 0));
156 }
157#endif
158 if (this->ring_buffer_ != nullptr) {
159 return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
160 }
161 return (this->available() > 0);
162}
163
164// AudioSourceTransferBuffer is deprecated. Remove before 2027.5.0.
165#pragma GCC diagnostic push
166#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
167std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
168 std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
169
170 if (!source_buffer->allocate_buffer_(buffer_size)) {
171 return nullptr;
172 }
173
174 return source_buffer;
175}
176
177size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
178 if (pre_shift) {
179 // Shift data in buffer to start
180 if (this->buffer_length_ > 0) {
181 std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
182 }
183 this->data_start_ = this->buffer_;
184 }
185
186 size_t bytes_to_read = AudioTransferBuffer::free();
187 size_t bytes_read = 0;
188 if (bytes_to_read > 0) {
189 if (this->ring_buffer_ != nullptr) {
190 bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
191 }
192
193 this->increase_buffer_length(bytes_read);
194 }
195 return bytes_read;
196}
197
198size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); }
199
200bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); }
201#pragma GCC diagnostic pop
202
203void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) {
204 this->data_start_ = data;
205 this->length_ = length;
206}
207
209 bytes = std::min(bytes, this->length_);
210 this->length_ -= bytes;
211 this->data_start_ += bytes;
212}
213
214std::unique_ptr<RingBufferAudioSource> RingBufferAudioSource::create(
215 std::shared_ptr<ring_buffer::RingBuffer> ring_buffer, size_t max_fill_bytes, uint8_t alignment_bytes) {
216 if (ring_buffer == nullptr || max_fill_bytes == 0 || alignment_bytes == 0 || alignment_bytes > MAX_ALIGNMENT_BYTES) {
217 return nullptr;
218 }
219 return std::unique_ptr<RingBufferAudioSource>(
220 new RingBufferAudioSource(std::move(ring_buffer), max_fill_bytes, alignment_bytes));
221}
222
224 if (this->acquired_item_ != nullptr) {
225 this->ring_buffer_->receive_release(this->acquired_item_);
226 this->acquired_item_ = nullptr;
227 }
228}
229
231 if (this->acquired_item_ == nullptr) {
232 return;
233 }
234 if (this->item_trailing_length_ > 0) {
235 // Copy the trailing sub-frame bytes into the splice buffer before returning the item; the next
236 // fill() will complete the frame from the head of the next chunk.
237 std::memcpy(this->splice_buffer_, this->item_trailing_ptr_, this->item_trailing_length_);
239 this->item_trailing_ptr_ = nullptr;
240 this->item_trailing_length_ = 0;
241 }
242 this->ring_buffer_->receive_release(this->acquired_item_);
243 this->acquired_item_ = nullptr;
244}
245
247 bytes = std::min(bytes, this->current_available_);
248 this->current_data_ += bytes;
249 this->current_available_ -= bytes;
250 // Promotion of queued data is deferred to fill() so callers see new data as a fresh return value
251 // rather than appearing silently after consume(). When the held item has nothing left depending
252 // on it (no exposed bytes and no queued region), release it now so the ring buffer can be
253 // reclaimed by writers even if fill() is never called again.
254 if (this->current_available_ == 0 && this->queued_length_ == 0) {
255 this->release_item_();
256 }
257}
258
260 // Release the held item before reset() so the source no longer references memory the reset will reclaim.
261 if (this->acquired_item_ != nullptr) {
262 this->ring_buffer_->receive_release(this->acquired_item_);
263 this->acquired_item_ = nullptr;
264 }
265 this->current_data_ = nullptr;
266 this->current_available_ = 0;
267 this->queued_data_ = nullptr;
268 this->queued_length_ = 0;
269 this->item_trailing_ptr_ = nullptr;
270 this->item_trailing_length_ = 0;
271 this->splice_length_ = 0;
272 this->ring_buffer_->reset();
273}
274
276 if (this->current_available_ > 0 || this->queued_length_ > 0) {
277 return 0;
278 }
279 // Moves any trailing bytes still inside a held item into the splice buffer
280 this->release_item_();
281 const size_t real_bytes = this->splice_length_;
282 if (real_bytes == 0 || frame_bytes == 0 || real_bytes % frame_bytes != 0) {
283 return 0;
284 }
285 std::memset(this->splice_buffer_ + real_bytes, 0, this->alignment_bytes_ - real_bytes);
286 this->splice_length_ = 0;
287 this->current_data_ = this->splice_buffer_;
289 return real_bytes;
290}
291
293 // splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion
294 // bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports.
295 // Counting it separately would strand a drain loop when a stream ends mid-frame and those completion
296 // bytes never come.
297 return (this->current_available_ > 0) || (this->queued_length_ > 0) || (this->ring_buffer_->available() > 0);
298}
299
300size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait) {
301 if (this->current_available_ > 0) {
302 // Caller has not finished consuming the current exposure
303 return 0;
304 }
305
306 // If a queued region (the aligned remainder of the new chunk after a splice frame) is waiting,
307 // promote it to the exposed region and report its size as fresh data.
308 if (this->queued_length_ > 0) {
309 this->current_data_ = this->queued_data_;
311 this->queued_data_ = nullptr;
312 this->queued_length_ = 0;
313 return this->current_available_;
314 }
315
316 // Nothing exposed and nothing queued: release the previously held item (saving any sub-frame tail
317 // to splice_buffer_) and acquire a new chunk.
318 this->release_item_();
319
320 size_t chunk_length = 0;
321 void *item = this->ring_buffer_->receive_acquire(chunk_length, this->max_fill_bytes_, ticks_to_wait);
322 if (item == nullptr) {
323 return 0;
324 }
325
326 uint8_t *chunk_data = static_cast<uint8_t *>(item);
327 bool exposing_splice_frame = false;
328
329 // Complete any pending splice frame from the head of the new chunk.
330 if (this->splice_length_ > 0) {
331 const size_t needed = static_cast<size_t>(this->alignment_bytes_) - this->splice_length_;
332 if (chunk_length < needed) {
333 // Not enough data to complete the spliced frame yet; absorb everything and wait for more.
334 std::memcpy(this->splice_buffer_ + this->splice_length_, chunk_data, chunk_length);
335 this->splice_length_ += chunk_length;
336 this->ring_buffer_->receive_release(item);
337 return 0;
338 }
339 std::memcpy(this->splice_buffer_ + this->splice_length_, chunk_data, needed);
340 chunk_data += needed;
341 chunk_length -= needed;
342 this->splice_length_ = 0;
343 exposing_splice_frame = true;
344 }
345
346 this->acquired_item_ = item;
347
348 // Split the remaining chunk into its aligned region and a (possibly zero) sub-frame trailing tail.
349 const size_t trailing = (this->alignment_bytes_ > 1) ? (chunk_length % this->alignment_bytes_) : 0;
350 const size_t aligned_bytes = chunk_length - trailing;
351 if (trailing > 0) {
352 this->item_trailing_ptr_ = chunk_data + aligned_bytes;
353 this->item_trailing_length_ = trailing;
354 }
355
356 if (exposing_splice_frame) {
357 // Expose the spliced frame from splice_buffer_, queuing the chunk's aligned region for the next
358 // fill() call.
359 this->current_data_ = this->splice_buffer_;
361 this->queued_data_ = chunk_data;
362 this->queued_length_ = aligned_bytes;
363 return this->alignment_bytes_;
364 }
365
366 if (aligned_bytes == 0) {
367 // The entire chunk is a sub-frame tail (only possible when alignment exceeds chunk size). Save it
368 // to the splice buffer and release the item so the next fill() can complete the frame.
369 this->release_item_();
370 return 0;
371 }
372
373 this->current_data_ = chunk_data;
374 this->current_available_ = aligned_bytes;
375 return aligned_bytes;
376}
377
378} // namespace esphome::audio
379
380#endif
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
T * reallocate(T *p, size_t n)
Definition helpers.h:2340
void deallocate(T *p, size_t n)
Definition helpers.h:2355
T * allocate(size_t n)
Definition helpers.h:2325
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 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.
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 expose_padded_partial(size_t frame_bytes)
Zero-pads held whole frames short of an alignment unit and exposes them, so a stream's tail is not st...
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.
void clear_buffered_data()
Discards all buffered audio: releases any held ring buffer item, clears the source's in-flight state,...
size_t fill(TickType_t ticks_to_wait) override
An unconsumed exposure stays in place and fill() returns 0 until it is fully consumed.
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