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