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:
+20
-1
@@ -9,6 +9,24 @@ dependencies:
|
|||||||
registry_url: https://components.espressif.com/
|
registry_url: https://components.espressif.com/
|
||||||
type: service
|
type: service
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
|
espressif/esp_audio_effects:
|
||||||
|
component_hash: 6ad72ee106e25b07c9ae5b8ec77b566ef8d697473095f1c982e13247005b9ba6
|
||||||
|
dependencies:
|
||||||
|
- name: espressif/gmf_fft
|
||||||
|
registry_url: https://components.espressif.com
|
||||||
|
require: private
|
||||||
|
version: ~1.0
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.3.0~1
|
||||||
|
espressif/gmf_fft:
|
||||||
|
component_hash: b59451ef2f96d22b80c27a162515b21f1b85954dda434c0b092172164028779d
|
||||||
|
dependencies: []
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com
|
||||||
|
type: service
|
||||||
|
version: 1.0.0
|
||||||
espressif/mdns:
|
espressif/mdns:
|
||||||
component_hash: b679eafd0acae2066e2645bd91d073e33ca5be515e2545cd3c4e55a3e3bce3cb
|
component_hash: b679eafd0acae2066e2645bd91d073e33ca5be515e2545cd3c4e55a3e3bce3cb
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -25,7 +43,8 @@ dependencies:
|
|||||||
version: 5.5.5
|
version: 5.5.5
|
||||||
direct_dependencies:
|
direct_dependencies:
|
||||||
- espressif/esp_audio_codec
|
- espressif/esp_audio_codec
|
||||||
|
- espressif/esp_audio_effects
|
||||||
- espressif/mdns
|
- espressif/mdns
|
||||||
manifest_hash: e85f2bff1cc890def1a35e7da5210473c09ec845364a2bc2e54f6d2a7de54cbc
|
manifest_hash: 95404bd3492778da35a8aeee53935ee8ad13b09c56ae5fcbe77a4e921dafbb23
|
||||||
target: esp32p4
|
target: esp32p4
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
|||||||
+67
-5
@@ -6,11 +6,21 @@
|
|||||||
#include "esp_audio_dec_default.h"
|
#include "esp_audio_dec_default.h"
|
||||||
#include "esp_audio_simple_dec.h"
|
#include "esp_audio_simple_dec.h"
|
||||||
#include "esp_audio_simple_dec_default.h"
|
#include "esp_audio_simple_dec_default.h"
|
||||||
|
#include "esp_ae_rate_cvt.h"
|
||||||
|
#include "player.h"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
|
||||||
static const char *TAG = "decoder";
|
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 esp_audio_simple_dec_handle_t s_dec;
|
||||||
static uint8_t *s_pcm;
|
static uint8_t *s_pcm;
|
||||||
static uint32_t s_pcm_size = 8192;
|
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)
|
static void report_segment(void)
|
||||||
{
|
{
|
||||||
if (s_seg_count && s_info.sample_rate) {
|
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)",
|
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, (double)s_seg_frames / s_info.sample_rate,
|
s_seg_es, s_seg_frames, (unsigned long)s_info.sample_rate,
|
||||||
(unsigned long)s_info.sample_rate, s_info.channel, s_info.bits_per_sample);
|
(double)s_seg_frames / s_info.sample_rate, s_seg_out, (double)s_seg_out / OUT_RATE);
|
||||||
}
|
}
|
||||||
s_seg_frames = 0;
|
s_seg_frames = 0;
|
||||||
s_seg_es = 0;
|
s_seg_es = 0;
|
||||||
|
s_seg_out = 0;
|
||||||
s_seg_count++;
|
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.
|
// Feed ADTS-AAC elementary stream bytes to the decoder.
|
||||||
static void decode_es(const uint8_t *data, size_t len)
|
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,
|
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_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.buffer += raw.consumed;
|
||||||
raw.len -= raw.consumed;
|
raw.len -= raw.consumed;
|
||||||
@@ -168,5 +228,7 @@ esp_err_t decoder_init(void)
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
s_pcm = heap_caps_malloc(s_pcm_size, MALLOC_CAP_SPIRAM);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-17
@@ -19,6 +19,7 @@
|
|||||||
#define LIVE_BACK 3 // start this many segments behind the live edge
|
#define LIVE_BACK 3 // start this many segments behind the live edge
|
||||||
#define CHUNK 4096
|
#define CHUNK 4096
|
||||||
#define MAX_REDIRECTS 5
|
#define MAX_REDIRECTS 5
|
||||||
|
#define SEG_MAX (4 * 1024 * 1024) // largest segment we accept (PSRAM)
|
||||||
|
|
||||||
static const char *TAG = "hls";
|
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 media_pl_t *s_pl; // parsed media playlist (PSRAM)
|
||||||
static char s_url[URL_MAX]; // configured URL
|
static char s_url[URL_MAX]; // configured URL
|
||||||
static char s_media_url[URL_MAX];
|
static char s_media_url[URL_MAX];
|
||||||
|
static uint8_t *s_seg; // current segment (PSRAM)
|
||||||
|
|
||||||
// hls_url when source.mode needs HLS, else "".
|
// hls_url when source.mode needs HLS, else "".
|
||||||
static void wanted_url(char *out, size_t n)
|
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) {
|
if (!c) {
|
||||||
return false;
|
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;
|
size_t total = 0;
|
||||||
int r;
|
int r = 0;
|
||||||
bool ok = true, first = true;
|
bool ok = true;
|
||||||
while ((r = esp_http_client_read(c, (char *)buf, sizeof(buf))) > 0) {
|
while (total < SEG_MAX && (r = esp_http_client_read(c, (char *)s_seg + total, SEG_MAX - total)) > 0) {
|
||||||
if (s_sink && !s_sink(buf, r, first)) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
first = false;
|
|
||||||
total += r;
|
total += r;
|
||||||
if (!still_wanted()) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (r < 0) {
|
if (r < 0 || total == SEG_MAX) {
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
esp_http_client_close(c);
|
esp_http_client_close(c);
|
||||||
esp_http_client_cleanup(c);
|
esp_http_client_cleanup(c);
|
||||||
int64_t us = esp_timer_get_time() - t0;
|
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,
|
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 ? "" : ", aborted");
|
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;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +297,8 @@ esp_err_t hls_start(hls_sink_t sink)
|
|||||||
s_sink = sink;
|
s_sink = sink;
|
||||||
s_text = heap_caps_malloc(PLAYLIST_MAX, MALLOC_CAP_SPIRAM);
|
s_text = heap_caps_malloc(PLAYLIST_MAX, MALLOC_CAP_SPIRAM);
|
||||||
s_pl = heap_caps_malloc(sizeof(media_pl_t), 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 ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
return xTaskCreate(hls_task, "hls", 10240, NULL, 4, NULL) == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
|
return xTaskCreate(hls_task, "hls", 10240, NULL, 4, NULL) == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
# 2.6+ uses P4 assembly that needs chip rev >= 3.0; this board is rev 1.3 (see CLAUDE.md).
|
# Newer versions use P4 assembly that needs chip rev >= 3.0; this board is rev 1.3 (see CLAUDE.md).
|
||||||
espressif/esp_audio_codec: "~2.5.0"
|
espressif/esp_audio_codec: "~2.5.0"
|
||||||
|
espressif/esp_audio_effects: "~1.3.0"
|
||||||
|
|||||||
+13
-17
@@ -1,6 +1,5 @@
|
|||||||
#include "player.h"
|
#include "player.h"
|
||||||
|
|
||||||
#include <math.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "aes67_cfg.h"
|
#include "aes67_cfg.h"
|
||||||
@@ -72,24 +71,22 @@ void player_apply(const cJSON *source)
|
|||||||
ESP_LOGI(TAG, "source: %s", SRC_NAME[src]);
|
ESP_LOGI(TAG, "source: %s", SRC_NAME[src]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- Step 7.1: temporary test producer (440 Hz into the ring), replaced by the HLS player ----- */
|
// Source side (HLS task): write converted 48 kHz frames, waiting for space at playback speed.
|
||||||
|
// Gives up when the source is no longer HLS.
|
||||||
static void test_producer(void *arg)
|
size_t player_write(const int32_t *frames, size_t n)
|
||||||
{
|
{
|
||||||
static int32_t chunk[480 * CHANNELS];
|
size_t done = 0;
|
||||||
uint32_t phase = 0;
|
while (done < n) {
|
||||||
const double amp = pow(10.0, -18.0 / 20.0) * 2147483647.0;
|
if (s_src != SRC_HLS) {
|
||||||
while (1) {
|
return done;
|
||||||
if (s_src != SRC_HLS || audio_ring_space() < 480) {
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < 480; i++, phase++) {
|
size_t w = audio_ring_write(frames + done * CHANNELS, n - done);
|
||||||
int32_t v = (int32_t)(amp * sin(2.0 * M_PI * 440.0 * (phase % RATE) / RATE));
|
if (!w) {
|
||||||
chunk[i * 2] = chunk[i * 2 + 1] = v;
|
vTaskDelay(pdMS_TO_TICKS(20));
|
||||||
}
|
}
|
||||||
audio_ring_write(chunk, 480);
|
done += w;
|
||||||
}
|
}
|
||||||
|
return done;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void player_status(cJSON *st)
|
static void player_status(cJSON *st)
|
||||||
@@ -113,12 +110,11 @@ esp_err_t player_init(void)
|
|||||||
s_src = (src_t)-1;
|
s_src = (src_t)-1;
|
||||||
player_apply(src);
|
player_apply(src);
|
||||||
cJSON_Delete(src);
|
cJSON_Delete(src);
|
||||||
xTaskCreate(test_producer, "test_prod", 3072, NULL, 5, NULL);
|
|
||||||
err = decoder_init();
|
err = decoder_init();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
err = hls_start(decoder_feed); // 7.3: decode and log; resampling + ring in 7.4
|
err = hls_start(decoder_feed);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
// Project player: picks the audio source for AES67 TX (test tone, silence, HLS, Spotify).
|
// Project player: picks the audio source for AES67 TX (test tone, silence, HLS, Spotify).
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
esp_err_t player_init(void);
|
esp_err_t player_init(void);
|
||||||
// Apply the "source" config group (mode etc.).
|
// Apply the "source" config group (mode etc.).
|
||||||
void player_apply(const cJSON *source);
|
void player_apply(const cJSON *source);
|
||||||
|
// Source side: write 48 kHz stereo int32 frames, blocking while the ring is full.
|
||||||
|
size_t player_write(const int32_t *frames, size_t n);
|
||||||
|
|||||||
Reference in New Issue
Block a user