039ec75473
- external/cspot: git submodule, pinned to philippe44/cspot 3010349 (bell ed2d6e9); GPL-3.0. - components/spotify: project component wrapping cspot. bell trimmed to what cspot needs (Tremor Vorbis; no codec wrapper, sinks, MQTT, web server, fmt, regex; cJSON from IDF). Xtensa biquad assembly filtered out (P4 is RISC-V). CMAKE_POLICY_VERSION_MINIMUM 3.5 for CMake 4. - Patches in components/spotify/patches, applied at configure time: nanopb generator works with protobuf >= 5 (MakeClass removed); URLParser.cpp missing <cstdio>/<cstring> for GCC 14. - CONFIG_COMPILER_CXX_EXCEPTIONS=y (cspot needs exceptions). - spotify_init() only builds a LoginBlob (link check). - CLAUDE.md: submodule init and IDF-venv Python packages for nanopb. - Verified on board: "cspot linked: device "P4 AES67", zeroconf info 588 bytes"; HLS still playing, PTP locked; image 1.83 MB (70% free). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
4.2 KiB
C
131 lines
4.2 KiB
C
#include "player.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include "aes67_cfg.h"
|
|
#include "aes67_tx.h"
|
|
#include "aes67_web.h"
|
|
#include "audio_ring.h"
|
|
#include "decoder.h"
|
|
#include "hls.h"
|
|
#include "spotify.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
|
|
#define RATE 48000
|
|
#define CHANNELS 2
|
|
#define RING_FRAMES (4 * RATE) // 4 s in PSRAM
|
|
#define PREFILL_FRAMES (RATE) // 1 s before (re)starting output
|
|
|
|
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 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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
// AES67 TX pull callback (TX task, must not block). Silence while buffering or off: not an underrun.
|
|
static size_t player_read(int32_t *buf, size_t frames)
|
|
{
|
|
if (s_flush) {
|
|
audio_ring_flush();
|
|
s_flush = false;
|
|
s_playing = false;
|
|
}
|
|
if (s_src == SRC_OFF || s_src == SRC_SPOTIFY) { // Spotify: step 7 (cspot)
|
|
memset(buf, 0, frames * CHANNELS * sizeof(int32_t));
|
|
return frames;
|
|
}
|
|
if (!s_playing) {
|
|
if (audio_ring_level() < PREFILL_FRAMES) {
|
|
memset(buf, 0, frames * CHANNELS * sizeof(int32_t));
|
|
return frames;
|
|
}
|
|
s_playing = true;
|
|
}
|
|
size_t got = audio_ring_read(buf, frames);
|
|
if (got < frames) {
|
|
s_playing = false; // ran dry: underrun (counted by TX), prefill again
|
|
}
|
|
return got;
|
|
}
|
|
|
|
void player_apply(const cJSON *source)
|
|
{
|
|
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);
|
|
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)
|
|
{
|
|
size_t done = 0;
|
|
while (done < n) {
|
|
if (s_src != SRC_HLS) {
|
|
return done;
|
|
}
|
|
size_t w = audio_ring_write(frames + done * CHANNELS, n - done);
|
|
if (!w) {
|
|
vTaskDelay(pdMS_TO_TICKS(20));
|
|
}
|
|
done += w;
|
|
}
|
|
return done;
|
|
}
|
|
|
|
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";
|
|
cJSON_AddStringToObject(st, "active_source", SRC_NAME[s_src]);
|
|
cJSON_AddStringToObject(st, "source_state", state);
|
|
cJSON *src = cfg_get("source");
|
|
bool creds = cJSON_GetObjectItemCaseSensitive(src, "spotify_client_id")->valuestring[0] &&
|
|
cJSON_GetObjectItemCaseSensitive(src, "spotify_client_secret")->valuestring[0];
|
|
cJSON_Delete(src);
|
|
cJSON_AddStringToObject(st, "spotify_state", creds ? "not implemented" : "no client credentials");
|
|
cJSON_AddNumberToObject(st, "buffer_ms", (double)(audio_ring_level() * 1000 / RATE));
|
|
}
|
|
|
|
esp_err_t player_init(void)
|
|
{
|
|
esp_err_t err = audio_ring_init(RING_FRAMES, CHANNELS);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "ring buffer: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
cJSON *src = cfg_get("source");
|
|
s_src = (src_t)-1;
|
|
player_apply(src);
|
|
cJSON_Delete(src);
|
|
err = decoder_init();
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
cJSON *sc = cfg_get("source");
|
|
spotify_init(cJSON_GetObjectItemCaseSensitive(sc, "spotify_name")->valuestring);
|
|
cJSON_Delete(sc);
|
|
err = hls_start(decoder_feed);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
return status_register(player_status);
|
|
}
|