Skip to content

Commit

Permalink
Merge branch 'master-v4.3' of https://github.com/sle118/squeezelite-e…
Browse files Browse the repository at this point in the history
…sp32 into dev-tembed-s3
  • Loading branch information
wizmo2 committed Sep 28, 2023
2 parents 6d27876 + f6fd117 commit 580a9c6
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 50 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Squeezelite-esp32 is an audio software suite made to run on espressif's esp32 an
- Stream your local music and connect to all major on-line music providers (Spotify, Deezer, Tidal, Qobuz) using [Logitech Media Server - a.k.a LMS](https://forums.slimdevices.com/) and enjoy multi-room audio synchronization. LMS can be extended by numerous plugins and can be controlled using a Web browser or dedicated applications (iPhone, Android). It can also send audio to UPnP, Sonos, ChromeCast and AirPlay speakers/devices.
- Stream from a **Bluetooth** device (iPhone, Android)
- Stream from an **AirPlay** controller (iPhone, iTunes ...) and enjoy synchronization multiroom as well (although it's AirPlay 1 only)
- Stream directly from **Spotify** using SpotifyConnect (thanks to [cspot](https://github.com/feelfreelinux/cspot))
- Stream directly from **Spotify** using SpotifyConnect (thanks to [cspot](https://github.com/feelfreelinux/cspot)) - please read carefully [this](#spotify)

Depending on the hardware connected to the esp32, you can send audio to a local DAC, to SPDIF or to a Bluetooth speaker. The bare minimum required hardware is a WROVER module with 4MB of Flash and 4MB of PSRAM (https://www.espressif.com/en/products/modules/esp32). With that module standalone, just apply power and you can stream to a Bluetooth speaker. You can also send audio to most I2S DAC as well as to SPDIF receivers using just a cable or an optical transducer.

Expand Down Expand Up @@ -229,7 +229,7 @@ Ground -------------------------- coax signal ground
The NVS parameter "display_config" sets the parameters for an optional display. It can be I2C (see [here](#i2c) for shared bus) or SPI (see [here](#spi) for shared bus) Syntax is
```
I2C,width=<pixels>,height=<pixels>[address=<i2c_address>][,reset=<gpio>][,HFlip][,VFlip][driver=SSD1306|SSD1326[:1|4]|SSD1327|SH1106]
SPI,width=<pixels>,height=<pixels>,cs=<gpio>[,back=<gpio>][,reset=<gpio>][,speed=<speed>][,HFlip][,VFlip][driver=SSD1306|SSD1322|SSD1326[:1|4]|SSD1327|SH1106|SSD1675|ST7735[:x=<offset>][:y=<offset>]|ST7789|ILI9341[:16|18][,rotate]]
SPI,width=<pixels>,height=<pixels>,cs=<gpio>[,back=<gpio>][,reset=<gpio>][,speed=<speed>][,HFlip][,VFlip][driver=SSD1306|SSD1322|SSD1326[:1|4]|SSD1327|SH1106|SSD1675|ST7735|ST7789[:x=<offset>][:y=<offset>]|ILI9341[:16|18][,rotate]]
```
- back: a LED backlight used by some older devices (ST7735). It is PWM controlled for brightness
- reset: some display have a reset pin that is should normally be pulled up if unused. Most displays require reset and will not initialize well otherwise.
Expand Down Expand Up @@ -537,7 +537,7 @@ The option to use multiple GPIOs is very limited on esp32 and the esp-idf 4.3.x

Some have asked for a soft power on/off option. Although this is not built-in, it's easy to create yours as long as the regulator/power supply of the board can be controlled by Vcc or GND. Depending on how it is active, add a pull-up/down resistor to the regulator's control and connect it also to one GPIO of the esp32. Then using set_GPIO, set that GPIO to Vcc or GND. Use a hardware button that forces the regulator on with a pull- up/down and once the esp32 has booted, it will force the GPIO to the desired value maintaining the board on by software. To power it off by software, just use the deep sleep option which will suspend all GPIO hence switching off the regulator.

# Configuration
# Software configuration

## Setup WiFi
- Boot the esp, look for a new wifi access point showing up and connect to it. Default build ssid and passwords are "squeezelite"/"squeezelite".
Expand All @@ -558,6 +558,15 @@ At this point, the device should have disabled its built-in access point and sho
- The toggle switch should be set to 'ON' to ensure that squeezelite is active after booting (you might have to fiddle with it a few times)
- You can enable access to NVS parameters under 'credits'

## Spotify
By default, SqueezeESP32 will use ZeroConf to advertise its Spotify capabilties. This means that until at least one local Spotify Connect application controllers discovers and connects to it, SqueezeESP32 will not be registered to Spotify servers. As a consequence, Spotify's WebAPI will not be able to see it (for example, Home Assistant services will miss it). Once you are connected to it using for example Spotify Desktop app, it will be registered and displayed everywhere.

If you want the player to be registered at start-up, you need to disable the ZeroConf option using the WebUI or `cspot_config::ZeroConf`. In that mode, the first time you run SqueezeESP32, it will be in ZeroConf mode and when you connect to it using a controller for the firt time, it receives and store credentials that will be used next time (after reboot).

Set ZeroConf to 1 will always force ZeroConf mode to be used.

The ZeroConf mode consumes less memory as it uses the built-in HTTP and mDNS servers to broadcast its capabilities. A Spotify controller will then discover these and trigger the SqueezeESP32 Spotify stack (cspot) to start. When the controller disconnects, the stack is shut down. In non-ZeroConf mode, the stack starts immediately (providing stored credentials are valid) and always run - a disconnect will not shut it down.

## Monitor
In addition of the esp-idf serial link monitor option, you can also enable a telnet server (see NVS parameters) where you'll have access to a ton of logs of what's happening inside the WROVER.

Expand Down Expand Up @@ -652,4 +661,3 @@ If you have already cloned the repository and you are getting compile errors on
- libmad has been patched to avoid using a lot of stack and is not provided here. There is an issue with sync detection in 1.15.1b from where the original stack patch was done but since a few fixes have been made wrt sync detection. This 1.15.1b-10 found on debian fixes the issue where mad thinks it has reached sync but has not and so returns a wrong sample rate. It comes at the expense of 8KB (!) of code where a simple check in squeezelite/mad.c that next_frame[0] is 0xff and next_frame[1] & 0xf0 is 0xf0 does the trick ...

# Footnotes
(1) SPDIF is made by tricking the I2S bus but this consumes a fair bit of CPU as it multiplies by four the throughput on the i2s bus. To optimize some computation, the parity of the spdif frames must always be 0, so at least one bit has to be available to force it. As SPDIF samples are 20+4 bits length maximum, the LSB is used for that purpose, so the bit 24 is randomly toggling. It does not matter for 16 bits samples but it has been chosen to truncate the last 4 bits for 24 bits samples. I'm sure that some smart dude can further optimize spdif_convert() and use the user bit instead. You're welcome to do a PR but, as said above, I (philippe44) am not interested by 24 bits mental illness :-) and I've already made an effort to provide 20 bits which already way more what's needed :-)
10 changes: 5 additions & 5 deletions components/led_strip/led_vu.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ static int led_addr(int pos ) {
}

static void battery_svc(float value, int cells) {
battery_status = battery_level_svc();
battery_status = battery_level_svc();
ESP_LOGI(TAG, "Called for battery service with volt:%f cells:%d status:%d", value, cells, battery_status);

if (battery_handler_chain) battery_handler_chain(value, cells);
Expand All @@ -87,10 +87,10 @@ void led_vu_init()
goto done;
}

battery_handler_chain = battery_handler_svc;
battery_handler_svc = battery_svc;
battery_handler_chain = battery_handler_svc;
battery_handler_svc = battery_svc;
battery_status = battery_level_svc();
if (strip.length > LED_VU_MAX_LENGTH) strip.length = LED_VU_MAX_LENGTH;
// initialize vu meter settings
if (strip.length < 10) {
Expand All @@ -105,7 +105,7 @@ void led_vu_init()
strip.vu_start_r = strip.vu_length + 1;
strip.vu_status = strip.vu_length;
}
ESP_LOGD(TAG, "vu meter using length:%d left:%d right:%d status:%d", strip.vu_length, strip.vu_start_l, strip.vu_start_r, strip.vu_status);
ESP_LOGI(TAG, "vu meter using length:%d left:%d right:%d status:%d", strip.vu_length, strip.vu_start_l, strip.vu_start_r, strip.vu_status);

// create driver configuration
if (strcasestr(config, "APA102")) { // TODO: Need to add options to web ui
Expand Down
8 changes: 4 additions & 4 deletions components/platform_config/nvs_utilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ esp_err_t store_nvs_value_len(nvs_type_t type, const char *key, void * data, siz
esp_err_t store_nvs_value(nvs_type_t type, const char *key, void * data);
esp_err_t get_nvs_value(nvs_type_t type, const char *key, void*value, const uint8_t buf_size);
void * get_nvs_value_alloc(nvs_type_t type, const char *key);
void * get_nvs_value_alloc_for_partition(const char * partition,const char * namespace,nvs_type_t type, const char *key, size_t * size);
esp_err_t erase_nvs_for_partition(const char * partition, const char * namespace,const char *key);
esp_err_t store_nvs_value_len_for_partition(const char * partition,const char * namespace,nvs_type_t type, const char *key, const void * data,size_t data_len);
void * get_nvs_value_alloc_for_partition(const char * partition,const char * ns,nvs_type_t type, const char *key, size_t * size);
esp_err_t erase_nvs_for_partition(const char * partition, const char * ns,const char *key);
esp_err_t store_nvs_value_len_for_partition(const char * partition,const char * ns,nvs_type_t type, const char *key, const void * data,size_t data_len);
esp_err_t erase_nvs(const char *key);
void print_blob(const char *blob, size_t len);
const char *type_to_str(nvs_type_t type);
nvs_type_t str_to_type(const char *type);
esp_err_t erase_nvs_partition(const char * partition, const char * namespace);
esp_err_t erase_nvs_partition(const char * partition, const char * ns);
void erase_settings_partition();
#ifdef __cplusplus
}
Expand Down
12 changes: 12 additions & 0 deletions components/platform_console/cmd_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ static struct {
struct arg_str *deviceName;
// struct arg_int *volume;
struct arg_int *bitrate;
struct arg_int *zeroConf;
struct arg_end *end;
} cspot_args;
static struct {
Expand Down Expand Up @@ -657,6 +658,9 @@ static int do_cspot_config(int argc, char **argv){
if(cspot_args.bitrate->count>0){
cjson_update_number(&cspot_config,cspot_args.bitrate->hdr.longopts,cspot_args.bitrate->ival[0]);
}
if(cspot_args.zeroConf->count>0){
cjson_update_number(&cspot_config,cspot_args.zeroConf->hdr.longopts,cspot_args.zeroConf->ival[0]);
}

if(!nerrors ){
fprintf(f,"Storing cspot parameters.\n");
Expand All @@ -669,6 +673,9 @@ static int do_cspot_config(int argc, char **argv){
if(cspot_args.bitrate->count>0){
fprintf(f,"Bitrate changed to %u\n",cspot_args.bitrate->ival[0]);
}
if(cspot_args.zeroConf->count>0){
fprintf(f,"ZeroConf changed to %u\n",cspot_args.zeroConf->ival[0]);
}
}
if(!nerrors ){
fprintf(f,"Done.\n");
Expand Down Expand Up @@ -865,6 +872,10 @@ cJSON * cspot_cb(){
if(cspot_values){
cJSON_AddNumberToObject(values,cspot_args.bitrate->hdr.longopts,cJSON_GetNumberValue(cspot_values));
}
cspot_values = cJSON_GetObjectItem(cspot_config,cspot_args.zeroConf->hdr.longopts);
if(cspot_values){
cJSON_AddNumberToObject(values,cspot_args.zeroConf->hdr.longopts,cJSON_GetNumberValue(cspot_values));
}

cJSON_Delete(cspot_config);
return values;
Expand Down Expand Up @@ -1301,6 +1312,7 @@ static void register_known_templates_config(){
static void register_cspot_config(){
cspot_args.deviceName = arg_str1(NULL,"deviceName","","Device Name");
cspot_args.bitrate = arg_int1(NULL,"bitrate","96|160|320","Streaming Bitrate (kbps)");
cspot_args.zeroConf = arg_int1(NULL,"zeroConf","0|1","Force use of ZeroConf");
// cspot_args.volume = arg_int1(NULL,"volume","","Spotify Volume");
cspot_args.end = arg_end(1);
const esp_console_cmd_t cmd = {
Expand Down
123 changes: 94 additions & 29 deletions components/spotify/Shim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@
#include "cspot_private.h"
#include "cspot_sink.h"
#include "platform_config.h"
#include "nvs_utilities.h"
#include "tools.h"

static class cspotPlayer *player;

static const struct {
const char *ns;
const char *credentials;
} spotify_ns = { .ns = "spotify", .credentials = "credentials" };

/****************************************************************************************
* Player's main class & task
*/
Expand All @@ -42,7 +48,11 @@ class cspotPlayer : public bell::Task {
private:
std::string name;
bell::WrappedSemaphore clientConnected;
std::atomic<bool> isPaused, isConnected;
std::atomic<bool> isPaused;
enum states { ABORT, LINKED, DISCO };
std::atomic<states> state;
std::string credentials;
bool zeroConf;

int startOffset, volume = 0, bitrate = 160;
httpd_handle_t serverHandle;
Expand All @@ -57,6 +67,7 @@ class cspotPlayer : public bell::Task {
void eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event);
void trackHandler(void);
size_t pcmWrite(uint8_t *pcm, size_t bytes, std::string_view trackId);
void enableZeroConf(void);

void runTask();

Expand All @@ -79,8 +90,25 @@ cspotPlayer::cspotPlayer(const char* name, httpd_handle_t server, int port, cspo
if ((item = cJSON_GetObjectItem(config, "volume")) != NULL) volume = item->valueint;
if ((item = cJSON_GetObjectItem(config, "bitrate")) != NULL) bitrate = item->valueint;
if ((item = cJSON_GetObjectItem(config, "deviceName") ) != NULL) this->name = item->valuestring;
else this->name = name;
cJSON_Delete(config);
else this->name = name;

if ((item = cJSON_GetObjectItem(config, "zeroConf")) != NULL) {
zeroConf = item->valueint;
cJSON_Delete(config);
} else {
zeroConf = true;
cJSON_AddNumberToObject(config, "zeroConf", 1);
config_set_cjson_str_and_free("cspot_config", config);
}

// get optional credentials from own NVS
if (!zeroConf) {
char *credentials = (char*) get_nvs_value_alloc_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, NULL);
if (credentials) {
this->credentials = credentials;
free(credentials);
}
}

if (bitrate != 96 && bitrate != 160 && bitrate != 320) bitrate = 160;
}
Expand Down Expand Up @@ -207,7 +235,7 @@ void cspotPlayer::eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event
}
case cspot::SpircHandler::EventType::DISC:
cmdHandler(CSPOT_DISC);
isConnected = false;
state = DISCO;
break;
case cspot::SpircHandler::EventType::SEEK: {
cmdHandler(CSPOT_SEEK, std::get<int>(event->data));
Expand Down Expand Up @@ -265,7 +293,7 @@ void cspotPlayer::command(cspot_event_t event) {
* generate any cspot::event */
case CSPOT_DISC:
cmdHandler(CSPOT_DISC);
isConnected = false;
state = ABORT;
break;
// spirc->setRemoteVolume does not generate a cspot::event so call cmdHandler
case CSPOT_VOLUME_UP:
Expand All @@ -285,34 +313,48 @@ void cspotPlayer::command(cspot_event_t event) {
}
}

void cspotPlayer::runTask() {
void cspotPlayer::enableZeroConf(void) {
httpd_uri_t request = {
.uri = "/spotify_info",
.method = HTTP_GET,
.handler = ::handleGET,
.user_ctx = NULL,
};

};
// register GET and POST handler for built-in server
httpd_register_uri_handler(serverHandle, &request);
request.method = HTTP_POST;
request.handler = ::handlePOST;
httpd_register_uri_handler(serverHandle, &request);

// construct blob for that player
blob = std::make_unique<cspot::LoginBlob>(name);

CSPOT_LOG(info, "ZeroConf mode (port %d)", serverPort);

// Register mdns service, for spotify to find us
bell::MDNSService::registerService( blob->getDeviceName(), "_spotify-connect", "_tcp", "", serverPort,
{ {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });

{ {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });
}

void cspotPlayer::runTask() {
bool useZeroConf = zeroConf;

// construct blob for that player
blob = std::make_unique<cspot::LoginBlob>(name);

CSPOT_LOG(info, "CSpot instance service name %s (id %s)", blob->getDeviceName().c_str(), blob->getDeviceId().c_str());

if (!zeroConf && !credentials.empty()) {
blob->loadJson(credentials);
CSPOT_LOG(info, "Reusable credentials mode");
} else {
// whether we want it or not we must use ZeroConf
useZeroConf = true;
enableZeroConf();
}

// gone with the wind...
while (1) {
clientConnected.wait();

CSPOT_LOG(info, "Spotify client connected for %s", name.c_str());
if (useZeroConf) clientConnected.wait();
CSPOT_LOG(info, "Spotify client launched for %s", name.c_str());

auto ctx = cspot::Context::createFromBlob(blob);

Expand All @@ -321,12 +363,26 @@ void cspotPlayer::runTask() {
else ctx->config.audioFormat = AudioFormat_OGG_VORBIS_160;

ctx->session->connectWithRandomAp();
auto token = ctx->session->authenticate(blob);
ctx->config.authData = ctx->session->authenticate(blob);

// Auth successful
if (token.size() > 0) {
if (ctx->config.authData.size() > 0) {
// we might have been forced to use zeroConf, so store credentials and reset zeroConf usage
if (!zeroConf) {
useZeroConf = false;
// can't call store_nvs... from a task running on EXTRAM stack
TimerHandle_t timer = xTimerCreate( "credentials", 1, pdFALSE, strdup(ctx->getCredentialsJson().c_str()),
[](TimerHandle_t xTimer) {
auto credentials = (char*) pvTimerGetTimerID(xTimer);
store_nvs_value_len_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, credentials, 0);
free(credentials);
xTimerDelete(xTimer, portMAX_DELAY);
} );
xTimerStart(timer, portMAX_DELAY);
}

spirc = std::make_unique<cspot::SpircHandler>(ctx);
isConnected = true;
state = LINKED;

// set call back to calculate a hash on trackId
spirc->getTrackPlayer()->setDataCallback(
Expand All @@ -347,7 +403,7 @@ void cspotPlayer::runTask() {
cmdHandler(CSPOT_VOLUME, volume);

// exit when player has stopped (received a DISC)
while (isConnected) {
while (state == LINKED) {
ctx->session->handlePacket();

// low-accuracy polling events
Expand All @@ -371,23 +427,32 @@ void cspotPlayer::runTask() {
spirc->setPause(true);
}
}

// on disconnect, stay in the core loop unless we are in ZeroConf mode
if (state == DISCO) {
// update volume then
cJSON *config = config_alloc_get_cjson("cspot_config");
cJSON_DeleteItemFromObject(config, "volume");
cJSON_AddNumberToObject(config, "volume", volume);
config_set_cjson_str_and_free("cspot_config", config);

// in ZeroConf mod, stay connected (in this loop)
if (!zeroConf) state = LINKED;
}
}

spirc->disconnect();
spirc.reset();

CSPOT_LOG(info, "disconnecting player %s", name.c_str());
} else {
CSPOT_LOG(error, "failed authentication, forcing ZeroConf");
if (!useZeroConf) enableZeroConf();
useZeroConf = true;
}

// we want to release memory ASAP and for sure
ctx.reset();
token.clear();

// update volume when we disconnect
cJSON *config = config_alloc_get_cjson("cspot_config");
cJSON_DeleteItemFromObject(config, "volume");
cJSON_AddNumberToObject(config, "volume", volume);
config_set_cjson_str_and_free("cspot_config", config);
ctx.reset();
}
}

Expand Down
Loading

0 comments on commit 580a9c6

Please sign in to comment.