From 4a382546608854c9c1e5305b976850d1e8732e4c Mon Sep 17 00:00:00 2001 From: Ben Nicholson Date: Fri, 25 Sep 2026 18:41:49 +1000 Subject: [PATCH] Step 7.5b3b: Spotify audio on AES67 - spotify_init(pcm_cb, event_cb): cspot's data callback hands 44.1 kHz stereo s16 PCM to the player and returns what was taken; the player's blocking ring write paces decoding to playback speed. - Events: FLUSH/SEEK/PLAYBACK_START empty the ring (skip/seek sound at once); PLAY_PAUSE pauses output (silence, buffer kept, no underrun); VOLUME logged (applied in b4). - player: Spotify via its own audio_out converter (44.1 -> 48 kHz); source_state playing / paused / buffering. - Verified from a Mac: music on the AES67 stream (10 s: 0 gaps, no silent packets, RMS -9.6 dBFS, peak 0.0 dBFS), buffer 4.0 s full, 0 underruns, pause/play/skip/seek events. Seen: first 3 connects "Can't connect to spotify servers", 4th worked. Co-Authored-By: Claude Opus 5.5 --- components/spotify/include/spotify.h | 16 ++++++++- components/spotify/spotify.cpp | 52 ++++++++++++++++++++++------ main/player.c | 41 ++++++++++++++++++++-- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/components/spotify/include/spotify.h b/components/spotify/include/spotify.h index e42f6dd..5fde0e5 100644 --- a/components/spotify/include/spotify.h +++ b/components/spotify/include/spotify.h @@ -1,15 +1,29 @@ // Spotify Connect (cspot). Project code; GPL-3.0 through cspot. #pragma once +#include +#include + #include "esp_err.h" #ifdef __cplusplus extern "C" { #endif +typedef enum { + SPOTIFY_EV_PLAY, // playback (re)started or resumed + SPOTIFY_EV_PAUSE, + SPOTIFY_EV_FLUSH, // skip, seek, new track: drop buffered audio + SPOTIFY_EV_VOLUME, // value: 0..65535 +} spotify_event_t; + +// 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); + // Registers the zeroconf endpoints (/spotify_info) and, when source.mode is spotify/auto and the // client credentials are set, advertises the device and starts the session task. -esp_err_t spotify_init(void); +esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event); // Current state for status.spotify_state. const char *spotify_state(void); diff --git a/components/spotify/spotify.cpp b/components/spotify/spotify.cpp index 8abe502..291d777 100644 --- a/components/spotify/spotify.cpp +++ b/components/spotify/spotify.cpp @@ -30,6 +30,8 @@ static std::shared_ptr s_blob; static std::atomic s_got_blob{false}; static std::atomic s_state{"disabled"}; static std::atomic s_pcm_bytes{0}; +static spotify_pcm_cb_t s_pcm_cb; +static spotify_event_cb_t s_event_cb; const char *spotify_state(void) { @@ -182,18 +184,46 @@ static void session(void) ctx->session->startTask(); auto handler = std::make_shared(ctx); handler->subscribeToMercury(); - // 7.5b2: count PCM only; 7.5b3 feeds it to the AES67 ring. - handler->getTrackPlayer()->setDataCallback([](uint8_t *data, size_t len, std::string_view id) { - s_pcm_bytes += len; - return len; + // 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 { + size_t frames = len / 4; + size_t taken = s_pcm_cb ? s_pcm_cb(reinterpret_cast(data), frames) : frames; + s_pcm_bytes += taken * 4; + return taken * 4; }); handler->setEventHandler([](std::unique_ptr ev) { - if (ev->eventType == cspot::SpircHandler::EventType::TRACK_INFO) { + using E = cspot::SpircHandler::EventType; + switch (ev->eventType) { + case E::TRACK_INFO: { auto &ti = std::get(ev->data); ESP_LOGI(TAG, "track: %s - %s (%lu ms)", ti.artist.c_str(), ti.name.c_str(), (unsigned long)ti.duration); - } else { - ESP_LOGI(TAG, "event %s (PCM so far %llu bytes)", event_name(ev->eventType), s_pcm_bytes.load()); + return; } + case E::PLAY_PAUSE: + if (s_event_cb) { + s_event_cb(std::get(ev->data) ? SPOTIFY_EV_PAUSE : SPOTIFY_EV_PLAY, 0); + } + break; + case E::FLUSH: + case E::SEEK: + case E::PLAYBACK_START: + if (s_event_cb) { + s_event_cb(SPOTIFY_EV_FLUSH, 0); + if (ev->eventType == E::PLAYBACK_START) { + s_event_cb(SPOTIFY_EV_PLAY, 0); + } + } + break; + case E::VOLUME: + if (s_event_cb) { + s_event_cb(SPOTIFY_EV_VOLUME, std::get(ev->data)); + } + break; + default: + break; + } + ESP_LOGI(TAG, "event %s", event_name(ev->eventType)); }); s_state = "connected"; ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str()); @@ -220,8 +250,10 @@ static void spotify_task(void *arg) } } -extern "C" esp_err_t spotify_init(void) +extern "C" esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event) { + s_pcm_cb = pcm; + s_event_cb = event; if (!bell::bellGlobalLogger) { bell::bellGlobalLogger = new EspLogger(); } @@ -242,8 +274,8 @@ extern "C" esp_err_t spotify_init(void) return ESP_OK; } s_blob = std::make_shared(name); - static const httpd_uri_t get = { .uri = "/spotify_info", .method = HTTP_GET, .handler = info_get }; - static const httpd_uri_t post = { .uri = "/spotify_info", .method = HTTP_POST, .handler = info_post }; + static const httpd_uri_t get = { .uri = "/spotify_info", .method = HTTP_GET, .handler = info_get, .user_ctx = nullptr }; + static const httpd_uri_t post = { .uri = "/spotify_info", .method = HTTP_POST, .handler = info_post, .user_ctx = nullptr }; web_register_uri(&get); web_register_uri(&post); mdns_txt_item_t txt[] = { { "VERSION", "1.0" }, { "CPath", "/spotify_info" }, { "Stack", "SP" } }; diff --git a/main/player.c b/main/player.c index 6b7778e..a002559 100644 --- a/main/player.c +++ b/main/player.c @@ -5,6 +5,7 @@ #include "aes67_cfg.h" #include "aes67_tx.h" #include "aes67_web.h" +#include "audio_out.h" #include "audio_ring.h" #include "decoder.h" #include "hls.h" @@ -25,6 +26,8 @@ static const char *const SRC_NAME[] = { "tone", "off", "hls", "spotify" }; static volatile player_src_t s_src = PLAYER_SRC_OFF; static volatile bool s_playing; // ring output running (after prefill) static volatile bool s_flush; // consumer drops buffered audio on the next read +static volatile bool s_paused; // Spotify paused: silence, buffer kept, no underrun +static audio_conv_t *s_spotify_conv; static player_src_t mode_of(const char *m) { @@ -40,7 +43,7 @@ static size_t player_read(int32_t *buf, size_t frames) s_flush = false; s_playing = false; } - if (s_src == PLAYER_SRC_OFF || s_src == PLAYER_SRC_SPOTIFY) { // Spotify: step 7 (cspot) + if (s_src == PLAYER_SRC_OFF || (s_src == PLAYER_SRC_SPOTIFY && s_paused)) { memset(buf, 0, frames * CHANNELS * sizeof(int32_t)); return frames; } @@ -89,10 +92,38 @@ size_t player_write(player_src_t src, const int32_t *frames, size_t n) return done; } +/* ----- Spotify ----- */ + +static size_t spotify_pcm(const int16_t *pcm, size_t frames) +{ + audio_conv_write(s_spotify_conv, pcm, frames, 2, 44100); // blocks while the ring is full + return frames; +} + +static void spotify_event(spotify_event_t ev, int value) +{ + switch (ev) { + case SPOTIFY_EV_PLAY: + s_paused = false; + break; + case SPOTIFY_EV_PAUSE: + s_paused = true; + break; + case SPOTIFY_EV_FLUSH: + if (s_src == PLAYER_SRC_SPOTIFY) { + s_flush = true; // skip/seek: don't play out the old buffer + } + break; + case SPOTIFY_EV_VOLUME: + ESP_LOGI(TAG, "Spotify volume %d%% (applied in step 7.5b4)", value * 100 / 65535); + break; + } +} + static void player_status(cJSON *st) { const char *state = s_src == PLAYER_SRC_TONE ? "playing" : s_src == PLAYER_SRC_OFF ? "idle" : - s_src == PLAYER_SRC_SPOTIFY ? "not implemented" : s_playing ? "playing" : "buffering"; + s_src == PLAYER_SRC_SPOTIFY && s_paused ? "paused" : s_playing ? "playing" : "buffering"; cJSON_AddStringToObject(st, "active_source", SRC_NAME[s_src]); cJSON_AddStringToObject(st, "source_state", state); cJSON_AddStringToObject(st, "spotify_state", spotify_state()); @@ -114,7 +145,11 @@ esp_err_t player_init(void) if (err != ESP_OK) { return err; } - spotify_init(); + s_spotify_conv = audio_conv_create(PLAYER_SRC_SPOTIFY, "spotify"); + if (!s_spotify_conv) { + return ESP_ERR_NO_MEM; + } + spotify_init(spotify_pcm, spotify_event); err = hls_start(decoder_feed); if (err != ESP_OK) { return err;