#include "player.h" #include #include #include "aes67_cfg.h" #include "aes67_tx.h" #include "aes67_web.h" #include "audio_ring.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]); } /* ----- Step 7.1: temporary test producer (440 Hz into the ring), replaced by the HLS player ----- */ static void test_producer(void *arg) { static int32_t chunk[480 * CHANNELS]; uint32_t phase = 0; const double amp = pow(10.0, -18.0 / 20.0) * 2147483647.0; while (1) { if (s_src != SRC_HLS || audio_ring_space() < 480) { vTaskDelay(pdMS_TO_TICKS(10)); continue; } for (int i = 0; i < 480; i++, phase++) { int32_t v = (int32_t)(amp * sin(2.0 * M_PI * 440.0 * (phase % RATE) / RATE)); chunk[i * 2] = chunk[i * 2 + 1] = v; } audio_ring_write(chunk, 480); } } 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); xTaskCreate(test_producer, "test_prod", 3072, NULL, 5, NULL); return status_register(player_status); }