Spotify: tell cspot when a new track becomes audible
The Mac dropped the device soon after the first track change: we never
called SpircHandler::notifyAudioReachedPlayback(), so cspot's queue did
not advance and the state sent to Spotify stalled.
- The data callback records a boundary (output write position, track
id) when the track id changes; the session loop (<= 200 ms) calls
notifyAudioReachedPlayback(id) once playback (ring read position)
passes it, and notifyAudioEnded() after DEPLETED once the buffer is
empty. PLAYBACK_START clears boundaries (next data is a new one),
seek/flush drop pending ones.
- audio_ring exposes its write/read frame counters (wrap-safe compare).
- Verified: 10 min on the Mac without a disconnect, every track change
notified ("track audible, Spotify notified"), app shows the playing
track (switches up to ~3-5 s early), 0 underruns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -40,6 +41,54 @@ static std::atomic<uint64_t> s_pcm_bytes{0};
|
||||
static std::shared_ptr<cspot::SpircHandler> 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<Boundary> s_bounds;
|
||||
static std::string s_last_id; // track of the most recent data callback
|
||||
static std::atomic<bool> s_depleted{false};
|
||||
|
||||
static void clear_boundaries(bool new_load)
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<cspot::SpircHandler> &handler)
|
||||
{
|
||||
if (!s_written || !s_played) {
|
||||
return;
|
||||
}
|
||||
uint32_t played = s_played();
|
||||
std::string reached;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<const int16_t *>(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<int>(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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user