ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
mixer_speaker.cpp
Go to the documentation of this file.
1#include "mixer_speaker.h"
2
3#ifdef USE_ESP32
4
6#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
10#include <mixer.h> // esp-audio-libs
11#include <pcm_convert.h> // esp-audio-libs
12
13#include <algorithm>
14#include <cstring>
15
16namespace esphome::mixer_speaker {
17
18static const UBaseType_t MIXER_TASK_PRIORITY = 10;
19
20static const uint32_t STOPPING_TIMEOUT_MS = 5000;
21static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
22static const uint32_t TASK_DELAY_MS = 25;
23static const uint32_t MIXER_AUTO_STOP_DEBOUNCE_MS = 200;
24
25static const size_t TASK_STACK_SIZE = 4096;
26
27static const char *const TAG = "speaker_mixer";
28
29// Event bits for SourceSpeaker command processing
35
36// Event bits for mixer task control and state
45 MIXER_TASK_ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
46};
47
48static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
49 uint32_t current = var.load(std::memory_order_acquire);
50 uint32_t subtracted = 0;
51 if (current > 0) {
52 uint32_t new_value;
53 do {
54 subtracted = std::min(amount, current);
55 new_value = current - subtracted;
56 } while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire));
57 }
58 return subtracted;
59}
60
61static bool create_event_group(EventGroupHandle_t &event_group, Component *component) {
62 event_group = xEventGroupCreate();
63 if (event_group == nullptr) {
64 ESP_LOGE(TAG, "Failed to create event group");
65 component->mark_failed();
66 return false;
67 }
68 return true;
69}
70
72 ESP_LOGCONFIG(TAG,
73 "Mixer Source Speaker\n"
74 " Buffer Duration: %" PRIu32 " ms",
76 if (this->timeout_ms_.has_value()) {
77 ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_ms_.value());
78 } else {
79 ESP_LOGCONFIG(TAG, " Timeout: never");
80 }
81}
82
84 if (!create_event_group(this->event_group_, this)) {
85 return;
86 }
87
88 // Start with loop disabled since we begin in STATE_STOPPED with no pending commands
89 this->disable_loop();
90
91 this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
92 // First, drain the playback delay (frames in pipeline before this source started contributing)
93 uint32_t delay_to_drain = atomic_subtract_clamped(this->playback_delay_frames_, new_frames);
94 uint32_t remaining_frames = new_frames - delay_to_drain;
95
96 // Then, count towards this source's pending playback frames
97 if (remaining_frames > 0) {
98 uint32_t speakers_playback_frames = atomic_subtract_clamped(this->pending_playback_frames_, remaining_frames);
99 if (speakers_playback_frames > 0) {
100 this->audio_output_callback_(speakers_playback_frames, write_timestamp);
101 }
102 }
103 });
104}
105
107 uint32_t event_bits = xEventGroupGetBits(this->event_group_);
108
109 // Process commands with priority: STOP > FINISH > START
110 // This ensures stop commands take precedence over conflicting start commands
111 if (event_bits & SOURCE_SPEAKER_COMMAND_STOP) {
112 if (this->state_ == speaker::STATE_RUNNING) {
113 // Clear both STOP and START bits - stop takes precedence
114 xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
115 this->enter_stopping_state_();
116 } else if (this->state_ == speaker::STATE_STOPPED) {
117 // Already stopped, just clear the command bits
118 xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
119 }
120 // Leave bits set if transitioning states (STARTING/STOPPING) - will be processed once state allows
121 } else if (event_bits & SOURCE_SPEAKER_COMMAND_FINISH) {
122 if (this->state_ == speaker::STATE_RUNNING) {
123 xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
124 this->stop_gracefully_ = true;
125 } else if (this->state_ == speaker::STATE_STOPPED) {
126 // Already stopped, just clear the command bit
127 xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
128 }
129 // Leave bit set if transitioning states - will be processed once state allows
130 } else if (event_bits & SOURCE_SPEAKER_COMMAND_START) {
131 if (this->state_ == speaker::STATE_STOPPED) {
132 xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
134 } else if (this->state_ == speaker::STATE_RUNNING) {
135 // Already running, just clear the command bit
136 xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
137 }
138 // Leave bit set if transitioning states - will be processed once state allows
139 }
140 // Process state machine
141 switch (this->state_) {
143 esp_err_t err = this->start_();
144 if (err == ESP_OK) {
145 this->pending_playback_frames_.store(0, std::memory_order_release); // reset pending playback frames
146 this->playback_delay_frames_.store(0, std::memory_order_release); // reset playback delay
147 this->has_contributed_.store(false, std::memory_order_release); // reset contribution tracking
149 this->stop_gracefully_ = false;
150 this->last_seen_data_ms_ = millis();
151 this->status_clear_error();
152 } else {
153 switch (err) {
154 case ESP_ERR_NO_MEM:
155 this->status_set_error(LOG_STR("Not enough memory"));
156 break;
157 case ESP_ERR_NOT_SUPPORTED:
158 this->status_set_error(LOG_STR("Unsupported bit depth"));
159 break;
160 case ESP_ERR_INVALID_ARG:
161 this->status_set_error(LOG_STR("Incompatible audio streams"));
162 break;
163 case ESP_ERR_INVALID_STATE:
164 this->status_set_error(LOG_STR("Task failed"));
165 break;
166 default:
167 this->status_set_error(LOG_STR("Failed"));
168 break;
169 }
170
171 this->enter_stopping_state_();
172 }
173 break;
174 }
176 if (!this->audio_source_->has_buffered_data() &&
177 (this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
178 // No audio data in buffer waiting to get mixed and no frames are pending playback
179 if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
180 this->stop_gracefully_) {
181 // Timeout exceeded or graceful stop requested
182 this->enter_stopping_state_();
183 }
184 }
185 break;
187 if ((this->parent_->get_output_speaker()->get_pause_state()) ||
188 ((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
189 // If parent speaker is paused or if the stopping timeout is exceeded, force stop the output speaker
190 this->parent_->get_output_speaker()->stop();
191 }
192
193 if (this->parent_->get_output_speaker()->is_stopped() ||
194 (this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
195 // Output speaker is stopped OR all pending playback frames have played
196 this->pending_playback_frames_.store(0, std::memory_order_release);
197 this->stop_gracefully_ = false;
198
200 }
201 break;
202 }
204 // Re-check event bits for any new commands that may have arrived
205 event_bits = xEventGroupGetBits(this->event_group_);
206 if (!(event_bits &
208 // No pending commands, disable loop to save CPU cycles
209 this->disable_loop();
210 }
211 break;
212 }
213}
214
215size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
216 if (this->is_stopped()) {
217 this->start();
218 }
219 size_t bytes_written = 0;
220 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
221 if (temp_ring_buffer.use_count() > 0) {
222 // Only write to the ring buffer if the reference is valid
223 bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
224 if (bytes_written > 0) {
225 this->last_seen_data_ms_ = millis();
226 }
227 } else {
228 // Delay to avoid repeatedly hammering while waiting for the speaker to start
229 vTaskDelay(ticks_to_wait);
230 }
231 return bytes_written;
232}
233
234void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
236 uint32_t event_bits = xEventGroupGetBits(this->event_group_);
237 if (!(event_bits & command_bit)) {
238 xEventGroupSetBits(this->event_group_, command_bit);
239 if (wake_loop) {
241 }
242 }
243}
244
246
248 const size_t bytes_per_frame = this->audio_stream_info_.frames_to_bytes(1);
249 // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and
250 // avoids unnecessary single-frame splices.
251 const size_t ring_buffer_size =
252 (this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame;
253 if (this->audio_source_.use_count() == 0) {
254 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
255 if (!temp_ring_buffer) {
256 temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
257 this->ring_buffer_ = temp_ring_buffer;
258 }
259
260 if (!temp_ring_buffer) {
261 return ESP_ERR_NO_MEM;
262 }
263
264 std::unique_ptr<audio::RingBufferAudioSource> source = audio::RingBufferAudioSource::create(
265 temp_ring_buffer, this->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS),
266 static_cast<uint8_t>(bytes_per_frame));
267 if (source == nullptr) {
268 return ESP_ERR_NO_MEM;
269 }
270 this->audio_source_ = std::move(source);
271 }
272
273 return this->parent_->start(this->audio_stream_info_);
274}
275
277
279
281 return ((this->audio_source_.use_count() > 0) && this->audio_source_->has_buffered_data());
282}
283
284void SourceSpeaker::set_mute_state(bool mute_state) {
285 this->mute_state_ = mute_state;
286 this->parent_->get_output_speaker()->set_mute_state(mute_state);
287}
288
290
291void SourceSpeaker::set_volume(float volume) {
292 this->volume_ = volume;
293 this->parent_->get_output_speaker()->set_volume(volume);
294}
295
297
298size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBufferAudioSource> &audio_source,
299 TickType_t ticks_to_wait) {
300 if (audio_source->available() > 0) {
301 // Existing exposure was ducked when fill() promoted it; do not re-duck on partial-consume re-entry.
302 return 0;
303 }
304
305 size_t bytes_read = audio_source->fill(ticks_to_wait, false);
306
307 uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
308 if (samples_to_duck > 0) {
309 esp_audio_libs::ducking::apply(audio_source->mutable_data(),
310 static_cast<uint8_t>(this->audio_stream_info_.get_bits_per_sample() / 8),
311 samples_to_duck, this->ducking_state_);
312 }
313
314 return bytes_read;
315}
316
317void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
318 const uint32_t transition_samples = duration > 0 ? this->audio_stream_info_.ms_to_samples(duration) : 0;
319 esp_audio_libs::ducking::set_target(this->ducking_state_, decibel_reduction, transition_samples);
320}
321
327
329 ESP_LOGCONFIG(TAG,
330 "Speaker Mixer:\n"
331 " Number of output channels: %" PRIu8 "\n"
332 " Output bits per sample: %" PRIu8,
334}
335
337 if (!create_event_group(this->event_group_, this)) {
338 return;
339 }
340
341 // Register callback to track frames in the output pipeline
342 this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
343 atomic_subtract_clamped(this->frames_in_pipeline_, new_frames);
344 });
345
346 // Start with loop disabled since no task is running and no commands are pending
347 this->disable_loop();
348}
349
351 uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
352
353 // Handle pending start request
354 if (event_group_bits & MIXER_TASK_COMMAND_START) {
355 // Only start the task if it's fully stopped and cleaned up
356 if (!this->status_has_error() && !this->task_.is_created()) {
357 if (this->task_.create(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this, MIXER_TASK_PRIORITY,
358 this->task_stack_in_psram_)) {
359 xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START);
360 } else {
361 ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
362 this->status_momentary_error("failure", 1000);
363 return;
364 }
365 }
366 }
367
368 if (event_group_bits & MIXER_TASK_STATE_STARTING) {
369 ESP_LOGD(TAG, "Starting");
370 xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
371 }
372 if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
373 this->status_set_error(LOG_STR("Not enough memory"));
374 xEventGroupClearBits(this->event_group_, MIXER_TASK_ERR_ESP_NO_MEM);
375 }
376 if (event_group_bits & MIXER_TASK_STATE_RUNNING) {
377 ESP_LOGV(TAG, "Started");
378 this->status_clear_error();
379 xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
380 }
381 if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
382 ESP_LOGV(TAG, "Stopping");
383 xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
384 }
385 if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
386 this->task_.deallocate();
387 ESP_LOGD(TAG, "Stopped");
388 xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
389 this->all_stopped_since_ms_ = 0;
390 }
391
392 if (this->task_.is_created()) {
393 // If the mixer task is running, check if all source speakers are stopped
394
395 bool all_stopped = true;
396
397 for (auto &speaker : this->source_speakers_) {
398 all_stopped &= speaker->is_stopped();
399 }
400
401 if (all_stopped) {
402 if (this->all_stopped_since_ms_ == 0) {
404 } else if ((millis() - this->all_stopped_since_ms_) >= MIXER_AUTO_STOP_DEBOUNCE_MS) {
405 // Send stop command only after a short debounce to avoid stop/start thrash during rapid seeks.
406 xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
407 }
408 } else {
409 this->all_stopped_since_ms_ = 0;
410 // New activity detected; clear any stale auto-stop request before it can stop the running task.
411 if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
412 xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
413 }
414 }
415 } else {
416 // Task is fully stopped and cleaned up, check if we can disable loop
417 event_group_bits = xEventGroupGetBits(this->event_group_);
418 if (event_group_bits == 0) {
419 // No pending events, disable loop to save CPU cycles
420 this->disable_loop();
421 }
422 }
423}
424
426 if (!this->audio_stream_info_.has_value()) {
427 this->audio_stream_info_ =
430 } else {
431 if (!this->queue_mode_ && (stream_info.get_sample_rate() != this->audio_stream_info_.value().get_sample_rate())) {
432 // The two audio streams must have the same sample rate to mix properly if not in queue mode
433 return ESP_ERR_INVALID_ARG;
434 }
435 }
436
437 this->enable_loop_soon_any_context(); // ensure loop processes command
438
439 // Starting a new stream supersedes any previously queued stop request.
440 xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
441
442 uint32_t event_bits = xEventGroupGetBits(this->event_group_);
443 if (!(event_bits & MIXER_TASK_COMMAND_START)) {
444 // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency
445 xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START);
447 }
448
449 return ESP_OK;
450}
451
452// NOLINTBEGIN(bugprone-unchecked-optional-access) -- audio_stream_info_ always set before this task is created
454 MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
455
456 xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
457
458 { // Ensure C++ objects fall out of scope to ensure proper cleanup before stopping the task
459 std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
460 this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
461
462 if (output_transfer_buffer == nullptr) {
463 xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
464
465 vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
466 }
467
468 output_transfer_buffer->set_sink(this_mixer->output_speaker_);
469
470 xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
471
472 bool sent_finished = false;
473
474 // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
475 FixedVector<SourceSpeaker *> speakers_with_data;
477 speakers_with_data.init(this_mixer->source_speakers_.size());
478 audio_sources_with_data.init(this_mixer->source_speakers_.size());
479
480 while (true) {
481 uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
482 if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
483 break;
484 }
485
486 // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
487 output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);
488
489 const uint32_t output_frames_free =
490 this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
491
492 speakers_with_data.clear();
493 audio_sources_with_data.clear();
494
495 for (auto &speaker : this_mixer->source_speakers_) {
496 if (speaker->is_running() && !speaker->get_pause_state()) {
497 // Speaker is running and not paused, so it possibly can provide audio data
498 std::shared_ptr<audio::RingBufferAudioSource> audio_source = speaker->get_audio_source().lock();
499 if (audio_source.use_count() == 0) {
500 // No audio source allocated, so skip processing this speaker
501 continue;
502 }
503 speaker->process_data_from_source(audio_source, 0); // Exposes and ducks audio from source ring buffers
504
505 if (audio_source->available() > 0) {
506 // Retain shared ownership across the mixing pass so the source isn't released mid-mix
507 audio_sources_with_data.push_back(audio_source);
508 speakers_with_data.push_back(speaker);
509 }
510 }
511 }
512
513 if (audio_sources_with_data.empty()) {
514 // No audio available for transferring, block task temporarily
515 delay(TASK_DELAY_MS);
516 continue;
517 }
518
519 uint32_t frames_to_mix = output_frames_free;
520
521 const audio::AudioStreamInfo &output_info = this_mixer->audio_stream_info_.value();
522 const uint8_t output_bps = output_info.get_bits_per_sample() / 8;
523 const uint8_t output_channels = output_info.get_channels();
524
525 if ((audio_sources_with_data.size() == 1) || this_mixer->queue_mode_) {
526 // Only one speaker has audio data, just copy samples over
527
528 audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info();
529
530 if (active_stream_info.get_sample_rate() ==
532 // Speaker's sample rate matches the output speaker's, convert directly into the output buffer
533
534 const uint32_t frames_available_in_buffer =
535 active_stream_info.bytes_to_frames(audio_sources_with_data[0]->available());
536 frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
537 esp_audio_libs::pcm_convert::copy_frames(
538 audio_sources_with_data[0]->data(), output_transfer_buffer->get_buffer_end(),
539 static_cast<uint8_t>(active_stream_info.get_bits_per_sample() / 8), active_stream_info.get_channels(),
540 output_bps, output_channels, frames_to_mix);
541
542 // Set playback delay for newly contributing source
543 if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
544 speakers_with_data[0]->playback_delay_frames_.store(
545 this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
546 speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
547 }
548
549 // Update source speaker pending frames
550 speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
551 audio_sources_with_data[0]->consume(active_stream_info.frames_to_bytes(frames_to_mix));
552
553 // Update output transfer buffer length and pipeline frame count
554 output_transfer_buffer->increase_buffer_length(output_info.frames_to_bytes(frames_to_mix));
555 this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
556 } else {
557 // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
558 if (!this_mixer->output_speaker_->is_stopped()) {
559 if (!sent_finished) {
560 this_mixer->output_speaker_->finish();
561 sent_finished = true; // Avoid repeatedly sending the finish command
562 }
563 } else {
564 // Speaker has finished writing the current audio, update the stream information and restart the speaker
565 this_mixer->audio_stream_info_ =
567 active_stream_info.get_sample_rate());
568 this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
569 this_mixer->output_speaker_->start();
570 // Reset pipeline frame count since we're starting fresh with a new sample rate
571 this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
572 sent_finished = false;
573 }
574 }
575 } else {
576 // Determine how many frames to mix
577 for (size_t i = 0; i < audio_sources_with_data.size(); ++i) {
578 const uint32_t frames_available_in_buffer =
579 speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(audio_sources_with_data[i]->available());
580 frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
581 }
582 const uint8_t *primary_buffer = audio_sources_with_data[0]->data();
583 audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
584
585 // Mix two streams together at a time, accumulating into the output buffer.
586 for (size_t i = 1; i < audio_sources_with_data.size(); ++i) {
587 esp_audio_libs::mixer::mix_frames(
588 primary_buffer, static_cast<uint8_t>(primary_stream_info.get_bits_per_sample() / 8),
589 primary_stream_info.get_channels(), audio_sources_with_data[i]->data(),
590 static_cast<uint8_t>(speakers_with_data[i]->get_audio_stream_info().get_bits_per_sample() / 8),
591 speakers_with_data[i]->get_audio_stream_info().get_channels(), output_transfer_buffer->get_buffer_end(),
592 output_bps, output_channels, frames_to_mix);
593
594 if (i != audio_sources_with_data.size() - 1) {
595 // Need to mix more streams together, point primary buffer and stream info to the already mixed output
596 primary_buffer = output_transfer_buffer->get_buffer_end();
597 primary_stream_info = output_info;
598 }
599 }
600
601 // Get current pipeline depth for delay calculation (before incrementing)
602 uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
603
604 // Update source audio source consumption and add new audio durations to the source speaker pending playbacks
605 for (size_t i = 0; i < audio_sources_with_data.size(); ++i) {
606 // Set playback delay for newly contributing sources
607 if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
608 speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
609 speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
610 }
611
612 speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
613 audio_sources_with_data[i]->consume(
614 speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
615 }
616
617 // Update output transfer buffer length and pipeline frame count (once, not per source)
618 output_transfer_buffer->increase_buffer_length(output_info.frames_to_bytes(frames_to_mix));
619 this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
620 }
621 }
622
623 xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
624 }
625
626 // Reset pipeline frame count since the task is stopping
627 this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
628
629 xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
630
631 vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
632}
633// NOLINTEND(bugprone-unchecked-optional-access)
634
635} // namespace esphome::mixer_speaker
636
637#endif
void wake_loop_threadsafe()
Wake the main event loop from another thread or callback.
void status_momentary_error(const char *name, uint32_t length=5000)
Set error status flag and automatically clear it after a timeout.
void status_clear_error()
Definition component.h:295
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
bool status_has_error() const
Definition component.h:280
void disable_loop()
Disable this component's loop.
Fixed-capacity vector - allocates once at runtime, never reallocates This avoids std::vector template...
Definition helpers.h:529
bool empty() const
Definition helpers.h:687
size_t size() const
Definition helpers.h:686
void push_back(const T &value)
Add element without bounds checking Caller must ensure sufficient capacity was allocated via init() S...
Definition helpers.h:646
void init(size_t n)
Definition helpers.h:619
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, bool use_psram)
Allocate stack and create task.
bool is_created() const
Check if the task has been created and not yet destroyed.
Definition static_task.h:18
void deallocate()
Delete the task (if running) and free the stack buffer.
static std::unique_ptr< AudioSinkTransferBuffer > create(size_t buffer_size)
Creates a new sink transfer buffer.
size_t ms_to_bytes(uint32_t ms) const
Converts duration to bytes.
Definition audio.h:73
size_t frames_to_bytes(uint32_t frames) const
Converts frames to bytes.
Definition audio.h:53
uint8_t get_bits_per_sample() const
Definition audio.h:28
uint32_t ms_to_samples(uint32_t ms) const
Converts duration to samples.
Definition audio.h:68
uint32_t bytes_to_frames(size_t bytes) const
Convert bytes to frames.
Definition audio.h:43
uint8_t get_channels() const
Definition audio.h:29
uint32_t get_sample_rate() const
Definition audio.h:30
uint32_t bytes_to_samples(size_t bytes) const
Convert bytes to samples.
Definition audio.h:48
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.
esp_err_t start(audio::AudioStreamInfo &stream_info)
Starts the mixer task.
FixedVector< SourceSpeaker * > source_speakers_
speaker::Speaker * get_output_speaker() const
std::atomic< uint32_t > frames_in_pipeline_
static void audio_mixer_task(void *params)
optional< audio::AudioStreamInfo > audio_stream_info_
esp_audio_libs::ducking::DuckingState ducking_state_
void set_mute_state(bool mute_state) override
Mute state changes are passed to the parent's output speaker.
void send_command_(uint32_t command_bit, bool wake_loop=false)
std::shared_ptr< audio::RingBufferAudioSource > audio_source_
std::atomic< uint32_t > playback_delay_frames_
std::weak_ptr< ring_buffer::RingBuffer > ring_buffer_
void apply_ducking(uint8_t decibel_reduction, uint32_t duration)
Sets the ducking level for the source speaker.
size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override
size_t process_data_from_source(std::shared_ptr< audio::RingBufferAudioSource > &audio_source, TickType_t ticks_to_wait)
Exposes the next ring buffer chunk (zero-copy) and ducks the freshly exposed bytes in place.
void set_volume(float volume) override
Volume state changes are passed to the parent's output speaker.
std::atomic< uint32_t > pending_playback_frames_
static std::unique_ptr< RingBuffer > create(size_t len, MemoryPreference preference=MemoryPreference::EXTERNAL_FIRST)
virtual void set_volume(float volume)
Definition speaker.h:70
virtual float get_volume()
Definition speaker.h:78
virtual bool get_pause_state() const
Definition speaker.h:61
CallbackManager< void(uint32_t, int64_t)> audio_output_callback_
Definition speaker.h:122
void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info)
Definition speaker.h:98
audio::AudioStreamInfo & get_audio_stream_info()
Definition speaker.h:102
void add_audio_output_callback(F &&callback)
Callback function for sending the duration of the audio written to the speaker since the last callbac...
Definition speaker.h:108
virtual bool get_mute_state()
Definition speaker.h:92
virtual void set_mute_state(bool mute_state)
Definition speaker.h:80
audio::AudioStreamInfo audio_stream_info_
Definition speaker.h:114
virtual void start()=0
virtual void finish()
Definition speaker.h:57
bool is_stopped() const
Definition speaker.h:66
virtual void stop()=0
const Component * component
Definition component.cpp:34
uint8_t duration
Definition msa3xx.h:0
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0