From 11d6c49f02d4c467644492f12375b4d796b15d58 Mon Sep 17 00:00:00 2001 From: raul Date: Sun, 15 Feb 2026 16:31:14 +0100 Subject: [PATCH 01/16] Add player state and refactor inter task communication --- components/lightsnapcast/include/player.h | 9 +- components/lightsnapcast/player.c | 168 ++++++++++----- main/main.c | 236 ++++++++++++++-------- 3 files changed, 279 insertions(+), 134 deletions(-) diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index dcd69aee..0bb97ea5 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -62,15 +62,20 @@ typedef struct snapcastSetting_s { i2s_data_bit_width_t bits; bool muted; - uint32_t volume; char *pcmBuf; uint32_t pcmBufSize; } snapcastSetting_t; -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_); +typedef enum { IDLE = 0, PLAYING, PAUSED } player_state_e; +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool, bool)); int deinit_player(void); int start_player(snapcastSetting_t *setting); +void pause_player(bool pause); + +void call_state_cb(void); +void add_player_state_cb(void (*cb)(void)); +player_state_e get_player_state(void); int32_t allocate_pcm_chunk_memory(pcm_chunk_message_t **pcmChunk, size_t bytes); int32_t insert_pcm_chunk(pcm_chunk_message_t *pcmChunk); diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index 209de796..562a3d82 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -117,11 +117,20 @@ static bool gpTimerRunning = false; static void player_task(void *pvParameters); +//player state bool gotSettings = false; -bool playerstarted = false; +bool playerStarted = false; +bool playerPaused = false; +static SemaphoreHandle_t playerStateMux = NULL; -extern void audio_set_mute(bool mute); -extern void audio_dac_enable(bool enabled); +typedef struct state_cb_s { + void (*cb)(void); + struct state_cb_s *next; +} state_cb_t; + +static state_cb_t *state_cb_head = NULL; + +static void (*audio_set_mute)(bool mute, bool set_state); static i2s_chan_handle_t tx_chan = NULL; // I2S tx channel handler static bool i2sEnabled = false; @@ -390,17 +399,20 @@ int deinit_player(void) { // must disable i2s before stopping player task or it will hang my_i2s_channel_disable(tx_chan); - + + if (playerStateMux != NULL) { + xSemaphoreTake(playerStateMux, pdMS_TO_TICKS(10000)); + } //wait max 10s for task to stop itself for(int i = 0; i< 100; i++) { - if (playerstarted) { + if (playerStarted) { vTaskDelay(pdMS_TO_TICKS(100)); } else { break; } } - // stop the task f still running + // stop the task if still running if (playerTaskHandle != NULL) { vTaskDelete(playerTaskHandle); playerTaskHandle = NULL; @@ -411,6 +423,10 @@ int deinit_player(void) { tx_chan = NULL; } + if (playerStateMux != NULL) { + vSemaphoreDelete(playerStateMux); + playerStateMux = NULL; + } if (snapcastSettingsMux != NULL) { vSemaphoreDelete(snapcastSettingsMux); snapcastSettingsMux = NULL; @@ -445,8 +461,9 @@ int deinit_player(void) { /** * call before http task creation! */ -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_) { +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool, bool)) { int ret = 0; + audio_set_mute = set_mute_cb; deinit_player(); @@ -459,14 +476,17 @@ int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_) { currentSnapcastSetting.sr = 0; currentSnapcastSetting.ch = 0; currentSnapcastSetting.bits = 0; - currentSnapcastSetting.muted = true; - currentSnapcastSetting.volume = 0; if (snapcastSettingsMux == NULL) { snapcastSettingsMux = xSemaphoreCreateMutex(); xSemaphoreGive(snapcastSettingsMux); } + if (playerStateMux == NULL) { + playerStateMux = xSemaphoreCreateMutex(); + xSemaphoreGive(playerStateMux); + } + /** ret = player_setup_i2s(¤tSnapcastSetting); if (ret < 0) { @@ -516,18 +536,25 @@ int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_) { * call to start the player task */ int start_player(snapcastSetting_t *setting) { - if (playerstarted){ - return -1; - } - playerstarted = true; + if (xSemaphoreTake(playerStateMux, 0) != pdTRUE) { + // shutdown in progress, don't start player + return -1; + } + if (playerStarted || playerPaused){ + xSemaphoreGive(playerStateMux); + return -1; + } + playerStarted = true; int ret = 0; ret = player_setup_i2s(setting); if (ret < 0) { ESP_LOGE(TAG, "player_setup_i2s failed: %d", ret); - playerstarted = false; + playerStarted = false; + xSemaphoreGive(playerStateMux); return -1; } + xSemaphoreGive(playerStateMux); tg0_timer_init(); @@ -574,12 +601,63 @@ int start_player(snapcastSetting_t *setting) { SYNC_TASK_PRIORITY, &playerTaskHandle, SYNC_TASK_CORE_ID); - + call_state_cb(); ESP_LOGI(TAG, "start player done"); return 0; } +void pause_player(bool pause) { + xSemaphoreTake(playerStateMux, portMAX_DELAY); + if (pause != playerPaused) { + playerPaused = pause; + if (pause && playerTaskHandle != NULL) { + xTaskNotifyGiveIndexed(playerTaskHandle, 1); + } + if (!pause) { + call_state_cb(); // notify state change, e.g. for http task to send pcm + } + } + xSemaphoreGive(playerStateMux); +} + +player_state_e get_player_state(void) { + xSemaphoreTake(playerStateMux, portMAX_DELAY); + player_state_e state = IDLE; + if (playerPaused) { + state = PAUSED; + } else if (playerStarted) { + state = PLAYING; + } + xSemaphoreGive(playerStateMux); + return state; +} + +void call_state_cb(void) { + state_cb_t *current = state_cb_head; + while (current != NULL) { + if (current->cb != NULL) { + current->cb(); + } + current = current->next; + } +} + +/** + * add callback to be called when player state changes, e.g. from not started to started. + * Callbacks needs to be implemented thread safe as they will be called from player task + */ +void add_player_state_cb(void (*cb)()) { + state_cb_t *new_cb = malloc(sizeof(state_cb_t)); + if (new_cb == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for state callback"); + return; + } + new_cb->cb = cb; + new_cb->next = state_cb_head; + state_cb_head = new_cb; +} + /** * */ @@ -681,8 +759,7 @@ int32_t player_send_snapcast_setting(snapcastSetting_t *setting) { if ((curSet.bits != setting->bits) || (curSet.buf_ms != setting->buf_ms) || (curSet.ch != setting->ch) || (curSet.chkInFrames != setting->chkInFrames) || - (curSet.codec != setting->codec) || (curSet.muted != setting->muted) || - (curSet.sr != setting->sr) || (curSet.volume != setting->volume) || + (curSet.codec != setting->codec) || (curSet.sr != setting->sr) || (curSet.cDacLat_ms != setting->cDacLat_ms)) { ret = player_set_snapcast_settings(setting); if (ret != pdPASS) { @@ -691,16 +768,7 @@ int32_t player_send_snapcast_setting(snapcastSetting_t *setting) { "snapcast setting"); } - // check if it is only volume / mute related setting, which is handled by - // http_get_task() - // if ((((curSet.muted != setting->muted) || - //(curSet.volume != setting->volume)) && - //((curSet.bits == setting->bits) && - //(curSet.buf_ms == setting->buf_ms) && (curSet.ch == setting->ch) && - //(curSet.chkInFrames == setting->chkInFrames) && - //(curSet.codec == setting->codec) && (curSet.sr == setting->sr) && - //(curSet.cDacLat_ms == setting->cDacLat_ms))) == false) { - // notify needed + // notify needed if ((playerTaskHandle != NULL) && (snapcastSettingQueueHandle != NULL)) { ret = xQueueOverwrite(snapcastSettingQueueHandle, &settingChanged); if (ret != pdPASS) { @@ -1354,14 +1422,18 @@ int32_t insert_pcm_chunk(pcm_chunk_message_t *pcmChunk) { return -1; } + if (playerPaused) { + free_pcm_chunk(pcmChunk); + return 0; + } if (pcmChkQHdl == NULL) { - ESP_LOGW(TAG, "pcm chunk queue not created. Player started: %s", playerstarted ? "True": "False"); + ESP_LOGW(TAG, "pcm chunk queue not created. Player started: %s", playerStarted ? "True": "False"); free_pcm_chunk(pcmChunk); snapcastSetting_t curSet; player_get_snapcast_settings(&curSet); - if (!curSet.muted && gotSettings) { + if (gotSettings) { start_player(&curSet); } @@ -1457,7 +1529,7 @@ static void player_task(void *pvParameters) { //audio_hal_ctrl_codec(audio_hal_handle_t audio_hal, audio_hal_codec_mode_t mode, audio_hal_ctrl_t audio_hal_ctrl) - audio_set_mute(true); + audio_set_mute(true, false); buf_us = (int64_t)(scSet.buf_ms) * 1000LL; clientDacLatency_us = (int64_t)scSet.cDacLat_ms * 1000LL; @@ -1483,7 +1555,7 @@ static void player_task(void *pvParameters) { // // ESP_LOGI(TAG, "created new queue with %d", entries); // } - audio_set_mute(scSet.muted); + audio_set_mute(false, false); // wait for early time syncs to be ready xSemaphoreTake(latencyBufFullSemaphoreHandle, portMAX_DELAY); @@ -1518,7 +1590,7 @@ static void player_task(void *pvParameters) { if ((scSet.sr != __scSet.sr) || (scSet.bits != __scSet.bits) || (scSet.ch != __scSet.ch)) { my_i2s_channel_enable(tx_chan); - audio_set_mute(true); + audio_set_mute(true, false); my_i2s_channel_disable(tx_chan); ret = player_setup_i2s(&__scSet); @@ -1564,13 +1636,11 @@ static void player_task(void *pvParameters) { (scSet.ch != __scSet.ch) || (scSet.buf_ms != __scSet.buf_ms)) { ESP_LOGI(TAG, "snapserver config changed, buffer %ldms, chunk %ld frames, " - "sample rate %ld, ch %d, bits %d mute %d latency %ld", + "sample rate %ld, ch %d, bits %d latency %ld", __scSet.buf_ms, __scSet.chkInFrames, __scSet.sr, __scSet.ch, - __scSet.bits, __scSet.muted, __scSet.cDacLat_ms); - } else { - audio_set_mute(__scSet.muted); - ESP_LOGI(TAG, "snapserver config changed, mute: %d", __scSet.muted); + __scSet.bits, __scSet.cDacLat_ms); } + audio_set_mute(false, false); scSet = __scSet; // store for next round @@ -1702,8 +1772,6 @@ static void player_task(void *pvParameters) { my_gptimer_stop(gptimer); - audio_dac_enable(true); - my_i2s_channel_enable(tx_chan); playback_start_time_us = esp_timer_get_time(); @@ -1719,7 +1787,7 @@ static void player_task(void *pvParameters) { // TODO: use a timer to un-mute non blocking vTaskDelay(pdMS_TO_TICKS(2)); - audio_set_mute(scSet.muted); + audio_set_mute(false, false); ESP_LOGI(TAG, "initial sync age: %lldus, chunk duration: %lldus", age, chunkDuration_us); @@ -1764,7 +1832,7 @@ static void player_task(void *pvParameters) { insertedSamplesCounter = 0; - audio_set_mute(true); + audio_set_mute(true, false); my_i2s_channel_disable(tx_chan); @@ -2020,7 +2088,7 @@ static void player_task(void *pvParameters) { my_gptimer_stop(gptimer); - audio_set_mute(true); + audio_set_mute(true, false); my_i2s_channel_disable(tx_chan); @@ -2110,17 +2178,23 @@ static void player_task(void *pvParameters) { dir = 0; initialSync = 0; - audio_set_mute(true); - audio_dac_enable(false); + audio_set_mute(true, false); my_i2s_channel_disable(tx_chan); i2s_del_channel(tx_chan); tx_chan = NULL; break; } + if (ulTaskNotifyTakeIndexed(1, pdTRUE, 0) == pdTRUE) { + audio_set_mute(true, false); + my_i2s_channel_disable(tx_chan); + i2s_del_channel(tx_chan); + tx_chan = NULL; + break; + } } ret = 0; - + xSemaphoreTake(playerStateMux, portMAX_DELAY); xSemaphoreTake(snapcastSettingsMux, portMAX_DELAY); // delete the queue vQueueDelete(snapcastSettingQueueHandle); @@ -2134,8 +2208,10 @@ static void player_task(void *pvParameters) { ret = destroy_pcm_queue(&pcmChkQHdl); tg0_timer_deinit(); - playerstarted = false; + playerStarted = false; + call_state_cb(); ESP_LOGI(TAG, "stop player done"); playerTaskHandle = NULL; + xSemaphoreGive(playerStateMux); vTaskDelete(NULL); } diff --git a/main/main.c b/main/main.c index 98ed9a69..9c4c61f6 100644 --- a/main/main.c +++ b/main/main.c @@ -88,6 +88,7 @@ const char *VERSION_STRING = "0.0.3"; #define OTA_TASK_CORE_ID tskNO_AFFINITY // 1 // tskNO_AFFINITY +TaskHandle_t t_main_task = NULL; TaskHandle_t t_ota_task = NULL; TaskHandle_t t_http_get_task = NULL; @@ -104,16 +105,19 @@ static const char *TAG = "SC"; SemaphoreHandle_t timeSyncSemaphoreHandle = NULL; SemaphoreHandle_t idCounterSemaphoreHandle = NULL; +SemaphoreHandle_t playerStateChangedMutex = NULL; typedef struct audioDACdata_s { - bool mute; + bool playerMute; + bool stateMute; int volume; - bool enabled; } audioDACdata_t; static audioDACdata_t audioDAC_data; static QueueHandle_t audioDACQHdl = NULL; static SemaphoreHandle_t audioDACSemaphore = NULL; +static void (*set_volume_cb)(int volume); +static void (*set_mute_cb)(bool mute, bool set_state); void time_sync_msg_cb(void *args); @@ -498,49 +502,12 @@ void error_callback(const FLAC__StreamDecoder *decoder, /** * */ -void init_snapcast(QueueHandle_t audioQHdl) { - audioDACQHdl = audioQHdl; - audioDACSemaphore = xSemaphoreCreateMutex(); - audioDAC_data.mute = true; - audioDAC_data.volume = -1; - audioDAC_data.enabled = false; +void init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool)) { + set_volume_cb = set_volume; + set_mute_cb = set_mute; } -/** - * - */ -void audio_dac_enable(bool enabled) { - xSemaphoreTake(audioDACSemaphore, portMAX_DELAY); - if (enabled != audioDAC_data.enabled) { - audioDAC_data.enabled = enabled; - xQueueOverwrite(audioDACQHdl, &audioDAC_data); - } - xSemaphoreGive(audioDACSemaphore); -} -/** - * - */ -void audio_set_mute(bool mute) { - xSemaphoreTake(audioDACSemaphore, portMAX_DELAY); - if (mute != audioDAC_data.mute) { - audioDAC_data.mute = mute; - xQueueOverwrite(audioDACQHdl, &audioDAC_data); - } - xSemaphoreGive(audioDACSemaphore); -} - -/** - * - */ -void audio_set_volume(int volume) { - xSemaphoreTake(audioDACSemaphore, portMAX_DELAY); - if (volume != audioDAC_data.volume) { - audioDAC_data.volume = volume; - xQueueOverwrite(audioDACQHdl, &audioDAC_data); - } - xSemaphoreGive(audioDACSemaphore); -} /** * @@ -548,6 +515,7 @@ void audio_set_volume(int volume) { int server_settings_msg_received( server_settings_message_t *server_settings_message, snapcastSetting_t *scSet) { + static int volume = 0; // log mute state, buffer, latency ESP_LOGI(TAG, "Buffer length: %ld", server_settings_message->buffer_ms); ESP_LOGI(TAG, "Latency: %ld", server_settings_message->latency); @@ -564,30 +532,34 @@ int server_settings_msg_received( dsp_processor_set_volome((double)server_settings_message->volume / 100); } #endif - audio_set_mute(server_settings_message->muted); + set_mute_cb(server_settings_message->muted, true); } - if (scSet->volume != server_settings_message->volume) { + if (volume != server_settings_message->volume) { #if SNAPCAST_USE_SOFT_VOL if (!server_settings_message->muted) { dsp_processor_set_volome((double)server_settings_message->volume / 100); } #else - audio_set_volume(server_settings_message->volume); + set_volume_cb(server_settings_message->volume); #endif } - scSet->cDacLat_ms = server_settings_message->latency; - scSet->buf_ms = server_settings_message->buffer_ms; scSet->muted = server_settings_message->muted; - scSet->volume = server_settings_message->volume; + volume = server_settings_message->volume; - if (player_send_snapcast_setting(scSet) != pdPASS) { - ESP_LOGE(TAG, - "Failed to notify sync task. " - "Did you init player?"); + if (scSet->cDacLat_ms != server_settings_message->latency || + scSet->buf_ms != server_settings_message->buffer_ms) { + scSet->cDacLat_ms = server_settings_message->latency; + scSet->buf_ms = server_settings_message->buffer_ms; + + if (player_send_snapcast_setting(scSet) != pdPASS) { + ESP_LOGE(TAG, + "Failed to notify sync task. " + "Did you init player?"); return -1; // fatal, this triggers return from http_get_task + } } return 0; } @@ -991,7 +963,7 @@ int handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, int process_data(snapcast_protocol_parser_t *parser, time_sync_data_t *time_sync_data, bool *received_codec_header, codec_type_t *codec, snapcastSetting_t *scSet, - pcm_chunk_message_t **pcmData) { + pcm_chunk_message_t **pcmData, bool paused) { base_message_t base_message_rx; if (parse_base_message(parser, &base_message_rx) == PARSER_COMPLETE) { @@ -1011,6 +983,17 @@ int process_data(snapcast_protocol_parser_t *parser, *received_codec_header, *codec, pcmData, &wire_chnk, &decoderChunk)) { case PARSER_COMPLETE: { + if (paused) { + if (*pcmData != NULL) { + free_pcm_chunk(*pcmData); + *pcmData = NULL; + } + if (decoderChunk.inData != NULL) { + free(decoderChunk.inData); + decoderChunk.inData = NULL; + } + break; + } if (handle_chunk_message(*codec, scSet, pcmData, &wire_chnk) != 0) { return -1; } @@ -1140,6 +1123,12 @@ void before_receive_callback(before_receive_callback_data_t *data) { } } + +void http_player_state_changed() { + xTaskNotifyGive(t_http_get_task); + //ESP_LOGI(TAG, "http task cb"); +} + /** * */ @@ -1161,6 +1150,7 @@ static void http_get_task(void *pvParameters) { codec_type_t codec = NONE; snapcastSetting_t scSet; pcm_chunk_message_t *pcmData = NULL; + bool paused = false; // create a timer to send time sync messages every x µs // esp_timer_create(&tSyncArgs, &time_sync_data.timeSyncMessageTimer); @@ -1172,6 +1162,8 @@ static void http_get_task(void *pvParameters) { return; } + add_player_state_cb(http_player_state_changed); + while (1) { // do some house keeping { @@ -1304,7 +1296,6 @@ static void http_get_task(void *pvParameters) { scSet.ch = 2; scSet.sr = 44100; scSet.chkInFrames = 0; - scSet.volume = 0; scSet.muted = true; snapcast_protocol_parser_t parser; @@ -1335,9 +1326,14 @@ static void http_get_task(void *pvParameters) { // Main connection loop - state machine + data processing while (1) { + if (ulTaskNotifyTake(pdTRUE, 1) == pdTRUE) { + // state change, e.g. pause/play + paused = get_player_state() == PAUSED; + //ESP_LOGI(TAG, "http got cb. %s", paused ? "paused" : "playing/idle"); + } int result = process_data(&parser, &time_sync_data, &received_codec_header, &codec, - &scSet, &pcmData); + &scSet, &pcmData, paused); if (result == -1) { return; // critical error in data processing } else if (result == -2) { @@ -1350,38 +1346,63 @@ static void http_get_task(void *pvParameters) { /** * */ -static void dac_control_task(audio_board_handle_t board_handle, - QueueHandle_t audioQHdl) { - audioDACdata_t dac_data; - audioDACdata_t dac_data_old = { - .mute = true, +static void dac_control(audio_board_handle_t board_handle, + audioDACdata_t dac_data) { + static audioDACdata_t dac_data_old = { + .playerMute = true, + .stateMute = true, .volume = -1, - .enabled = false, }; + static bool muted = true; // TODO: can and should we pass audio_hal_handle_t instead of // audio_board_handle_t? - while (1) { - if (xQueueReceive(audioQHdl, &dac_data, portMAX_DELAY) == pdTRUE) { - if (dac_data.mute != dac_data_old.mute) { - audio_hal_set_mute(board_handle->audio_hal, dac_data.mute); - } - if (dac_data.volume != dac_data_old.volume) { - audio_hal_set_volume(board_handle->audio_hal, dac_data.volume); - } - if (dac_data.enabled != dac_data_old.enabled) { - if (dac_data.enabled) { - audio_hal_ctrl_codec(board_handle->audio_hal, - AUDIO_HAL_CODEC_MODE_DECODE, - AUDIO_HAL_CTRL_START); - } else { - audio_hal_ctrl_codec(board_handle->audio_hal, - AUDIO_HAL_CODEC_MODE_DECODE, - AUDIO_HAL_CTRL_STOP); - } - } - dac_data_old = dac_data; + if (dac_data.playerMute != dac_data_old.playerMute || + dac_data.stateMute != dac_data_old.stateMute) { + bool mute = dac_data.playerMute || dac_data.stateMute; + if (mute != muted) { + muted = mute; + audio_hal_set_mute(board_handle->audio_hal, muted); } } + if (dac_data.volume != dac_data_old.volume) { + audio_hal_set_volume(board_handle->audio_hal, dac_data.volume); + } + dac_data_old = dac_data; +} + +/** + * + */ +void audio_set_mute(bool mute, bool set_state) { + xSemaphoreTake(audioDACSemaphore, portMAX_DELAY); + if (set_state && (mute != audioDAC_data.stateMute)) { + audioDAC_data.stateMute = mute; + xQueueOverwrite(audioDACQHdl, &audioDAC_data); + } + if (!set_state && mute != audioDAC_data.playerMute) { + audioDAC_data.playerMute = mute; + xQueueOverwrite(audioDACQHdl, &audioDAC_data); + } + xSemaphoreGive(audioDACSemaphore); +} + +/** + * + */ +void audio_set_volume(int volume) { + xSemaphoreTake(audioDACSemaphore, portMAX_DELAY); + if (volume != audioDAC_data.volume) { + audioDAC_data.volume = volume; + xQueueOverwrite(audioDACQHdl, &audioDAC_data); + } + xSemaphoreGive(audioDACSemaphore); +} + +void player_state_changed() { + if (playerStateChangedMutex != NULL) { + xSemaphoreGive(playerStateChangedMutex); + } + ESP_LOGI(TAG, "main task cb"); } /** @@ -1413,6 +1434,8 @@ void app_main(void) { esp_log_level_set("UI_HTTP", ESP_LOG_WARN); esp_log_level_set("dspProc", ESP_LOG_DEBUG); + t_main_task = xTaskGetCurrentTaskHandle(); + #if CONFIG_SNAPCLIENT_USE_INTERNAL_ETHERNET || \ CONFIG_SNAPCLIENT_USE_SPI_ETHERNET // clang-format off @@ -1542,10 +1565,22 @@ void app_main(void) { }, }; - QueueHandle_t audioQHdl = xQueueCreate(1, sizeof(audioDACdata_t)); + audioDACQHdl = xQueueCreate(1, sizeof(audioDACdata_t)); + audioDACSemaphore = xSemaphoreCreateMutex(); + audioDAC_data.stateMute = true; + audioDAC_data.playerMute = true; + audioDAC_data.volume = -1; + + init_snapcast(audio_set_volume, audio_set_mute); + init_player(i2s_pin_config0, I2S_NUM_0, audio_set_mute); + add_player_state_cb(player_state_changed); - init_snapcast(audioQHdl); - init_player(i2s_pin_config0, I2S_NUM_0); + // Create binary semaphore for player state change notification + playerStateChangedMutex = xSemaphoreCreateBinary(); + if (playerStateChangedMutex == NULL) { + ESP_LOGE(TAG, "Failed to create playerStateChangedMutex"); + return; + } #if CONFIG_DAC_TAS5805M // Apply persisted TAS5805M settings now that the codec has been initialized @@ -1614,6 +1649,35 @@ void app_main(void) { }; esp_pm_configure(&pmConfig); #endif - - dac_control_task(board_handle, audioQHdl); + audioDACdata_t dac_data; + player_state_e state = IDLE; + int counter = 0; + while (1) { + if (xQueueReceive(audioDACQHdl, &dac_data, pdMS_TO_TICKS(100)) == pdTRUE) { + dac_control(board_handle, dac_data); + } + if (xSemaphoreTake(playerStateChangedMutex, pdMS_TO_TICKS(10)) == pdTRUE) { + player_state_e state_new = get_player_state(); + ESP_LOGI(TAG, "main got cb: %d", state_new); + if (state_new != state) { + ESP_LOGI(TAG, "Player state changed: %d -> %d", state, state_new); + if (state_new == PLAYING) { + audio_hal_ctrl_codec(board_handle->audio_hal, + AUDIO_HAL_CODEC_MODE_DECODE, + AUDIO_HAL_CTRL_START); + } else if (state == PLAYING) { + audio_hal_ctrl_codec(board_handle->audio_hal, + AUDIO_HAL_CODEC_MODE_DECODE, + AUDIO_HAL_CTRL_STOP); + } + state = state_new; + } + } + // test pause/play toggle every 200 loops + // counter++; + // if (counter % 200 == 0) { + // ESP_LOGI(TAG, "toggle pause: %d", state == PLAYING); + // pause_player(state == PLAYING); + // } + } } From dfe124ae839b5447fed03d866183e985659f5749 Mon Sep 17 00:00:00 2001 From: raul Date: Fri, 20 Feb 2026 18:25:29 +0100 Subject: [PATCH 02/16] maximize partitions --- partitions.csv | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/partitions.csv b/partitions.csv index 04073e0a..44cbbfb0 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,6 +1,6 @@ # Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 32K, -otadata, data, ota, 0x11000, 8K, -phy_init, data, phy, 0x13000, 4K, -ota_0, app, ota_0, 0x20000, 1664K, -ota_1, app, ota_1, 0x1C0000, 1664K, +nvs, data, nvs, , 80K, +otadata, data, ota, , 8K, +phy_init, data, phy, , 4K, +ota_0, app, ota_0, , 1984K, +ota_1, app, ota_1, , 1984K, \ No newline at end of file From 2074ba985bb2bff70493e2db8395b476d1a11d7a Mon Sep 17 00:00:00 2001 From: raul Date: Fri, 20 Feb 2026 23:44:11 +0100 Subject: [PATCH 03/16] Fix dead lock in deinit_player. Refactor callbacks and check for NULL --- components/lightsnapcast/include/player.h | 2 +- components/lightsnapcast/player.c | 34 ++++++++++++--------- main/main.c | 36 ++++++++++++++++++----- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index 0bb97ea5..3e598319 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -68,7 +68,7 @@ typedef struct snapcastSetting_s { } snapcastSetting_t; typedef enum { IDLE = 0, PLAYING, PAUSED } player_state_e; -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool, bool)); +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool)); int deinit_player(void); int start_player(snapcastSetting_t *setting); void pause_player(bool pause); diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index 562a3d82..255a7c02 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -130,7 +130,7 @@ typedef struct state_cb_s { static state_cb_t *state_cb_head = NULL; -static void (*audio_set_mute)(bool mute, bool set_state); +static void (*audio_set_mute)(bool mute); static i2s_chan_handle_t tx_chan = NULL; // I2S tx channel handler static bool i2sEnabled = false; @@ -400,9 +400,11 @@ int deinit_player(void) { // must disable i2s before stopping player task or it will hang my_i2s_channel_disable(tx_chan); - if (playerStateMux != NULL) { - xSemaphoreTake(playerStateMux, pdMS_TO_TICKS(10000)); - } + //don't take playerStateMux here, otherwise we might block player task from stopping + //if (playerStateMux != NULL) { + // xSemaphoreTake(playerStateMux, pdMS_TO_TICKS(10000)); + //} + //wait max 10s for task to stop itself for(int i = 0; i< 100; i++) { if (playerStarted) { @@ -461,8 +463,12 @@ int deinit_player(void) { /** * call before http task creation! */ -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool, bool)) { +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool)) { int ret = 0; + if (set_mute_cb == NULL) { + ESP_LOGE(TAG, "set_mute_cb is NULL"); + return -1; + } audio_set_mute = set_mute_cb; deinit_player(); @@ -1529,7 +1535,7 @@ static void player_task(void *pvParameters) { //audio_hal_ctrl_codec(audio_hal_handle_t audio_hal, audio_hal_codec_mode_t mode, audio_hal_ctrl_t audio_hal_ctrl) - audio_set_mute(true, false); + audio_set_mute(true); buf_us = (int64_t)(scSet.buf_ms) * 1000LL; clientDacLatency_us = (int64_t)scSet.cDacLat_ms * 1000LL; @@ -1555,7 +1561,7 @@ static void player_task(void *pvParameters) { // // ESP_LOGI(TAG, "created new queue with %d", entries); // } - audio_set_mute(false, false); + audio_set_mute(false); // wait for early time syncs to be ready xSemaphoreTake(latencyBufFullSemaphoreHandle, portMAX_DELAY); @@ -1590,7 +1596,7 @@ static void player_task(void *pvParameters) { if ((scSet.sr != __scSet.sr) || (scSet.bits != __scSet.bits) || (scSet.ch != __scSet.ch)) { my_i2s_channel_enable(tx_chan); - audio_set_mute(true, false); + audio_set_mute(true); my_i2s_channel_disable(tx_chan); ret = player_setup_i2s(&__scSet); @@ -1640,7 +1646,7 @@ static void player_task(void *pvParameters) { __scSet.buf_ms, __scSet.chkInFrames, __scSet.sr, __scSet.ch, __scSet.bits, __scSet.cDacLat_ms); } - audio_set_mute(false, false); + audio_set_mute(false); scSet = __scSet; // store for next round @@ -1787,7 +1793,7 @@ static void player_task(void *pvParameters) { // TODO: use a timer to un-mute non blocking vTaskDelay(pdMS_TO_TICKS(2)); - audio_set_mute(false, false); + audio_set_mute(false); ESP_LOGI(TAG, "initial sync age: %lldus, chunk duration: %lldus", age, chunkDuration_us); @@ -1832,7 +1838,7 @@ static void player_task(void *pvParameters) { insertedSamplesCounter = 0; - audio_set_mute(true, false); + audio_set_mute(true); my_i2s_channel_disable(tx_chan); @@ -2088,7 +2094,7 @@ static void player_task(void *pvParameters) { my_gptimer_stop(gptimer); - audio_set_mute(true, false); + audio_set_mute(true); my_i2s_channel_disable(tx_chan); @@ -2178,7 +2184,7 @@ static void player_task(void *pvParameters) { dir = 0; initialSync = 0; - audio_set_mute(true, false); + audio_set_mute(true); my_i2s_channel_disable(tx_chan); i2s_del_channel(tx_chan); tx_chan = NULL; @@ -2186,7 +2192,7 @@ static void player_task(void *pvParameters) { break; } if (ulTaskNotifyTakeIndexed(1, pdTRUE, 0) == pdTRUE) { - audio_set_mute(true, false); + audio_set_mute(true); my_i2s_channel_disable(tx_chan); i2s_del_channel(tx_chan); tx_chan = NULL; diff --git a/main/main.c b/main/main.c index 9c4c61f6..3cc42527 100644 --- a/main/main.c +++ b/main/main.c @@ -117,7 +117,7 @@ static audioDACdata_t audioDAC_data; static QueueHandle_t audioDACQHdl = NULL; static SemaphoreHandle_t audioDACSemaphore = NULL; static void (*set_volume_cb)(int volume); -static void (*set_mute_cb)(bool mute, bool set_state); +static void (*set_mute_cb)(bool mute); void time_sync_msg_cb(void *args); @@ -502,9 +502,21 @@ void error_callback(const FLAC__StreamDecoder *decoder, /** * */ -void init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool)) { +int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool)) { + if (set_volume == NULL) { + ESP_LOGE(TAG, "Volume callback is NULL"); + + return -1; + } + if (set_mute == NULL) { + ESP_LOGE(TAG, "Mute callback is NULL"); + + return -1; + } set_volume_cb = set_volume; set_mute_cb = set_mute; + + return 0; } @@ -532,7 +544,7 @@ int server_settings_msg_received( dsp_processor_set_volome((double)server_settings_message->volume / 100); } #endif - set_mute_cb(server_settings_message->muted, true); + set_mute_cb(server_settings_message->muted); } if (volume != server_settings_message->volume) { @@ -1358,6 +1370,7 @@ static void dac_control(audio_board_handle_t board_handle, // audio_board_handle_t? if (dac_data.playerMute != dac_data_old.playerMute || dac_data.stateMute != dac_data_old.stateMute) { + // if either player or state mute is active, we need to mute the output bool mute = dac_data.playerMute || dac_data.stateMute; if (mute != muted) { muted = mute; @@ -1371,7 +1384,8 @@ static void dac_control(audio_board_handle_t board_handle, } /** - * + * Set mute state. If set_state is true, it reflects the snapclient state. Otherwise it + * is coming from player for temporary muting e.g. during startup. */ void audio_set_mute(bool mute, bool set_state) { xSemaphoreTake(audioDACSemaphore, portMAX_DELAY); @@ -1379,13 +1393,21 @@ void audio_set_mute(bool mute, bool set_state) { audioDAC_data.stateMute = mute; xQueueOverwrite(audioDACQHdl, &audioDAC_data); } - if (!set_state && mute != audioDAC_data.playerMute) { + else if (!set_state && mute != audioDAC_data.playerMute) { audioDAC_data.playerMute = mute; xQueueOverwrite(audioDACQHdl, &audioDAC_data); } xSemaphoreGive(audioDACSemaphore); } +void player_set_mute(bool mute) { + audio_set_mute(mute, false); +} + +void set_mute_state(bool mute) { + audio_set_mute(mute, true); +} + /** * */ @@ -1571,8 +1593,8 @@ void app_main(void) { audioDAC_data.playerMute = true; audioDAC_data.volume = -1; - init_snapcast(audio_set_volume, audio_set_mute); - init_player(i2s_pin_config0, I2S_NUM_0, audio_set_mute); + init_snapcast(audio_set_volume, set_mute_state); + init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute); add_player_state_cb(player_state_changed); // Create binary semaphore for player state change notification From d4923a9fa7af8ad2d41144f5896907fb747f4182 Mon Sep 17 00:00:00 2001 From: raul Date: Sat, 28 Feb 2026 16:40:01 +0100 Subject: [PATCH 04/16] Keep volume in settings struct --- components/lightsnapcast/include/player.h | 1 + main/main.c | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index 3e598319..73e71bfc 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -62,6 +62,7 @@ typedef struct snapcastSetting_s { i2s_data_bit_width_t bits; bool muted; + uint32_t volume; char *pcmBuf; uint32_t pcmBufSize; diff --git a/main/main.c b/main/main.c index 3cc42527..e7cfe508 100644 --- a/main/main.c +++ b/main/main.c @@ -527,7 +527,6 @@ int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool)) { int server_settings_msg_received( server_settings_message_t *server_settings_message, snapcastSetting_t *scSet) { - static int volume = 0; // log mute state, buffer, latency ESP_LOGI(TAG, "Buffer length: %ld", server_settings_message->buffer_ms); ESP_LOGI(TAG, "Latency: %ld", server_settings_message->latency); @@ -547,7 +546,7 @@ int server_settings_msg_received( set_mute_cb(server_settings_message->muted); } - if (volume != server_settings_message->volume) { + if (scSet->volume != server_settings_message->volume) { #if SNAPCAST_USE_SOFT_VOL if (!server_settings_message->muted) { dsp_processor_set_volome((double)server_settings_message->volume / 100); @@ -558,7 +557,7 @@ int server_settings_msg_received( } scSet->muted = server_settings_message->muted; - volume = server_settings_message->volume; + scSet->volume = server_settings_message->volume; if (scSet->cDacLat_ms != server_settings_message->latency || scSet->buf_ms != server_settings_message->buffer_ms) { From 61a54a7d731ffd88f31fdea5a38b7e9e36ffab02 Mon Sep 17 00:00:00 2001 From: raul Date: Sat, 28 Feb 2026 16:48:50 +0100 Subject: [PATCH 05/16] Move log message so it does not spam the logs when paused --- components/lightsnapcast/snapcast_protocol_parser.c | 2 -- main/main.c | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/lightsnapcast/snapcast_protocol_parser.c b/components/lightsnapcast/snapcast_protocol_parser.c index 19a7d3a1..53944ffe 100644 --- a/components/lightsnapcast/snapcast_protocol_parser.c +++ b/components/lightsnapcast/snapcast_protocol_parser.c @@ -327,7 +327,5 @@ parser_return_state_t parser_skip_typed_message(snapcast_protocol_parser_t* pars if (!read_byte(parser, &dummy_byte)) return PARSER_RESTART_CONNECTION; } - ESP_LOGI(TAG, "done skipping typed message %d", base_message_rx->type); - return PARSER_OK; } diff --git a/main/main.c b/main/main.c index 21756435..da25aa01 100644 --- a/main/main.c +++ b/main/main.c @@ -1048,6 +1048,8 @@ int process_data(snapcast_protocol_parser_t *parser, if (parser_skip_typed_message(parser, &base_message_rx) != PARSER_OK) { return -1; } + + ESP_LOGI(TAG, "done skipping typed message %d", base_message_rx.type); return 0; } } From 029bf60113c350c7c51fb83e2ef0d4f3196fcc20 Mon Sep 17 00:00:00 2001 From: raul Date: Sat, 28 Feb 2026 20:13:15 +0100 Subject: [PATCH 06/16] fix --- main/main.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/main.c b/main/main.c index da25aa01..19941abf 100644 --- a/main/main.c +++ b/main/main.c @@ -569,8 +569,9 @@ void server_settings_msg_received( "Failed to notify sync task. " "Did you init player?"); - // critical error - esp_restart(); + // critical error + esp_restart(); + } } } From b3c95321c6b7d6788072ca07884851c47e5aade6 Mon Sep 17 00:00:00 2001 From: raul Date: Sat, 28 Feb 2026 23:52:26 +0100 Subject: [PATCH 07/16] Set CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 --- components/lightsnapcast/player.c | 21 +++++++-------------- main/main.c | 7 ------- sdkconfig.defaults | 1 + sdkconfig_MAX98357A_ESP32-S2 | 2 +- sdkconfig_PCM5102A | 2 +- sdkconfig_PCM5102A_Olimex-ESP32-PoE | 2 +- sdkconfig_PCM5102A_WT32-ETH01 | 2 +- sdkconfig_TAS5805M | 2 +- sdkconfig_adau1961 | 2 +- sdkconfig_for_esp_snapserver | 2 +- sdkconfig_lyrat_v4.3 | 2 +- 11 files changed, 16 insertions(+), 29 deletions(-) diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index 255a7c02..98c2a94c 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -617,14 +617,16 @@ void pause_player(bool pause) { xSemaphoreTake(playerStateMux, portMAX_DELAY); if (pause != playerPaused) { playerPaused = pause; + xSemaphoreGive(playerStateMux); if (pause && playerTaskHandle != NULL) { xTaskNotifyGiveIndexed(playerTaskHandle, 1); } if (!pause) { call_state_cb(); // notify state change, e.g. for http task to send pcm } + } else { + xSemaphoreGive(playerStateMux); } - xSemaphoreGive(playerStateMux); } player_state_e get_player_state(void) { @@ -2180,25 +2182,16 @@ static void player_task(void *pvParameters) { "diff2Server: %llds, %lld.%lldms", uxQueueMessagesWaiting(pcmChkQHdl), sec, msec, usec); } - - dir = 0; - initialSync = 0; - - audio_set_mute(true); - my_i2s_channel_disable(tx_chan); - i2s_del_channel(tx_chan); - tx_chan = NULL; - break; } if (ulTaskNotifyTakeIndexed(1, pdTRUE, 0) == pdTRUE) { - audio_set_mute(true); - my_i2s_channel_disable(tx_chan); - i2s_del_channel(tx_chan); - tx_chan = NULL; break; } } + audio_set_mute(true); + my_i2s_channel_disable(tx_chan); + i2s_del_channel(tx_chan); + tx_chan = NULL; ret = 0; xSemaphoreTake(playerStateMux, portMAX_DELAY); xSemaphoreTake(snapcastSettingsMux, portMAX_DELAY); diff --git a/main/main.c b/main/main.c index 19941abf..a497cfea 100644 --- a/main/main.c +++ b/main/main.c @@ -1618,7 +1618,6 @@ void app_main(void) { #endif audioDACdata_t dac_data; player_state_e state = IDLE; - int counter = 0; while (1) { if (xQueueReceive(audioDACQHdl, &dac_data, pdMS_TO_TICKS(100)) == pdTRUE) { dac_control(board_handle, dac_data); @@ -1640,11 +1639,5 @@ void app_main(void) { state = state_new; } } - // test pause/play toggle every 200 loops - // counter++; - // if (counter % 200 == 0) { - // ESP_LOGI(TAG, "toggle pause: %d", state == PLAYING); - // pause_player(state == PLAYING); - // } } } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 0bd8708b..36cb5f63 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -37,6 +37,7 @@ CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_USE_TICKLESS_IDLE=y CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 CONFIG_LOG_DEFAULT_LEVEL_NONE=y CONFIG_LOG_MAXIMUM_LEVEL_INFO=y CONFIG_LOG_COLORS=y diff --git a/sdkconfig_MAX98357A_ESP32-S2 b/sdkconfig_MAX98357A_ESP32-S2 index dd98d5dd..9235e98e 100644 --- a/sdkconfig_MAX98357A_ESP32-S2 +++ b/sdkconfig_MAX98357A_ESP32-S2 @@ -1190,7 +1190,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set # end of Kernel diff --git a/sdkconfig_PCM5102A b/sdkconfig_PCM5102A index f287231c..854f4065 100644 --- a/sdkconfig_PCM5102A +++ b/sdkconfig_PCM5102A @@ -1136,7 +1136,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set CONFIG_FREERTOS_USE_TICKLESS_IDLE=y diff --git a/sdkconfig_PCM5102A_Olimex-ESP32-PoE b/sdkconfig_PCM5102A_Olimex-ESP32-PoE index 7d7e5aba..314cd2d7 100644 --- a/sdkconfig_PCM5102A_Olimex-ESP32-PoE +++ b/sdkconfig_PCM5102A_Olimex-ESP32-PoE @@ -1252,7 +1252,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set diff --git a/sdkconfig_PCM5102A_WT32-ETH01 b/sdkconfig_PCM5102A_WT32-ETH01 index 36f82ca9..92112d05 100644 --- a/sdkconfig_PCM5102A_WT32-ETH01 +++ b/sdkconfig_PCM5102A_WT32-ETH01 @@ -1138,7 +1138,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set # end of Kernel diff --git a/sdkconfig_TAS5805M b/sdkconfig_TAS5805M index 347372a1..311993de 100644 --- a/sdkconfig_TAS5805M +++ b/sdkconfig_TAS5805M @@ -1214,7 +1214,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set CONFIG_FREERTOS_USE_TICKLESS_IDLE=y diff --git a/sdkconfig_adau1961 b/sdkconfig_adau1961 index 7786e673..3f980c39 100644 --- a/sdkconfig_adau1961 +++ b/sdkconfig_adau1961 @@ -1147,7 +1147,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set CONFIG_FREERTOS_USE_TICKLESS_IDLE=y diff --git a/sdkconfig_for_esp_snapserver b/sdkconfig_for_esp_snapserver index fdcc5d9b..0637c068 100644 --- a/sdkconfig_for_esp_snapserver +++ b/sdkconfig_for_esp_snapserver @@ -1097,7 +1097,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set # end of Kernel diff --git a/sdkconfig_lyrat_v4.3 b/sdkconfig_lyrat_v4.3 index bc7af6c5..a4003b15 100644 --- a/sdkconfig_lyrat_v4.3 +++ b/sdkconfig_lyrat_v4.3 @@ -1184,7 +1184,7 @@ CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=1536 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=5 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set CONFIG_FREERTOS_USE_TICKLESS_IDLE=y From b6daa5967daf4db8a0731dd79bba02d26e84d670 Mon Sep 17 00:00:00 2001 From: raul Date: Sun, 1 Mar 2026 09:36:50 +0100 Subject: [PATCH 08/16] fix state cb --- components/lightsnapcast/player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index 98c2a94c..708460ec 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -621,7 +621,7 @@ void pause_player(bool pause) { if (pause && playerTaskHandle != NULL) { xTaskNotifyGiveIndexed(playerTaskHandle, 1); } - if (!pause) { + else { call_state_cb(); // notify state change, e.g. for http task to send pcm } } else { From db77bee476373a6265ece8faee0720f67e3b926a Mon Sep 17 00:00:00 2001 From: raul Date: Tue, 3 Mar 2026 23:20:35 +0100 Subject: [PATCH 09/16] fix --- components/lightsnapcast/player.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index cc3e2a7f..40717f9f 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -1003,7 +1003,7 @@ static bool IRAM_ATTR timer_group0_alarm_cb( uint64_t timer_counter_value = edata->count_value; // Notify the task in the task's notification value. - xTaskNotifyFromISR(playerTaskHandle, (uint32_t)timer_counter_value, + xTaskNotifyIndexedFromISR(playerTaskHandle, 0, (uint32_t)timer_counter_value, eSetValueWithOverwrite, &xHigherPriorityTaskWoken); return xHigherPriorityTaskWoken == pdTRUE; @@ -1771,7 +1771,7 @@ static void player_task(void *pvParameters) { } // Wait to be notified of a timer interrupt. - xTaskNotifyWait(pdFALSE, // Don't clear bits on entry. + xTaskNotifyWaitIndexed(0, pdFALSE, // Don't clear bits on entry. pdFALSE, // Don't clear bits on exit. ¬ifiedValue, // Stores the notified value. portMAX_DELAY); From cf4f9845151dd3e34b02e18da93e005530ec6bb2 Mon Sep 17 00:00:00 2001 From: raul Date: Sun, 29 Mar 2026 23:19:16 +0200 Subject: [PATCH 10/16] Apply volume/mute only while playing --- main/main.c | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/main/main.c b/main/main.c index bcfa940e..4451d989 100644 --- a/main/main.c +++ b/main/main.c @@ -528,7 +528,7 @@ int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool)) { */ void server_settings_msg_received( server_settings_message_t *server_settings_message, - snapcastSetting_t *scSet) { + snapcastSetting_t *scSet, bool playing) { // log mute state, buffer, latency ESP_LOGI(TAG, "Buffer length: %ld", server_settings_message->buffer_ms); ESP_LOGI(TAG, "Latency: %ld", server_settings_message->latency); @@ -537,7 +537,7 @@ void server_settings_msg_received( // Volume setting using ADF HAL // abstraction - if (scSet->muted != server_settings_message->muted) { + if (playing && scSet->muted != server_settings_message->muted) { #if SNAPCAST_USE_SOFT_VOL if (server_settings_message->muted) { dsp_processor_set_volome(0.0); @@ -548,7 +548,7 @@ void server_settings_msg_received( set_mute_cb(server_settings_message->muted); } - if (scSet->volume != server_settings_message->volume) { + if (playing && scSet->volume != server_settings_message->volume) { #if SNAPCAST_USE_SOFT_VOL if (!server_settings_message->muted) { dsp_processor_set_volome((double)server_settings_message->volume / 100); @@ -981,7 +981,7 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, int process_data(snapcast_protocol_parser_t *parser, time_sync_data_t *time_sync_data, bool *received_codec_header, codec_type_t *codec, snapcastSetting_t *scSet, - pcm_chunk_message_t **pcmData, bool paused) { + pcm_chunk_message_t **pcmData, player_state_e state) { base_message_t base_message_rx; if (parse_base_message(parser, &base_message_rx) != PARSER_OK) { @@ -997,7 +997,7 @@ int process_data(snapcast_protocol_parser_t *parser, wire_chunk_message_t wire_chnk = {{0, 0}, 0, NULL}; // is wire_chnk.payload ever used? // skip this wires chunk message if codec header message was not received yet! - if (*received_codec_header == false || paused) { + if (*received_codec_header == false || state == PAUSED) { if (parser_skip_typed_message(parser, &base_message_rx) != PARSER_OK) { return -1; } @@ -1034,7 +1034,7 @@ int process_data(snapcast_protocol_parser_t *parser, if (parse_sever_settings_message(parser, &base_message_rx, &server_settings_message) != PARSER_OK) { return -1; } - server_settings_msg_received(&server_settings_message, scSet); + server_settings_msg_received(&server_settings_message, scSet, state == PLAYING); return 0; } @@ -1109,7 +1109,7 @@ static void http_get_task(void *pvParameters) { codec_type_t codec = NONE; snapcastSetting_t scSet; pcm_chunk_message_t *pcmData = NULL; - bool paused = false; + player_state_e player_state = IDLE; // create a timer to send time sync messages every x µs // esp_timer_create(&tSyncArgs, &time_sync_data.timeSyncMessageTimer); @@ -1289,12 +1289,25 @@ static void http_get_task(void *pvParameters) { while (1) { if (ulTaskNotifyTake(pdTRUE, 1) == pdTRUE) { // state change, e.g. pause/play - paused = get_player_state() == PAUSED; + player_state_e state = get_player_state(); + if (state != player_state && state == PLAYING) { +#if SNAPCAST_USE_SOFT_VOL + if (!scSet.muted) { + dsp_processor_set_volome((double)scSet.volume / 100); + } else { + dsp_processor_set_volome(0.0); + } +#else + set_volume_cb(scSet.volume); +#endif + set_mute_cb(scSet.muted); + } + player_state = state; //ESP_LOGI(TAG, "http got cb. %s", paused ? "paused" : "playing/idle"); } int result = process_data(&parser, &time_sync_data, &received_codec_header, &codec, - &scSet, &pcmData, paused); + &scSet, &pcmData, player_state); if (result != 0) { break; // restart connection } @@ -1626,7 +1639,7 @@ void app_main(void) { audioDACdata_t dac_data; player_state_e state = IDLE; while (1) { - if (xQueueReceive(audioDACQHdl, &dac_data, pdMS_TO_TICKS(100)) == pdTRUE) { + if (xQueueReceive(audioDACQHdl, &dac_data, pdMS_TO_TICKS(90)) == pdTRUE) { dac_control(board_handle, dac_data); } if (xSemaphoreTake(playerStateChangedMutex, pdMS_TO_TICKS(10)) == pdTRUE) { From 5ec9a485482542d081952096a693f4d010a239e5 Mon Sep 17 00:00:00 2001 From: raul Date: Sat, 28 Mar 2026 19:27:48 +0100 Subject: [PATCH 11/16] Use snapcast state instead of player state --- components/lightsnapcast/include/player.h | 5 +- components/lightsnapcast/player.c | 53 +--- main/main.c | 286 +++++++++++++++++----- 3 files changed, 238 insertions(+), 106 deletions(-) diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index 73e71bfc..1be896b2 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -68,15 +68,12 @@ typedef struct snapcastSetting_s { uint32_t pcmBufSize; } snapcastSetting_t; -typedef enum { IDLE = 0, PLAYING, PAUSED } player_state_e; -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool)); +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool)); int deinit_player(void); int start_player(snapcastSetting_t *setting); void pause_player(bool pause); void call_state_cb(void); -void add_player_state_cb(void (*cb)(void)); -player_state_e get_player_state(void); int32_t allocate_pcm_chunk_memory(pcm_chunk_message_t **pcmChunk, size_t bytes); int32_t insert_pcm_chunk(pcm_chunk_message_t *pcmChunk); diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index b095f6ee..d20ebe1a 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -123,12 +123,7 @@ bool playerStarted = false; bool playerPaused = false; static SemaphoreHandle_t playerStateMux = NULL; -typedef struct state_cb_s { - void (*cb)(void); - struct state_cb_s *next; -} state_cb_t; - -static state_cb_t *state_cb_head = NULL; +static void (*state_cb)(bool) = NULL; static void (*audio_set_mute)(bool mute); @@ -463,13 +458,17 @@ int deinit_player(void) { /** * call before http task creation! */ -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool)) { +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool)) { int ret = 0; if (set_mute_cb == NULL) { ESP_LOGE(TAG, "set_mute_cb is NULL"); return -1; } audio_set_mute = set_mute_cb; + if (cb != NULL) { + state_cb = cb; + } + deinit_player(); @@ -629,41 +628,13 @@ void pause_player(bool pause) { } } -player_state_e get_player_state(void) { - xSemaphoreTake(playerStateMux, portMAX_DELAY); - player_state_e state = IDLE; - if (playerPaused) { - state = PAUSED; - } else if (playerStarted) { - state = PLAYING; - } - xSemaphoreGive(playerStateMux); - return state; -} - void call_state_cb(void) { - state_cb_t *current = state_cb_head; - while (current != NULL) { - if (current->cb != NULL) { - current->cb(); - } - current = current->next; - } -} - -/** - * add callback to be called when player state changes, e.g. from not started to started. - * Callbacks needs to be implemented thread safe as they will be called from player task - */ -void add_player_state_cb(void (*cb)()) { - state_cb_t *new_cb = malloc(sizeof(state_cb_t)); - if (new_cb == NULL) { - ESP_LOGE(TAG, "Failed to allocate memory for state callback"); - return; + if (state_cb != NULL) { + xSemaphoreTake(playerStateMux, portMAX_DELAY); + bool paused = playerPaused; + xSemaphoreGive(playerStateMux); + state_cb(paused); } - new_cb->cb = cb; - new_cb->next = state_cb_head; - state_cb_head = new_cb; } /** @@ -2211,10 +2182,10 @@ static void player_task(void *pvParameters) { tg0_timer_deinit(); playerStarted = false; - call_state_cb(); ESP_LOGI(TAG, "stop player done"); playerTaskHandle = NULL; xSemaphoreGive(playerStateMux); + call_state_cb(); vTaskDelete(NULL); } diff --git a/main/main.c b/main/main.c index 4451d989..0ba55244 100644 --- a/main/main.c +++ b/main/main.c @@ -107,7 +107,7 @@ static const char *TAG = "SC"; SemaphoreHandle_t timeSyncSemaphoreHandle = NULL; SemaphoreHandle_t idCounterSemaphoreHandle = NULL; -SemaphoreHandle_t playerStateChangedMutex = NULL; +SemaphoreHandle_t snapcastStateChangedMutex = NULL; typedef struct audioDACdata_s { bool playerMute; @@ -119,7 +119,8 @@ static audioDACdata_t audioDAC_data; static QueueHandle_t audioDACQHdl = NULL; static SemaphoreHandle_t audioDACSemaphore = NULL; static void (*set_volume_cb)(int volume); -static void (*set_mute_cb)(bool mute); +static void (*set_mute_cb)(bool mute, bool state); +static SemaphoreHandle_t snapcastStateMux = NULL; void time_sync_msg_cb(void *args); @@ -501,26 +502,89 @@ void error_callback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatusString[status]); } -/** - * - */ -int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool)) { - if (set_volume == NULL) { - ESP_LOGE(TAG, "Volume callback is NULL"); +typedef enum { IDLE = 0, STOPPED, PLAYING, PAUSED } snapcast_state_t; //defined in player.h +typedef enum { STOP = 0, START, RESTART, PAUSE, UNPAUSE } snapcast_commands_t; - return -1; +typedef struct state_cb_s { + void (*cb)(void); + struct state_cb_s *next; +} state_cb_t; + +static snapcast_state_t sc_state = STOPPED; +static state_cb_t *state_cb_head = NULL; + +void player_set_mute(bool mute) { + set_mute_cb(mute, false); +} + +void set_mute_state(bool mute) { + set_mute_cb(mute, true); +} + +void sc_send_command(snapcast_commands_t command) { + if (t_http_get_task != NULL) { + xTaskNotify(t_http_get_task, (uint32_t) command, eSetValueWithOverwrite); } - if (set_mute == NULL) { - ESP_LOGE(TAG, "Mute callback is NULL"); +} - return -1; +void player_state_paused(bool paused) { + if (paused) { + sc_send_command(PAUSE); + } else { + sc_send_command(UNPAUSE); } - set_volume_cb = set_volume; - set_mute_cb = set_mute; +} - return 0; +void sc_start_snapcast() { + sc_send_command(START); } +void sc_restart_snapcast() { + sc_send_command(RESTART); +} + +void sc_stop_snapcast() { + sc_send_command(STOP); +} + +void sc_pause_snapcast(bool pause) { + pause_player(pause); + if (!pause) { + //sc_send_command(UNPAUSE); + } +} + +snapcast_state_t sc_get_snapcast_state(void) { + xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + snapcast_state_t state = sc_state; + xSemaphoreGive(snapcastStateMux); + return state; +} + +void sc_call_state_cb(void) { + state_cb_t *current = state_cb_head; + while (current != NULL) { + if (current->cb != NULL) { + current->cb(); + } + current = current->next; + } +} + +/** + * add callback to be called when snapcast state changes, e.g. from not started to started. + * Callbacks needs to be implemented thread safe as they will be called from http task + */ +void sc_add_state_cb(void (*cb)()) { + state_cb_t *new_cb = malloc(sizeof(state_cb_t)); + if (new_cb == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for state callback"); + return; + } + new_cb->cb = cb; + new_cb->next = state_cb_head; + state_cb_head = new_cb; +} /** @@ -545,7 +609,7 @@ void server_settings_msg_received( dsp_processor_set_volome((double)server_settings_message->volume / 100); } #endif - set_mute_cb(server_settings_message->muted); + set_mute_state(server_settings_message->muted); } if (playing && scSet->volume != server_settings_message->volume) { @@ -973,6 +1037,46 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, } } +void update_state(bool *received_wire_chnk, bool *playback, bool paused) { + static int64_t last = 0; + static snapcast_state_t state = IDLE; //Todo + if ((paused || state != PLAYING) && (!paused || state != PAUSED) && *received_wire_chnk) { + xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + if (paused) { + sc_state = PAUSED; + *playback = false; + ESP_LOGI(TAG, "Set paused"); + }else{ + sc_state = PLAYING; + ESP_LOGI(TAG, "Set playing"); + *playback = true; + } + state = sc_state; + xSemaphoreGive(snapcastStateMux); + sc_call_state_cb(); + last = esp_timer_get_time(); + *received_wire_chnk = false; + } + else if (state == PLAYING || state == PAUSED) { + int64_t now = esp_timer_get_time(); + if (now-last > 1000000) { //update once per sec + if (!(*received_wire_chnk)) { + xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + sc_state = IDLE; + *playback = false; + state = sc_state; + xSemaphoreGive(snapcastStateMux); + sc_call_state_cb(); + ESP_LOGI(TAG, "Set idle"); + } + last = now; + *received_wire_chnk = false; + } + } + +} + + /* * returns: * 0 if a message was (partially) processed sucessfully @@ -981,9 +1085,12 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, int process_data(snapcast_protocol_parser_t *parser, time_sync_data_t *time_sync_data, bool *received_codec_header, codec_type_t *codec, snapcastSetting_t *scSet, - pcm_chunk_message_t **pcmData, player_state_e state) { + pcm_chunk_message_t **pcmData, bool *playback, bool paused) { base_message_t base_message_rx; + static bool received_wire_chnk = false; + update_state(&received_wire_chnk, playback, paused); + if (parse_base_message(parser, &base_message_rx) != PARSER_OK) { return -1; // restart connection } @@ -995,9 +1102,9 @@ int process_data(snapcast_protocol_parser_t *parser, switch (base_message_rx.type) { case SNAPCAST_MESSAGE_WIRE_CHUNK: { wire_chunk_message_t wire_chnk = {{0, 0}, 0, NULL}; // is wire_chnk.payload ever used? - + received_wire_chnk = true; // skip this wires chunk message if codec header message was not received yet! - if (*received_codec_header == false || state == PAUSED) { + if (*received_codec_header == false || paused) { if (parser_skip_typed_message(parser, &base_message_rx) != PARSER_OK) { return -1; } @@ -1034,7 +1141,7 @@ int process_data(snapcast_protocol_parser_t *parser, if (parse_sever_settings_message(parser, &base_message_rx, &server_settings_message) != PARSER_OK) { return -1; } - server_settings_msg_received(&server_settings_message, scSet, state == PLAYING); + server_settings_msg_received(&server_settings_message, scSet, *playback); return 0; } @@ -1082,12 +1189,6 @@ void before_receive_callback(before_receive_callback_data_t *data) { } } - -void http_player_state_changed() { - xTaskNotifyGive(t_http_get_task); - //ESP_LOGI(TAG, "http task cb"); -} - /** * */ @@ -1109,7 +1210,9 @@ static void http_get_task(void *pvParameters) { codec_type_t codec = NONE; snapcastSetting_t scSet; pcm_chunk_message_t *pcmData = NULL; - player_state_e player_state = IDLE; + uint32_t command = STOP; + bool paused = false; + bool playback = false; // create a timer to send time sync messages every x µs // esp_timer_create(&tSyncArgs, &time_sync_data.timeSyncMessageTimer); @@ -1121,8 +1224,6 @@ static void http_get_task(void *pvParameters) { esp_restart(); } - add_player_state_cb(http_player_state_changed); - while (1) { // do some house keeping { @@ -1156,6 +1257,21 @@ static void http_get_task(void *pvParameters) { } } + // block if state = STOPPED + xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + if (sc_state == STOPPED) { + xSemaphoreGive(snapcastStateMux); + command = STOP; + while(command != START) { + xTaskNotifyWait( 0, 0, &command, portMAX_DELAY); + } + xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + } + sc_state = IDLE; + xSemaphoreGive(snapcastStateMux); + sc_call_state_cb(); + playback = false; + // NETWORK setup ends here ( or before getting mac address ) setup_network(&connection.netif); @@ -1287,10 +1403,40 @@ static void http_get_task(void *pvParameters) { // Main connection loop - state machine + data processing while (1) { - if (ulTaskNotifyTake(pdTRUE, 1) == pdTRUE) { - // state change, e.g. pause/play - player_state_e state = get_player_state(); - if (state != player_state && state == PLAYING) { + bool restart = false; + static bool playback_old = false; + if (xTaskNotifyWait(0, 0, &command, 1) == pdTRUE) { + switch(command) { + case STOP: + xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + sc_state = STOPPED; + xSemaphoreGive(snapcastStateMux); + sc_call_state_cb(); + case RESTART: + restart = true; + break; + case UNPAUSE: + paused = false; + break; + case PAUSE: + paused = true; + break; + default: + break; + } + //ESP_LOGI(TAG, "http got cb. %s", paused ? "paused" : "playing/idle"); + } + if (restart) { + //restart required + netconn_close(lwipNetconn); + netconn_delete(lwipNetconn); + lwipNetconn = NULL; + break; // restart connection + } + + if (playback_old != playback) { + if (playback) { + // need to apply settings when starting to play #if SNAPCAST_USE_SOFT_VOL if (!scSet.muted) { dsp_processor_set_volome((double)scSet.volume / 100); @@ -1300,14 +1446,14 @@ static void http_get_task(void *pvParameters) { #else set_volume_cb(scSet.volume); #endif - set_mute_cb(scSet.muted); + set_mute_state(scSet.muted); } - player_state = state; - //ESP_LOGI(TAG, "http got cb. %s", paused ? "paused" : "playing/idle"); + playback_old = playback; } + int result = process_data(&parser, &time_sync_data, &received_codec_header, &codec, - &scSet, &pcmData, player_state); + &scSet, &pcmData, &playback, paused); if (result != 0) { break; // restart connection } @@ -1315,6 +1461,35 @@ static void http_get_task(void *pvParameters) { } } +/** + * + */ +int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool), i2s_std_gpio_config_t i2s_pin_config0, i2s_port_t I2S_NUM_0) { + if (set_volume == NULL) { + ESP_LOGE(TAG, "Volume callback is NULL"); + + return -1; + } + if (set_mute == NULL) { + ESP_LOGE(TAG, "Mute callback is NULL"); + + return -1; + } + set_volume_cb = set_volume; + set_mute_cb = set_mute; + if (snapcastStateMux == NULL) { + snapcastStateMux = xSemaphoreCreateMutex(); + } + init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute, player_state_paused); + + xTaskCreatePinnedToCore(&http_get_task, "http", 15 * 1024, NULL, + HTTP_TASK_PRIORITY, &t_http_get_task, + HTTP_TASK_CORE_ID); + + return 0; +} + + /** * */ @@ -1360,14 +1535,6 @@ void audio_set_mute(bool mute, bool set_state) { xSemaphoreGive(audioDACSemaphore); } -void player_set_mute(bool mute) { - audio_set_mute(mute, false); -} - -void set_mute_state(bool mute) { - audio_set_mute(mute, true); -} - /** * */ @@ -1380,9 +1547,9 @@ void audio_set_volume(int volume) { xSemaphoreGive(audioDACSemaphore); } -void player_state_changed() { - if (playerStateChangedMutex != NULL) { - xSemaphoreGive(playerStateChangedMutex); +void sc_state_changed() { + if (snapcastStateChangedMutex != NULL) { + xSemaphoreGive(snapcastStateChangedMutex); } ESP_LOGI(TAG, "main task cb"); } @@ -1555,14 +1722,14 @@ void app_main(void) { audioDAC_data.playerMute = true; audioDAC_data.volume = -1; - init_snapcast(audio_set_volume, set_mute_state); - init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute); - add_player_state_cb(player_state_changed); + init_snapcast(audio_set_volume, audio_set_mute, i2s_pin_config0, I2S_NUM_0); + //init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute); + sc_add_state_cb(sc_state_changed); // Create binary semaphore for player state change notification - playerStateChangedMutex = xSemaphoreCreateBinary(); - if (playerStateChangedMutex == NULL) { - ESP_LOGE(TAG, "Failed to create playerStateChangedMutex"); + snapcastStateChangedMutex = xSemaphoreCreateBinary(); + if (snapcastStateChangedMutex == NULL) { + ESP_LOGE(TAG, "Failed to create snapcastStateChangedMutex"); return; } @@ -1606,10 +1773,7 @@ void app_main(void) { xTaskCreatePinnedToCore(&ota_server_task, "ota", 14 * 256, NULL, OTA_TASK_PRIORITY, &t_ota_task, OTA_TASK_CORE_ID); - - xTaskCreatePinnedToCore(&http_get_task, "http", 15 * 1024, NULL, - HTTP_TASK_PRIORITY, &t_http_get_task, - HTTP_TASK_CORE_ID); + sc_start_snapcast(); // while (1) { // // audio_event_iface_msg_t msg; @@ -1637,13 +1801,13 @@ void app_main(void) { esp_pm_configure(&pmConfig); #endif audioDACdata_t dac_data; - player_state_e state = IDLE; + snapcast_state_t state = IDLE; while (1) { if (xQueueReceive(audioDACQHdl, &dac_data, pdMS_TO_TICKS(90)) == pdTRUE) { dac_control(board_handle, dac_data); } - if (xSemaphoreTake(playerStateChangedMutex, pdMS_TO_TICKS(10)) == pdTRUE) { - player_state_e state_new = get_player_state(); + if (xSemaphoreTake(snapcastStateChangedMutex, pdMS_TO_TICKS(10)) == pdTRUE) { + snapcast_state_t state_new = sc_get_snapcast_state(); ESP_LOGI(TAG, "main got cb: %d", state_new); if (state_new != state) { ESP_LOGI(TAG, "Player state changed: %d -> %d", state, state_new); From 49154583ec1f5e1bed3dab3ed56d5570642af9de Mon Sep 17 00:00:00 2001 From: raul Date: Sun, 29 Mar 2026 15:44:05 +0200 Subject: [PATCH 12/16] Add i2s lock --- components/lightsnapcast/include/player.h | 2 +- components/lightsnapcast/player.c | 31 +++++++++++++++-------- main/main.c | 27 +++++++++++++++----- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index 1be896b2..49ece93e 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -68,7 +68,7 @@ typedef struct snapcastSetting_s { uint32_t pcmBufSize; } snapcastSetting_t; -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool)); +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool), bool (*lock)(bool, TickType_t)); int deinit_player(void); int start_player(snapcastSetting_t *setting); void pause_player(bool pause); diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index d20ebe1a..cd9ac623 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -124,8 +124,8 @@ bool playerPaused = false; static SemaphoreHandle_t playerStateMux = NULL; static void (*state_cb)(bool) = NULL; - static void (*audio_set_mute)(bool mute); +static bool (*lock_i2s)(bool, TickType_t) = NULL; static i2s_chan_handle_t tx_chan = NULL; // I2S tx channel handler static bool i2sEnabled = false; @@ -215,7 +215,14 @@ static void ensure_noiseless(i2s_chan_handle_t tx) { /** * */ -static esp_err_t player_setup_i2s(snapcastSetting_t *setting) { +static esp_err_t player_setup_i2s(snapcastSetting_t *setting, bool lock) { + + if (lock_i2s != NULL && lock) { + if (lock_i2s(true, pdMS_TO_TICKS(10)) != pdTRUE) { + return -1; + } + } + // ensure save setting int32_t sr = setting->sr; if (sr == 0) { @@ -419,6 +426,9 @@ int deinit_player(void) { i2s_del_channel(tx_chan); tx_chan = NULL; } + if (lock_i2s != NULL) { + lock_i2s(false, 0); + } if (playerStateMux != NULL) { vSemaphoreDelete(playerStateMux); @@ -458,16 +468,15 @@ int deinit_player(void) { /** * call before http task creation! */ -int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool)) { +int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool), bool (*lock)(bool, TickType_t)) { int ret = 0; if (set_mute_cb == NULL) { ESP_LOGE(TAG, "set_mute_cb is NULL"); return -1; } audio_set_mute = set_mute_cb; - if (cb != NULL) { - state_cb = cb; - } + state_cb = cb; // can be NULL + lock_i2s = lock; // can be NULL deinit_player(); @@ -552,7 +561,7 @@ int start_player(snapcastSetting_t *setting) { playerStarted = true; int ret = 0; - ret = player_setup_i2s(setting); + ret = player_setup_i2s(setting, true); if (ret < 0) { ESP_LOGE(TAG, "player_setup_i2s failed: %d", ret); playerStarted = false; @@ -1575,7 +1584,7 @@ static void player_task(void *pvParameters) { audio_set_mute(true); my_i2s_channel_disable(tx_chan); - ret = player_setup_i2s(&__scSet); + ret = player_setup_i2s(&__scSet, false); if (ret < 0) { ESP_LOGE(TAG, "player_setup_i2s failed: %d", ret); @@ -2166,7 +2175,9 @@ static void player_task(void *pvParameters) { my_i2s_channel_disable(tx_chan); i2s_del_channel(tx_chan); tx_chan = NULL; - ret = 0; + if (lock_i2s != NULL) { + lock_i2s(false, 0); + } xSemaphoreTake(playerStateMux, portMAX_DELAY); xSemaphoreTake(snapcastSettingsMux, portMAX_DELAY); // delete the queue @@ -2178,7 +2189,7 @@ static void player_task(void *pvParameters) { esp_pm_lock_release(player_pm_lock_handle); #endif - ret = destroy_pcm_queue(&pcmChkQHdl); + destroy_pcm_queue(&pcmChkQHdl); tg0_timer_deinit(); playerStarted = false; diff --git a/main/main.c b/main/main.c index 0ba55244..c895c49e 100644 --- a/main/main.c +++ b/main/main.c @@ -106,8 +106,8 @@ static const char *TAG = "SC"; // static QueueHandle_t playerChunkQueueHandle = NULL; SemaphoreHandle_t timeSyncSemaphoreHandle = NULL; -SemaphoreHandle_t idCounterSemaphoreHandle = NULL; -SemaphoreHandle_t snapcastStateChangedMutex = NULL; +static SemaphoreHandle_t idCounterSemaphoreHandle = NULL; +static SemaphoreHandle_t snapcastStateChangedMutex = NULL; typedef struct audioDACdata_s { bool playerMute; @@ -121,6 +121,7 @@ static SemaphoreHandle_t audioDACSemaphore = NULL; static void (*set_volume_cb)(int volume); static void (*set_mute_cb)(bool mute, bool state); static SemaphoreHandle_t snapcastStateMux = NULL; +static SemaphoreHandle_t i2sLockMutex = NULL; void time_sync_msg_cb(void *args); @@ -502,7 +503,7 @@ void error_callback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatusString[status]); } -typedef enum { IDLE = 0, STOPPED, PLAYING, PAUSED } snapcast_state_t; //defined in player.h +typedef enum { STOPPED = 0, IDLE, PLAYING, PAUSED } snapcast_state_t; //defined in player.h typedef enum { STOP = 0, START, RESTART, PAUSE, UNPAUSE } snapcast_commands_t; typedef struct state_cb_s { @@ -1464,7 +1465,7 @@ static void http_get_task(void *pvParameters) { /** * */ -int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool), i2s_std_gpio_config_t i2s_pin_config0, i2s_port_t I2S_NUM_0) { +int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool), i2s_std_gpio_config_t i2s_pin_config0, i2s_port_t I2S_NUM_0, bool (*lock)(bool, TickType_t)) { if (set_volume == NULL) { ESP_LOGE(TAG, "Volume callback is NULL"); @@ -1480,7 +1481,7 @@ int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool), i2s_std if (snapcastStateMux == NULL) { snapcastStateMux = xSemaphoreCreateMutex(); } - init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute, player_state_paused); + init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute, player_state_paused, lock); xTaskCreatePinnedToCore(&http_get_task, "http", 15 * 1024, NULL, HTTP_TASK_PRIORITY, &t_http_get_task, @@ -1554,6 +1555,18 @@ void sc_state_changed() { ESP_LOGI(TAG, "main task cb"); } +bool i2s_lock(bool lock, TickType_t wait) { + if (i2sLockMutex == NULL) { + return false; + } + if (lock) { + return xSemaphoreTake(i2sLockMutex, wait); + } + else { + return xSemaphoreGive(i2sLockMutex); + } +} + /** * */ @@ -1722,7 +1735,9 @@ void app_main(void) { audioDAC_data.playerMute = true; audioDAC_data.volume = -1; - init_snapcast(audio_set_volume, audio_set_mute, i2s_pin_config0, I2S_NUM_0); + i2sLockMutex = xSemaphoreCreateBinary(); + + init_snapcast(audio_set_volume, audio_set_mute, i2s_pin_config0, I2S_NUM_0, i2s_lock); //init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute); sc_add_state_cb(sc_state_changed); From a274fa1bf42901000ab826c5790ede6cb04118c4 Mon Sep 17 00:00:00 2001 From: raul Date: Mon, 30 Mar 2026 15:57:03 +0200 Subject: [PATCH 13/16] Refactor handling of scSettings object --- components/dsp_processor/dsp_processor.c | 17 +- .../dsp_processor/include/dsp_processor.h | 2 +- components/lightsnapcast/include/player.h | 25 +-- components/lightsnapcast/player.c | 183 +++++------------- main/main.c | 132 +++++++------ 5 files changed, 134 insertions(+), 225 deletions(-) diff --git a/components/dsp_processor/dsp_processor.c b/components/dsp_processor/dsp_processor.c index 9874ca8f..11d44591 100644 --- a/components/dsp_processor/dsp_processor.c +++ b/components/dsp_processor/dsp_processor.c @@ -259,23 +259,13 @@ static inline int16_t float_to_int16_clamped(float value) { /** * */ -int dsp_processor_worker(void *p_pcmChnk, const void *p_scSet) { +int dsp_processor_worker(char *pcmChnk, uint16_t len, uint32_t samplerate, int ch) { ESP_LOGV(TAG, "%s: processing audio chunk", __func__); - const snapcastSetting_t *scSet = (const snapcastSetting_t *)p_scSet; - pcm_chunk_message_t *pcmChnk = (pcm_chunk_message_t *)p_pcmChnk; - uint32_t samplerate = scSet->sr; - if (!pcmChnk || !pcmChnk->fragment->payload) { + if (!pcmChnk) { return -1; } - int bits = scSet->bits; - int ch = scSet->ch; - - if (bits == 0) { - bits = 16; - } - if (ch == 0) { ch = 2; } @@ -285,12 +275,11 @@ int dsp_processor_worker(void *p_pcmChnk, const void *p_scSet) { ESP_LOGW(TAG, "%s: Sample rate is not set, using default: %lu", __func__, (unsigned long)samplerate); } - int16_t len = pcmChnk->fragment->size / ((bits / 8) * ch); int16_t valint; uint16_t i; // volatile needed to ensure 32 bit access volatile uint32_t *audio_tmp = - (volatile uint32_t *)(pcmChnk->fragment->payload); + (volatile uint32_t *)(pcmChnk); // Local working copy of filter parameters static filterParams_t currentFilterParams = {0}; diff --git a/components/dsp_processor/include/dsp_processor.h b/components/dsp_processor/include/dsp_processor.h index 5099dcf4..5f9c3b3c 100644 --- a/components/dsp_processor/include/dsp_processor.h +++ b/components/dsp_processor/include/dsp_processor.h @@ -25,7 +25,7 @@ enum filtertypes { void dsp_processor_init(void); void dsp_processor_uninit(void); -int dsp_processor_worker(void *pcmChnk, const void *scSet); +int dsp_processor_worker(char *pcmChnk, uint16_t len, uint32_t samplerate, int ch); esp_err_t dsp_processor_update_filter_params(filterParams_t *params); void dsp_processor_set_volome(double volume); diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index 49ece93e..e50abfe0 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -51,30 +51,20 @@ typedef struct pcmData { typedef enum codec_type_e { NONE = 0, PCM, FLAC, OGG, OPUS } codec_type_t; -typedef struct snapcastSetting_s { - uint32_t buf_ms; - uint32_t chkInFrames; - int32_t cDacLat_ms; - - codec_type_t codec; +typedef struct playerSetting_s { + uint16_t buf_ms; + uint16_t chkInFrames; + int16_t cDacLat_ms; int32_t sr; uint8_t ch; i2s_data_bit_width_t bits; - - bool muted; - uint32_t volume; - - char *pcmBuf; - uint32_t pcmBufSize; -} snapcastSetting_t; +} playerSetting_t; int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool), bool (*lock)(bool, TickType_t)); int deinit_player(void); -int start_player(snapcastSetting_t *setting); +int start_player(void); void pause_player(bool pause); -void call_state_cb(void); - int32_t allocate_pcm_chunk_memory(pcm_chunk_message_t **pcmChunk, size_t bytes); int32_t insert_pcm_chunk(pcm_chunk_message_t *pcmChunk); @@ -90,8 +80,7 @@ int32_t player_latency_insert(int64_t newValue); int32_t get_diff_to_server(int64_t *tDiff, int64_t now); int32_t latency_buffer_full(bool *is_full); -int32_t player_send_snapcast_setting(snapcastSetting_t *setting); -int8_t player_get_snapcast_settings(snapcastSetting_t *setting); +int32_t player_send_snapcast_setting(playerSetting_t *setting); int32_t reset_latency_buffer(void); diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index cd9ac623..3ad0a9dd 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -106,9 +106,7 @@ static QueueHandle_t snapcastSettingQueueHandle = NULL; static uint32_t i2sDmaBufCnt; static uint32_t i2sDmaBufMaxLen; - -static SemaphoreHandle_t snapcastSettingsMux = NULL; -static snapcastSetting_t currentSnapcastSetting; +static playerSetting_t *scSet; // should be used only from http_task static void tg0_timer_init(void); static void tg0_timer_deinit(void); @@ -118,7 +116,6 @@ static bool gpTimerRunning = false; static void player_task(void *pvParameters); //player state -bool gotSettings = false; bool playerStarted = false; bool playerPaused = false; static SemaphoreHandle_t playerStateMux = NULL; @@ -215,7 +212,7 @@ static void ensure_noiseless(i2s_chan_handle_t tx) { /** * */ -static esp_err_t player_setup_i2s(snapcastSetting_t *setting, bool lock) { +static esp_err_t player_setup_i2s(playerSetting_t *setting, bool lock) { if (lock_i2s != NULL && lock) { if (lock_i2s(true, pdMS_TO_TICKS(10)) != pdTRUE) { @@ -434,9 +431,10 @@ int deinit_player(void) { vSemaphoreDelete(playerStateMux); playerStateMux = NULL; } - if (snapcastSettingsMux != NULL) { - vSemaphoreDelete(snapcastSettingsMux); - snapcastSettingsMux = NULL; + if (snapcastSettingQueueHandle != NULL) { + // delete the queue + vQueueDelete(snapcastSettingQueueHandle); + snapcastSettingQueueHandle = NULL; } ret = destroy_pcm_queue(&pcmChkQHdl); @@ -465,15 +463,24 @@ int deinit_player(void) { return ret; } +void call_state_cb(void) { + if (state_cb != NULL) { + xSemaphoreTake(playerStateMux, portMAX_DELAY); + bool paused = playerPaused; + xSemaphoreGive(playerStateMux); + state_cb(paused); + } +} + /** * call before http task creation! */ int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*set_mute_cb)(bool), void (*cb)(bool), bool (*lock)(bool, TickType_t)) { - int ret = 0; if (set_mute_cb == NULL) { ESP_LOGE(TAG, "set_mute_cb is NULL"); return -1; } + audio_set_mute = set_mute_cb; state_cb = cb; // can be NULL lock_i2s = lock; // can be NULL @@ -484,31 +491,14 @@ int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*s pin_config0 = pin_config0_; i2sNum = i2sNum_; - currentSnapcastSetting.buf_ms = 0; - currentSnapcastSetting.chkInFrames = 0; - currentSnapcastSetting.codec = NONE; - currentSnapcastSetting.sr = 0; - currentSnapcastSetting.ch = 0; - currentSnapcastSetting.bits = 0; - - if (snapcastSettingsMux == NULL) { - snapcastSettingsMux = xSemaphoreCreateMutex(); - xSemaphoreGive(snapcastSettingsMux); - } + // create message queue to inform task of changed settings + snapcastSettingQueueHandle = xQueueCreate(1, sizeof(playerSetting_t)); if (playerStateMux == NULL) { playerStateMux = xSemaphoreCreateMutex(); xSemaphoreGive(playerStateMux); } - /** - ret = player_setup_i2s(¤tSnapcastSetting); - if (ret < 0) { - ESP_LOGE(TAG, "player_setup_i2s failed: %d", ret); - - return -1; - }*/ - // create semaphore for time diff buffer to server if (latencyBufSemaphoreHandle == NULL) { latencyBufSemaphoreHandle = xSemaphoreCreateMutex(); @@ -549,7 +539,7 @@ int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*s /** * call to start the player task */ -int start_player(snapcastSetting_t *setting) { +int start_player() { if (xSemaphoreTake(playerStateMux, 0) != pdTRUE) { // shutdown in progress, don't start player return -1; @@ -558,10 +548,14 @@ int start_player(snapcastSetting_t *setting) { xSemaphoreGive(playerStateMux); return -1; } + if (scSet == NULL || !(( scSet->buf_ms > 0) && (scSet->chkInFrames > 0))) { + xSemaphoreGive(playerStateMux); + return -1; + } playerStarted = true; int ret = 0; - ret = player_setup_i2s(setting, true); + ret = player_setup_i2s(scSet, true); if (ret < 0) { ESP_LOGE(TAG, "player_setup_i2s failed: %d", ret); playerStarted = false; @@ -582,27 +576,15 @@ int start_player(snapcastSetting_t *setting) { esp_pm_lock_acquire(player_pm_lock_handle); #endif - // create message queue to inform task of changed settings - snapcastSettingQueueHandle = xQueueCreate(1, sizeof(uint8_t)); - if (pcmChkQHdl == NULL) { - snapcastSetting_t scSet; - memset(&scSet, 0, sizeof(snapcastSetting_t)); - player_get_snapcast_settings(&scSet); - // ensure we don't have a divide by zero situation - uint32_t chkInFrames = scSet.chkInFrames; - if (chkInFrames == 0) { - chkInFrames = 1152; // choose a good default for now - } - - int entries = ceil(((float)scSet.sr / (float)chkInFrames) * - ((float)scSet.buf_ms / 1000)); + 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) / chkInFrames); + entries -= ((i2sDmaBufMaxLen * i2sDmaBufCnt) / scSet->chkInFrames); pcmChkQHdl = xQueueCreate(entries, sizeof(pcm_chunk_message_t *)); @@ -611,7 +593,7 @@ int start_player(snapcastSetting_t *setting) { ESP_LOGI(TAG, "Start player_task"); - xTaskCreatePinnedToCore(player_task, "player", 1024 * 3, NULL, + xTaskCreatePinnedToCore(player_task, "player", 1024 * 3, (void*) scSet, SYNC_TASK_PRIORITY, &playerTaskHandle, SYNC_TASK_CORE_ID); @@ -637,45 +619,6 @@ void pause_player(bool pause) { } } -void call_state_cb(void) { - if (state_cb != NULL) { - xSemaphoreTake(playerStateMux, portMAX_DELAY); - bool paused = playerPaused; - xSemaphoreGive(playerStateMux); - state_cb(paused); - } -} - -/** - * - */ -int8_t player_set_snapcast_settings(snapcastSetting_t *setting) { - int8_t ret = pdPASS; - - xSemaphoreTake(snapcastSettingsMux, portMAX_DELAY); - - memcpy(¤tSnapcastSetting, setting, sizeof(snapcastSetting_t)); - - xSemaphoreGive(snapcastSettingsMux); - - return ret; -} - -/** - * - */ -int8_t player_get_snapcast_settings(snapcastSetting_t *setting) { - int8_t ret = pdPASS; - - xSemaphoreTake(snapcastSettingsMux, portMAX_DELAY); - - memcpy(setting, ¤tSnapcastSetting, sizeof(snapcastSetting_t)); - - xSemaphoreGive(snapcastSettingsMux); - - return ret; -} - #if USE_TIMEFILTER /** * @@ -737,42 +680,23 @@ int32_t player_latency_insert(int64_t newValue) { /** * */ -int32_t player_send_snapcast_setting(snapcastSetting_t *setting) { +int32_t player_send_snapcast_setting(playerSetting_t *setting) { int ret; - snapcastSetting_t curSet; - uint8_t settingChanged = 1; - ret = player_get_snapcast_settings(&curSet); - - if ((curSet.bits != setting->bits) || (curSet.buf_ms != setting->buf_ms) || - (curSet.ch != setting->ch) || - (curSet.chkInFrames != setting->chkInFrames) || - (curSet.codec != setting->codec) || (curSet.sr != setting->sr) || - (curSet.cDacLat_ms != setting->cDacLat_ms)) { - ret = player_set_snapcast_settings(setting); + if ((playerTaskHandle != NULL) && (snapcastSettingQueueHandle != NULL)) { + ret = xQueueOverwrite(snapcastSettingQueueHandle, setting); if (ret != pdPASS) { ESP_LOGE(TAG, - "player_send_snapcast_setting: couldn't change " - "snapcast setting"); - } - - // notify needed - if ((playerTaskHandle != NULL) && (snapcastSettingQueueHandle != NULL)) { - ret = xQueueOverwrite(snapcastSettingQueueHandle, &settingChanged); - if (ret != pdPASS) { - ESP_LOGE(TAG, - "player_send_snapcast_setting: couldn't notify " - "snapcast setting"); - } else { - ESP_LOGI(TAG, - "got settings and notified player_task"); - } + "player_send_snapcast_setting: couldn't notify " + "snapcast setting"); + } else { + ESP_LOGI(TAG, + "got settings and notified player_task"); } - } - - if (!gotSettings && (setting->bits > 0) && ( setting->buf_ms > 0) && (setting->ch > 0) && - (setting->chkInFrames > 0) && (setting->sr > 0)) { - gotSettings = true; + } else if (scSet == NULL) { + scSet = setting; + ESP_LOGI(TAG, + "got initial settings"); } return pdPASS; @@ -1416,14 +1340,8 @@ int32_t insert_pcm_chunk(pcm_chunk_message_t *pcmChunk) { } if (pcmChkQHdl == NULL) { ESP_LOGW(TAG, "pcm chunk queue not created. Player started: %s", playerStarted ? "True": "False"); - free_pcm_chunk(pcmChunk); - - snapcastSetting_t curSet; - player_get_snapcast_settings(&curSet); - if (gotSettings) { - start_player(&curSet); - } + start_player(); return -2; } @@ -1485,8 +1403,7 @@ static void player_task(void *pvParameters) { char *p_payload = NULL; size_t size = 0; uint32_t notifiedValue; - snapcastSetting_t scSet; - uint8_t scSetChgd = 0; + playerSetting_t scSet; uint64_t timer_val; int initialSync = 0; int dir = 0; @@ -1505,8 +1422,7 @@ static void player_task(void *pvParameters) { uint64_t samples_written = 0; UBaseType_t uxHighWaterMark; - memset(&scSet, 0, sizeof(snapcastSetting_t)); - player_get_snapcast_settings(&scSet); + memcpy(&scSet, (playerSetting_t*)pvParameters, sizeof(playerSetting_t)); ESP_LOGI(TAG, "started sync task"); @@ -1566,11 +1482,9 @@ static void player_task(void *pvParameters) { // check if we got changed setting available, if so we need to // reinitialize - ret = xQueueReceive(snapcastSettingQueueHandle, &scSetChgd, 0); + playerSetting_t __scSet; + ret = xQueueReceive(snapcastSettingQueueHandle, &__scSet, 0); if (ret == pdTRUE) { - snapcastSetting_t __scSet; - - player_get_snapcast_settings(&__scSet); if ((__scSet.buf_ms > 0) && (__scSet.chkInFrames > 0) && (__scSet.sr > 0)) { @@ -1580,6 +1494,8 @@ static void player_task(void *pvParameters) { if ((scSet.sr != __scSet.sr) || (scSet.bits != __scSet.bits) || (scSet.ch != __scSet.ch)) { + ESP_LOGI(TAG, "reinitializing i2s with new settings: sample rate %ld, ch %d, bits %d", + __scSet.sr, __scSet.ch, __scSet.bits); my_i2s_channel_enable(tx_chan); audio_set_mute(true); my_i2s_channel_disable(tx_chan); @@ -2179,11 +2095,6 @@ static void player_task(void *pvParameters) { lock_i2s(false, 0); } xSemaphoreTake(playerStateMux, portMAX_DELAY); - xSemaphoreTake(snapcastSettingsMux, portMAX_DELAY); - // delete the queue - vQueueDelete(snapcastSettingQueueHandle); - snapcastSettingQueueHandle = NULL; - xSemaphoreGive(snapcastSettingsMux); #if CONFIG_PM_ENABLE esp_pm_lock_release(player_pm_lock_handle); diff --git a/main/main.c b/main/main.c index c895c49e..07850886 100644 --- a/main/main.c +++ b/main/main.c @@ -109,6 +109,13 @@ SemaphoreHandle_t timeSyncSemaphoreHandle = NULL; static SemaphoreHandle_t idCounterSemaphoreHandle = NULL; static SemaphoreHandle_t snapcastStateChangedMutex = NULL; +typedef struct snapcastSetting_s { + playerSetting_t playerSetting; + + bool muted; + uint32_t volume; +} snapcastSetting_t; + typedef struct audioDACdata_s { bool playerMute; bool stateMute; @@ -317,10 +324,10 @@ void time_sync_msg_received(base_message_t *base_message_rx, static FLAC__StreamDecoderReadStatus read_callback( const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data) { - snapcastSetting_t *scSet = (snapcastSetting_t *)client_data; + //snapcastSetting_t *scSet = (snapcastSetting_t *)client_data; // decoderData_t *flacData; - (void)scSet; + //(void)scSet; // xQueueReceive(decoderReadQHdl, &flacData, portMAX_DELAY); // if (xQueueReceive(decoderReadQHdl, &flacData, pdMS_TO_TICKS(100))) @@ -386,7 +393,7 @@ static FLAC__StreamDecoderWriteStatus write_callback( const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) { size_t i; - snapcastSetting_t *scSet = (snapcastSetting_t *)client_data; + playerSetting_t *scSet = (playerSetting_t *)client_data; size_t bytes = frame->header.blocksize * frame->header.channels * frame->header.bits_per_sample / 8; @@ -473,7 +480,7 @@ static FLAC__StreamDecoderWriteStatus write_callback( void metadata_callback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) { - snapcastSetting_t *scSet = (snapcastSetting_t *)client_data; + playerSetting_t *scSet = (playerSetting_t *)client_data; (void)decoder; @@ -626,12 +633,12 @@ void server_settings_msg_received( scSet->muted = server_settings_message->muted; scSet->volume = server_settings_message->volume; - if (scSet->cDacLat_ms != server_settings_message->latency || - scSet->buf_ms != server_settings_message->buffer_ms) { - scSet->cDacLat_ms = server_settings_message->latency; - scSet->buf_ms = server_settings_message->buffer_ms; + if (scSet->playerSetting.cDacLat_ms != server_settings_message->latency || + scSet->playerSetting.buf_ms != server_settings_message->buffer_ms) { + scSet->playerSetting.cDacLat_ms = server_settings_message->latency; + scSet->playerSetting.buf_ms = server_settings_message->buffer_ms; - if (player_send_snapcast_setting(scSet) != pdPASS) { + if (playing && player_send_snapcast_setting(&(scSet->playerSetting)) != pdPASS) { ESP_LOGE(TAG, "Failed to notify sync task. " "Did you init player?"); @@ -646,7 +653,7 @@ void server_settings_msg_received( * */ void codec_header_received(char *codecPayload, uint32_t codecPayloadLen, - codec_type_t codec, snapcastSetting_t *scSet, + codec_type_t codec, playerSetting_t *scSet, time_sync_data_t *time_sync_data) { // first ensure everything is set up // correctly and resources are @@ -672,7 +679,6 @@ void codec_header_received(char *codecPayload, uint32_t codecPayloadLen, memcpy(&bits, codecPayload + 8, sizeof(bits)); memcpy(&channels, codecPayload + 10, sizeof(channels)); - scSet->codec = codec; scSet->bits = bits; scSet->ch = channels; scSet->sr = rate; @@ -731,7 +737,6 @@ void codec_header_received(char *codecPayload, uint32_t codecPayloadLen, memcpy(&rate, codecPayload + 24, sizeof(rate)); memcpy(&bits, codecPayload + 34, sizeof(bits)); - scSet->codec = codec; scSet->bits = bits; scSet->ch = channels; scSet->sr = rate; @@ -769,9 +774,10 @@ void codec_header_received(char *codecPayload, uint32_t codecPayloadLen, /** * */ -void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, +void handle_chunk_message(codec_type_t codec, playerSetting_t *scSet, pcm_chunk_message_t **pcmData, wire_chunk_message_t *wire_chnk) { + static uint32_t chkInFrames = 0; switch (codec) { case OPUS: { int frame_size = -1; @@ -827,6 +833,20 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, // ESP_LOGW(TAG, "OPUS decode: %d", frame_size); + if (chkInFrames != scSet->chkInFrames) { + if (player_send_snapcast_setting(scSet) != pdPASS) { + ESP_LOGE(TAG, + "Failed to notify " + "sync task about " + "codec. Did you " + "init player?"); + + // critical error + esp_restart(); + } + chkInFrames = scSet->chkInFrames; + } + if (allocate_pcm_chunk_memory(&new_pcmChunk, bytes) < 0) { *pcmData = NULL; } else { @@ -853,24 +873,15 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, #if CONFIG_USE_DSP_PROCESSOR if (new_pcmChunk->fragment->payload) { - dsp_processor_worker((void *)new_pcmChunk, (void *)scSet); + dsp_processor_worker(new_pcmChunk->fragment->payload, + new_pcmChunk->fragment->size / ((scSet->bits / 8) * scSet->ch), + scSet->sr, scSet->ch); } #endif insert_pcm_chunk(new_pcmChunk); } - if (player_send_snapcast_setting(scSet) != pdPASS) { - ESP_LOGE(TAG, - "Failed to notify " - "sync task about " - "codec. Did you " - "init player?"); - - // critical error - esp_restart(); - } - break; } @@ -914,6 +925,21 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, // ESP_LOGI(TAG, "new_pcmChunk with size %ld", // new_pcmChunk->totalSize); + + if (chkInFrames != scSet->chkInFrames) { + if (player_send_snapcast_setting(scSet) != pdPASS) { + ESP_LOGE(TAG, + "Failed to notify " + "sync task about " + "codec. Did you " + "init player?"); + + // critical error + esp_restart(); + } + chkInFrames = scSet->chkInFrames; + } + if (ret == 0) { pcm_chunk_fragment_t *fragment = new_pcmChunk->fragment; uint32_t fragmentCnt = 0; @@ -948,7 +974,9 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, #if CONFIG_USE_DSP_PROCESSOR if (new_pcmChunk->fragment->payload) { - dsp_processor_worker((void *)new_pcmChunk, (void *)scSet); + dsp_processor_worker(new_pcmChunk->fragment->payload, + new_pcmChunk->fragment->size / ((scSet->bits / 8) * scSet->ch), + scSet->sr, scSet->ch); } #endif @@ -965,19 +993,6 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, pcmChunk.outData = NULL; pcmChunk.bytes = 0; - if (player_send_snapcast_setting(scSet) != pdPASS) { - ESP_LOGE(TAG, - "Failed to " - "notify " - "sync task " - "about " - "codec. Did you " - "init player?"); - - // critical error - esp_restart(); - } - break; } @@ -999,20 +1014,26 @@ void handle_chunk_message(codec_type_t codec, snapcastSetting_t *scSet, // "got PCM decoded chunk size: %ld // frames", scSet->chkInFrames); - if (player_send_snapcast_setting(scSet) != pdPASS) { - ESP_LOGE(TAG, - "Failed to notify " - "sync task about " - "codec. Did you " - "init player?"); - // critical error - esp_restart(); + if (chkInFrames != scSet->chkInFrames) { + if (player_send_snapcast_setting(scSet) != pdPASS) { + ESP_LOGE(TAG, + "Failed to notify " + "sync task about " + "codec. Did you " + "init player?"); + + // critical error + esp_restart(); + } + chkInFrames = scSet->chkInFrames; } #if CONFIG_USE_DSP_PROCESSOR if ((*pcmData) && ((*pcmData)->fragment->payload)) { - dsp_processor_worker((void *)(*pcmData), (void *)scSet); + dsp_processor_worker((*pcmData)->fragment->payload, + (*pcmData)->fragment->size / ((scSet->bits / 8) * scSet->ch), + scSet->sr, scSet->ch); } #endif if (*pcmData) { @@ -1115,7 +1136,7 @@ int process_data(snapcast_protocol_parser_t *parser, if (parse_wire_chunk_message(parser, &base_message_rx, *codec, pcmData, &wire_chnk, &decoderChunk) != PARSER_OK) { return -1; } - handle_chunk_message(*codec, scSet, pcmData, &wire_chnk); + handle_chunk_message(*codec, &(scSet->playerSetting), pcmData, &wire_chnk); return 0; } @@ -1126,7 +1147,7 @@ int process_data(snapcast_protocol_parser_t *parser, if (parse_codec_header_message(parser, received_codec_header, codec, &codecPayload, &codecPayloadLen) != PARSER_OK) { return_value = -1; } else { - codec_header_received(codecPayload, codecPayloadLen, *codec, scSet, time_sync_data); + codec_header_received(codecPayload, codecPayloadLen, *codec, &(scSet->playerSetting), time_sync_data); } // in all cases: free Payload @@ -1368,12 +1389,11 @@ static void http_get_task(void *pvParameters) { hello_message_serialized = NULL; // init default setting - scSet.buf_ms = 500; - scSet.codec = NONE; - scSet.bits = 16; - scSet.ch = 2; - scSet.sr = 44100; - scSet.chkInFrames = 0; + scSet.playerSetting.buf_ms = 0; + scSet.playerSetting.bits = 16; + scSet.playerSetting.ch = 2; + scSet.playerSetting.sr = 44100; + scSet.playerSetting.chkInFrames = 0; scSet.muted = true; snapcast_protocol_parser_t parser; From b3907e7dd4ee322688a5c76ab0d4014c6b012210 Mon Sep 17 00:00:00 2001 From: raul Date: Tue, 12 May 2026 22:28:02 +0200 Subject: [PATCH 14/16] Stop snapclient on OTA, rename functions, add docu. --- components/ota_server/ota_server.c | 3 + main/main.c | 112 +++++++++++++++++------------ 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/components/ota_server/ota_server.c b/components/ota_server/ota_server.c index b2c4501d..172d15e8 100644 --- a/components/ota_server/ota_server.c +++ b/components/ota_server/ota_server.c @@ -22,6 +22,7 @@ #include "player.h" extern TaskHandle_t t_http_get_task; +extern void sc_stop_snapclient(); const int OTA_CONNECTED_BIT = BIT0; static const char *TAG = "OTA"; @@ -162,6 +163,8 @@ void ota_server_start_my(void) { // SuspendAllThreads(); // KillAllThreads(); // dsp_i2s_task_deinit(); + sc_stop_snapclient(); + vTaskDelay(1000 / portTICK_PERIOD_MS); // give snapclient some time to stop before we kill the http task that might be using the player vTaskDelete(t_http_get_task); deinit_player(); // ensure this is called after http_task was killed diff --git a/main/main.c b/main/main.c index 07850886..11090d6d 100644 --- a/main/main.c +++ b/main/main.c @@ -107,7 +107,7 @@ static const char *TAG = "SC"; SemaphoreHandle_t timeSyncSemaphoreHandle = NULL; static SemaphoreHandle_t idCounterSemaphoreHandle = NULL; -static SemaphoreHandle_t snapcastStateChangedMutex = NULL; +static SemaphoreHandle_t snapclientStateChangedMutex = NULL; typedef struct snapcastSetting_s { playerSetting_t playerSetting; @@ -127,7 +127,7 @@ static QueueHandle_t audioDACQHdl = NULL; static SemaphoreHandle_t audioDACSemaphore = NULL; static void (*set_volume_cb)(int volume); static void (*set_mute_cb)(bool mute, bool state); -static SemaphoreHandle_t snapcastStateMux = NULL; +static SemaphoreHandle_t snapclientStateMux = NULL; static SemaphoreHandle_t i2sLockMutex = NULL; void time_sync_msg_cb(void *args); @@ -510,32 +510,48 @@ void error_callback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatusString[status]); } -typedef enum { STOPPED = 0, IDLE, PLAYING, PAUSED } snapcast_state_t; //defined in player.h -typedef enum { STOP = 0, START, RESTART, PAUSE, UNPAUSE } snapcast_commands_t; +/** + * Snapclient state functions and types + */ + +typedef enum { STOPPED = 0, IDLE, PLAYING, PAUSED } snapclient_state_t; //defined in player.h +typedef enum { STOP = 0, START, RESTART, PAUSE, UNPAUSE } snapclient_commands_t; typedef struct state_cb_s { void (*cb)(void); struct state_cb_s *next; } state_cb_t; -static snapcast_state_t sc_state = STOPPED; +static snapclient_state_t sc_state = STOPPED; static state_cb_t *state_cb_head = NULL; +/** + * Set mute state called by player + */ void player_set_mute(bool mute) { set_mute_cb(mute, false); } +/** + * Set mute state called by snapclient + */ void set_mute_state(bool mute) { set_mute_cb(mute, true); } -void sc_send_command(snapcast_commands_t command) { +/** + * Sends command to http task, e.g. to start snapclient or pause player. + */ +void sc_send_command(snapclient_commands_t command) { if (t_http_get_task != NULL) { xTaskNotify(t_http_get_task, (uint32_t) command, eSetValueWithOverwrite); } } -void player_state_paused(bool paused) { +/** + * Callback for player state changes, e.g. to send pause command to snapclient when player is paused. + */ +void on_player_state_paused(bool paused) { if (paused) { sc_send_command(PAUSE); } else { @@ -543,29 +559,35 @@ void player_state_paused(bool paused) { } } -void sc_start_snapcast() { +void sc_start_snapclient() { sc_send_command(START); } -void sc_restart_snapcast() { +void sc_restart_snapclient() { sc_send_command(RESTART); } -void sc_stop_snapcast() { +void sc_stop_snapclient() { sc_send_command(STOP); } -void sc_pause_snapcast(bool pause) { +/** + * Pause snapclient. This will first pause the player. Snapclient is then paused by the player callback. + */ +void sc_pause_snapclient(bool pause) { pause_player(pause); if (!pause) { //sc_send_command(UNPAUSE); } } -snapcast_state_t sc_get_snapcast_state(void) { - xSemaphoreTake(snapcastStateMux, portMAX_DELAY); - snapcast_state_t state = sc_state; - xSemaphoreGive(snapcastStateMux); +/** + * Get snapclient state + */ +snapclient_state_t sc_get_snapclient_state(void) { + xSemaphoreTake(snapclientStateMux, portMAX_DELAY); + snapclient_state_t state = sc_state; + xSemaphoreGive(snapclientStateMux); return state; } @@ -580,7 +602,7 @@ void sc_call_state_cb(void) { } /** - * add callback to be called when snapcast state changes, e.g. from not started to started. + * add callback to be called when snapclient state changes, e.g. from not started to started. * Callbacks needs to be implemented thread safe as they will be called from http task */ void sc_add_state_cb(void (*cb)()) { @@ -1061,9 +1083,9 @@ void handle_chunk_message(codec_type_t codec, playerSetting_t *scSet, void update_state(bool *received_wire_chnk, bool *playback, bool paused) { static int64_t last = 0; - static snapcast_state_t state = IDLE; //Todo + static snapclient_state_t state = IDLE; //Todo if ((paused || state != PLAYING) && (!paused || state != PAUSED) && *received_wire_chnk) { - xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + xSemaphoreTake(snapclientStateMux, portMAX_DELAY); if (paused) { sc_state = PAUSED; *playback = false; @@ -1074,7 +1096,7 @@ void update_state(bool *received_wire_chnk, bool *playback, bool paused) { *playback = true; } state = sc_state; - xSemaphoreGive(snapcastStateMux); + xSemaphoreGive(snapclientStateMux); sc_call_state_cb(); last = esp_timer_get_time(); *received_wire_chnk = false; @@ -1083,11 +1105,11 @@ void update_state(bool *received_wire_chnk, bool *playback, bool paused) { int64_t now = esp_timer_get_time(); if (now-last > 1000000) { //update once per sec if (!(*received_wire_chnk)) { - xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + xSemaphoreTake(snapclientStateMux, portMAX_DELAY); sc_state = IDLE; *playback = false; state = sc_state; - xSemaphoreGive(snapcastStateMux); + xSemaphoreGive(snapclientStateMux); sc_call_state_cb(); ESP_LOGI(TAG, "Set idle"); } @@ -1280,17 +1302,17 @@ static void http_get_task(void *pvParameters) { } // block if state = STOPPED - xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + xSemaphoreTake(snapclientStateMux, portMAX_DELAY); if (sc_state == STOPPED) { - xSemaphoreGive(snapcastStateMux); + xSemaphoreGive(snapclientStateMux); command = STOP; while(command != START) { xTaskNotifyWait( 0, 0, &command, portMAX_DELAY); } - xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + xSemaphoreTake(snapclientStateMux, portMAX_DELAY); } sc_state = IDLE; - xSemaphoreGive(snapcastStateMux); + xSemaphoreGive(snapclientStateMux); sc_call_state_cb(); playback = false; @@ -1429,9 +1451,9 @@ static void http_get_task(void *pvParameters) { if (xTaskNotifyWait(0, 0, &command, 1) == pdTRUE) { switch(command) { case STOP: - xSemaphoreTake(snapcastStateMux, portMAX_DELAY); + xSemaphoreTake(snapclientStateMux, portMAX_DELAY); sc_state = STOPPED; - xSemaphoreGive(snapcastStateMux); + xSemaphoreGive(snapclientStateMux); sc_call_state_cb(); case RESTART: restart = true; @@ -1485,7 +1507,7 @@ static void http_get_task(void *pvParameters) { /** * */ -int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool), i2s_std_gpio_config_t i2s_pin_config0, i2s_port_t I2S_NUM_0, bool (*lock)(bool, TickType_t)) { +int init_snapclient(void (*set_volume)(int), void (*set_mute)(bool, bool), i2s_std_gpio_config_t i2s_pin_config0, i2s_port_t I2S_NUM_0, bool (*lock)(bool, TickType_t)) { if (set_volume == NULL) { ESP_LOGE(TAG, "Volume callback is NULL"); @@ -1498,10 +1520,10 @@ int init_snapcast(void (*set_volume)(int), void (*set_mute)(bool, bool), i2s_std } set_volume_cb = set_volume; set_mute_cb = set_mute; - if (snapcastStateMux == NULL) { - snapcastStateMux = xSemaphoreCreateMutex(); + if (snapclientStateMux == NULL) { + snapclientStateMux = xSemaphoreCreateMutex(); } - init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute, player_state_paused, lock); + init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute, on_player_state_paused, lock); xTaskCreatePinnedToCore(&http_get_task, "http", 15 * 1024, NULL, HTTP_TASK_PRIORITY, &t_http_get_task, @@ -1567,10 +1589,12 @@ void audio_set_volume(int volume) { } xSemaphoreGive(audioDACSemaphore); } - -void sc_state_changed() { - if (snapcastStateChangedMutex != NULL) { - xSemaphoreGive(snapcastStateChangedMutex); +/** + * Callback for snapclient state changes (playing, paused, idle). Used to trigger actions on state changes, e.g. muting the audio output when paused or idle. + */ +void on_sc_state_changed() { + if (snapclientStateChangedMutex != NULL) { + xSemaphoreGive(snapclientStateChangedMutex); } ESP_LOGI(TAG, "main task cb"); } @@ -1757,14 +1781,14 @@ void app_main(void) { i2sLockMutex = xSemaphoreCreateBinary(); - init_snapcast(audio_set_volume, audio_set_mute, i2s_pin_config0, I2S_NUM_0, i2s_lock); + init_snapclient(audio_set_volume, audio_set_mute, i2s_pin_config0, I2S_NUM_0, i2s_lock); //init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute); - sc_add_state_cb(sc_state_changed); + sc_add_state_cb(on_sc_state_changed); // Create binary semaphore for player state change notification - snapcastStateChangedMutex = xSemaphoreCreateBinary(); - if (snapcastStateChangedMutex == NULL) { - ESP_LOGE(TAG, "Failed to create snapcastStateChangedMutex"); + snapclientStateChangedMutex = xSemaphoreCreateBinary(); + if (snapclientStateChangedMutex == NULL) { + ESP_LOGE(TAG, "Failed to create snapclientStateChangedMutex"); return; } @@ -1808,7 +1832,7 @@ void app_main(void) { xTaskCreatePinnedToCore(&ota_server_task, "ota", 14 * 256, NULL, OTA_TASK_PRIORITY, &t_ota_task, OTA_TASK_CORE_ID); - sc_start_snapcast(); + sc_start_snapclient(); // while (1) { // // audio_event_iface_msg_t msg; @@ -1836,13 +1860,13 @@ void app_main(void) { esp_pm_configure(&pmConfig); #endif audioDACdata_t dac_data; - snapcast_state_t state = IDLE; + snapclient_state_t state = IDLE; while (1) { if (xQueueReceive(audioDACQHdl, &dac_data, pdMS_TO_TICKS(90)) == pdTRUE) { dac_control(board_handle, dac_data); } - if (xSemaphoreTake(snapcastStateChangedMutex, pdMS_TO_TICKS(10)) == pdTRUE) { - snapcast_state_t state_new = sc_get_snapcast_state(); + if (xSemaphoreTake(snapclientStateChangedMutex, pdMS_TO_TICKS(10)) == pdTRUE) { + snapclient_state_t state_new = sc_get_snapclient_state(); ESP_LOGI(TAG, "main got cb: %d", state_new); if (state_new != state) { ESP_LOGI(TAG, "Player state changed: %d -> %d", state, state_new); From d0bc6948ff540141c020567ee5510e4af874139c Mon Sep 17 00:00:00 2001 From: raul Date: Sun, 17 May 2026 21:11:36 +0200 Subject: [PATCH 15/16] Use sc callback in ota, stop player task early on sc_stop --- components/lightsnapcast/include/player.h | 1 + components/lightsnapcast/player.c | 10 ++++-- components/ota_server/include/ota_server.h | 1 + components/ota_server/ota_server.c | 42 +++++++++++++++++++--- main/main.c | 10 +++--- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index e50abfe0..dac04aa5 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -64,6 +64,7 @@ int init_player(i2s_std_gpio_config_t pin_config0_, i2s_port_t i2sNum_, void (*s int deinit_player(void); int start_player(void); void pause_player(bool pause); +void stop_player_task(void); int32_t allocate_pcm_chunk_memory(pcm_chunk_message_t **pcmChunk, size_t bytes); int32_t insert_pcm_chunk(pcm_chunk_message_t *pcmChunk); diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index 3ad0a9dd..8395fa10 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -608,8 +608,8 @@ void pause_player(bool pause) { if (pause != playerPaused) { playerPaused = pause; xSemaphoreGive(playerStateMux); - if (pause && playerTaskHandle != NULL) { - xTaskNotifyGiveIndexed(playerTaskHandle, 1); + if (pause) { + stop_player_task(); } else { call_state_cb(); // notify state change, e.g. for http task to send pcm @@ -619,6 +619,12 @@ void pause_player(bool pause) { } } +void stop_player_task() { + if (playerTaskHandle != NULL) { + xTaskNotifyGiveIndexed(playerTaskHandle, 1); + } +} + #if USE_TIMEFILTER /** * diff --git a/components/ota_server/include/ota_server.h b/components/ota_server/include/ota_server.h index 30ff48dc..76e2fabb 100644 --- a/components/ota_server/include/ota_server.h +++ b/components/ota_server/include/ota_server.h @@ -9,5 +9,6 @@ extern const int OTA_CONNECTED_BIT; void ota_server_task(void *param); void ota_server_start_my(void); +void ota_on_sc_state_changed(); extern EventGroupHandle_t ota_event_group; diff --git a/components/ota_server/ota_server.c b/components/ota_server/ota_server.c index 172d15e8..fff3d419 100644 --- a/components/ota_server/ota_server.c +++ b/components/ota_server/ota_server.c @@ -23,12 +23,15 @@ extern TaskHandle_t t_http_get_task; extern void sc_stop_snapclient(); +typedef enum { STOPPED = 0, IDLE, PLAYING, PAUSED } snapclient_state_t; +extern snapclient_state_t sc_get_snapclient_state(void); const int OTA_CONNECTED_BIT = BIT0; static const char *TAG = "OTA"; EventGroupHandle_t ota_event_group; /*socket*/ static int connect_socket = 0; +static TaskHandle_t otaTaskHandle = NULL; void ota_server_task(void *param) { // xEventGroupWaitBits(ota_event_group, OTA_CONNECTED_BIT, false, true, @@ -153,19 +156,50 @@ static esp_err_t create_tcp_server() { return ESP_OK; } +void ota_on_sc_state_changed() { + if (otaTaskHandle != NULL) { + xTaskNotifyGive(otaTaskHandle); + } +} + +void wait_for_sc_stopped() { + snapclient_state_t state = sc_get_snapclient_state(); + if (state == STOPPED) { + return; + } + // drain old notifications that might have come in while waiting for connection + ulTaskNotifyTake(pdTRUE, 0); + //ESP_LOGI(TAG, "Stopping snapclient..."); + sc_stop_snapclient(); + bool stopped = false; + while(!stopped) { + esp_err_t ret = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10000)); + if (ret != pdTRUE) { + ESP_LOGW(TAG, "wait for sc stopped: timeout"); + vTaskDelete(t_http_get_task); + break; + } + state = sc_get_snapclient_state(); + if (state == STOPPED) { + stopped = true; + } + } +} + void ota_server_start_my(void) { uint8_t percent_loaded; uint8_t old_percent_loaded; + otaTaskHandle = xTaskGetCurrentTaskHandle(); + ESP_ERROR_CHECK(create_tcp_server()); // We don't want any other thread running during this update. // SuspendAllThreads(); // KillAllThreads(); // dsp_i2s_task_deinit(); - sc_stop_snapclient(); - vTaskDelay(1000 / portTICK_PERIOD_MS); // give snapclient some time to stop before we kill the http task that might be using the player - vTaskDelete(t_http_get_task); + + wait_for_sc_stopped(); deinit_player(); // ensure this is called after http_task was killed const esp_partition_t *update_partition = @@ -232,7 +266,7 @@ void ota_server_start_my(void) { int send_len; send_len = sprintf(res_buff, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); send(connect_socket, res_buff, send_len, 0); - vTaskDelay(2000 / portTICK_PERIOD_MS); + vTaskDelay(pdMS_TO_TICKS(2000)); close(connect_socket); ESP_ERROR_CHECK(esp_ota_end(ota_handle)); diff --git a/main/main.c b/main/main.c index 11090d6d..b4487270 100644 --- a/main/main.c +++ b/main/main.c @@ -514,7 +514,7 @@ void error_callback(const FLAC__StreamDecoder *decoder, * Snapclient state functions and types */ -typedef enum { STOPPED = 0, IDLE, PLAYING, PAUSED } snapclient_state_t; //defined in player.h +typedef enum { STOPPED = 0, IDLE, PLAYING, PAUSED } snapclient_state_t; typedef enum { STOP = 0, START, RESTART, PAUSE, UNPAUSE } snapclient_commands_t; typedef struct state_cb_s { @@ -576,9 +576,6 @@ void sc_stop_snapclient() { */ void sc_pause_snapclient(bool pause) { pause_player(pause); - if (!pause) { - //sc_send_command(UNPAUSE); - } } /** @@ -1305,6 +1302,7 @@ static void http_get_task(void *pvParameters) { xSemaphoreTake(snapclientStateMux, portMAX_DELAY); if (sc_state == STOPPED) { xSemaphoreGive(snapclientStateMux); + sc_call_state_cb(); // call callbacks here so we can be sure that netconn is closed. Player task could still be running. command = STOP; while(command != START) { xTaskNotifyWait( 0, 0, &command, portMAX_DELAY); @@ -1451,10 +1449,11 @@ static void http_get_task(void *pvParameters) { if (xTaskNotifyWait(0, 0, &command, 1) == pdTRUE) { switch(command) { case STOP: + stop_player_task(); // stop player task for faster teardown xSemaphoreTake(snapclientStateMux, portMAX_DELAY); sc_state = STOPPED; xSemaphoreGive(snapclientStateMux); - sc_call_state_cb(); + // fall through to restart connection and wait for START command case RESTART: restart = true; break; @@ -1784,6 +1783,7 @@ void app_main(void) { init_snapclient(audio_set_volume, audio_set_mute, i2s_pin_config0, I2S_NUM_0, i2s_lock); //init_player(i2s_pin_config0, I2S_NUM_0, player_set_mute); sc_add_state_cb(on_sc_state_changed); + sc_add_state_cb(ota_on_sc_state_changed); // Create binary semaphore for player state change notification snapclientStateChangedMutex = xSemaphoreCreateBinary(); From 22aeabb6a4687eacab14a3b6bf9b43d8dd887b00 Mon Sep 17 00:00:00 2001 From: raul Date: Sun, 17 May 2026 21:54:50 +0200 Subject: [PATCH 16/16] small delay and increase task stack --- components/ota_server/ota_server.c | 1 + main/main.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ota_server/ota_server.c b/components/ota_server/ota_server.c index fff3d419..50c10193 100644 --- a/components/ota_server/ota_server.c +++ b/components/ota_server/ota_server.c @@ -201,6 +201,7 @@ void ota_server_start_my(void) { wait_for_sc_stopped(); deinit_player(); // ensure this is called after http_task was killed + vTaskDelay(pdMS_TO_TICKS(200)); // short delay to ensure all resources are freed before starting OTA const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL); diff --git a/main/main.c b/main/main.c index b4487270..8becac32 100644 --- a/main/main.c +++ b/main/main.c @@ -1830,7 +1830,7 @@ void app_main(void) { dsp_settings_init(); // Then settings can restore params into the processor #endif - xTaskCreatePinnedToCore(&ota_server_task, "ota", 14 * 256, NULL, + xTaskCreatePinnedToCore(&ota_server_task, "ota", 15 * 256, NULL, OTA_TASK_PRIORITY, &t_ota_task, OTA_TASK_CORE_ID); sc_start_snapclient();