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
+67 -5
View File
@@ -6,11 +6,21 @@
#include "esp_audio_dec_default.h"
#include "esp_audio_simple_dec.h"
#include "esp_audio_simple_dec_default.h"
#include "esp_ae_rate_cvt.h"
#include "player.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
static const char *TAG = "decoder";
#define OUT_RATE 48000
#define CONV_FRAMES 4096 // per rate converter call
static esp_ae_rate_cvt_handle_t s_cvt; // NULL: no conversion (source is 48 kHz)
static uint32_t s_cvt_rate; // input rate the converter was opened for
static int32_t *s_in32, *s_out32; // stereo int32 work buffers (PSRAM)
static uint64_t s_seg_out; // 48 kHz frames written this segment
static esp_audio_simple_dec_handle_t s_dec;
static uint8_t *s_pcm;
static uint32_t s_pcm_size = 8192;
@@ -25,15 +35,61 @@ static int s_pmt_pid = -1, s_audio_pid = -1;
static void report_segment(void)
{
if (s_seg_count && s_info.sample_rate) {
ESP_LOGI(TAG, "segment: %llu ES bytes -> %llu PCM frames = %.3f s (%lu Hz, %u ch, %u bit)",
s_seg_es, s_seg_frames, (double)s_seg_frames / s_info.sample_rate,
(unsigned long)s_info.sample_rate, s_info.channel, s_info.bits_per_sample);
ESP_LOGI(TAG, "segment: %llu ES bytes -> %llu frames at %lu Hz = %.3f s -> %llu frames at 48 kHz = %.3f s",
s_seg_es, s_seg_frames, (unsigned long)s_info.sample_rate,
(double)s_seg_frames / s_info.sample_rate, s_seg_out, (double)s_seg_out / OUT_RATE);
}
s_seg_frames = 0;
s_seg_es = 0;
s_seg_out = 0;
s_seg_count++;
}
// Decoded PCM (s16, any channel count) -> stereo int32 -> 48 kHz -> ring.
static void output_pcm(const int16_t *pcm, uint32_t frames)
{
int ch = s_info.channel;
if (s_info.sample_rate != s_cvt_rate) {
if (s_cvt) {
esp_ae_rate_cvt_close(s_cvt);
s_cvt = NULL;
}
s_cvt_rate = s_info.sample_rate;
if (s_cvt_rate != OUT_RATE) {
esp_ae_rate_cvt_cfg_t cfg = {
.src_rate = s_cvt_rate, .dest_rate = OUT_RATE, .channel = 2, .bits_per_sample = 32,
.complexity = 3, .perf_type = ESP_AE_RATE_CVT_PERF_TYPE_SPEED,
};
if (esp_ae_rate_cvt_open(&cfg, &s_cvt) != ESP_AE_ERR_OK) {
ESP_LOGE(TAG, "rate converter %lu -> %d Hz: open failed", (unsigned long)s_cvt_rate, OUT_RATE);
} else {
ESP_LOGI(TAG, "rate converter %lu -> %d Hz", (unsigned long)s_cvt_rate, OUT_RATE);
}
}
}
while (frames) {
uint32_t n = frames < CONV_FRAMES ? frames : CONV_FRAMES;
for (uint32_t i = 0; i < n; i++) { // to stereo int32 (full scale = INT32_MAX)
int32_t l = (int32_t)pcm[i * ch] << 16;
s_in32[i * 2] = l;
s_in32[i * 2 + 1] = ch > 1 ? (int32_t)pcm[i * ch + 1] << 16 : l;
}
const int32_t *out = s_in32;
uint32_t out_n = n;
if (s_cvt) {
out_n = CONV_FRAMES * 2;
if (esp_ae_rate_cvt_process(s_cvt, s_in32, n, s_out32, &out_n) != ESP_AE_ERR_OK) {
ESP_LOGW(TAG, "rate conversion failed");
return;
}
out = s_out32;
}
s_seg_out += player_write(out, out_n);
pcm += n * ch;
frames -= n;
}
}
// Feed ADTS-AAC elementary stream bytes to the decoder.
static void decode_es(const uint8_t *data, size_t len)
{
@@ -62,7 +118,11 @@ static void decode_es(const uint8_t *data, size_t len)
ESP_LOGI(TAG, "stream: %lu Hz, %u ch, %u bit, %lu bit/s", (unsigned long)s_info.sample_rate,
s_info.channel, s_info.bits_per_sample, (unsigned long)s_info.bitrate);
}
s_seg_frames += out.decoded_size / (s_info.channel * s_info.bits_per_sample / 8);
uint32_t n = out.decoded_size / (s_info.channel * s_info.bits_per_sample / 8);
s_seg_frames += n;
if (s_info.bits_per_sample == 16) {
output_pcm((const int16_t *)out.buffer, n);
}
}
raw.buffer += raw.consumed;
raw.len -= raw.consumed;
@@ -168,5 +228,7 @@ esp_err_t decoder_init(void)
return ESP_FAIL;
}
s_pcm = heap_caps_malloc(s_pcm_size, MALLOC_CAP_SPIRAM);
return s_pcm ? ESP_OK : ESP_ERR_NO_MEM;
s_in32 = heap_caps_malloc(CONV_FRAMES * 2 * sizeof(int32_t), MALLOC_CAP_SPIRAM);
s_out32 = heap_caps_malloc(CONV_FRAMES * 2 * 2 * sizeof(int32_t), MALLOC_CAP_SPIRAM);
return s_pcm && s_in32 && s_out32 ? ESP_OK : ESP_ERR_NO_MEM;
}