Step 7.4: HLS audio on AES67 (44.1 -> 48 kHz into the ring)

- decoder: s16 PCM -> stereo int32 -> esp_ae_rate_cvt 44.1 -> 48 kHz
  (32-bit, complexity 3; bypassed at 48 kHz; mono duplicated) -> ring.
  esp_audio_effects pinned to ~1.3.0 (1.4+ needs P4 rev >= 3).
- hls: each segment is downloaded completely into PSRAM (max 4 MB), then
  decoded; the connection is not held open while the decoder waits for
  ring space at playback speed.
- player: player_write() blocks while the ring is full and gives up when
  the source changes; the temporary 440 Hz producer is removed.
- Verified with Triple J Hottest: 441344 -> 480375 frames (10.008 s) and
  440320 -> 479260 (9.985 s) per segment; RTP 15000 packets, 0 gaps,
  peak -10 dBFS / RMS -22 dBFS; ring ~4.0 s, 0 underruns over ~50 s;
  heap 422 KB, PSRAM 27.5 MB free.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 14:52:19 +10:00
parent 8a6cb53e3b
commit 3ebb27e4bb
6 changed files with 122 additions and 41 deletions
+15 -17
View File
@@ -19,6 +19,7 @@
#define LIVE_BACK 3 // start this many segments behind the live edge
#define CHUNK 4096
#define MAX_REDIRECTS 5
#define SEG_MAX (4 * 1024 * 1024) // largest segment we accept (PSRAM)
static const char *TAG = "hls";
@@ -34,6 +35,7 @@ static char *s_text; // playlist buffer (PSRAM)
static media_pl_t *s_pl; // parsed media playlist (PSRAM)
static char s_url[URL_MAX]; // configured URL
static char s_media_url[URL_MAX];
static uint8_t *s_seg; // current segment (PSRAM)
// hls_url when source.mode needs HLS, else "".
static void wanted_url(char *out, size_t n)
@@ -209,30 +211,25 @@ static bool fetch_segment(long long seq, const char *url)
if (!c) {
return false;
}
static uint8_t buf[CHUNK];
// Download completely first, then decode: the connection is not held open while the
// decoder waits for space in the ring (a stalled TCP stream for ~10 s may get cut by the CDN).
size_t total = 0;
int r;
bool ok = true, first = true;
while ((r = esp_http_client_read(c, (char *)buf, sizeof(buf))) > 0) {
if (s_sink && !s_sink(buf, r, first)) {
ok = false;
break;
}
first = false;
int r = 0;
bool ok = true;
while (total < SEG_MAX && (r = esp_http_client_read(c, (char *)s_seg + total, SEG_MAX - total)) > 0) {
total += r;
if (!still_wanted()) {
ok = false;
break;
}
}
if (r < 0) {
if (r < 0 || total == SEG_MAX) {
ok = false;
}
esp_http_client_close(c);
esp_http_client_cleanup(c);
int64_t us = esp_timer_get_time() - t0;
ESP_LOGI(TAG, "segment %lld: %u bytes in %.2f s (%.1f Mbit/s)%s", seq, (unsigned)total, us / 1e6,
us ? total * 8.0 / us : 0.0, ok ? "" : ", aborted");
ESP_LOGD(TAG, "segment %lld: %u bytes in %.2f s (%.1f Mbit/s)%s", seq, (unsigned)total, us / 1e6,
us ? total * 8.0 / us : 0.0, ok ? "" : ", failed");
if (ok && s_sink) {
ok = s_sink(s_seg, total, true); // blocks at playback speed while the ring is full
}
return ok;
}
@@ -300,7 +297,8 @@ esp_err_t hls_start(hls_sink_t sink)
s_sink = sink;
s_text = heap_caps_malloc(PLAYLIST_MAX, MALLOC_CAP_SPIRAM);
s_pl = heap_caps_malloc(sizeof(media_pl_t), MALLOC_CAP_SPIRAM);
if (!s_text || !s_pl) {
s_seg = heap_caps_malloc(SEG_MAX, MALLOC_CAP_SPIRAM);
if (!s_text || !s_pl || !s_seg) {
return ESP_ERR_NO_MEM;
}
return xTaskCreate(hls_task, "hls", 10240, NULL, 4, NULL) == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;