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 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 18:41:49 +10:00
parent 5b0d7c9ba4
commit 4a38254660
3 changed files with 95 additions and 14 deletions
+15 -1
View File
@@ -1,15 +1,29 @@
// Spotify Connect (cspot). Project code; GPL-3.0 through cspot.
#pragma once
#include <stddef.h>
#include <stdint.h>
#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);
+42 -10
View File
@@ -30,6 +30,8 @@ static std::shared_ptr<cspot::LoginBlob> s_blob;
static std::atomic<bool> s_got_blob{false};
static std::atomic<const char *> s_state{"disabled"};
static std::atomic<uint64_t> 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<cspot::SpircHandler>(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<const int16_t *>(data), frames) : frames;
s_pcm_bytes += taken * 4;
return taken * 4;
});
handler->setEventHandler([](std::unique_ptr<cspot::SpircHandler::Event> 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<cspot::TrackInfo>(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<bool>(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<int>(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<cspot::LoginBlob>(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" } };