Pink noise as a source (mode "pink")
Core: aes67_tx_pink_noise(), a ready-made TX source: xorshift32 white noise through Paul Kellet's pink filter, -18 dBFS RMS (peaks about -6 dBFS), same on L and R. The player uses it like the tone (straight to TX, no volume or gain). Checked on the board: -18 dBFS RMS, octave bands 63 Hz-4 kHz flat within +-0.4 dB, L = R. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,36 @@ 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.
|
||||
// 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).
|
||||
#define PINK_SCALE 0.0714f
|
||||
|
||||
size_t aes67_tx_pink_noise(int32_t *buf, size_t frames)
|
||||
{
|
||||
static uint32_t rnd = 0x12345678;
|
||||
static float b0, b1, b2, b3, b4, b5, b6;
|
||||
for (size_t i = 0; i < frames; i++) {
|
||||
rnd ^= rnd << 13;
|
||||
rnd ^= rnd >> 17;
|
||||
rnd ^= rnd << 5;
|
||||
float w = (int32_t)rnd * (1.0f / 2147483648.0f); // -1..1
|
||||
b0 = 0.99886f * b0 + w * 0.0555179f;
|
||||
b1 = 0.99332f * b1 + w * 0.0750759f;
|
||||
b2 = 0.96900f * b2 + w * 0.1538520f;
|
||||
b3 = 0.86650f * b3 + w * 0.3104856f;
|
||||
b4 = 0.55000f * b4 + w * 0.5329522f;
|
||||
b5 = -0.7616f * b5 - w * 0.0168980f;
|
||||
float pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362f;
|
||||
b6 = w * 0.115926f;
|
||||
float x = pink * PINK_SCALE;
|
||||
x = x > 0.999f ? 0.999f : x < -0.999f ? -0.999f : x; // never reached in practice
|
||||
int32_t v = (int32_t)(x * 2147483647.0f);
|
||||
buf[i * AES67_TX_SRC_CHANNELS] = v;
|
||||
buf[i * AES67_TX_SRC_CHANNELS + 1] = v;
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -19,3 +19,6 @@ esp_err_t aes67_tx_init(void);
|
||||
esp_err_t aes67_tx_start(void);
|
||||
// Set the audio source. NULL = built-in 1 kHz test tone at -18 dBFS, phase-locked to PTP time.
|
||||
void aes67_tx_set_source(aes67_tx_read_cb_t read);
|
||||
// Ready-made source: pink noise at -18 dBFS RMS (peaks about -6 dBFS), same on L and R.
|
||||
// Pass it to aes67_tx_set_source(). Keeps state: use for one stream only.
|
||||
size_t aes67_tx_pink_noise(int32_t *buf, size_t frames);
|
||||
|
||||
@@ -142,7 +142,7 @@ components/
|
||||
aes67_board/ board pin profiles (waveshare_esp32p4_eth.h, …), eth init
|
||||
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_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_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); built-in test signals: 1 kHz tone at -18 dBFS phase-locked to PTP (source NULL), pink noise at -18 dBFS RMS (aes67_tx_pink_noise)
|
||||
aes67_sdp_sap/ SDP builder/parser + SAP announcer
|
||||
aes67_syslog/ remote syslog
|
||||
aes67_health/ uptime, heap/PSRAM, temperature sensors (temps[] with min/max/warn)
|
||||
|
||||
@@ -18,7 +18,7 @@ Core AES67 parts (PTP, TX, SDP/SAP, network/VLAN, syslog, web UI base, config sc
|
||||
|
||||
## Project pipeline
|
||||
source (cspot Spotify Connect | HLS player) -> decode (Vorbis/AAC/MP3) -> SRC 44.1k->48k -> PSRAM ring buffer -> volume/gain -> aes67_tx pull callback
|
||||
- Volume is applied after the ring (in the pull callback) so a change is heard at once, not one ring length (4 s) later; ramped over one packet to avoid zipper noise. Curve: amplitude = (volume/100)^3 (about -18 dB at 50 %, 0 = mute). The built-in test tone (mode "tone") is not affected.
|
||||
- Volume is applied after the ring (in the pull callback) so a change is heard at once, not one ring length (4 s) later; ramped over one packet to avoid zipper noise. Curve: amplitude = (volume/100)^3 (about -18 dB at 50 %, 0 = mute). The built-in test signals (modes "tone", "pink") are not affected.
|
||||
- Spotify Connect: cspot (feelfreelinux/cspot), needs Premium, zeroconf via mDNS (on the internet interface when VLAN split). Spotify occasionally changes auth for 3rd-party clients — expect breakage risk.
|
||||
- HLS: HTTPS (mbedTLS), m3u8 parse (master->media playlist), segment fetch, TS demux or fMP4/ADTS, AAC decode (esp_audio_codec / esp-gmf).
|
||||
- Stereo L24/48k at 1 ms is about 3 Mbit/s on the wire; 100 Mb has plenty of headroom.
|
||||
@@ -31,10 +31,10 @@ source (cspot Spotify Connect | HLS player) -> decode (Vorbis/AAC/MP3) -> SRC 44
|
||||
- Firmware needs Spotify events: cspot connect, disconnect, play, pause.
|
||||
|
||||
## Project config group
|
||||
- source: {mode: spotify|hls|auto|tone|off, spotify_name, spotify_bitrate, hls_url, autoplay, gain_db, failover_delay_s, failover_on_pause, spotify_client_id, spotify_client_secret}
|
||||
- source: {mode: spotify|hls|auto|tone|pink|off, spotify_name, spotify_bitrate, hls_url, autoplay, gain_db, failover_delay_s, failover_on_pause, spotify_client_id, spotify_client_secret}
|
||||
- spotify_client_id / spotify_client_secret: each user's own Spotify developer app (developer.spotify.com, Premium account), needed by the maintained cspot fork (philippe44/cspot) since Spotify's 2025 API restrictions. The secret is write-only (never returned by GET /api/config). spotify_state reports "no client credentials" while either is missing.
|
||||
- Project status fields: active_source, source_state, spotify_state, buffer_ms
|
||||
- mode "tone": the core's 1 kHz / -18 dBFS test tone, phase-locked to PTP (commissioning, e.g. Riedel import tests). "off": silence.
|
||||
- mode "tone": the core's 1 kHz / -18 dBFS test tone, phase-locked to PTP (commissioning, e.g. Riedel import tests). "pink": the core's pink noise, -18 dBFS RMS (peaks about -6 dBFS), same on L and R (level/EQ checks). Neither is affected by volume or gain. "off": silence.
|
||||
|
||||
## Player control API (project routes)
|
||||
- GET /api/player -> {source, forced, state(playing|paused|stopped|buffering|idle), artist, title, album, position_ms, duration_ms, volume(0-100), can:{pause,next,prev,seek}}
|
||||
|
||||
+17
-9
@@ -27,9 +27,9 @@
|
||||
|
||||
static const char *TAG = "player";
|
||||
|
||||
typedef enum { MODE_TONE, MODE_OFF, MODE_HLS, MODE_SPOTIFY, MODE_AUTO } mode_t_;
|
||||
static const char *const SRC_NAME[] = { "tone", "off", "hls", "spotify" };
|
||||
static const char *const MODE_NAME[] = { "tone", "off", "hls", "spotify", "auto" };
|
||||
typedef enum { MODE_TONE, MODE_OFF, MODE_HLS, MODE_SPOTIFY, MODE_AUTO, MODE_PINK } mode_t_;
|
||||
static const char *const SRC_NAME[] = { "tone", "off", "hls", "spotify", "pink" };
|
||||
static const char *const MODE_NAME[] = { "tone", "off", "hls", "spotify", "auto", "pink" };
|
||||
|
||||
static volatile mode_t_ s_mode = MODE_OFF;
|
||||
static volatile player_src_t s_src = PLAYER_SRC_OFF; // what is playing now
|
||||
@@ -110,7 +110,7 @@ static void apply_fade(int32_t *buf, size_t frames, int *left, bool in)
|
||||
static mode_t_ mode_of(const char *m)
|
||||
{
|
||||
return !strcmp(m, "tone") ? MODE_TONE : !strcmp(m, "hls") ? MODE_HLS : !strcmp(m, "spotify") ? MODE_SPOTIFY :
|
||||
!strcmp(m, "auto") ? MODE_AUTO : MODE_OFF;
|
||||
!strcmp(m, "auto") ? MODE_AUTO : !strcmp(m, "pink") ? MODE_PINK : MODE_OFF;
|
||||
}
|
||||
|
||||
/* ----- output (AES67 TX pull callback, TX task, must not block) ----- */
|
||||
@@ -168,8 +168,14 @@ static size_t player_read(int32_t *buf, size_t frames)
|
||||
return got;
|
||||
}
|
||||
|
||||
// Set the source to play. Tone comes from TX itself (phase-locked to PTP), so switches from/to it
|
||||
// are immediate; everything else fades through player_read.
|
||||
// Test signals: straight from the core to TX, not through the ring, volume or gain.
|
||||
static bool is_test_signal(player_src_t src)
|
||||
{
|
||||
return src == PLAYER_SRC_TONE || src == PLAYER_SRC_PINK;
|
||||
}
|
||||
|
||||
// Set the source to play. Test signals come from the core (the tone phase-locked to PTP), so
|
||||
// switches from/to them are immediate; everything else fades through player_read.
|
||||
static void select_source(player_src_t src)
|
||||
{
|
||||
if (src == s_target) {
|
||||
@@ -180,10 +186,11 @@ static void select_source(player_src_t src)
|
||||
s_hls_paused = false;
|
||||
hls_suspend(HLS_SUSPEND_USER, false);
|
||||
}
|
||||
if (src == PLAYER_SRC_TONE || s_src == PLAYER_SRC_TONE) {
|
||||
if (is_test_signal(src) || is_test_signal(s_src)) {
|
||||
s_target = s_src = src;
|
||||
s_flush = true;
|
||||
aes67_tx_set_source(src == PLAYER_SRC_TONE ? NULL : player_read);
|
||||
aes67_tx_set_source(src == PLAYER_SRC_TONE ? NULL : src == PLAYER_SRC_PINK ? aes67_tx_pink_noise :
|
||||
player_read);
|
||||
return;
|
||||
}
|
||||
s_target = src; // player_read fades out and switches
|
||||
@@ -213,6 +220,7 @@ static void apply_mode(void)
|
||||
hls_suspend(HLS_SUSPEND_FAILOVER, mode == MODE_AUTO);
|
||||
switch (mode) {
|
||||
case MODE_TONE: select_source(PLAYER_SRC_TONE); break;
|
||||
case MODE_PINK: select_source(PLAYER_SRC_PINK); break;
|
||||
case MODE_HLS: select_source(PLAYER_SRC_HLS); break;
|
||||
case MODE_SPOTIFY: select_source(PLAYER_SRC_SPOTIFY); break;
|
||||
case MODE_AUTO: select_source(PLAYER_SRC_SPOTIFY); break; // Spotify inactive: HLS after the delay
|
||||
@@ -369,7 +377,7 @@ static void on_ota(bool starting)
|
||||
|
||||
static const char *source_state(player_src_t src)
|
||||
{
|
||||
return src == PLAYER_SRC_TONE ? "playing" : src == PLAYER_SRC_OFF ? "idle" :
|
||||
return is_test_signal(src) ? "playing" : src == PLAYER_SRC_OFF ? "idle" :
|
||||
src == PLAYER_SRC_SPOTIFY && !spotify_session_active() ? "idle" :
|
||||
src == PLAYER_SRC_SPOTIFY && s_paused ? "paused" :
|
||||
src == PLAYER_SRC_HLS && s_hls_paused ? "paused" : s_playing ? "playing" : "buffering";
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// 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, pink noise, silence, HLS, Spotify).
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef enum { PLAYER_SRC_TONE, PLAYER_SRC_OFF, PLAYER_SRC_HLS, PLAYER_SRC_SPOTIFY } player_src_t;
|
||||
typedef enum { PLAYER_SRC_TONE, PLAYER_SRC_OFF, PLAYER_SRC_HLS, PLAYER_SRC_SPOTIFY, PLAYER_SRC_PINK } player_src_t;
|
||||
|
||||
esp_err_t player_init(void);
|
||||
// Apply the "source" config group (mode etc.).
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ static const char SOURCE_DEFAULTS[] =
|
||||
|
||||
static bool source_validate(const cJSON *g, char *err, size_t n)
|
||||
{
|
||||
static const char *const modes[] = { "spotify", "hls", "auto", "tone", "off", NULL };
|
||||
static const char *const modes[] = { "spotify", "hls", "auto", "tone", "pink", "off", NULL };
|
||||
static const double bitrates[] = { 96, 160, 320 };
|
||||
return cfg_check_enum(g, "mode", modes, err, n) &&
|
||||
cfg_check_str(g, "spotify_name", 1, 63, err, n) &&
|
||||
|
||||
@@ -66,6 +66,7 @@ e.g. curl -X POST http://p4-aes67.local/api/player/next
|
||||
<option value="hls">HLS / M3U8 URL</option>
|
||||
<option value="auto">Spotify, fail over to HLS</option>
|
||||
<option value="tone">Test tone (1 kHz, -18 dBFS)</option>
|
||||
<option value="pink">Pink noise (-18 dBFS RMS)</option>
|
||||
<option value="off">Off (silence)</option></select></label>
|
||||
<label><span>Failover delay (s)</span><input name="source.failover_delay_s" type="number" min="0" max="3600"></label>
|
||||
<label><span>Also fail over when paused</span><input name="source.failover_on_pause" type="checkbox"></label>
|
||||
|
||||
Reference in New Issue
Block a user