Step 7.5b3a: per-source PCM converter (audio_out), shared by HLS and Spotify

- main/audio_out: one converter instance per source (own rate converter
  state): s16 any rate/channels -> 48 kHz stereo int32 -> ring.
- player_write(src, ...) only writes for the active source, drops the
  rest; player_src_t is public in player.h.
- HLS decoder uses audio_out (no behaviour change).
- Verified: HLS still sample exact (441344 -> 480375 frames per
  10.008 s segment), buffer ~4 s, 0 underruns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 18:34:15 +10:00
parent c240ac9a3c
commit 5b0d7c9ba4
6 changed files with 131 additions and 73 deletions
+15 -16
View File
@@ -20,17 +20,16 @@
static const char *TAG = "player";
typedef enum { SRC_TONE, SRC_OFF, SRC_HLS, SRC_SPOTIFY } src_t;
static const char *const SRC_NAME[] = { "tone", "off", "hls", "spotify" };
static volatile src_t s_src = 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_flush; // consumer drops buffered audio on the next read
static src_t mode_of(const char *m)
static player_src_t mode_of(const char *m)
{
return !strcmp(m, "tone") ? SRC_TONE : !strcmp(m, "hls") ? SRC_HLS :
!strcmp(m, "spotify") ? SRC_SPOTIFY : !strcmp(m, "auto") ? SRC_HLS /* failover: step 7 */ : SRC_OFF;
return !strcmp(m, "tone") ? PLAYER_SRC_TONE : !strcmp(m, "hls") ? PLAYER_SRC_HLS :
!strcmp(m, "spotify") ? PLAYER_SRC_SPOTIFY : !strcmp(m, "auto") ? PLAYER_SRC_HLS /* failover: step 7 */ : PLAYER_SRC_OFF;
}
// AES67 TX pull callback (TX task, must not block). Silence while buffering or off: not an underrun.
@@ -41,7 +40,7 @@ static size_t player_read(int32_t *buf, size_t frames)
s_flush = false;
s_playing = false;
}
if (s_src == SRC_OFF || s_src == SRC_SPOTIFY) { // Spotify: step 7 (cspot)
if (s_src == PLAYER_SRC_OFF || s_src == PLAYER_SRC_SPOTIFY) { // Spotify: step 7 (cspot)
memset(buf, 0, frames * CHANNELS * sizeof(int32_t));
return frames;
}
@@ -61,25 +60,25 @@ static size_t player_read(int32_t *buf, size_t frames)
void player_apply(const cJSON *source)
{
src_t src = mode_of(cJSON_GetObjectItemCaseSensitive(source, "mode")->valuestring);
player_src_t src = mode_of(cJSON_GetObjectItemCaseSensitive(source, "mode")->valuestring);
if (src == s_src) {
return;
}
s_src = src;
s_flush = true;
// The test tone is TX's own (phase-locked to PTP); everything else goes through player_read.
aes67_tx_set_source(src == SRC_TONE ? NULL : player_read);
aes67_tx_set_source(src == PLAYER_SRC_TONE ? NULL : player_read);
ESP_LOGI(TAG, "source: %s", SRC_NAME[src]);
}
// Source side (HLS task): write converted 48 kHz frames, waiting for space at playback speed.
// Gives up when the source is no longer HLS.
size_t player_write(const int32_t *frames, size_t n)
// Source side: write converted 48 kHz frames, waiting for space at playback speed.
// Frames of a source that is not active are dropped.
size_t player_write(player_src_t src, const int32_t *frames, size_t n)
{
size_t done = 0;
while (done < n) {
if (s_src != SRC_HLS) {
return done;
if (s_src != src) {
return n; // not the active source: drop
}
size_t w = audio_ring_write(frames + done * CHANNELS, n - done);
if (!w) {
@@ -92,8 +91,8 @@ size_t player_write(const int32_t *frames, size_t n)
static void player_status(cJSON *st)
{
const char *state = s_src == SRC_TONE ? "playing" : s_src == SRC_OFF ? "idle" :
s_src == SRC_SPOTIFY ? "not implemented" : s_playing ? "playing" : "buffering";
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";
cJSON_AddStringToObject(st, "active_source", SRC_NAME[s_src]);
cJSON_AddStringToObject(st, "source_state", state);
cJSON_AddStringToObject(st, "spotify_state", spotify_state());
@@ -108,7 +107,7 @@ esp_err_t player_init(void)
return err;
}
cJSON *src = cfg_get("source");
s_src = (src_t)-1;
s_src = (player_src_t)-1;
player_apply(src);
cJSON_Delete(src);
err = decoder_init();