aes67_tx: mono sum; the source always delivers stereo
The pull callback's contract said "stream channels", but the player always delivers stereo, so a 1-channel stream went out as L,R,L,R at half speed. Sources now always deliver AES67_TX_SRC_CHANNELS (2); TX maps to the stream: 2 ch as is, 1 ch = (L+R)/2 in 64 bit with mono_sum (can't clip), else L. Checked: mono packets 156 B, 1000/s, 48 samples each, SDP L24/48000/1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,7 @@ typedef struct {
|
|||||||
uint8_t ttl, dscp, pt;
|
uint8_t ttl, dscp, pt;
|
||||||
uint32_t ssrc, clk_offset;
|
uint32_t ssrc, clk_offset;
|
||||||
int channels, rate, bytes; // bytes per sample: 3 (L24) or 2 (L16)
|
int channels, rate, bytes; // bytes per sample: 3 (L24) or 2 (L16)
|
||||||
|
bool mono_sum; // 1-channel stream carries (L + R) / 2, else L
|
||||||
int frames; // samples per channel per packet
|
int frames; // samples per channel per packet
|
||||||
} tx_cfg_t;
|
} tx_cfg_t;
|
||||||
|
|
||||||
@@ -127,6 +128,7 @@ static bool load(tx_cfg_t *c)
|
|||||||
c->ssrc = (uint32_t)NUM("ssrc");
|
c->ssrc = (uint32_t)NUM("ssrc");
|
||||||
c->clk_offset = (uint32_t)NUM("clk_offset");
|
c->clk_offset = (uint32_t)NUM("clk_offset");
|
||||||
c->channels = (int)NUM("channels");
|
c->channels = (int)NUM("channels");
|
||||||
|
c->mono_sum = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(a, "mono_sum"));
|
||||||
c->rate = (int)NUM("rate");
|
c->rate = (int)NUM("rate");
|
||||||
c->bytes = strcmp(cJSON_GetObjectItemCaseSensitive(a, "encoding")->valuestring, "L16") == 0 ? 2 : 3;
|
c->bytes = strcmp(cJSON_GetObjectItemCaseSensitive(a, "encoding")->valuestring, "L16") == 0 ? 2 : 3;
|
||||||
c->frames = (int)lround(NUM("ptime") * c->rate / 1000.0); // 0.333 ms -> 16 at 48 kHz
|
c->frames = (int)lround(NUM("ptime") * c->rate / 1000.0); // 0.333 ms -> 16 at 48 kHz
|
||||||
@@ -168,6 +170,15 @@ static int64_t samples_to_ns(int64_t s, int rate)
|
|||||||
|
|
||||||
// 1 kHz tone, phase from the PTP sample index, so every sender's tone lines up.
|
// 1 kHz tone, phase from the PTP sample index, so every sender's tone lines up.
|
||||||
// One period is rate / 1000 samples (48 at 48 kHz, 96 at 96 kHz): precomputed table.
|
// One period is rate / 1000 samples (48 at 48 kHz, 96 at 96 kHz): precomputed table.
|
||||||
|
// Stereo source -> 1-channel stream, in place. The sum is in 64 bit, so it can't clip; identical
|
||||||
|
// L/R comes out at the same level.
|
||||||
|
static void to_mono(int32_t *buf, int frames, bool sum)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < frames; i++) {
|
||||||
|
buf[i] = sum ? (int32_t)(((int64_t)buf[2 * i] + buf[2 * i + 1]) / 2) : buf[2 * i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void tone(int32_t *buf, int frames, int channels, int rate, int64_t s0)
|
static void tone(int32_t *buf, int frames, int channels, int rate, int64_t s0)
|
||||||
{
|
{
|
||||||
static int32_t table[96000 / TONE_HZ];
|
static int32_t table[96000 / TONE_HZ];
|
||||||
@@ -202,7 +213,7 @@ static void tx_task(void *arg)
|
|||||||
esp_timer_handle_t timer = NULL;
|
esp_timer_handle_t timer = NULL;
|
||||||
const esp_timer_create_args_t targs = { .callback = timer_cb, .name = "aes67_tx" };
|
const esp_timer_create_args_t targs = { .callback = timer_cb, .name = "aes67_tx" };
|
||||||
esp_timer_create(&targs, &timer);
|
esp_timer_create(&targs, &timer);
|
||||||
static int32_t pcm[MAX_FRAMES * MAX_CH];
|
static int32_t pcm[MAX_FRAMES * (MAX_CH > AES67_TX_SRC_CHANNELS ? MAX_CH : AES67_TX_SRC_CHANNELS)];
|
||||||
static uint8_t pkt[RTP_HDR + MAX_FRAMES * MAX_CH * 3];
|
static uint8_t pkt[RTP_HDR + MAX_FRAMES * MAX_CH * 3];
|
||||||
uint16_t seq = (uint16_t)esp_random();
|
uint16_t seq = (uint16_t)esp_random();
|
||||||
int64_t next = 0; // sample index of the next packet; 0 = resync needed
|
int64_t next = 0; // sample index of the next packet; 0 = resync needed
|
||||||
@@ -250,9 +261,13 @@ static void tx_task(void *arg)
|
|||||||
if (read) {
|
if (read) {
|
||||||
size_t got = read(pcm, c.frames);
|
size_t got = read(pcm, c.frames);
|
||||||
if (got < (size_t)c.frames) {
|
if (got < (size_t)c.frames) {
|
||||||
memset(pcm + got * c.channels, 0, (c.frames - got) * c.channels * sizeof(int32_t));
|
memset(pcm + got * AES67_TX_SRC_CHANNELS, 0,
|
||||||
|
(c.frames - got) * AES67_TX_SRC_CHANNELS * sizeof(int32_t));
|
||||||
s_underruns++;
|
s_underruns++;
|
||||||
}
|
}
|
||||||
|
if (c.channels == 1) {
|
||||||
|
to_mono(pcm, c.frames, c.mono_sum);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tone(pcm, c.frames, c.channels, c.rate, next);
|
tone(pcm, c.frames, c.channels, c.rate, next);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
// Audio source: fill buf with frames x channels interleaved samples (full scale = INT32_MAX).
|
// Audio source: fill buf with frames x AES67_TX_SRC_CHANNELS interleaved samples (stereo, full scale =
|
||||||
|
// INT32_MAX), whatever the stream's channel count: TX maps it (mono stream: L, or (L+R)/2 with mono_sum).
|
||||||
// Return the number of frames delivered; fewer than asked counts as an underrun (padded with silence).
|
// Return the number of frames delivered; fewer than asked counts as an underrun (padded with silence).
|
||||||
|
#define AES67_TX_SRC_CHANNELS 2
|
||||||
typedef size_t (*aes67_tx_read_cb_t)(int32_t *buf, size_t frames);
|
typedef size_t (*aes67_tx_read_cb_t)(int32_t *buf, size_t frames);
|
||||||
|
|
||||||
// Registers the "aes67" config group and the TX status fields.
|
// Registers the "aes67" config group and the TX status fields.
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ components/
|
|||||||
aes67_board/ board pin profiles (waveshare_esp32p4_eth.h, …), eth init
|
aes67_board/ board pin profiles (waveshare_esp32p4_eth.h, …), eth init
|
||||||
aes67_net/ netif setup, optional VLAN split, DSCP/PCP helpers
|
aes67_net/ netif setup, optional VLAN split, DSCP/PCP helpers
|
||||||
aes67_ptp/ PTPv2 ordinary clock: BMCA, servo, master, multicast/hybrid, EMAC HW timestamps, stats
|
aes67_ptp/ PTPv2 ordinary clock: BMCA, servo, master, multicast/hybrid, EMAC HW timestamps, stats
|
||||||
aes67_tx/ RTP sender task, PTP-paced, pull callback: size_t (*read)(int32_t *buf, size_t frames); optional mono sum
|
aes67_tx/ RTP sender task, PTP-paced, pull callback: size_t (*read)(int32_t *buf, size_t frames), always stereo (AES67_TX_SRC_CHANNELS); TX maps it to the stream (mono: L, or (L+R)/2 with mono_sum)
|
||||||
aes67_sdp_sap/ SDP builder/parser + SAP announcer
|
aes67_sdp_sap/ SDP builder/parser + SAP announcer
|
||||||
aes67_syslog/ remote syslog
|
aes67_syslog/ remote syslog
|
||||||
aes67_health/ uptime, heap/PSRAM, temperature sensors (temps[] with min/max/warn)
|
aes67_health/ uptime, heap/PSRAM, temperature sensors (temps[] with min/max/warn)
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
#include "spotify.h"
|
#include "spotify.h"
|
||||||
|
|
||||||
#define RATE 48000
|
#define RATE 48000
|
||||||
#define CHANNELS 2
|
#define CHANNELS AES67_TX_SRC_CHANNELS // the ring holds what TX pulls: stereo
|
||||||
#define RING_FRAMES (4 * RATE) // 4 s in PSRAM
|
#define RING_FRAMES (4 * RATE) // 4 s in PSRAM
|
||||||
#define PREFILL_FRAMES (RATE) // 1 s before (re)starting output
|
#define PREFILL_FRAMES (RATE) // 1 s before (re)starting output
|
||||||
#define FADE_FRAMES (RATE * 30 / 1000) // 30 ms ramp at a source switch
|
#define FADE_FRAMES (RATE * 30 / 1000) // 30 ms ramp at a source switch
|
||||||
|
|||||||
Reference in New Issue
Block a user