Step 7.1: player plumbing (PSRAM ring -> AES67 TX), test tone mode
- main/audio_ring: SPSC ring of interleaved int32 frames in PSRAM (4 s at 48 kHz stereo), lock-free with acquire/release counters; the TX pull callback never blocks. - main/player: source selection from source.mode and the pull callback for aes67_tx. tone -> the core's PTP-phased 1 kHz tone; off -> silence; hls/spotify -> ring with 1 s prefill (silence while buffering is not an underrun; running dry is, and prefills again). Status fields active_source, source_state, spotify_state, buffer_ms. source config applies live. - New source.mode "tone" (validation, UI dropdown, doc) for commissioning. - Temporary 440 Hz producer in hls mode (until the HLS player exists). - Verified: tone 999.7 Hz -18 dBFS; off silent; hls 440.0 Hz with max sample step 60816 (ideal sine 60825, i.e. no discontinuities), buffer 3983 ms, 0 underruns; spotify silent/not implemented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
||||
idf_component_register(SRCS "main.c" "project_cfg.c"
|
||||
idf_component_register(SRCS "main.c" "project_cfg.c" "player.c" "audio_ring.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES esp_app_format esp_hw_support
|
||||
REQUIRES esp_app_format esp_hw_support heap
|
||||
aes67_board aes67_health aes67_net aes67_ota aes67_ptp aes67_sdp_sap
|
||||
aes67_syslog aes67_tx aes67_web)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "audio_ring.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
static int32_t *s_buf;
|
||||
static size_t s_cap; // frames
|
||||
static int s_ch;
|
||||
// Monotonic frame counters; level = wr - rd. Written by one side each (acquire/release).
|
||||
static uint32_t s_wr, s_rd;
|
||||
|
||||
esp_err_t audio_ring_init(size_t capacity_frames, int channels)
|
||||
{
|
||||
s_buf = heap_caps_malloc(capacity_frames * channels * sizeof(int32_t), MALLOC_CAP_SPIRAM);
|
||||
if (!s_buf) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
s_cap = capacity_frames;
|
||||
s_ch = channels;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
size_t audio_ring_level(void)
|
||||
{
|
||||
return __atomic_load_n(&s_wr, __ATOMIC_ACQUIRE) - __atomic_load_n(&s_rd, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
size_t audio_ring_space(void)
|
||||
{
|
||||
return s_cap - audio_ring_level();
|
||||
}
|
||||
|
||||
// Copy n frames between the ring (starting at frame index pos) and a linear buffer.
|
||||
static void copy(int32_t *ring_to_lin, const int32_t *lin_to_ring, uint32_t pos, size_t n)
|
||||
{
|
||||
size_t start = pos % s_cap;
|
||||
size_t first = n < s_cap - start ? n : s_cap - start;
|
||||
size_t fb = first * s_ch * sizeof(int32_t), rb = (n - first) * s_ch * sizeof(int32_t);
|
||||
if (ring_to_lin) {
|
||||
memcpy(ring_to_lin, s_buf + start * s_ch, fb);
|
||||
memcpy(ring_to_lin + first * s_ch, s_buf, rb);
|
||||
} else {
|
||||
memcpy(s_buf + start * s_ch, lin_to_ring, fb);
|
||||
memcpy(s_buf, lin_to_ring + first * s_ch, rb);
|
||||
}
|
||||
}
|
||||
|
||||
size_t audio_ring_write(const int32_t *frames, size_t n)
|
||||
{
|
||||
uint32_t wr = __atomic_load_n(&s_wr, __ATOMIC_RELAXED);
|
||||
size_t space = s_cap - (wr - __atomic_load_n(&s_rd, __ATOMIC_ACQUIRE));
|
||||
n = n < space ? n : space;
|
||||
if (n) {
|
||||
copy(NULL, frames, wr, n);
|
||||
__atomic_store_n(&s_wr, wr + n, __ATOMIC_RELEASE);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t audio_ring_read(int32_t *frames, size_t n)
|
||||
{
|
||||
uint32_t rd = __atomic_load_n(&s_rd, __ATOMIC_RELAXED);
|
||||
size_t level = __atomic_load_n(&s_wr, __ATOMIC_ACQUIRE) - rd;
|
||||
n = n < level ? n : level;
|
||||
if (n) {
|
||||
copy(frames, NULL, rd, n);
|
||||
__atomic_store_n(&s_rd, rd + n, __ATOMIC_RELEASE);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void audio_ring_flush(void)
|
||||
{
|
||||
__atomic_store_n(&s_rd, __atomic_load_n(&s_wr, __ATOMIC_ACQUIRE), __ATOMIC_RELEASE);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Single-producer / single-consumer ring of interleaved int32 frames in PSRAM.
|
||||
// The producer is a source task, the consumer the AES67 TX pull callback (must never block).
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t audio_ring_init(size_t capacity_frames, int channels);
|
||||
size_t audio_ring_level(void); // frames available to read
|
||||
size_t audio_ring_space(void); // frames that can be written
|
||||
size_t audio_ring_write(const int32_t *frames, size_t n); // producer; returns frames written
|
||||
size_t audio_ring_read(int32_t *frames, size_t n); // consumer; returns frames read
|
||||
void audio_ring_flush(void); // consumer side: drop everything buffered
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "esp_app_desc.h"
|
||||
#include "esp_chip_info.h"
|
||||
#include "esp_log.h"
|
||||
#include "player.h"
|
||||
#include "project_cfg.h"
|
||||
|
||||
static const char *TAG = "main";
|
||||
@@ -39,6 +40,7 @@ void app_main(void)
|
||||
project_cfg_register();
|
||||
ESP_ERROR_CHECK(aes67_ota_init());
|
||||
ESP_ERROR_CHECK(aes67_sdp_sap_init());
|
||||
ESP_ERROR_CHECK(player_init());
|
||||
ESP_ERROR_CHECK(aes67_tx_start());
|
||||
ESP_ERROR_CHECK(aes67_web_start());
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
#include "player.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Project player: picks the audio source for AES67 TX (test tone, silence, HLS, Spotify).
|
||||
#pragma once
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t player_init(void);
|
||||
// Apply the "source" config group (mode etc.).
|
||||
void player_apply(const cJSON *source);
|
||||
+3
-2
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "aes67_cfg.h"
|
||||
#include "esp_err.h"
|
||||
#include "player.h"
|
||||
|
||||
static const char SOURCE_DEFAULTS[] =
|
||||
"{\"mode\":\"spotify\",\"spotify_name\":\"P4 AES67\",\"spotify_bitrate\":320,\"hls_url\":\"\","
|
||||
@@ -10,7 +11,7 @@ static const char SOURCE_DEFAULTS[] =
|
||||
|
||||
static bool source_validate(const cJSON *g, char *err, size_t n)
|
||||
{
|
||||
static const char *const modes[] = { "spotify", "hls", "auto", "off", NULL };
|
||||
static const char *const modes[] = { "spotify", "hls", "auto", "tone", "off", NULL };
|
||||
static const double bitrates[] = { 96, 160, 320 };
|
||||
return cfg_check_enum(g, "mode", modes, err, n) &&
|
||||
cfg_check_str(g, "spotify_name", 1, 63, err, n) &&
|
||||
@@ -29,5 +30,5 @@ void project_cfg_defaults(void)
|
||||
|
||||
void project_cfg_register(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(cfg_register("source", SOURCE_DEFAULTS, source_validate, NULL));
|
||||
ESP_ERROR_CHECK(cfg_register("source", SOURCE_DEFAULTS, source_validate, player_apply));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user