Compare commits
2 Commits
faf427c897
...
bbeb7d3fd2
| Author | SHA1 | Date | |
|---|---|---|---|
| bbeb7d3fd2 | |||
| c9e06da47c |
@@ -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)
|
||||||
|
|||||||
+18
-7
@@ -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
|
||||||
@@ -55,21 +55,30 @@ static volatile bool s_failover_on_pause;
|
|||||||
static int s_fade_out; // frames left in the fade-out before a switch
|
static int s_fade_out; // frames left in the fade-out before a switch
|
||||||
static int s_fade_in; // frames left in the fade-in after a switch
|
static int s_fade_in; // frames left in the fade-in after a switch
|
||||||
|
|
||||||
// Player volume, applied after the ring so a change is heard at once (not a ring length later).
|
// Player volume and the fixed output trim (source.gain_db), applied after the ring so a change is
|
||||||
|
// heard at once (not a ring length later).
|
||||||
static volatile float s_target_gain = 1.0f;
|
static volatile float s_target_gain = 1.0f;
|
||||||
static float s_gain = 1.0f; // TX task only; ramps to s_target_gain
|
static float s_gain = 1.0f; // TX task only; ramps to s_target_gain
|
||||||
static volatile int s_volume_pct = 100;
|
static volatile int s_volume_pct = 100;
|
||||||
|
static volatile float s_trim = 1.0f; // source.gain_db as amplitude
|
||||||
|
|
||||||
// 0..100 % -> amplitude: cubic curve (about -18 dB at 50 %, 0 % = mute).
|
// 0..100 % -> amplitude: cubic curve (about -18 dB at 50 %, 0 % = mute), times the trim.
|
||||||
static void set_volume(int pct)
|
static void set_volume(int pct)
|
||||||
{
|
{
|
||||||
pct = pct < 0 ? 0 : pct > 100 ? 100 : pct;
|
pct = pct < 0 ? 0 : pct > 100 ? 100 : pct;
|
||||||
float r = pct / 100.0f;
|
float r = pct / 100.0f;
|
||||||
s_volume_pct = pct;
|
s_volume_pct = pct;
|
||||||
s_target_gain = r * r * r;
|
s_target_gain = r * r * r * s_trim;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the volume, ramping linearly from the previous gain over this buffer (no zipper noise).
|
// Scale one sample, clipping at full scale (the trim can boost up to +12 dB).
|
||||||
|
static inline int32_t scale(int32_t x, float g)
|
||||||
|
{
|
||||||
|
float v = x * g;
|
||||||
|
return v >= 2147483520.0f ? INT32_MAX : v <= -2147483648.0f ? INT32_MIN : (int32_t)v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the gain, ramping linearly from the previous gain over this buffer (no zipper noise).
|
||||||
static void apply_gain(int32_t *buf, size_t frames)
|
static void apply_gain(int32_t *buf, size_t frames)
|
||||||
{
|
{
|
||||||
float target = s_target_gain;
|
float target = s_target_gain;
|
||||||
@@ -79,8 +88,8 @@ static void apply_gain(int32_t *buf, size_t frames)
|
|||||||
float step = (target - s_gain) / frames;
|
float step = (target - s_gain) / frames;
|
||||||
for (size_t i = 0; i < frames; i++) {
|
for (size_t i = 0; i < frames; i++) {
|
||||||
s_gain += step;
|
s_gain += step;
|
||||||
buf[i * 2] = (int32_t)(buf[i * 2] * s_gain);
|
buf[i * 2] = scale(buf[i * 2], s_gain);
|
||||||
buf[i * 2 + 1] = (int32_t)(buf[i * 2 + 1] * s_gain);
|
buf[i * 2 + 1] = scale(buf[i * 2 + 1], s_gain);
|
||||||
}
|
}
|
||||||
s_gain = target;
|
s_gain = target;
|
||||||
}
|
}
|
||||||
@@ -215,6 +224,8 @@ void player_apply(const cJSON *source)
|
|||||||
{
|
{
|
||||||
s_failover_delay_s = (int)cJSON_GetObjectItemCaseSensitive(source, "failover_delay_s")->valuedouble;
|
s_failover_delay_s = (int)cJSON_GetObjectItemCaseSensitive(source, "failover_delay_s")->valuedouble;
|
||||||
s_failover_on_pause = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(source, "failover_on_pause"));
|
s_failover_on_pause = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(source, "failover_on_pause"));
|
||||||
|
s_trim = powf(10.0f, (float)cJSON_GetObjectItemCaseSensitive(source, "gain_db")->valuedouble / 20.0f);
|
||||||
|
set_volume(s_volume_pct);
|
||||||
s_cfg_mode = mode_of(cJSON_GetObjectItemCaseSensitive(source, "mode")->valuestring);
|
s_cfg_mode = mode_of(cJSON_GetObjectItemCaseSensitive(source, "mode")->valuestring);
|
||||||
strlcpy(s_cfg_url, cJSON_GetObjectItemCaseSensitive(source, "hls_url")->valuestring, sizeof(s_cfg_url));
|
strlcpy(s_cfg_url, cJSON_GetObjectItemCaseSensitive(source, "hls_url")->valuestring, sizeof(s_cfg_url));
|
||||||
apply_mode();
|
apply_mode();
|
||||||
|
|||||||
Reference in New Issue
Block a user