48 kHz only: drop 96 kHz from the AES67 output

All sources (player, test signals) are 48 kHz; at 96 kHz Spotify and HLS
played at the wrong speed. Validation, the page and the docs allow 48000
only, and a 96000 stored before is reset to 48000 at boot (stored values
aren't re-validated, and the SDP would still have said 96 kHz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 07:30:39 +10:00
parent 62b32c31ff
commit 05f4ce45c3
4 changed files with 17 additions and 9 deletions
+14 -5
View File
@@ -16,6 +16,7 @@
#include "lwip/inet.h" #include "lwip/inet.h"
#include "lwip/sockets.h" #include "lwip/sockets.h"
#define AES67_RATE 48000 // the only sample rate (all sources are 48 kHz)
#define MAX_FRAMES 192 // 4 ms at 48 kHz #define MAX_FRAMES 192 // 4 ms at 48 kHz
#define MAX_CH 2 #define MAX_CH 2
#define RTP_HDR 12 #define RTP_HDR 12
@@ -56,7 +57,7 @@ static bool aes67_validate(const cJSON *g, char *err, size_t n)
{ {
static const char *const discovery[] = { "manual", "sap", NULL }; static const char *const discovery[] = { "manual", "sap", NULL };
static const char *const encodings[] = { "L16", "L24", NULL }; static const char *const encodings[] = { "L16", "L24", NULL };
static const double rates[] = { 48000, 96000 }; static const double rates[] = { AES67_RATE }; // the sources (player, test signals) are 48 kHz
static const double ptimes[] = { 0.125, 0.25, 0.333, 1, 4 }; static const double ptimes[] = { 0.125, 0.25, 0.333, 1, 4 };
bool ok = cfg_check_str(g, "name", 1, 63, err, n) && bool ok = cfg_check_str(g, "name", 1, 63, err, n) &&
@@ -67,7 +68,7 @@ static bool aes67_validate(const cJSON *g, char *err, size_t n)
cfg_check_int(g, "dscp", 0, 63, err, n) && cfg_check_int(g, "dscp", 0, 63, err, n) &&
cfg_check_int(g, "channels", 1, 2, err, n) && cfg_check_int(g, "channels", 1, 2, err, n) &&
cfg_check_enum(g, "encoding", encodings, err, n) && cfg_check_enum(g, "encoding", encodings, err, n) &&
cfg_check_num_in(g, "rate", rates, 2, err, n) && cfg_check_num_in(g, "rate", rates, 1, err, n) &&
cfg_check_num_in(g, "ptime", ptimes, 5, err, n) && cfg_check_num_in(g, "ptime", ptimes, 5, err, n) &&
cfg_check_int(g, "pt", 96, 127, err, n) && cfg_check_int(g, "pt", 96, 127, err, n) &&
cfg_check_int(g, "ssrc", 0, 4294967295.0, err, n) && cfg_check_int(g, "ssrc", 0, 4294967295.0, err, n) &&
@@ -168,8 +169,6 @@ static int64_t samples_to_ns(int64_t s, int rate)
return (s / rate) * 1000000000LL + ((s % rate) * 1000000000LL + rate - 1) / rate; return (s / rate) * 1000000000LL + ((s % rate) * 1000000000LL + rate - 1) / 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.
// Pink noise: xorshift32 white noise through Paul Kellet's refined pink filter (within 0.05 dB // Pink noise: xorshift32 white noise through Paul Kellet's refined pink filter (within 0.05 dB
// above 9 Hz). The scale gives -18 dBFS RMS (measured over 10 s; peaks about -6 dBFS). // above 9 Hz). The scale gives -18 dBFS RMS (measured over 10 s; peaks about -6 dBFS).
#define PINK_SCALE 0.0714f #define PINK_SCALE 0.0714f
@@ -209,9 +208,11 @@ static void to_mono(int32_t *buf, int frames, bool sum)
} }
} }
// 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): precomputed table.
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[AES67_RATE / TONE_HZ];
static int table_rate; static int table_rate;
int period = rate / TONE_HZ; int period = rate / TONE_HZ;
if (table_rate != rate) { if (table_rate != rate) {
@@ -361,5 +362,13 @@ esp_err_t aes67_tx_init(void)
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
// Stored before 96 kHz was dropped: stored values aren't re-validated, so fix it here (SDP too).
cJSON *a = cfg_get("aes67");
double rate = cJSON_GetObjectItemCaseSensitive(a, "rate")->valuedouble;
cJSON_Delete(a);
if (rate != AES67_RATE) {
ESP_LOGW(TAG, "stored sample rate %.0f not supported: using %d", rate, AES67_RATE);
cfg_set_number("aes67", "rate", AES67_RATE, false);
}
return status_register(tx_status); return status_register(tx_status);
} }
+1 -1
View File
@@ -19,7 +19,7 @@ Reference devices for UI and defaults: Riedel Bolero (PTP status), Riedel Direct
## Config schema (core groups) ## Config schema (core groups)
- ptp: {mode: multicast|hybrid, role: slave|auto|master, domain, priority1, priority2, log_sync, log_announce, announce_timeout, log_delay_req, dscp, hw_ts} - ptp: {mode: multicast|hybrid, role: slave|auto|master, domain, priority1, priority2, log_sync, log_announce, announce_timeout, log_delay_req, dscp, hw_ts}
- aes67: {name, enabled, discovery: manual|sap, mcast, port, ttl, dscp, channels, mono_sum, encoding: L16|L24, rate, ptime, pt, ssrc, clk_offset, session_id, session_ver} - aes67: {name, enabled, discovery: manual|sap, mcast, port, ttl, dscp, channels, mono_sum, encoding: L16|L24, rate: 48000 only (all sources are 48 kHz), ptime, pt, ssrc, clk_offset, session_id, session_ver}
- net: {hostname, vlan, web_on: both|inet|aes67, dhcp, ip, mask, gw, dns} (AES67 / untagged interface) - net: {hostname, vlan, web_on: both|inet|aes67, dhcp, ip, mask, gw, dns} (AES67 / untagged interface)
- inet: {vlan_id, pcp, dhcp, ip, mask, gw, dns} (internet / tagged, only if net.vlan) - inet: {vlan_id, pcp, dhcp, ip, mask, gw, dns} (internet / tagged, only if net.vlan)
- log: {syslog, host, port, level: error|warn|info|debug, facility, format: rfc5424|rfc3164} - log: {syslog, host, port, level: error|warn|info|debug, facility, format: rfc5424|rfc3164}
+1 -2
View File
@@ -105,7 +105,7 @@ The secret is write-only: after saving, the field shows empty. Leave it empty to
| Channels | 2 | 1 or 2. | | Channels | 2 | 1 or 2. |
| Mono sum | off | Sends (left + right) / 2 as a 1-channel stream. Forces *Channels* to 1; it can't clip. | | Mono sum | off | Sends (left + right) / 2 as a 1-channel stream. Forces *Channels* to 1; it can't clip. |
| Bit depth | L24 | L24 or L16. | | Bit depth | L24 | L24 or L16. |
| Sample rate | 48000 | **Use 48000 with Spotify and HLS** (see [Limitations](#limitations)). | | Sample rate | 48000 | Fixed; all sources are 48 kHz. |
| Packet time | 1 ms | 0.125, 0.25, 0.333, 1 or 4 ms. 1 ms is the AES67 default and works with nearly all receivers. | | Packet time | 1 ms | 0.125, 0.25, 0.333, 1 or 4 ms. 1 ms is the AES67 default and works with nearly all receivers. |
| Payload type / SSRC / time stamp offset | 96 / 0 / 0 | Only change these if a receiver needs it. | | Payload type / SSRC / time stamp offset | 96 / 0 / 0 | Only change these if a receiver needs it. |
@@ -220,7 +220,6 @@ The full device API (status, config, SDP, firmware) is described in `docs/aes67-
## Limitations ## Limitations
These are known limitations in the current firmware: These are known limitations in the current firmware:
- **96 kHz:** only the test tone and pink noise work correctly at 96 kHz. Spotify and HLS are 48 kHz sources and must be streamed at **48000**. At 96 kHz they play at the wrong speed.
- **Autoplay stream on boot:** the checkbox has no effect yet. HLS always starts on boot. - **Autoplay stream on boot:** the checkbox has no effect yet. HLS always starts on boot.
- **VLAN split** (separate VLANs for AES67 and internet) isn't available yet. - **VLAN split** (separate VLANs for AES67 and internet) isn't available yet.
- **No password** on the web page or API. - **No password** on the web page or API.
+1 -1
View File
@@ -145,7 +145,7 @@ e.g. curl -X POST http://p4-aes67.local/api/player/next
<small>(Ch 1 + Ch 2) / 2 → 1-channel stream</small></label> <small>(Ch 1 + Ch 2) / 2 → 1-channel stream</small></label>
<label><span>Bit depth</span><select name="aes67.encoding"><option>L24</option><option>L16</option></select> <label><span>Bit depth</span><select name="aes67.encoding"><option>L24</option><option>L16</option></select>
<small>Default L24</small></label> <small>Default L24</small></label>
<label><span>Sample rate</span><select name="aes67.rate"><option>48000</option><option>96000</option></select></label> <label><span>Sample rate</span><select name="aes67.rate"><option>48000</option></select></label>
<label><span>Packet time</span><select name="aes67.ptime"> <label><span>Packet time</span><select name="aes67.ptime">
<option value="0.125">0.125 ms</option><option value="0.25">0.250 ms</option><option value="0.333">0.333 ms</option> <option value="0.125">0.125 ms</option><option value="0.25">0.250 ms</option><option value="0.333">0.333 ms</option>
<option value="1">1.000 ms</option><option value="4">4.000 ms</option></select> <option value="1">1.000 ms</option><option value="4">4.000 ms</option></select>