Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78c480a7c2 | |||
| b8702e6faa | |||
| 5ce4588c80 | |||
| 8008aa175f | |||
| f0fe53d5d1 |
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user