diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..73de24e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/cspot"] + path = external/cspot + url = https://github.com/philippe44/cspot.git diff --git a/CLAUDE.md b/CLAUDE.md index dd78d95..c1c6102 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,7 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4 - Our board: **32 MB** flash (GigaDevice c8/4019). App slots must stay below 16 MB (cache mapping above 16 MB is experimental in IDF). - Rev < 3 also limits Espressif's prebuilt audio libraries: `esp_audio_codec` must stay < 2.6 and `esp_audio_effects` < 1.4 (newer versions use P4 assembly that needs rev >= 3; the build fails with a message saying so). Check this for any new Espressif binary component. - Embed `web/index.html` via `EMBED_TXTFILES` in `aes67_web`. +- cspot (Spotify, `external/cspot` git submodule, philippe44 fork, pinned): after cloning run `git submodule update --init external/cspot && git -C external/cspot submodule update --init cspot/bell`. Its nanopb code generator needs, in the IDF Python env: `python -m pip install protobuf grpcio-tools 'setuptools<81'` (after `. export.sh`). Our fixes to cspot/bell live in `components/spotify/patches/{cspot,bell}/*.patch` and are applied automatically at configure time; don't edit the submodule directly, add a patch. - Flash over the network (normal way since step 2b; keep USB for recovery): `curl -f --data-binary @build/aes67_p4.bin -H 'Content-Type: application/octet-stream' http://p4-aes67/api/ota` The board reboots into the new image on trial; check `GET /api/ota` shows the new version with `pending_verify: false`. diff --git a/components/spotify/CMakeLists.txt b/components/spotify/CMakeLists.txt new file mode 100644 index 0000000..f804fb2 --- /dev/null +++ b/components/spotify/CMakeLists.txt @@ -0,0 +1,49 @@ +# Spotify Connect via cspot (philippe44 fork, git submodule external/cspot). GPL-3.0. +# Project component (not part of the reusable aes67_* core). +idf_component_register(SRCS "spotify.cpp" + INCLUDE_DIRS "include" + PRIV_REQUIRES espressif__mdns mbedtls json pthread lwip esp_timer) + +# bell: only what cspot needs (Tremor Vorbis inside; no codec wrapper, sinks, MQTT, web server). +set(BELL_DISABLE_CODECS ON CACHE BOOL "" FORCE) +set(BELL_DISABLE_SINKS ON CACHE BOOL "" FORCE) +set(BELL_DISABLE_MQTT ON CACHE BOOL "" FORCE) +set(BELL_DISABLE_WEBSERVER ON CACHE BOOL "" FORCE) +set(BELL_DISABLE_FMT ON CACHE BOOL "" FORCE) +set(BELL_DISABLE_REGEX ON CACHE BOOL "" FORCE) +set(BELL_ONLY_CJSON ON CACHE BOOL "" FORCE) +set(BELL_EXTERNAL_CJSON idf::json CACHE STRING "" FORCE) +option(BUILD_TESTING OFF) + +# Our fixes to the pinned cspot / bell sources (newer GCC, protobuf, CMake), kept as patch files: +# patches/cspot/*.patch apply in external/cspot, patches/bell/*.patch in external/cspot/cspot/bell. +# Each is applied once (skipped when it already reverse-applies). +function(spotify_apply_patches dir repo) + file(GLOB patches "${CMAKE_CURRENT_LIST_DIR}/patches/${dir}/*.patch") + list(SORT patches) + foreach(p ${patches}) + execute_process(COMMAND git apply --reverse --check "${p}" WORKING_DIRECTORY "${repo}" + RESULT_VARIABLE applied OUTPUT_QUIET ERROR_QUIET) + if(NOT applied EQUAL 0) + execute_process(COMMAND git apply "${p}" WORKING_DIRECTORY "${repo}" RESULT_VARIABLE rc) + if(NOT rc EQUAL 0) + message(FATAL_ERROR "spotify: patch does not apply: ${p}") + endif() + message(STATUS "spotify: applied ${p}") + endif() + endforeach() +endfunction() +spotify_apply_patches(cspot "${PROJECT_DIR}/external/cspot") +spotify_apply_patches(bell "${PROJECT_DIR}/external/cspot/cspot/bell") + +# cspot/bell declare cmake_minimum_required(2.8.12); CMake 4 rejects < 3.5 unless told otherwise. +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) +add_subdirectory(${PROJECT_DIR}/external/cspot/cspot ${CMAKE_CURRENT_BINARY_DIR}/cspot) + +# bell adds Xtensa (ESP32) assembly for a DSP biquad; the P4 is RISC-V and cspot does not use it. +get_target_property(BELL_SRCS bell SOURCES) +list(FILTER BELL_SRCS EXCLUDE REGEX "\\.S$") +set_target_properties(bell PROPERTIES SOURCES "${BELL_SRCS}") + +target_link_libraries(${COMPONENT_LIB} PRIVATE cspot) +target_compile_options(${COMPONENT_LIB} PRIVATE -std=gnu++20) diff --git a/components/spotify/include/spotify.h b/components/spotify/include/spotify.h new file mode 100644 index 0000000..38be433 --- /dev/null +++ b/components/spotify/include/spotify.h @@ -0,0 +1,14 @@ +// Spotify Connect (cspot). Project code; GPL-3.0 through cspot. +#pragma once + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t spotify_init(const char *device_name); + +#ifdef __cplusplus +} +#endif diff --git a/components/spotify/patches/bell/0001-nanopb-generator-protobuf5.patch b/components/spotify/patches/bell/0001-nanopb-generator-protobuf5.patch new file mode 100644 index 0000000..d6935e2 --- /dev/null +++ b/components/spotify/patches/bell/0001-nanopb-generator-protobuf5.patch @@ -0,0 +1,18 @@ +diff --git a/external/nanopb/generator/nanopb_generator.py b/external/nanopb/generator/nanopb_generator.py +index e40f18b..64dad5e 100755 +--- a/external/nanopb/generator/nanopb_generator.py ++++ b/external/nanopb/generator/nanopb_generator.py +@@ -1463,7 +1463,12 @@ class Message(ProtoElement): + optional_only.ClearField(str('extension')) + optional_only.ClearField(str('enum_type')) + desc = google.protobuf.descriptor.MakeDescriptor(optional_only) +- msg = reflection.MakeClass(desc)() ++ # protobuf >= 5 removed reflection.MakeClass (patch for this project) ++ try: ++ msg = reflection.MakeClass(desc)() ++ except AttributeError: ++ from google.protobuf import message_factory ++ msg = message_factory.GetMessageClass(desc)() + + for field in optional_only.field: + if field.type == FieldD.TYPE_STRING: diff --git a/components/spotify/patches/bell/0002-urlparser-includes.patch b/components/spotify/patches/bell/0002-urlparser-includes.patch new file mode 100644 index 0000000..8c5ebe3 --- /dev/null +++ b/components/spotify/patches/bell/0002-urlparser-includes.patch @@ -0,0 +1,13 @@ +diff --git a/main/io/URLParser.cpp b/main/io/URLParser.cpp +index 4a50ab1..af679ec 100644 +--- a/main/io/URLParser.cpp ++++ b/main/io/URLParser.cpp +@@ -1,5 +1,8 @@ + #include "URLParser.h" + ++#include // sscanf (GCC 14 / ESP-IDF 5.5: not included transitively) ++#include // strstr ++ + namespace bell { + + #ifdef BELL_DISABLE_REGEX diff --git a/components/spotify/spotify.cpp b/components/spotify/spotify.cpp new file mode 100644 index 0000000..fd2dae0 --- /dev/null +++ b/components/spotify/spotify.cpp @@ -0,0 +1,17 @@ +#include "spotify.h" + +#include + +#include "LoginBlob.h" +#include "esp_log.h" + +static const char *TAG = "spotify"; + +// Step 7.5b1: link check only. Zeroconf, login and audio follow in b2/b3. +extern "C" esp_err_t spotify_init(const char *device_name) +{ + auto blob = std::make_shared(device_name); + std::string info = blob->buildZeroconfInfo(); + ESP_LOGI(TAG, "cspot linked: device \"%s\", zeroconf info %u bytes", device_name, (unsigned)info.size()); + return ESP_OK; +} diff --git a/external/cspot b/external/cspot new file mode 160000 index 0000000..3010349 --- /dev/null +++ b/external/cspot @@ -0,0 +1 @@ +Subproject commit 3010349b465560fdc00e8b07f9a921eb060e3ea8 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 1f2cce8..e609688 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -2,4 +2,4 @@ idf_component_register(SRCS "main.c" "project_cfg.c" "player.c" "audio_ring.c" " INCLUDE_DIRS "." REQUIRES esp_app_format esp_hw_support heap esp_http_client mbedtls esp_timer aes67_board aes67_health aes67_net aes67_ota aes67_ptp aes67_sdp_sap - aes67_syslog aes67_tx aes67_web) + aes67_syslog aes67_tx aes67_web spotify) diff --git a/main/player.c b/main/player.c index a3e3f83..9238a31 100644 --- a/main/player.c +++ b/main/player.c @@ -8,6 +8,7 @@ #include "audio_ring.h" #include "decoder.h" #include "hls.h" +#include "spotify.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -118,7 +119,10 @@ esp_err_t player_init(void) if (err != ESP_OK) { return err; } - err = hls_start(decoder_feed); + cJSON *sc = cfg_get("source"); + spotify_init(cJSON_GetObjectItemCaseSensitive(sc, "spotify_name")->valuestring); + cJSON_Delete(sc); + err = hls_start(decoder_feed); if (err != ESP_OK) { return err; } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 1a36545..9694236 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -28,3 +28,6 @@ CONFIG_LWIP_MAX_SOCKETS=16 # 32 MB PSRAM in the P4 package (hex mode, 200 MHz; 250 MHz needs rev >= 3). Needed for step 7: # HLS segment buffers, cspot, decoders. malloc() puts blocks >16 KB in PSRAM; DMA buffers stay internal. CONFIG_SPIRAM=y + +# cspot (Spotify Connect, step 7.5) needs C++ exceptions. +CONFIG_COMPILER_CXX_EXCEPTIONS=y