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
+38 -3
View File
@@ -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;