ESPHome 2026.9.0
Loading...
Searching...
No Matches
router_speaker.cpp
Go to the documentation of this file.
1#include "router_speaker.h"
2
3#ifdef USE_ESP32
4
6#include "esphome/core/hal.h"
7#include "esphome/core/log.h"
8
9#include "esp_timer.h"
10
11#include <algorithm>
12
13namespace esphome::router {
14
15static const char *const TAG = "router.speaker";
16
17// Maximum time to wait for the active output to report running after start() before giving up
18static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000;
19
20static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
21 uint32_t current = var.load(std::memory_order_acquire);
22 uint32_t subtracted = 0;
23 if (current > 0) {
24 uint32_t new_value;
25 do {
26 subtracted = std::min(amount, current);
27 new_value = current - subtracted;
28 } while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire));
29 }
30 return subtracted;
31}
32
34 // Register a callback on every configured output. Each lambda captures its own
35 // index and only forwards when that output is the active one. This is required
36 // because CallbackManager has no remove() API.
37 for (size_t i = 0; i < this->outputs_.size(); i++) {
38 this->outputs_[i]->add_audio_output_callback([this, i](uint32_t frames, int64_t timestamp_us) {
39 // Always suppress the draining previous output during a switch, even if it's
40 // also the reselected active output (switching back to the bus holder).
41 // loop() fires one synthetic credit for its in-flight frames instead.
42 if (this->pending_start_prev_idx_.load(std::memory_order_relaxed) == static_cast<int8_t>(i)) {
43 return;
44 }
45 if (this->active_output_idx_.load(std::memory_order_relaxed) != static_cast<int8_t>(i)) {
46 return;
47 }
48 atomic_subtract_clamped(this->frames_in_pipeline_, frames);
49 this->audio_output_callback_.call(frames, timestamp_us);
50 });
51 }
52}
53
55 speaker::Speaker *active = this->get_active_output();
56
57 // Mid-switch: the new output's start() is deferred until the previous output
58 // fully releases shared hardware (e.g. a single i2s_audio bus driving two
59 // speakers). Starting earlier produces "Parent bus is busy" retries. The
60 // synthetic-credit callback is also deferred until prev is fully stopped, so
61 // that once its task has drained no natural callbacks can race ours.
62 const int8_t pending_prev_idx = this->pending_start_prev_idx_.load(std::memory_order_relaxed);
63 if (pending_prev_idx >= 0) {
64 speaker::Speaker *prev = this->outputs_[pending_prev_idx];
65 if (prev->is_stopped()) {
66 this->pending_start_prev_idx_.store(-1, std::memory_order_relaxed);
67
68 // Credit any frames left in prev's ring buffer / DMA so producer frame
69 // accounting (SpeakerSourceMediaPlayer pending_frames, sendspin/AEC
70 // clocks) clears cleanly. The leftover audio is intentionally dropped and
71 // the producer is told it played "now", giving a clean discontinuity that
72 // keeps frame accounting consistent across the switch.
73 const uint32_t in_flight = this->frames_in_pipeline_.exchange(0, std::memory_order_acq_rel);
74 if (in_flight > 0) {
75 this->audio_output_callback_.call(in_flight, esp_timer_get_time());
76 }
77
81 active->start();
82 }
83 return;
84 }
85
86 // Mirror the active output's running/stopped state into our own state_ so that
87 // is_running() / is_stopped() stay accurate from the producer's perspective.
88 // Also catch the active output self-stopping (e.g. i2s_audio silence timeout):
89 // without this, our state_ would stay RUNNING forever and the next play() would
90 // skip start(). The output retains its own volume/mute across a restart (and we
91 // forward those live regardless), but stream info arrives via the non-virtual
92 // set_audio_stream_info() and never reaches the output on its own; if the format
93 // changed while stopped, only start()'s apply_cached_state_to_active_() pushes it
94 // down before the output's play()-side auto-start locks in the stale format.
95 // While STARTING, ignore a transient stopped report as speaker running state
96 // is set asynchronously from start(). Timeout if the speaker never transitions.
97 if (this->state_ == speaker::STATE_STARTING) {
98 if (active->is_running()) {
100 } else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) {
101 ESP_LOGW(TAG, "Active output did not start; giving up");
103 }
104 } else if (active->is_stopped()) {
106 }
107}
108
110 ESP_LOGCONFIG(TAG,
111 "Router Speaker:\n"
112 " Outputs: %u",
113 static_cast<unsigned>(this->outputs_.size()));
114}
115
116size_t Router::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
117 speaker::Speaker *active = this->get_active_output();
118
119 // Drop frames during a mid-switch until the old output releases shared hardware;
120 // forwarding now would trigger the new output's play()-side auto-start while
121 // the bus is still busy.
122 if (this->pending_start_prev_idx_.load(std::memory_order_relaxed) >= 0) {
123 vTaskDelay(ticks_to_wait);
124 return 0;
125 }
126
127 // Producers (e.g. mixer) set stream info on us and then drive play() from a
128 // task without ever calling our start(). i2s_audio's play() auto-starts the
129 // underlying driver, so we must push our cached stream info to the active
130 // output before that auto-start, or it locks to its default (16k mono).
131 if (this->state_ == speaker::STATE_STOPPED) {
132 this->start();
133 vTaskDelay(ticks_to_wait);
134 ticks_to_wait = 0;
135 }
136
137 size_t written = active->play(data, length, ticks_to_wait);
138 if (written > 0) {
139 const uint32_t frames = this->audio_stream_info_.bytes_to_frames(written);
140 this->frames_in_pipeline_.fetch_add(frames, std::memory_order_release);
141 }
142 return written;
143}
144
146 this->frames_in_pipeline_.store(0, std::memory_order_release);
149 // May run on a producer task, so the cached loop timestamp is not usable here
150 this->state_start_ms_ = millis();
151 this->get_active_output()->start();
152}
153
155 // Cancel any pending mid-switch start; the producer wants us stopped.
156 this->pending_start_prev_idx_.store(-1, std::memory_order_relaxed);
158 this->get_active_output()->stop();
159}
160
162 this->pending_start_prev_idx_.store(-1, std::memory_order_relaxed);
164 this->get_active_output()->finish();
165}
166
168
169void Router::set_pause_state(bool pause_state) {
170 this->cached_pause_ = pause_state;
171 this->get_active_output()->set_pause_state(pause_state);
172}
173
174void Router::set_volume(float volume) {
175 this->volume_ = volume;
176 this->get_active_output()->set_volume(volume);
177}
178
179void Router::set_mute_state(bool mute_state) {
180 this->mute_state_ = mute_state;
181 this->get_active_output()->set_mute_state(mute_state);
182}
183
185 if (target == nullptr) {
186 return false;
187 }
188
189 int8_t new_idx = -1;
190 for (size_t i = 0; i < this->outputs_.size(); i++) {
191 if (this->outputs_[i] == target) {
192 new_idx = static_cast<int8_t>(i);
193 break;
194 }
195 }
196 if (new_idx < 0) {
197 ESP_LOGW(TAG, "Switch target is not a configured output");
198 return false;
199 }
200 if (new_idx == this->active_output_idx_.load(std::memory_order_relaxed)) {
201 return true;
202 }
203
204 // A switch is already in flight: pending_start_prev_idx_ is still releasing the
205 // shared bus and the current active output's start() is still deferred (it never
206 // started). Just redirect which output we start once the bus frees. Leave the bus
207 // holder (pending_start_prev_idx_), the in-flight frame counter (loop() still owes one
208 // synthetic credit for the bus holder's in-flight frames), and state_ alone, and
209 // don't stop the current active output, which never started.
210 if (this->pending_start_prev_idx_.load(std::memory_order_relaxed) >= 0) {
211 this->active_output_idx_.store(new_idx, std::memory_order_relaxed);
212 return true;
213 }
214
215 const bool was_active = (this->state_ == speaker::STATE_STARTING || this->state_ == speaker::STATE_RUNNING);
216 const int8_t old_idx = this->active_output_idx_.load(std::memory_order_relaxed);
217
218 if (was_active) {
219 this->outputs_[old_idx]->stop();
220 }
221
222 this->active_output_idx_.store(new_idx, std::memory_order_relaxed);
223
224 if (was_active) {
225 // Defer start and the synthetic-credit callback until the old output's
226 // task is fully stopped; loop() handles both. Firing the synthetic credit
227 // here would race the old task's still-in-flight natural callbacks,
228 // dispatching audio_output_callback_ concurrently from two threads, which
229 // some consumers (e.g. sendspin's progress sync) aren't reentrant-safe for.
230 // STATE_STOPPING keeps producers from observing a transient stopped state
231 // and lets our play() short-circuit so the new output's play() doesn't
232 // auto-start it while the shared bus is still being released.
234 this->pending_start_prev_idx_.store(old_idx, std::memory_order_relaxed);
235 } else {
236 this->frames_in_pipeline_.store(0, std::memory_order_release);
237 }
238 return true;
239}
240
242 speaker::Speaker *active = this->get_active_output();
244 active->set_volume(this->volume_);
245 active->set_mute_state(this->mute_state_);
246 active->set_pause_state(this->cached_pause_);
247}
248
249} // namespace esphome::router
250
251#endif // USE_ESP32
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
uint32_t bytes_to_frames(size_t bytes) const
Convert bytes to frames.
Definition audio.h:43
void set_mute_state(bool mute_state) override
size_t play(const uint8_t *data, size_t length) override
speaker::Speaker * get_active_output() const
void set_pause_state(bool pause_state) override
void set_volume(float volume) override
bool has_buffered_data() const override
std::atomic< uint32_t > frames_in_pipeline_
bool switch_to_output(speaker::Speaker *target)
Switch the active output to the given speaker.
std::atomic< int8_t > pending_start_prev_idx_
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
bool is_running() const
Definition speaker.h:65
virtual void set_volume(float volume)
Definition speaker.h:70
virtual void set_pause_state(bool pause_state)
Definition speaker.h:60
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
virtual void set_mute_state(bool mute_state)
Definition speaker.h:80
virtual bool has_buffered_data() const =0
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
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
int written
Definition helpers.h:1130
Application App
Global storage of Application pointer - only one Application can exist.
int64_t esp_timer_get_time(void)
static void uint32_t
uint16_t length
Definition tt21100.cpp:0