diff --git a/components/spotify/include/spotify.h b/components/spotify/include/spotify.h index e9dbd40..97ad67e 100644 --- a/components/spotify/include/spotify.h +++ b/components/spotify/include/spotify.h @@ -21,9 +21,13 @@ typedef enum { // PCM from Spotify: 44.1 kHz stereo s16 interleaved. Return the frames taken (may block). typedef size_t (*spotify_pcm_cb_t)(const int16_t *pcm, size_t frames); typedef void (*spotify_event_cb_t)(spotify_event_t ev, int value); +// Output positions in frames (monotonic, wrap at 2^32): written into the output buffer so far, and +// played out so far. Used to tell Spotify when a new track actually becomes audible. +typedef uint32_t (*spotify_pos_cb_t)(void); // Registers the zeroconf endpoints (/spotify_info), starts the session task and applies the config. -esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event); +esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event, spotify_pos_cb_t written, + spotify_pos_cb_t played); // Re-read the "source" config: enabled (advertised, sessions accepted) when mode is spotify/auto and // the client credentials are set; otherwise not advertised and a running session is ended. // A new device name ends the session and re-advertises. diff --git a/components/spotify/spotify.cpp b/components/spotify/spotify.cpp index f8d82c2..aa337b5 100644 --- a/components/spotify/spotify.cpp +++ b/components/spotify/spotify.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,54 @@ static std::atomic s_pcm_bytes{0}; static std::shared_ptr s_handler; // running session, else null static spotify_pcm_cb_t s_pcm_cb; static spotify_event_cb_t s_event_cb; +static spotify_pos_cb_t s_written, s_played; + +// Track boundaries in the output buffer: when playback passes one, the new track is audible and +// Spotify must be told (notifyAudioReachedPlayback); without it the app's state stalls after the +// first track and it drops the device. Guarded by s_bound_mutex (data callback vs session loop). +struct Boundary { + uint32_t pos; + std::string id; +}; +static std::mutex s_bound_mutex; +static std::deque s_bounds; +static std::string s_last_id; // track of the most recent data callback +static std::atomic s_depleted{false}; + +static void clear_boundaries(bool new_load) +{ + std::lock_guard lock(s_bound_mutex); + s_bounds.clear(); + if (new_load) { + s_last_id.clear(); // the next data (new track) marks a boundary + } +} + +// Session loop, every <= 200 ms. +static void check_playback(const std::shared_ptr &handler) +{ + if (!s_written || !s_played) { + return; + } + uint32_t played = s_played(); + std::string reached; + { + std::lock_guard lock(s_bound_mutex); + while (!s_bounds.empty() && (int32_t)(played - s_bounds.front().pos) >= 0) { + reached = s_bounds.front().id; // latest boundary passed wins + s_bounds.pop_front(); + } + } + if (!reached.empty()) { + handler->notifyAudioReachedPlayback(reached); + ESP_LOGI(TAG, "track audible, Spotify notified"); + } + if (s_depleted && played == s_written()) { + s_depleted = false; + handler->notifyAudioEnded(); + ESP_LOGI(TAG, "playlist ended, Spotify notified"); + } +} extern "C" void spotify_suspend(bool suspend) { @@ -251,6 +300,14 @@ static void session(void) // PCM (44.1 kHz stereo s16) to the player; returning fewer bytes makes cspot retry the rest, // so the player's blocking write paces decoding to playback speed. handler->getTrackPlayer()->setDataCallback([](uint8_t *data, size_t len, std::string_view id) -> size_t { + if (s_written) { + // First data of a new track: it becomes audible when playback reaches this position. + std::lock_guard lock(s_bound_mutex); + if (id != s_last_id) { + s_last_id = std::string(id); + s_bounds.push_back({ s_written(), s_last_id }); + } + } size_t frames = len / 4; size_t taken = s_pcm_cb ? s_pcm_cb(reinterpret_cast(data), frames) : frames; s_pcm_bytes += taken * 4; @@ -272,6 +329,10 @@ static void session(void) case E::FLUSH: case E::SEEK: case E::PLAYBACK_START: + // The output buffer is dropped: pending boundaries are void. A new load (PLAYBACK_START) + // gets a new boundary with its first data; a seek stays within the same track. + clear_boundaries(ev->eventType == E::PLAYBACK_START); + s_depleted = false; if (s_event_cb) { s_event_cb(SPOTIFY_EV_FLUSH, 0); if (ev->eventType == E::PLAYBACK_START) { @@ -285,6 +346,9 @@ static void session(void) s_event_cb(SPOTIFY_EV_VOLUME, std::get(ev->data)); } break; + case E::DEPLETED: + s_depleted = true; // queue done: tell Spotify once the buffer has played out + break; default: break; } @@ -293,8 +357,11 @@ static void session(void) s_handler = handler; s_state = "connected"; ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str()); + clear_boundaries(true); + s_depleted = false; while (!s_stop) { ctx->session->handlePacket(); // waits at most 200 ms + check_playback(handler); } // Stops the queue and player tasks (waits for them), then closes the connection. s_handler.reset(); @@ -367,10 +434,13 @@ extern "C" void spotify_apply(void) s_enabled = true; } -extern "C" esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event) +extern "C" esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event, spotify_pos_cb_t written, + spotify_pos_cb_t played) { s_pcm_cb = pcm; s_event_cb = event; + s_written = written; + s_played = played; if (!bell::bellGlobalLogger) { bell::bellGlobalLogger = new EspLogger(); } diff --git a/main/audio_ring.c b/main/audio_ring.c index 58fbe2b..3fabe7d 100644 --- a/main/audio_ring.c +++ b/main/audio_ring.c @@ -70,6 +70,16 @@ size_t audio_ring_read(int32_t *frames, size_t n) return n; } +uint32_t audio_ring_written(void) +{ + return __atomic_load_n(&s_wr, __ATOMIC_ACQUIRE); +} + +uint32_t audio_ring_read_pos(void) +{ + return __atomic_load_n(&s_rd, __ATOMIC_ACQUIRE); +} + void audio_ring_flush(void) { __atomic_store_n(&s_rd, __atomic_load_n(&s_wr, __ATOMIC_ACQUIRE), __ATOMIC_RELEASE); diff --git a/main/audio_ring.h b/main/audio_ring.h index cc99cd3..be3fe31 100644 --- a/main/audio_ring.h +++ b/main/audio_ring.h @@ -13,3 +13,6 @@ size_t audio_ring_space(void); // frames that can be written size_t audio_ring_write(const int32_t *frames, size_t n); // producer; returns frames written size_t audio_ring_read(int32_t *frames, size_t n); // consumer; returns frames read void audio_ring_flush(void); // consumer side: drop everything buffered +// Monotonic frame counters (wrap at 2^32): frames written so far / read (played) so far. +uint32_t audio_ring_written(void); +uint32_t audio_ring_read_pos(void); diff --git a/main/player.c b/main/player.c index d6410af..f265b91 100644 --- a/main/player.c +++ b/main/player.c @@ -207,7 +207,7 @@ esp_err_t player_init(void) if (!s_spotify_conv) { return ESP_ERR_NO_MEM; } - spotify_init(spotify_pcm, spotify_event); + spotify_init(spotify_pcm, spotify_event, audio_ring_written, audio_ring_read_pos); aes67_ota_on_update(on_ota); err = hls_start(decoder_feed); if (err != ESP_OK) {