Compare commits
2 Commits
4a38254660
...
8ba4de5eda
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ba4de5eda | |||
| cd747aec40 |
@@ -59,7 +59,12 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4
|
||||
- Download speed ~1.4 Mbit/s over TLS (fine for ~250 kbit/s; tune buffer sizes / per-chunk overhead).
|
||||
- Not yet tested: HE-AAC variant (140k), other stations, fMP4/ADTS-only playlists, discontinuities (#EXT-X-DISCONTINUITY), network loss and recovery, long runs.
|
||||
- Audio starts only after PTP lock (~20 s after boot): intended, TX needs PTP.
|
||||
- [ ] cspot (Spotify Connect)
|
||||
- [ ] cspot (Spotify Connect): login, audio, pause/skip/seek and app volume work (7.5b). Open:
|
||||
- OTA during an active Spotify session made the upload crawl (~10 kB/s) and ended in a reset; worked around by pausing Spotify / suspending HLS for the upload (aes67_ota_on_update). Root cause not found (suspect: flash writes stall PSRAM-heavy tasks). Watch for similar stalls.
|
||||
- First connects sometimes fail ("Can't connect to spotify servers"), a retry works.
|
||||
- Internal heap drops from ~408 KB to ~232 KB with a session; check what can move to PSRAM.
|
||||
- source.mode changes only start/stop Spotify at boot; in hls mode a running session's audio is dropped (it races through the queue).
|
||||
- Mute at 0 % volume not yet confirmed.
|
||||
- [ ] failover (auto mode) + /api/player
|
||||
- [ ] 8. Mono sum, gain, polish.
|
||||
|
||||
|
||||
@@ -23,6 +23,26 @@
|
||||
|
||||
static const char *TAG = "ota";
|
||||
|
||||
#define MAX_UPDATE_CBS 4
|
||||
static aes67_ota_update_cb_t s_update_cbs[MAX_UPDATE_CBS];
|
||||
static int s_update_n;
|
||||
|
||||
esp_err_t aes67_ota_on_update(aes67_ota_update_cb_t cb)
|
||||
{
|
||||
if (s_update_n >= MAX_UPDATE_CBS) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
s_update_cbs[s_update_n++] = cb;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void notify_update(bool starting)
|
||||
{
|
||||
for (int i = 0; i < s_update_n; i++) {
|
||||
s_update_cbs[i](starting);
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_pending(void)
|
||||
{
|
||||
esp_ota_img_states_t st;
|
||||
@@ -114,6 +134,7 @@ static esp_err_t ota_post(httpd_req_t *req)
|
||||
esp_ota_handle_t ota = 0;
|
||||
size_t total = 0, fill = 0;
|
||||
const char *err_msg = NULL;
|
||||
bool notified = false;
|
||||
int64_t t0 = esp_timer_get_time();
|
||||
|
||||
while (total < req->content_len) {
|
||||
@@ -137,6 +158,8 @@ static esp_err_t ota_post(httpd_req_t *req)
|
||||
}
|
||||
const esp_app_desc_t *d = (const esp_app_desc_t *)(buf + DESC_OFFSET);
|
||||
ESP_LOGI(TAG, "image %.32s %.32s", d->project_name, d->version);
|
||||
notify_update(true); // project quiets streaming/decoding before the flash writes
|
||||
notified = true;
|
||||
if (esp_ota_begin(dst, OTA_WITH_SEQUENTIAL_WRITES, &ota) != ESP_OK) {
|
||||
err_msg = "ota begin failed";
|
||||
break;
|
||||
@@ -154,15 +177,20 @@ static esp_err_t ota_post(httpd_req_t *req)
|
||||
if (ota) {
|
||||
esp_ota_abort(ota);
|
||||
}
|
||||
if (notified) {
|
||||
notify_update(false);
|
||||
}
|
||||
// Rejected before the whole body was read: close instead of draining it.
|
||||
httpd_resp_set_hdr(req, "Connection", "close");
|
||||
return reject(req, err_msg);
|
||||
}
|
||||
esp_err_t err = esp_ota_end(ota); // verifies the image (checksum/hash, chip)
|
||||
if (err != ESP_OK) {
|
||||
notify_update(false);
|
||||
return reject(req, err == ESP_ERR_OTA_VALIDATE_FAILED ? "image verification failed" : "ota end failed");
|
||||
}
|
||||
if (esp_ota_set_boot_partition(dst) != ESP_OK) {
|
||||
notify_update(false);
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "could not set boot partition");
|
||||
}
|
||||
ESP_LOGW(TAG, "installed to %s in %.1f s, rebooting", dst->label,
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
// Core component: must not depend on main/ (project code).
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
// Registers /api/ota routes. If the running image is pending verification, starts the
|
||||
// self-test: IP address + own web server answering within 60 s, else roll back.
|
||||
esp_err_t aes67_ota_init(void);
|
||||
|
||||
// Called with true right before an accepted upload starts writing flash, and with false if that
|
||||
// upload then fails (on success the device reboots). Lets the project quiet heavy work (streaming,
|
||||
// decoding) during the flash writes. Up to 4 callbacks.
|
||||
typedef void (*aes67_ota_update_cb_t)(bool starting);
|
||||
esp_err_t aes67_ota_on_update(aes67_ota_update_cb_t cb);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Spotify Connect (cspot). Project code; GPL-3.0 through cspot.
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -24,6 +25,8 @@ typedef void (*spotify_event_cb_t)(spotify_event_t ev, int value);
|
||||
// Registers the zeroconf endpoints (/spotify_info) and, when source.mode is spotify/auto and the
|
||||
// client credentials are set, advertises the device and starts the session task.
|
||||
esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event);
|
||||
// Pause/resume the running session (the Spotify app follows). No-op without a session.
|
||||
void spotify_pause(bool pause);
|
||||
// Current state for status.spotify_state.
|
||||
const char *spotify_state(void);
|
||||
|
||||
|
||||
@@ -30,9 +30,19 @@ static std::shared_ptr<cspot::LoginBlob> s_blob;
|
||||
static std::atomic<bool> s_got_blob{false};
|
||||
static std::atomic<const char *> s_state{"disabled"};
|
||||
static std::atomic<uint64_t> s_pcm_bytes{0};
|
||||
static std::shared_ptr<cspot::SpircHandler> s_handler; // running session, else null
|
||||
static spotify_pcm_cb_t s_pcm_cb;
|
||||
static spotify_event_cb_t s_event_cb;
|
||||
|
||||
extern "C" void spotify_pause(bool pause)
|
||||
{
|
||||
auto h = s_handler;
|
||||
if (h) {
|
||||
h->setPause(pause);
|
||||
ESP_LOGI(TAG, "%s", pause ? "paused" : "resumed");
|
||||
}
|
||||
}
|
||||
|
||||
const char *spotify_state(void)
|
||||
{
|
||||
return s_state.load();
|
||||
@@ -225,6 +235,7 @@ static void session(void)
|
||||
}
|
||||
ESP_LOGI(TAG, "event %s", event_name(ev->eventType));
|
||||
});
|
||||
s_handler = handler;
|
||||
s_state = "connected";
|
||||
ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str());
|
||||
while (true) {
|
||||
@@ -246,6 +257,7 @@ static void spotify_task(void *arg)
|
||||
s_state = "error";
|
||||
ESP_LOGE(TAG, "session ended: %s", e.what());
|
||||
}
|
||||
s_handler.reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+10
-1
@@ -36,6 +36,12 @@ static media_pl_t *s_pl; // parsed media playlist (PSRAM)
|
||||
static char s_url[URL_MAX]; // configured URL
|
||||
static char s_media_url[URL_MAX];
|
||||
static uint8_t *s_seg; // current segment (PSRAM)
|
||||
static volatile bool s_suspended;
|
||||
|
||||
void hls_suspend(bool suspend)
|
||||
{
|
||||
s_suspended = suspend;
|
||||
}
|
||||
|
||||
// hls_url when source.mode needs HLS, else "".
|
||||
static void wanted_url(char *out, size_t n)
|
||||
@@ -50,6 +56,9 @@ static void wanted_url(char *out, size_t n)
|
||||
|
||||
static bool still_wanted(void)
|
||||
{
|
||||
if (s_suspended) {
|
||||
return false;
|
||||
}
|
||||
char u[URL_MAX];
|
||||
wanted_url(u, sizeof(u));
|
||||
return strcmp(u, s_url) == 0;
|
||||
@@ -237,7 +246,7 @@ static void hls_task(void *arg)
|
||||
{
|
||||
while (1) {
|
||||
wanted_url(s_url, sizeof(s_url));
|
||||
if (!s_url[0]) {
|
||||
if (!s_url[0] || s_suspended) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -11,3 +11,5 @@
|
||||
typedef bool (*hls_sink_t)(const uint8_t *data, size_t len, bool segment_start);
|
||||
|
||||
esp_err_t hls_start(hls_sink_t sink);
|
||||
// Stop fetching (current download is abandoned) until resumed.
|
||||
void hls_suspend(bool suspend);
|
||||
|
||||
+59
-2
@@ -1,8 +1,10 @@
|
||||
#include "player.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "aes67_cfg.h"
|
||||
#include "aes67_ota.h"
|
||||
#include "aes67_tx.h"
|
||||
#include "aes67_web.h"
|
||||
#include "audio_out.h"
|
||||
@@ -28,6 +30,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 +86,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,11 +146,35 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Firmware upload: flash writes stall everything running from PSRAM; a Spotify session
|
||||
// streaming at the same time made uploads crawl and end in a reset. Quiet the sources meanwhile.
|
||||
static void on_ota(bool starting)
|
||||
{
|
||||
static bool paused_spotify;
|
||||
if (starting) {
|
||||
paused_spotify = s_src == PLAYER_SRC_SPOTIFY && !s_paused;
|
||||
spotify_pause(true);
|
||||
hls_suspend(true);
|
||||
ESP_LOGI(TAG, "firmware update: sources paused");
|
||||
} else {
|
||||
hls_suspend(false);
|
||||
if (paused_spotify) {
|
||||
spotify_pause(false);
|
||||
}
|
||||
ESP_LOGI(TAG, "firmware update failed: sources resumed");
|
||||
}
|
||||
}
|
||||
|
||||
static void player_status(cJSON *st)
|
||||
{
|
||||
@@ -150,6 +206,7 @@ esp_err_t player_init(void)
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
spotify_init(spotify_pcm, spotify_event);
|
||||
aes67_ota_on_update(on_ota);
|
||||
err = hls_start(decoder_feed);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
|
||||
Reference in New Issue
Block a user