Compare commits

..

5 Commits

Author SHA1 Message Date
bsncubed 78c480a7c2 CLAUDE.md: Spotify status after the connection fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:47:39 +10:00
bsncubed b8702e6faa 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>
2026-09-25 20:47:28 +10:00
bsncubed 5ce4588c80 cspot: delayed Pong (like librespot) stops the 6-minute connection resets
The Spotify AP reset the connection (recv -1, errno 104 ECONNRESET) on
the third Ping, every 6 minutes, like clockwork. cspot answered each
Ping at once; librespot (core/src/session.rs) waits 60 s:
Ping -> 60 s -> Pong -> PongAck -> 60 s -> Ping.
- 0003-delayed-pong: record the Ping, send the Pong 60 s later from
  triggerTimeout() (called on every 3 s receive timeout), through the
  PR #3 connection snapshot; dropped on reconnect.
- 0002-diag-log-recv-errors: log recv's return value/errno before
  "Error in read" (error path only); this is what showed the RST.
Verified: over 8+ minutes Ping/delayed Pong/PongAck every 2 min, no
reset at the 6-minute mark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:47:27 +10:00
bsncubed 8008aa175f cspot: patch in philippe44/cspot PR #3 (crash when a request races a reconnect)
MercurySession::reconnect() nulls conn/shanConn (for 5 s per failed
retry) while other tasks keep sending through them: a NULL dereference,
not a catchable exception. Our Spotify sessions are dropped by the
server and reconnect every ~6 minutes, so this race is hit regularly;
likely behind the sporadic resets during long sessions/OTA.
Patch applies cleanly to the pinned 3010349; builds, boots, confirmed.
Drop it once the fix is in the pinned cspot commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:17:01 +10:00
bsncubed f0fe53d5d1 Spotify: end the session for OTA; app volume starts at the player's
- OTA: the player now ends the Spotify session (spotify_suspend: stop
  flag, waits until the session's tasks are gone, refuses new ones)
  instead of pausing it, and suspends HLS. Pausing was not enough (an
  upload still failed once with a paused session). Verified twice with a
  session running for minutes: "Disconnecting mercury session / session
  ended / suspended", installed in ~26 s, clean reboot.
- Volume: createFromBlob() starts cspot at volume 0, which the app
  showed while we played at 100 %. The session now starts with the
  player's volume (spotify_set_volume); volumes coming from the app are
  stored exactly, without echo. Verified: slider starts at 100 %.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:15:29 +10:00
9 changed files with 407 additions and 16 deletions
+5 -4
View File
@@ -59,12 +59,13 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4
- Download speed ~1.4 Mbit/s over TLS (fine for ~250 kbit/s; tune buffer sizes / per-chunk overhead). - Download speed ~1.4 Mbit/s over TLS (fine for ~250 kbit/s; tune buffer sizes / per-chunk overhead).
- Not yet tested: HE-AAC variant (140k), other stations, fMP4/ADTS-only playlists, discontinuities (#EXT-X-DISCONTINUITY), network loss and recovery, long runs. - Not yet tested: HE-AAC variant (140k), other stations, fMP4/ADTS-only playlists, discontinuities (#EXT-X-DISCONTINUITY), network loss and recovery, long runs.
- Audio starts only after PTP lock (~20 s after boot): intended, TX needs PTP. - Audio starts only after PTP lock (~20 s after boot): intended, TX needs PTP.
- [ ] cspot (Spotify Connect): login, audio, pause/skip/seek and app volume work (7.5b). Open: - [ ] cspot (Spotify Connect): login, audio, pause/skip/seek, app volume, live mode switching work; stays connected across track changes and past 6 min (7.5b). Fixed so far: logger NULL crash, Vorbis symbol clash, volume starting at 0, reconnect race (PR #3 patch), 6-minute AP resets (delayed Pong patch), app dropping the device (notifyAudioReachedPlayback). Open:
- OTA during an active Spotify session made the upload crawl (~10 kB/s) and ended in a reset. With Spotify paused for the upload (aes67_ota_on_update) 2 of 3 later tries worked, 1 still failed: intermittent, root cause not found. Serial log of a good upload: flash writes stall the TX task > 20 ms about 20x/s (509 'aes67_tx: resync' in 26 s, stream stutters during OTA; none during normal playback). Likely the same stalls hit cspot's network/TLS tasks. Ideas: end the Spotify session (not just pause) for OTA; rate-limit the resync warning. - OTA with a session: now ends the session first; 2 of 2 uploads with a running session were clean since. Flash writes still stall TX > 20 ms during uploads (stream stutters while updating).
- Opening /dev/ttyACM0 resets the board even with DTR/RTS held low: start serial captures before setting up a test.
- First connects sometimes fail ("Can't connect to spotify servers"), a retry works. - First connects sometimes fail ("Can't connect to spotify servers"), a retry works.
- Internal heap drops from ~408 KB to ~232 KB with a session; check what can move to PSRAM. - Internal heap drops from ~365 KB to ~231 KB with a session; check what can move to PSRAM.
- Track display in the app can switch ~3-5 s early.
- Mute at 0 % volume not yet confirmed. - Mute at 0 % volume not yet confirmed.
- Consider offering the delayed-Pong fix upstream (philippe44/cspot).
- [ ] failover (auto mode) + /api/player - [ ] failover (auto mode) + /api/player
- [ ] 8. Mono sum, gain, polish. - [ ] 8. Mono sum, gain, polish.
+11 -1
View File
@@ -21,15 +21,25 @@ typedef enum {
// PCM from Spotify: 44.1 kHz stereo s16 interleaved. Return the frames taken (may block). // 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 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); 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. // 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 // 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. // the client credentials are set; otherwise not advertised and a running session is ended.
// A new device name ends the session and re-advertises. // A new device name ends the session and re-advertises.
void spotify_apply(void); void spotify_apply(void);
// Pause/resume the running session (the Spotify app follows). No-op without a session. // Pause/resume the running session (the Spotify app follows). No-op without a session.
void spotify_pause(bool pause); void spotify_pause(bool pause);
// Suspend: end any running session (waits until its tasks are gone, max ~3 s) and refuse new ones.
// Resume: back to what the config says. Used around firmware uploads.
void spotify_suspend(bool suspend);
// The player's volume (0..100 %): reported to the Spotify app when a session starts (else the app
// shows 0 while we play at the player's level), and pushed to a running session.
void spotify_set_volume(int pct);
// Current state for status.spotify_state. // Current state for status.spotify_state.
const char *spotify_state(void); const char *spotify_state(void);
@@ -0,0 +1,172 @@
From philippe44/cspot PR #3 (open, 2026-08): "MercurySession: crash when a request
races the reconnection (null conn/shanConn)". Our sessions reconnect every ~6 min, so the
race is hit regularly. Drop this patch once the fix is in the pinned cspot commit.
diff --git a/cspot/include/Session.h b/cspot/include/Session.h
index df7f03c8..7cc8e32f 100644
--- a/cspot/include/Session.h
+++ b/cspot/include/Session.h
@@ -2,6 +2,7 @@
#include <stdint.h> // for uint8_t
#include <memory> // for shared_ptr, unique_ptr
+#include <mutex> // for mutex
#include <string> // for string
#include <vector> // for vector
@@ -23,6 +24,11 @@ class Session {
std::shared_ptr<cspot::PlainConnection> conn;
std::shared_ptr<LoginBlob> authBlob;
+ /* conn and shanConn are swapped by the session's task during reconnection,
+ * but other tasks send through them; they must hold this while copying the
+ * pointers, and the session's task while replacing them */
+ std::mutex connMutex;
+
std::string deviceId = "142137fd329622137a14901634264e6f332e2411";
public:
diff --git a/cspot/src/MercurySession.cpp b/cspot/src/MercurySession.cpp
index 7aafbb76..29a6e546 100644
--- a/cspot/src/MercurySession.cpp
+++ b/cspot/src/MercurySession.cpp
@@ -66,8 +66,11 @@ void MercurySession::reconnect() {
isReconnecting = true;
try {
- this->conn = nullptr;
- this->shanConn = nullptr;
+ {
+ std::scoped_lock lock(connMutex);
+ this->conn = nullptr;
+ this->shanConn = nullptr;
+ }
this->connectWithRandomAp();
this->authenticate(this->authBlob);
@@ -127,7 +130,17 @@ void MercurySession::unregisterAudioKey(uint32_t sequenceId) {
void MercurySession::disconnect() {
CSPOT_LOG(info, "Disconnecting mercury session");
this->isRunning = false;
- conn->close();
+
+ /* conn is null while a reconnection is in flight; isRunning above already
+ * makes the retry loop exit, closing is just to unblock a pending read */
+ std::shared_ptr<PlainConnection> conn;
+ {
+ std::scoped_lock lock(connMutex);
+ conn = this->conn;
+ }
+ if (conn)
+ conn->close();
+
std::scoped_lock lock(this->isRunningMutex);
}
@@ -305,8 +318,22 @@ uint64_t MercurySession::executeSubscription(RequestType method,
// Bump sequence id
this->sequenceId += 1;
+ /* the session's task may be swapping shanConn for a reconnection right now,
+ * and dereferencing it here would not be a catchable failure, so take a
+ * snapshot; when disconnected, the request is simply lost */
+ std::shared_ptr<ShannonConnection> shanConn;
+ {
+ std::scoped_lock lock(connMutex);
+ shanConn = this->shanConn;
+ }
+
+ if (!shanConn) {
+ CSPOT_LOG(info, "Mercury request skipped, session is reconnecting");
+ return this->sequenceId - 1;
+ }
+
try {
- this->shanConn->sendPacket(
+ shanConn->sendPacket(
static_cast<std::underlying_type<RequestType>::type>(method),
sequenceIdBytes);
} catch (...) {
@@ -337,8 +364,21 @@ uint32_t MercurySession::requestAudioKey(const std::vector<uint8_t>& trackId,
// Used for broken connection detection
// this->lastRequestTimestamp = timeProvider->getSyncedTimestamp();
+
+ // same snapshot as executeSubscription: this runs on the track queue's task
+ std::shared_ptr<ShannonConnection> shanConn;
+ {
+ std::scoped_lock lock(connMutex);
+ shanConn = this->shanConn;
+ }
+
+ if (!shanConn) {
+ CSPOT_LOG(info, "Audio key request skipped, session is reconnecting");
+ return audioKeySequence - 1;
+ }
+
try {
- this->shanConn->sendPacket(
+ shanConn->sendPacket(
static_cast<uint8_t>(RequestType::AUDIO_KEY_REQUEST_COMMAND), buffer);
} catch (...) {
// @TODO: Handle disconnect
diff --git a/cspot/src/Session.cpp b/cspot/src/Session.cpp
index 74bd0633..be154bbe 100644
--- a/cspot/src/Session.cpp
+++ b/cspot/src/Session.cpp
@@ -4,6 +4,7 @@
#include <cstdint> // for uint8_t
#include <functional> // for __base
#include <memory> // for shared_ptr, unique_ptr, make_unique
+#include <mutex> // for scoped_lock
#include <random> // for default_random_engine, independent_bi...
#include <type_traits> // for remove_extent_t
#include <utility> // for move
@@ -33,7 +34,10 @@ Session::Session() {
Session::~Session() {}
void Session::connect(std::unique_ptr<cspot::PlainConnection> connection) {
- this->conn = std::move(connection);
+ {
+ std::scoped_lock lock(connMutex);
+ this->conn = std::move(connection);
+ }
conn->timeoutHandler = [this]() {
return this->triggerTimeout();
};
@@ -48,11 +52,15 @@ void Session::connect(std::unique_ptr<cspot::PlainConnection> connection) {
CSPOT_LOG(debug, "Received shannon keys");
// Generates the public and priv key
- this->shanConn = std::make_shared<ShannonConnection>();
+ auto shanConn = std::make_shared<ShannonConnection>();
+
+ // Init shanno-encrypted connection, and only then publish it so another
+ // task cannot pick up a connection that is not wrapped yet
+ shanConn->wrapConnection(this->conn, challenges->shanSendKey,
+ challenges->shanRecvKey);
- // Init shanno-encrypted connection
- this->shanConn->wrapConnection(this->conn, challenges->shanSendKey,
- challenges->shanRecvKey);
+ std::scoped_lock lock(connMutex);
+ this->shanConn = shanConn;
}
void Session::connectWithRandomAp() {
diff --git a/cspot/src/TrackReference.cpp b/cspot/src/TrackReference.cpp
index b4fafcb5..203fe041 100644
--- a/cspot/src/TrackReference.cpp
+++ b/cspot/src/TrackReference.cpp
@@ -43,9 +43,9 @@ bool TrackReference::pbEncodeTrackList(pb_ostream_t* stream,
// concurrent rebuild cannot reallocate the vector under us
std::scoped_lock lock(*locked->mutex);
auto& trackQueue = *locked->tracks;
-#ifdef ESP_PLATFORM
+#ifdef ESP_PLATFORM
static TrackRef msg = TrackRef_init_zero;
-#else
+#else
TrackRef msg = TrackRef_init_zero;
#endif
@@ -0,0 +1,15 @@
diff --git a/cspot/src/PlainConnection.cpp b/cspot/src/PlainConnection.cpp
index 1bff6cf..b4ee6ff 100644
--- a/cspot/src/PlainConnection.cpp
+++ b/cspot/src/PlainConnection.cpp
@@ -150,6 +150,10 @@ void PlainConnection::readBlock(const uint8_t* dst, size_t size) {
case EINTR:
break;
default:
+ // diagnostic (project patch): n == 0 means the peer closed (FIN); n < 0 with errno
+ // e.g. ECONNRESET (RST). Note errno is not updated when n == 0.
+ CSPOT_LOG(error, "recv returned %d, errno %d, %u of %u bytes, retry %d", (int)n,
+ getErrno(), idx, (unsigned)size, retries);
if (retries++ > 4)
throw std::runtime_error("Error in read");
goto READ;
@@ -0,0 +1,74 @@
Keep-alive like librespot (core/src/session.rs): the server sends a Ping every ~2 min; send
the Pong 60 s later, not at once. With an immediate Pong the server reset the AP connection
(ECONNRESET) on the third Ping, i.e. every 6 minutes, and the Spotify app lost the device.
--- a/cspot/include/MercurySession.h
+++ b/cspot/include/MercurySession.h
@@ -120,6 +120,14 @@
unsigned long long timestampDiff;
unsigned long long lastPingTimestamp = -1;
+
+ // Keep-alive as librespot does it: Ping -> wait 60 s -> Pong -> PongAck. Answering at once made
+ // the server reset the connection on the third Ping (every 6 minutes).
+ const int PONG_DELAY_MS = 60 * 1000;
+ bool pongPending = false;
+ unsigned long long pongDueTimestamp = 0;
+ std::vector<uint8_t> pongData;
+ void sendPendingPong();
std::string countryCode = "";
std::mutex isRunningMutex;
--- a/cspot/src/MercurySession.cpp
+++ b/cspot/src/MercurySession.cpp
@@ -45,7 +45,10 @@
timeProvider->syncWithPingPacket(packet.data);
this->lastPingTimestamp = timeProvider->getSyncedTimestamp();
- this->shanConn->sendPacket(0x49, packet.data);
+ // Pong 60 s later (sent from triggerTimeout(), called on every 3 s receive timeout)
+ this->pongData = packet.data;
+ this->pongDueTimestamp = this->lastPingTimestamp + PONG_DELAY_MS;
+ this->pongPending = true;
} else {
this->packetQueue.push(packet);
}
@@ -80,6 +83,7 @@
BELL_SLEEP_MS(100);
lastPingTimestamp = timeProvider->getSyncedTimestamp();
+ pongPending = false; // a Ping on the old connection needs no Pong on the new one
isReconnecting = false;
this->executeEstabilishedCallback = true;
@@ -98,9 +102,30 @@
this->connectionReadyCallback = callback;
}
+void MercurySession::sendPendingPong() {
+ if (!pongPending ||
+ timeProvider->getSyncedTimestamp() < pongDueTimestamp)
+ return;
+ pongPending = false;
+ std::shared_ptr<ShannonConnection> shanConn;
+ {
+ std::scoped_lock lock(connMutex);
+ shanConn = this->shanConn;
+ }
+ if (!shanConn)
+ return;
+ try {
+ shanConn->sendPacket(0x49, pongData);
+ CSPOT_LOG(info, "Sent delayed Pong");
+ } catch (...) {
+ CSPOT_LOG(error, "Failed to send Pong");
+ }
+}
+
bool MercurySession::triggerTimeout() {
if (!isRunning)
return true;
+ sendPendingPong();
auto currentTimestamp = timeProvider->getSyncedTimestamp();
if (currentTimestamp - this->lastPingTimestamp > PING_TIMEOUT_MS) {
+108 -1
View File
@@ -3,6 +3,7 @@
#include <atomic> #include <atomic>
#include <cstring> #include <cstring>
#include <exception> #include <exception>
#include <deque>
#include <map> #include <map>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@@ -33,11 +34,91 @@ static std::string s_name; // advertised device nam
static std::atomic<bool> s_enabled{false}; static std::atomic<bool> s_enabled{false};
static std::atomic<bool> s_stop{false}; // end the running session static std::atomic<bool> s_stop{false}; // end the running session
static std::atomic<bool> s_got_blob{false}; static std::atomic<bool> s_got_blob{false};
static std::atomic<bool> s_in_session{false}; // session() running (its tasks may exist)
static std::atomic<int> s_volume{65535}; // player volume in Spotify units (0..65535)
static std::atomic<const char *> s_state{"disabled"}; static std::atomic<const char *> s_state{"disabled"};
static std::atomic<uint64_t> s_pcm_bytes{0}; static std::atomic<uint64_t> s_pcm_bytes{0};
static std::shared_ptr<cspot::SpircHandler> s_handler; // running session, else null static std::shared_ptr<cspot::SpircHandler> s_handler; // running session, else null
static spotify_pcm_cb_t s_pcm_cb; static spotify_pcm_cb_t s_pcm_cb;
static spotify_event_cb_t s_event_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)
{
if (!suspend) {
spotify_apply();
return;
}
s_enabled = false;
s_stop = true;
s_got_blob = false;
s_state = "suspended";
for (int i = 0; i < 30 && s_in_session; i++) { // session loop exits within 200 ms, then disconnect
vTaskDelay(pdMS_TO_TICKS(100));
}
ESP_LOGI(TAG, "suspended%s", s_in_session ? " (session still ending)" : "");
}
extern "C" void spotify_set_volume(int pct)
{
pct = pct < 0 ? 0 : pct > 100 ? 100 : pct;
int v = pct * 65535 / 100;
if (v == s_volume) {
return;
}
s_volume = v;
auto h = s_handler;
if (h) {
h->setRemoteVolume(v);
}
}
extern "C" void spotify_pause(bool pause) extern "C" void spotify_pause(bool pause)
{ {
@@ -203,6 +284,8 @@ static void session(void)
ctx->config.clientSecret = client_secret; ctx->config.clientSecret = client_secret;
ctx->config.audioFormat = bitrate >= 320 ? AudioFormat_OGG_VORBIS_320 ctx->config.audioFormat = bitrate >= 320 ? AudioFormat_OGG_VORBIS_320
: bitrate >= 160 ? AudioFormat_OGG_VORBIS_160 : AudioFormat_OGG_VORBIS_96; : bitrate >= 160 ? AudioFormat_OGG_VORBIS_160 : AudioFormat_OGG_VORBIS_96;
// createFromBlob() starts at volume 0, which the app would show; report what we actually play.
ctx->config.volume = s_volume;
s_state = "connecting"; s_state = "connecting";
ctx->session->connectWithRandomAp(); ctx->session->connectWithRandomAp();
auto token = ctx->session->authenticate(login); auto token = ctx->session->authenticate(login);
@@ -217,6 +300,14 @@ static void session(void)
// PCM (44.1 kHz stereo s16) to the player; returning fewer bytes makes cspot retry the rest, // 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. // 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 { 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 frames = len / 4;
size_t taken = s_pcm_cb ? s_pcm_cb(reinterpret_cast<const int16_t *>(data), frames) : frames; size_t taken = s_pcm_cb ? s_pcm_cb(reinterpret_cast<const int16_t *>(data), frames) : frames;
s_pcm_bytes += taken * 4; s_pcm_bytes += taken * 4;
@@ -238,6 +329,10 @@ static void session(void)
case E::FLUSH: case E::FLUSH:
case E::SEEK: case E::SEEK:
case E::PLAYBACK_START: 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) { if (s_event_cb) {
s_event_cb(SPOTIFY_EV_FLUSH, 0); s_event_cb(SPOTIFY_EV_FLUSH, 0);
if (ev->eventType == E::PLAYBACK_START) { if (ev->eventType == E::PLAYBACK_START) {
@@ -246,10 +341,14 @@ static void session(void)
} }
break; break;
case E::VOLUME: case E::VOLUME:
s_volume = std::get<int>(ev->data); // exact value from the app, no echo back
if (s_event_cb) { if (s_event_cb) {
s_event_cb(SPOTIFY_EV_VOLUME, std::get<int>(ev->data)); s_event_cb(SPOTIFY_EV_VOLUME, std::get<int>(ev->data));
} }
break; break;
case E::DEPLETED:
s_depleted = true; // queue done: tell Spotify once the buffer has played out
break;
default: default:
break; break;
} }
@@ -258,8 +357,11 @@ static void session(void)
s_handler = handler; s_handler = handler;
s_state = "connected"; s_state = "connected";
ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str()); ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str());
clear_boundaries(true);
s_depleted = false;
while (!s_stop) { while (!s_stop) {
ctx->session->handlePacket(); // waits at most 200 ms ctx->session->handlePacket(); // waits at most 200 ms
check_playback(handler);
} }
// Stops the queue and player tasks (waits for them), then closes the connection. // Stops the queue and player tasks (waits for them), then closes the connection.
s_handler.reset(); s_handler.reset();
@@ -278,6 +380,7 @@ static void spotify_task(void *arg)
} }
s_got_blob = false; s_got_blob = false;
s_stop = false; s_stop = false;
s_in_session = true;
try { try {
session(); session();
} catch (const std::exception &e) { } catch (const std::exception &e) {
@@ -285,6 +388,7 @@ static void spotify_task(void *arg)
ESP_LOGE(TAG, "session ended: %s", e.what()); ESP_LOGE(TAG, "session ended: %s", e.what());
} }
s_handler.reset(); s_handler.reset();
s_in_session = false;
vTaskDelay(pdMS_TO_TICKS(2000)); vTaskDelay(pdMS_TO_TICKS(2000));
} }
} }
@@ -330,10 +434,13 @@ extern "C" void spotify_apply(void)
s_enabled = true; 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_pcm_cb = pcm;
s_event_cb = event; s_event_cb = event;
s_written = written;
s_played = played;
if (!bell::bellGlobalLogger) { if (!bell::bellGlobalLogger) {
bell::bellGlobalLogger = new EspLogger(); bell::bellGlobalLogger = new EspLogger();
} }
+10
View File
@@ -70,6 +70,16 @@ size_t audio_ring_read(int32_t *frames, size_t n)
return 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) void audio_ring_flush(void)
{ {
__atomic_store_n(&s_rd, __atomic_load_n(&s_wr, __ATOMIC_ACQUIRE), __ATOMIC_RELEASE); __atomic_store_n(&s_rd, __atomic_load_n(&s_wr, __ATOMIC_ACQUIRE), __ATOMIC_RELEASE);
+3
View File
@@ -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_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 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 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);
+9 -10
View File
@@ -160,21 +160,20 @@ static void spotify_event(spotify_event_t ev, int value)
} }
} }
// Firmware upload: flash writes stall everything running from PSRAM; a Spotify session // Firmware upload: flash writes stall tasks for > 20 ms at a time; with a Spotify session running,
// streaming at the same time made uploads crawl and end in a reset. Quiet the sources meanwhile. // uploads crawled and sometimes ended in a reset (pausing was not enough). End the session (its
// tasks and connections are gone before the first write) and stop HLS fetching meanwhile. The
// device reboots after a good upload anyway; after a failed one the sources come back (Spotify
// waits for the app to reconnect).
static void on_ota(bool starting) static void on_ota(bool starting)
{ {
static bool paused_spotify;
if (starting) { if (starting) {
paused_spotify = s_src == PLAYER_SRC_SPOTIFY && !s_paused; spotify_suspend(true);
spotify_pause(true);
hls_suspend(true); hls_suspend(true);
ESP_LOGI(TAG, "firmware update: sources paused"); ESP_LOGI(TAG, "firmware update: Spotify session ended, HLS suspended");
} else { } else {
hls_suspend(false); hls_suspend(false);
if (paused_spotify) { spotify_suspend(false);
spotify_pause(false);
}
ESP_LOGI(TAG, "firmware update failed: sources resumed"); ESP_LOGI(TAG, "firmware update failed: sources resumed");
} }
} }
@@ -208,7 +207,7 @@ esp_err_t player_init(void)
if (!s_spotify_conv) { if (!s_spotify_conv) {
return ESP_ERR_NO_MEM; 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); aes67_ota_on_update(on_ota);
err = hls_start(decoder_feed); err = hls_start(decoder_feed);
if (err != ESP_OK) { if (err != ESP_OK) {