Step 7.5b4a: player volume (Spotify app slider), applied after the ring

- player: volume 0..100 %, amplitude = (v/100)^3 (about -18 dB at 50 %,
  0 = mute), applied in the TX pull callback after the ring so a change
  is heard at once (the ring holds 4 s); ramped over one packet to avoid
  zipper noise. Spotify VOLUME events (0..65535) set it.
- Docs: pipeline order ring -> volume/gain, curve described.
- Verified from a Mac: RMS followed the slider immediately (100 % about
  -10 dBFS; steps to about -24.5 and -28 dBFS match the cubic curve for
  ~57 % / ~50 %); user: "sounds about right". Mute at 0 % not yet
  confirmed (check via /api/player/volume later).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 18:52:57 +10:00
parent 4a38254660
commit cd747aec40
2 changed files with 40 additions and 3 deletions
+2 -1
View File
@@ -17,7 +17,8 @@ Core AES67 parts (PTP, TX, SDP/SAP, network/VLAN, syslog, web UI base, config sc
- Sources: Waveshare wiki (waveshare.com/wiki/ESP32-P4-ETH), ESPHome device page (devices.esphome.io/devices/waveshare-esp32-p4-eth), espp board docs (esp-cpp.github.io/espp/dev_boards/waveshare/esp32_p4_eth.html). - Sources: Waveshare wiki (waveshare.com/wiki/ESP32-P4-ETH), ESPHome device page (devices.esphome.io/devices/waveshare-esp32-p4-eth), espp board docs (esp-cpp.github.io/espp/dev_boards/waveshare/esp32_p4_eth.html).
## Project pipeline ## Project pipeline
source (cspot Spotify Connect | HLS player) -> decode (Vorbis/AAC/MP3) -> SRC 44.1k->48k -> volume/gain -> PSRAM ring buffer -> aes67_tx pull callback 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.
- 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. - 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). - 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. - Stereo L24/48k at 1 ms is about 3 Mbit/s on the wire; 100 Mb has plenty of headroom.
+38 -2
View File
@@ -1,5 +1,6 @@
#include "player.h" #include "player.h"
#include <math.h>
#include <string.h> #include <string.h>
#include "aes67_cfg.h" #include "aes67_cfg.h"
@@ -28,6 +29,35 @@ static volatile bool s_playing; // ring output running (after prefill)
static volatile bool s_flush; // consumer drops buffered audio on the next read static volatile bool s_flush; // consumer drops buffered audio on the next read
static volatile bool s_paused; // Spotify paused: silence, buffer kept, no underrun static volatile bool s_paused; // Spotify paused: silence, buffer kept, no underrun
static audio_conv_t *s_spotify_conv; static audio_conv_t *s_spotify_conv;
// Player volume, 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 float s_gain = 1.0f; // TX task only; ramps to s_target_gain
static volatile int s_volume_pct = 100;
// 0..100 % -> amplitude: cubic curve (about -18 dB at 50 %, 0 % = mute).
static void set_volume(int pct)
{
pct = pct < 0 ? 0 : pct > 100 ? 100 : pct;
float r = pct / 100.0f;
s_volume_pct = pct;
s_target_gain = r * r * r;
}
// Apply the volume, ramping linearly from the previous gain over this buffer (no zipper noise).
static void apply_gain(int32_t *buf, size_t frames)
{
float target = s_target_gain;
if (s_gain == target && target == 1.0f) {
return;
}
float step = (target - s_gain) / frames;
for (size_t i = 0; i < frames; i++) {
s_gain += step;
buf[i * 2] = (int32_t)(buf[i * 2] * s_gain);
buf[i * 2 + 1] = (int32_t)(buf[i * 2 + 1] * s_gain);
}
s_gain = target;
}
static player_src_t mode_of(const char *m) static player_src_t mode_of(const char *m)
{ {
@@ -55,6 +85,7 @@ static size_t player_read(int32_t *buf, size_t frames)
s_playing = true; s_playing = true;
} }
size_t got = audio_ring_read(buf, frames); size_t got = audio_ring_read(buf, frames);
apply_gain(buf, got);
if (got < frames) { if (got < frames) {
s_playing = false; // ran dry: underrun (counted by TX), prefill again s_playing = false; // ran dry: underrun (counted by TX), prefill again
} }
@@ -114,10 +145,15 @@ static void spotify_event(spotify_event_t ev, int value)
s_flush = true; // skip/seek: don't play out the old buffer s_flush = true; // skip/seek: don't play out the old buffer
} }
break; break;
case SPOTIFY_EV_VOLUME: case SPOTIFY_EV_VOLUME: {
ESP_LOGI(TAG, "Spotify volume %d%% (applied in step 7.5b4)", value * 100 / 65535); // Spotify sends 0..65535; the app's slider stays the master of the player volume.
int pct = (value * 100 + 32767) / 65535;
set_volume(pct);
float g = s_target_gain;
ESP_LOGI(TAG, "volume %d%% (%.1f dB)", pct, g > 0 ? 20.0f * log10f(g) : -INFINITY);
break; break;
} }
}
} }
static void player_status(cJSON *st) static void player_status(cJSON *st)