From 044e9913a75af55c2ecbc9e6bc4c334586d85cde Mon Sep 17 00:00:00 2001 From: Ben Nicholson Date: Fri, 25 Sep 2026 06:48:47 +1000 Subject: [PATCH] AES67 TX: send each packet right after its last sample is due The periodic TX timer had an arbitrary phase against packet boundaries, so packets waited up to one packet time (different after every restart). - One-shot esp_timer aimed at the next packet's due time from PTP, with the clock re-read after sending. - 1 kHz tone from a per-rate lookup table (one period = rate/1000 samples) instead of sinf() per sample. Measured (RTP time - arrival, 4 ms vs 1 ms packets, expected -3.07 ms incl. wire time): -3.3 ms extra before, now -3.14 ms. Tone within 1.0 LSB of the ideal PTP-phased sine, no gaps. Co-Authored-By: Claude Opus 5.5 --- components/aes67_tx/aes67_tx.c | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/components/aes67_tx/aes67_tx.c b/components/aes67_tx/aes67_tx.c index bcd387d..4ff509f 100644 --- a/components/aes67_tx/aes67_tx.c +++ b/components/aes67_tx/aes67_tx.c @@ -22,6 +22,7 @@ #define MAX_LAG_NS 20000000 // more than 20 ms behind: resync instead of bursting #define TONE_HZ 1000 #define TONE_DBFS -18.0 +#define WAKE_MARGIN_US 20 // wake just after a packet's last sample is due static const char *TAG = "aes67_tx"; @@ -166,15 +167,23 @@ 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. +// One period is rate / 1000 samples (48 at 48 kHz, 96 at 96 kHz): precomputed table. static void tone(int32_t *buf, int frames, int channels, int rate, int64_t s0) { - static float amp; - if (!amp) { - amp = (float)(pow(10.0, TONE_DBFS / 20.0) * 2147483647.0); - } + static int32_t table[96000 / TONE_HZ]; + static int table_rate; int period = rate / TONE_HZ; + if (table_rate != rate) { + double amp = pow(10.0, TONE_DBFS / 20.0) * 2147483647.0; + for (int i = 0; i < period; i++) { + table[i] = (int32_t)(amp * sin(2.0 * M_PI * i / period)); + } + table_rate = rate; + } + int ph = (int)(s0 % period); for (int i = 0; i < frames; i++) { - int32_t v = (int32_t)(amp * sinf(2.0f * (float)M_PI * (float)((s0 + i) % period) / (float)period)); + int32_t v = table[ph]; + ph = ph + 1 == period ? 0 : ph + 1; for (int ch = 0; ch < channels; ch++) { buf[i * channels + ch] = v; } @@ -216,8 +225,6 @@ static void tx_task(void *arg) next = 0; if (c.enabled) { fd = open_socket(&c); - // wake once per packet time; late wakes are caught up below - esp_timer_start_periodic(timer, (uint64_t)c.frames * 1000000ULL / c.rate); } } @@ -277,6 +284,13 @@ static void tx_task(void *arg) seq++; next += c.frames; } + // Wake when the next packet's last sample is due (PTP time), not on a free-running + // period, so each packet leaves right after it is complete. Re-read the clock: + // building and sending took time. + aes67_ptp_now_ns(&now); + int64_t wait_us = (samples_to_ns(next + c.frames, c.rate) - now) / 1000 + WAKE_MARGIN_US; + esp_timer_stop(timer); + esp_timer_start_once(timer, wait_us < 50 ? 50 : wait_us > 10000 ? 10000 : wait_us); } if (new_state != state) { state = new_state;