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:
@@ -1,15 +1,29 @@
|
|||||||
// Spotify Connect (cspot). Project code; GPL-3.0 through cspot.
|
// Spotify Connect (cspot). Project code; GPL-3.0 through cspot.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#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
|
// 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.
|
// 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.
|
// Current state for status.spotify_state.
|
||||||
const char *spotify_state(void);
|
const char *spotify_state(void);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ static std::shared_ptr<cspot::LoginBlob> s_blob;
|
|||||||
static std::atomic<bool> s_got_blob{false};
|
static std::atomic<bool> s_got_blob{false};
|
||||||
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 spotify_pcm_cb_t s_pcm_cb;
|
||||||
|
static spotify_event_cb_t s_event_cb;
|
||||||
|
|
||||||
const char *spotify_state(void)
|
const char *spotify_state(void)
|
||||||
{
|
{
|
||||||
@@ -182,18 +184,46 @@ static void session(void)
|
|||||||
ctx->session->startTask();
|
ctx->session->startTask();
|
||||||
auto handler = std::make_shared<cspot::SpircHandler>(ctx);
|
auto handler = std::make_shared<cspot::SpircHandler>(ctx);
|
||||||
handler->subscribeToMercury();
|
handler->subscribeToMercury();
|
||||||
// 7.5b2: count PCM only; 7.5b3 feeds it to the AES67 ring.
|
// PCM (44.1 kHz stereo s16) to the player; returning fewer bytes makes cspot retry the rest,
|
||||||
handler->getTrackPlayer()->setDataCallback([](uint8_t *data, size_t len, std::string_view id) {
|
// so the player's blocking write paces decoding to playback speed.
|
||||||
s_pcm_bytes += len;
|
handler->getTrackPlayer()->setDataCallback([](uint8_t *data, size_t len, std::string_view id) -> size_t {
|
||||||
return len;
|
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) {
|
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);
|
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);
|
ESP_LOGI(TAG, "track: %s - %s (%lu ms)", ti.artist.c_str(), ti.name.c_str(), (unsigned long)ti.duration);
|
||||||
} else {
|
return;
|
||||||
ESP_LOGI(TAG, "event %s (PCM so far %llu bytes)", event_name(ev->eventType), s_pcm_bytes.load());
|
|
||||||
}
|
}
|
||||||
|
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";
|
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());
|
||||||
@@ -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) {
|
if (!bell::bellGlobalLogger) {
|
||||||
bell::bellGlobalLogger = new EspLogger();
|
bell::bellGlobalLogger = new EspLogger();
|
||||||
}
|
}
|
||||||
@@ -242,8 +274,8 @@ extern "C" esp_err_t spotify_init(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
s_blob = std::make_shared<cspot::LoginBlob>(name);
|
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 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 };
|
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(&get);
|
||||||
web_register_uri(&post);
|
web_register_uri(&post);
|
||||||
mdns_txt_item_t txt[] = { { "VERSION", "1.0" }, { "CPath", "/spotify_info" }, { "Stack", "SP" } };
|
mdns_txt_item_t txt[] = { { "VERSION", "1.0" }, { "CPath", "/spotify_info" }, { "Stack", "SP" } };
|
||||||
|
|||||||
+38
-3
@@ -5,6 +5,7 @@
|
|||||||
#include "aes67_cfg.h"
|
#include "aes67_cfg.h"
|
||||||
#include "aes67_tx.h"
|
#include "aes67_tx.h"
|
||||||
#include "aes67_web.h"
|
#include "aes67_web.h"
|
||||||
|
#include "audio_out.h"
|
||||||
#include "audio_ring.h"
|
#include "audio_ring.h"
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
#include "hls.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 player_src_t s_src = PLAYER_SRC_OFF;
|
||||||
static volatile bool s_playing; // ring output running (after prefill)
|
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_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)
|
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_flush = false;
|
||||||
s_playing = 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));
|
memset(buf, 0, frames * CHANNELS * sizeof(int32_t));
|
||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
@@ -89,10 +92,38 @@ size_t player_write(player_src_t src, const int32_t *frames, size_t n)
|
|||||||
return done;
|
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)
|
static void player_status(cJSON *st)
|
||||||
{
|
{
|
||||||
const char *state = s_src == PLAYER_SRC_TONE ? "playing" : s_src == PLAYER_SRC_OFF ? "idle" :
|
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, "active_source", SRC_NAME[s_src]);
|
||||||
cJSON_AddStringToObject(st, "source_state", state);
|
cJSON_AddStringToObject(st, "source_state", state);
|
||||||
cJSON_AddStringToObject(st, "spotify_state", spotify_state());
|
cJSON_AddStringToObject(st, "spotify_state", spotify_state());
|
||||||
@@ -114,7 +145,11 @@ esp_err_t player_init(void)
|
|||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
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);
|
err = hls_start(decoder_feed);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
|
|||||||
Reference in New Issue
Block a user