diff --git a/README.md b/README.md index b074cfa..1d8d51f 100644 --- a/README.md +++ b/README.md @@ -36,24 +36,24 @@ Components - audio-board : taken from ADF, stripped down to strictly necessary parts for usage with Lyrat v4.3 - audio-hal : taken from ADF, stripped down to strictly necessary parts for usage with Lyrat v4.3 - audio-sal : taken from ADF, stripped down to strictly necessary parts for usage with Lyrat v4.3 - - custom_board : + - custom_board : - custom-driver : modified I2S driver from IDF v4.3 which supports preloading DMA buffers with valid data - dsp_processor : Audio Processor, low pass filters, effects, etc. - esp-dsp : Submodule to the ESP-ADF done by David Douard - esp-peripherals : taken from ADF, stripped down to strictly necessary parts for usage with Lyrat v4.3 - flac : flac audio cider/decoder full submodule - libmedian: Median Filter implementation. Many thanks to @accabog https://github.com/accabog/MedianFilter - - libbuffer : Generic buffer abstraction - - lightsnapcast : + - libbuffer : Generic buffer abstraction + - lightsnapcast : * snapcast module, port of @bridadan scapcast packages decode library * player module, which is responsible for sync and low level I2S control - - net_functions : + - net_functions : - opus : Opus audio coder/decoder full submodule - - ota_server : - - protocol : + - ota_server : + - protocol : - rtprx : Alternative RTP audio client UDP low latency also opus based - - websocket : - - websocket_if : + - websocket : + - websocket_if : - wifi_interface : wifi provisoning and init code for wifi module and AP connection The snapclient functionanlity are implemented in a task included in main - but @@ -70,19 +70,19 @@ Normally these packages contain messages in the following order: - SERVER_SETTING : volume, mute state, playback delay etc - CODEC_HEADER : Setup client audio codec (FLAC, OPUS, OGG or PCM) bitrate, n channels and bits per sample - - WIRE_CHUNK : Coded audio data, also I calculate chunk duration here after + - WIRE_CHUNK : Coded audio data, also I calculate chunk duration here after decoding is done using received CODEC_HEADER parameters - TIME : Ping pong time keeping packages to keep track of time diff from server to client -Each WIRE_CHUNK of audio data comes with a timestamp in server time and clients -can use information from TIME and SERVER_SETTING messages to determine when playback +Each WIRE_CHUNK of audio data comes with a timestamp in server time and clients +can use information from TIME and SERVER_SETTING messages to determine when playback has to be started. We handle this using a buffer with a length that compensate for for playback-delay, network jitter and DAC to speaker (determined through SERVER_SETTING). In this implementation I have separated the sync task to a backend on the other end of a freeRTOS queue. Now the front end just needs to pass on the decoded audio -data to the queue with the server timestamp and chunk size. The backend reads +data to the queue with the server timestamp and chunk size. The backend reads timestamps and waits until the audio chunk has the correct playback-delay to be written to the DAC amplifer speaker pipeline. When the backend pipeline is in sync, any offset get rolled in by micro tuning the APLL on the ESP. No @@ -181,4 +181,4 @@ Then on every `git commit`, a few sanity/formatting checks will be performed. - [ok] Startup: do not start parsing on samples to codec before sample ring buffer hits requested buffer size. - [ok] Start from empty buffer - [ ] fill in missing component descriptions in Readme.md - - [ ] DAC latency setting from android app + - [ok] DAC latency setting from android app diff --git a/components/lightsnapcast/include/player.h b/components/lightsnapcast/include/player.h index 264ccae..3560ba5 100644 --- a/components/lightsnapcast/include/player.h +++ b/components/lightsnapcast/include/player.h @@ -9,7 +9,7 @@ #define I2S_PORT I2S_NUM_0 -#define LATENCY_MEDIAN_FILTER_LEN 99 +#define LATENCY_MEDIAN_FILTER_LEN 49 // 99 #define SHORT_BUFFER_LEN 29 diff --git a/components/lightsnapcast/player.c b/components/lightsnapcast/player.c index 5f27c7a..8616f6a 100644 --- a/components/lightsnapcast/player.c +++ b/components/lightsnapcast/player.c @@ -395,7 +395,8 @@ player_send_snapcast_setting (snapcastSetting_t *setting) if ((curSet.bits != setting->bits) || (curSet.buf_ms != setting->buf_ms) || (curSet.ch != setting->ch) || (curSet.chkDur_ms != setting->chkDur_ms) || (curSet.codec != setting->codec) || (curSet.muted != setting->muted) - || (curSet.sr != setting->sr) || (curSet.volume != setting->volume)) + || (curSet.sr != setting->sr) || (curSet.volume != setting->volume) + || (curSet.cDacLat_ms != setting->cDacLat_ms)) { // check if it is only volume / mute related setting, which is handled by // http_get_task() @@ -405,8 +406,8 @@ player_send_snapcast_setting (snapcastSetting_t *setting) && (curSet.buf_ms == setting->buf_ms) && (curSet.ch == setting->ch) && (curSet.chkDur_ms == setting->chkDur_ms) - && (curSet.codec == setting->codec) - && (curSet.sr == setting->sr))) + && (curSet.codec == setting->codec) && (curSet.sr == setting->sr) + && (curSet.cDacLat_ms == setting->cDacLat_ms))) { // no notify needed, only set changed parameters ret = player_set_snapcast_settings (setting); @@ -1238,9 +1239,9 @@ player_task (void *pvParameters) ESP_LOGI (TAG, "snapserver config changed, buffer %dms, chunk %dms, " - "sample rate %d, ch %d, bits %d mute %d", + "sample rate %d, ch %d, bits %d mute %d latency %d", scSet.buf_ms, scSet.chkDur_ms, scSet.sr, scSet.ch, - scSet.bits, scSet.muted); + scSet.bits, scSet.muted, scSet.cDacLat_ms); gotSnapserverConfig = true; } diff --git a/main/main.c b/main/main.c index ae03d29..72ecb4c 100644 --- a/main/main.c +++ b/main/main.c @@ -65,6 +65,7 @@ static void error_callback (const FLAC__StreamDecoder *decoder, //#include "ma120.h" +static FLAC__StreamDecoder *flacDecoder = NULL; static QueueHandle_t flacReadQHdl = NULL; static QueueHandle_t flacWriteQHdl = NULL; @@ -76,9 +77,12 @@ const char *VERSION_STRING = "0.0.2"; #define OTA_TASK_PRIORITY 6 #define OTA_TASK_CORE_ID tskNO_AFFINITY -xTaskHandle t_ota_task; -xTaskHandle t_http_get_task; -xTaskHandle t_flac_decoder_task; +#define FLAC_TASK_PRIORITY HTTP_TASK_PRIORITY + 1 +#define FLAC_TASK_CORE_ID 1 // tskNO_AFFINITY + +xTaskHandle t_ota_task = NULL; +xTaskHandle t_http_get_task = NULL; +xTaskHandle t_flac_decoder_task = NULL; struct timeval tdif, tavg; audio_board_handle_t board_handle = NULL; @@ -271,7 +275,6 @@ static void flac_decoder_task (void *pvParameters) { FLAC__bool ok = true; - FLAC__StreamDecoder *flacDecoder = NULL; FLAC__StreamDecoderInitStatus init_status; snapcastSetting_t *scSet = (snapcastSetting_t *)pvParameters; @@ -381,11 +384,6 @@ http_get_task (void *pvParameters) mdns_init (); #endif - // TODO: only create this task, if we need flac - // also add an ogg variant - xTaskCreatePinnedToCore (&flac_decoder_task, "flac_decoder_task", 4 * 4096, - &scSet, 6, &t_flac_decoder_task, 1); - while (1) { if (reset_latency_buffer () < 0) @@ -406,6 +404,31 @@ http_get_task (void *pvParameters) opusDecoder = NULL; } + if (t_flac_decoder_task != NULL) + { + vTaskDelete (t_flac_decoder_task); + t_flac_decoder_task = NULL; + } + + if (flacDecoder != NULL) + { + FLAC__stream_decoder_finish (flacDecoder); + FLAC__stream_decoder_delete (flacDecoder); + flacDecoder = NULL; + } + + if (flacWriteQHdl != NULL) + { + vQueueDelete (flacWriteQHdl); + flacWriteQHdl = NULL; + } + + if (flacReadQHdl != NULL) + { + vQueueDelete (flacReadQHdl); + flacReadQHdl = NULL; + } + #if SNAPCAST_SERVER_USE_MDNS // Find snapcast server // Connect to first snapcast server found @@ -675,6 +698,37 @@ http_get_task (void *pvParameters) size = codec_header_message.size; start = codec_header_message.payload; + if (opusDecoder != NULL) + { + opus_decoder_destroy (opusDecoder); + opusDecoder = NULL; + } + + if (t_flac_decoder_task != NULL) + { + vTaskDelete (t_flac_decoder_task); + t_flac_decoder_task = NULL; + } + + if (flacDecoder != NULL) + { + FLAC__stream_decoder_finish (flacDecoder); + FLAC__stream_decoder_delete (flacDecoder); + flacDecoder = NULL; + } + + if (flacWriteQHdl != NULL) + { + vQueueDelete (flacWriteQHdl); + flacWriteQHdl = NULL; + } + + if (flacReadQHdl != NULL) + { + vQueueDelete (flacReadQHdl); + flacReadQHdl = NULL; + } + // ESP_LOGI(TAG, "Received codec header message with size // %d", codec_header_message.size); @@ -702,11 +756,6 @@ http_get_task (void *pvParameters) } int error = 0; - if (opusDecoder != NULL) - { - opus_decoder_destroy (opusDecoder); - opusDecoder = NULL; - } opusDecoder = opus_decoder_create (rate, channels, &error); if (error != 0) @@ -732,6 +781,14 @@ http_get_task (void *pvParameters) { codec = FLAC; + if (t_flac_decoder_task == NULL) + { + xTaskCreatePinnedToCore ( + &flac_decoder_task, "flac_decoder_task", + 4 * 4096, &scSet, FLAC_TASK_PRIORITY, + &t_flac_decoder_task, FLAC_TASK_CORE_ID); + } + // check if audio buffer was previously allocated by some // other codec this would happen if codec is changed // while client was running @@ -751,6 +808,12 @@ http_get_task (void *pvParameters) flacData.inData = codec_header_message.payload; pFlacData = &flacData; + // wait for task creation done + while (flacReadQHdl == NULL) + { + vTaskDelay (10); + } + // send data to flac decoder xQueueSend (flacReadQHdl, &pFlacData, portMAX_DELAY); // and wait until it is done @@ -908,7 +971,7 @@ http_get_task (void *pvParameters) audio = (int16_t *)realloc ( audio, pcm_size * scSet.ch * (scSet.bits / 8)); - // audio = (int16_t + // audio = (int16_t, // *)heap_caps_realloc( // (int32_t // *)audio, frameSize * @@ -982,11 +1045,17 @@ http_get_task (void *pvParameters) flacData.inData = wire_chunk_message.payload; pFlacData = &flacData; + // startTime = + // esp_timer_get_time (); // send data to flac decoder xQueueSend (flacReadQHdl, &pFlacData, portMAX_DELAY); // and wait until it is done xQueueReceive (flacWriteQHdl, &pFlacData, portMAX_DELAY); + // endTime = + // esp_timer_get_time (); + // ESP_LOGW(TAG, + //"%lld", endTime - startTime); wire_chunk_message_t pcm_chunk_message; @@ -1267,7 +1336,7 @@ http_get_task (void *pvParameters) // Do a initial time sync with the server at boot // we need to fill diffBuff fast so we get a good // estimate of latency - timeout = 50000; + timeout = 100000; } esp_timer_start_once (timeSyncMessageTimer, timeout); diff --git a/sdkconfig b/sdkconfig index 7dbb1d9..ef0078e 100644 --- a/sdkconfig +++ b/sdkconfig @@ -546,7 +546,7 @@ CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 # CONFIG_ESP32_WIFI_RX_IRAM_OPT is not set CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y # CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set -CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y +# CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE is not set # end of Wi-Fi #