From cd747aec402f53fd5e27ddabe6c92f14af86ade5 Mon Sep 17 00:00:00 2001 From: Ben Nicholson Date: Fri, 25 Sep 2026 18:52:57 +1000 Subject: [PATCH] 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 --- docs/hardware-and-design-notes.md | 3 ++- main/player.c | 40 +++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/hardware-and-design-notes.md b/docs/hardware-and-design-notes.md index e5467fc..d3a57c4 100644 --- a/docs/hardware-and-design-notes.md +++ b/docs/hardware-and-design-notes.md @@ -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). ## 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. - 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. diff --git a/main/player.c b/main/player.c index a002559..3646529 100644 --- a/main/player.c +++ b/main/player.c @@ -1,5 +1,6 @@ #include "player.h" +#include #include #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_paused; // Spotify paused: silence, buffer kept, no underrun 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) { @@ -55,6 +85,7 @@ static size_t player_read(int32_t *buf, size_t frames) s_playing = true; } size_t got = audio_ring_read(buf, frames); + apply_gain(buf, got); if (got < frames) { 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 } break; - case SPOTIFY_EV_VOLUME: - ESP_LOGI(TAG, "Spotify volume %d%% (applied in step 7.5b4)", value * 100 / 65535); + case SPOTIFY_EV_VOLUME: { + // 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; } + } } static void player_status(cJSON *st)