diff --git a/components/lightsnapcast/Kconfig.projbuild b/components/lightsnapcast/Kconfig.projbuild new file mode 100644 index 00000000..a63f6a6e --- /dev/null +++ b/components/lightsnapcast/Kconfig.projbuild @@ -0,0 +1,19 @@ +menu "Lightsnapcast Player Settings" + config PLAYER_QUEUE_EMPTY_THRESHOLD + int "Queue empty threshold for hard resync" + range 1 10 + default 3 + help + Number of consecutive empty queue reads before triggering hard resync. + Higher values tolerate brief WiFi dropouts better. + Default: 3 (covers ~78ms of delay) + + config PLAYER_QUEUE_INSERT_TIMEOUT_MS + int "Queue insert timeout (ms)" + range 1 200 + default 50 + help + Timeout for inserting audio chunks into the playback queue. + Higher values allow queue to drain during burst arrivals. + Default: 50ms +endmenu diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index 6f0c8585..9821d731 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -1196,8 +1196,7 @@ int32_t insert_pcm_chunk(pcm_chunk_message_t *pcmChunk) { // free_pcm_chunk(element); // } - // if (xQueueSend(pcmChkQHdl, &pcmChunk, pdMS_TO_TICKS(10)) != pdTRUE) { - if (xQueueSend(pcmChkQHdl, &pcmChunk, pdMS_TO_TICKS(1)) != pdTRUE) { + if (xQueueSend(pcmChkQHdl, &pcmChunk, pdMS_TO_TICKS(CONFIG_PLAYER_QUEUE_INSERT_TIMEOUT_MS)) != pdTRUE) { ESP_LOGV(TAG, "send: pcmChunkQueue full, messages waiting %d", uxQueueMessagesWaiting(pcmChkQHdl)); @@ -1279,20 +1278,7 @@ static void player_task(void *pvParameters) { adjust_apll(0); #endif -// if (pcmChkQHdl == NULL) { -// int entries = ceil(((float)scSet.sr / (float)scSet.chkInFrames) * -// ((float)scSet.buf_ms / 1000)); -// -// // some chunks are placed in DMA buffer -// // so we can save a little RAM here -// entries -= (i2sDmaBufMaxLen * i2sDmaBufCnt) / scSet.chkInFrames; -// -// queueCreatedWithChkInFrames = scSet.chkInFrames; -// -// pcmChkQHdl = xQueueCreate(entries, sizeof(pcm_chunk_message_t *)); -// -// ESP_LOGI(TAG, "created new queue with %d", entries); -// } + // Queue is now created in start_player() for proper initialization audio_set_mute(scSet.muted); // wait for early time syncs to be ready @@ -1367,7 +1353,7 @@ static void player_task(void *pvParameters) { pcmChkQHdl = xQueueCreate(entries, sizeof(pcm_chunk_message_t *)); - ESP_LOGI(TAG, "created new queue with %d", entries); + ESP_LOGI(TAG, "created new queue with %d entries", entries); } if ((scSet.sr != __scSet.sr) || (scSet.bits != __scSet.bits) || @@ -1800,9 +1786,19 @@ static void player_task(void *pvParameters) { int msgWaiting = uxQueueMessagesWaiting(pcmChkQHdl); + // Track consecutive empty queue reads for hysteresis + static int consecutive_empty_count = 0; + + if (msgWaiting == 0) { + consecutive_empty_count++; + } else { + consecutive_empty_count = 0; + } + // resync hard if we are getting very late / early. // rest gets tuned in through apll speed control or sample insertion - if ((msgWaiting == 0) || + // Use hysteresis for queue empty to tolerate brief WiFi dropouts + if ((consecutive_empty_count >= CONFIG_PLAYER_QUEUE_EMPTY_THRESHOLD) || (MEDIANFILTER_isFull(&shortMedianFilter, 0) && ((shortMedian > hardResyncThreshold) || (shortMedian < -hardResyncThreshold)))) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 1aa0ac0a..01cd150e 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -51,3 +51,28 @@ menu "Snapclient Configuration" Both approaches have similar performance keeping clients in sync <= 500µs endmenu + +menu "WiFi Resilience Settings" + config WIFI_TCP_NODELAY + bool "Enable TCP_NODELAY" + default y + help + Disable Nagle's algorithm for lower latency on TCP connections. + Recommended for real-time audio streaming. + + config WIFI_RECONNECT_MIN_DELAY_MS + int "Reconnect minimum delay (ms)" + range 100 5000 + default 1000 + help + Initial delay before reconnection attempt. + Default: 1000 (1 second) + + config WIFI_RECONNECT_MAX_DELAY_MS + int "Reconnect maximum delay (ms)" + range 5000 60000 + default 30000 + help + Maximum delay with exponential backoff. + Default: 30000 (30 seconds) +endmenu diff --git a/main/main.c b/main/main.c index 439a0eb0..09d9d8e0 100644 --- a/main/main.c +++ b/main/main.c @@ -32,6 +32,7 @@ #include "lwip/sockets.h" #include "lwip/sys.h" #include "lwip/tcp.h" +#include "esp_random.h" #include "mdns.h" #include "net_functions.h" #include "network_interface.h" @@ -461,7 +462,8 @@ static void http_get_task(void *pvParameters) { int rc1 = ERR_OK, rc2 = ERR_OK; struct netbuf *firstNetBuf = NULL; uint16_t len; - uint64_t timeout; + uint64_t timeout = FAST_SYNC_LATENCY_BUF; + static uint32_t reconnect_attempts = 0; // For exponential backoff char *codecString = NULL; char *codecPayload = NULL; char *serverSettingsString = NULL; @@ -688,9 +690,18 @@ static void http_get_task(void *pvParameters) { continue; } - -// netconn_set_flags(lwipNetconn, TF_NODELAY); - + +#if CONFIG_WIFI_TCP_NODELAY + // Disable Nagle's algorithm for lower latency + { + struct tcp_pcb *pcb = (struct tcp_pcb *)lwipNetconn->pcb.tcp; + if (pcb != NULL) { + tcp_nagle_disable(pcb); + ESP_LOGI(TAG, "TCP_NODELAY enabled for lower latency"); + } + } +#endif + #define USE_INTERFACE_BIND #ifdef USE_INTERFACE_BIND // use interface to bind connection @@ -717,10 +728,6 @@ static void http_get_task(void *pvParameters) { if (rc2 != ERR_OK) { ESP_LOGE(TAG, "can't connect to remote %s:%d, err %d", ipaddr_ntoa(&remote_ip), remotePort, rc2); - -#if !SNAPCAST_SERVER_USE_MDNS - vTaskDelay(pdMS_TO_TICKS(1000)); -#endif } if (rc1 != ERR_OK || rc2 != ERR_OK) { @@ -728,9 +735,26 @@ static void http_get_task(void *pvParameters) { netconn_delete(lwipNetconn); lwipNetconn = NULL; + // Exponential backoff with jitter for reconnection + uint32_t delay_ms = (uint32_t)CONFIG_WIFI_RECONNECT_MIN_DELAY_MS << reconnect_attempts; + if (delay_ms > (uint32_t)CONFIG_WIFI_RECONNECT_MAX_DELAY_MS) { + delay_ms = (uint32_t)CONFIG_WIFI_RECONNECT_MAX_DELAY_MS; + } + delay_ms += (esp_random() % 500); // Add jitter + + ESP_LOGW(TAG, "Reconnect attempt %lu, waiting %lums", + (unsigned long)reconnect_attempts, (unsigned long)delay_ms); + vTaskDelay(pdMS_TO_TICKS(delay_ms)); + + if (reconnect_attempts < 10) { // Cap attempts counter + reconnect_attempts++; + } + continue; } + // Reset backoff on successful connection + reconnect_attempts = 0; ESP_LOGI(TAG, "netconn connected using %s", network_get_ifkey(netif)); //if (reset_latency_buffer() < 0) {