#include "player.h" #include #include "aes67_cfg.h" #include "aes67_tx.h" #include "aes67_web.h" #include "audio_ring.h" #include "decoder.h" #include "hls.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_AddStringToObject(st, "spotify_state", "not implemented"); 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; } err = hls_start(decoder_feed); if (err != ESP_OK) { return err; } return status_register(player_status); }