- OTA: the player now ends the Spotify session (spotify_suspend: stop
flag, waits until the session's tasks are gone, refuses new ones)
instead of pausing it, and suspends HLS. Pausing was not enough (an
upload still failed once with a paused session). Verified twice with a
session running for minutes: "Disconnecting mercury session / session
ended / suspended", installed in ~26 s, clean reboot.
- Volume: createFromBlob() starts cspot at volume 0, which the app
showed while we played at 100 %. The session now starts with the
player's volume (spotify_set_volume); volumes coming from the app are
stored exactly, without echo. Verified: slider starts at 100 %.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- spotify_init() sets up the logger, zeroconf routes and session task
once; spotify_apply() (at boot and on every source save) enables or
disables: enabled = mode spotify/auto + credentials. Disable removes
the mDNS entry, zeroconf answers 404 and a running session ends
(loop exits within 200 ms, SpircHandler::disconnect stops the queue
and player tasks). A new device name ends the session and
re-advertises. The login blob is swapped under a mutex.
- Verified: hls/spotify/rename switch the mDNS entry and /spotify_info
(404/200) live; switching to hls during Spotify playback ended the
session in < 2 s, HLS played after ~5 s, internal heap 221 -> 365 KB
(cspot frees everything).
- CLAUDE.md: OTA-with-session findings (intermittent, flash-write stalls
seen as TX resyncs during uploads), serial-port reset caveat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An upload during an active Spotify session crawled (~10 kB/s, 197 s)
and ended in a reset (seen twice). Workaround:
- aes67_ota_on_update(cb): cb(true) right before an accepted upload
writes flash, cb(false) if it then fails.
- The player pauses Spotify (spotify_pause -> SpircHandler::setPause, the
app follows) and suspends HLS fetching (hls_suspend); resumed if the
upload fails. AES67 TX keeps running (silence).
- Verified: upload during Spotify playback paused the session and
installed in 25.8 s (normal for 2 MB), clean reboot and self-test.
Root cause still open (noted in CLAUDE.md with the other cspot items).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- spotify_init(pcm_cb, event_cb): cspot's data callback hands 44.1 kHz
stereo s16 PCM to the player and returns what was taken; the player's
blocking ring write paces decoding to playback speed.
- Events: FLUSH/SEEK/PLAYBACK_START empty the ring (skip/seek sound at
once); PLAY_PAUSE pauses output (silence, buffer kept, no underrun);
VOLUME logged (applied in b4).
- player: Spotify via its own audio_out converter (44.1 -> 48 kHz);
source_state playing / paused / buffering.
- Verified from a Mac: music on the AES67 stream (10 s: 0 gaps, no
silent packets, RMS -9.6 dBFS, peak 0.0 dBFS), buffer 4.0 s full,
0 underruns, pause/play/skip/seek events.
Seen: first 3 connects "Can't connect to spotify servers", 4th worked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/audio_out: one converter instance per source (own rate converter
state): s16 any rate/channels -> 48 kHz stereo int32 -> ring.
- player_write(src, ...) only writes for the active source, drops the
rest; player_src_t is public in player.h.
- HLS decoder uses audio_out (no behaviour change).
- Verified: HLS still sample exact (441344 -> 480375 frames per
10.008 s segment), buffer ~4 s, 0 underruns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Zeroconf on our httpd (GET/POST /spotify_info, form body URL-decoded)
and _spotify-connect._tcp via our mDNS (VERSION, CPath, Stack TXT).
- Session task: waits for the app's login blob, connects with the
user's client ID/secret and the configured bitrate (96/160/320 ->
OGG_VORBIS_*), SpircHandler, events logged; cspot exceptions caught.
Starts when source.mode is spotify/auto and credentials are set.
- bell::bellGlobalLogger was NULL: every CSPOT_LOG crashed the board
right after addUser (Load access fault in Session.cpp:67 /
LoginBlob.cpp:58). cspot/bell logs now go to esp_log (UART + syslog);
signed CDN URL tokens are cut from the log.
- esp_audio_codec's own Vorbis decoder clashed with bell's Tremor
symbols: CONFIG_AUDIO_DECODER_VORBIS_SUPPORT / SIMPLE_DEC_OGG off.
- status.spotify_state from the session (waiting / connecting /
connected / login failed / error / disabled / no client credentials).
- Verified from a Mac: device appears, "connected as <user>", access
token fetched with the user's client credentials, track info, audio
key, CDN URL (the step broken upstream), PCM decoded. Without
backpressure a track decodes in ~26 s (b3 feeds the ring).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Core config: cfg_mark_secret(group, key). GET /api/config returns ""
for secret keys; a POST with "" keeps the stored value, null clears it;
cfg_get() returns the real value. Documented in aes67-core-base.md.
- source.spotify_client_id / spotify_client_secret (secret write-only):
each user's own Spotify developer app, needed by the maintained cspot
fork (philippe44/cspot) since Spotify's 2025 API restrictions.
- UI: client ID field, password field for the secret ("leave empty to
keep"), link to developer.spotify.com; enabled for spotify/auto modes.
- status.spotify_state: "no client credentials" while either is missing.
- Verified: secret never appears in GET; UI-style re-save keeps it;
survives reboot; null clears it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decoder: s16 PCM -> stereo int32 -> esp_ae_rate_cvt 44.1 -> 48 kHz
(32-bit, complexity 3; bypassed at 48 kHz; mono duplicated) -> ring.
esp_audio_effects pinned to ~1.3.0 (1.4+ needs P4 rev >= 3).
- hls: each segment is downloaded completely into PSRAM (max 4 MB), then
decoded; the connection is not held open while the decoder waits for
ring space at playback speed.
- player: player_write() blocks while the ring is full and gives up when
the source changes; the temporary 440 Hz producer is removed.
- Verified with Triple J Hottest: 441344 -> 480375 frames (10.008 s) and
440320 -> 479260 (9.985 s) per segment; RTP 15000 packets, 0 gaps,
peak -10 dBFS / RMS -22 dBFS; ring ~4.0 s, 0 underruns over ~50 s;
heap 422 KB, PSRAM 27.5 MB free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/decoder: MPEG-TS demux (packets reassembled across HTTP chunks,
PAT -> PMT -> first ADTS-AAC PID, PES headers stripped) feeding the
esp_audio_codec simple AAC decoder (ADTS, AAC-Plus enabled for HE-AAC
v1/v2 variants). 7.3 only counts and logs PCM per segment.
- esp_audio_codec pinned to ~2.5.0: 2.6+ needs P4 rev >= 3 (this board
is rev 1.3). Noted in CLAUDE.md, also for esp_audio_effects < 1.4.
- The library's combined TS decoder lost ~8% of the frames (segments
decoded to 7.9-9.6 s, "decode error -1"); with the own demux every
segment is sample exact: 441344 / 440320 frames = 10.008 / 9.985 s,
matching EXTINF 10.0078 / 9.9846 (431 / 430 AAC frames), no errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/hls: task active while source.mode is hls/auto and hls_url is
set. esp_http_client over HTTPS (IDF certificate bundle), redirects
followed (max 5), streamed reads in 4 KB chunks to a sink callback.
- Master playlist: highest BANDWIDTH variant; URL resolution for
absolute, host-relative and path-relative references; CRLF tolerant.
- Media playlist: TARGETDURATION, MEDIA-SEQUENCE, segments; start 3
segments behind the live edge, fetch each new one in order, skip ahead
if the window moved past us, reload after target/2 when nothing is new.
- Verified with Triple J Hottest (ABC, Akamai): 252 kbit/s AAC-LC
variant chosen, 3 back-fill segments then one new ~10 s segment at a
time, ~303 KB in ~1.25 s each; heap 439 KB, PSRAM 31.9 MB free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/audio_ring: SPSC ring of interleaved int32 frames in PSRAM
(4 s at 48 kHz stereo), lock-free with acquire/release counters; the
TX pull callback never blocks.
- main/player: source selection from source.mode and the pull callback
for aes67_tx. tone -> the core's PTP-phased 1 kHz tone; off -> silence;
hls/spotify -> ring with 1 s prefill (silence while buffering is not an
underrun; running dry is, and prefills again). Status fields
active_source, source_state, spotify_state, buffer_ms. source config
applies live.
- New source.mode "tone" (validation, UI dropdown, doc) for commissioning.
- Temporary 440 Hz producer in hls mode (until the HLS player exists).
- Verified: tone 999.7 Hz -18 dBFS; off silent; hls 440.0 Hz with max
sample step 60816 (ideal sine 60825, i.e. no discontinuities), buffer
3983 ms, 0 underruns; spotify silent/not implemented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>