Files
snapclient/main/main.c
2025-03-04 21:42:40 +01:00

2849 lines
91 KiB
C

#include <stdint.h>
#include <string.h>
#include "driver/timer_types_legacy.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif_ip_addr.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "esp_pm.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "freertos/task.h"
#include "hal/gpio_types.h"
#include "freertos/idf_additions.h"
#include "board.h"
#include "lwip/ip_addr.h"
#include "lwip/netif.h"
#include "esp_netif.h"
#include "lwip/api.h"
#include "lwip/dns.h"
#include "lwip/err.h"
#include "lwip/netdb.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "mdns.h"
#include "net_functions.h"
#include "network_interface.h"
#include "nvs_flash.h"
// Web socket server
// #include "websocket_if.h"
// #include "websocket_server.h"
#include <sys/time.h>
#include "driver/i2s_std.h"
#if CONFIG_USE_DSP_PROCESSOR
#include "dsp_processor.h"
#endif
// Opus decoder is implemented as a subcomponet from master git repo
#include "opus.h"
// flac decoder is implemented as a subcomponet from master git repo
#include "FLAC/stream_decoder.h"
#include "ota_server.h"
#include "player.h"
#include "snapcast.h"
#include "ui_http_server.h"
static bool isCachedChunk = false;
static uint32_t cachedBlocks = 0;
static FLAC__StreamDecoderReadStatus read_callback(
const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes,
void *client_data);
static FLAC__StreamDecoderWriteStatus write_callback(
const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame,
const FLAC__int32 *const buffer[], void *client_data);
static void metadata_callback(const FLAC__StreamDecoder *decoder,
const FLAC__StreamMetadata *metadata,
void *client_data);
static void error_callback(const FLAC__StreamDecoder *decoder,
FLAC__StreamDecoderErrorStatus status,
void *client_data);
static FLAC__StreamDecoder *flacDecoder = NULL;
const char *VERSION_STRING = "0.0.3";
#define HTTP_TASK_PRIORITY 5
#define HTTP_TASK_CORE_ID tskNO_AFFINITY
#define OTA_TASK_PRIORITY 6
#define OTA_TASK_CORE_ID tskNO_AFFINITY
// 1 // tskNO_AFFINITY
TaskHandle_t t_ota_task = NULL;
TaskHandle_t t_http_get_task = NULL;
#define FAST_SYNC_LATENCY_BUF 10000 // in µs
#define NORMAL_SYNC_LATENCY_BUF 1000000 // in µs
struct timeval tdif, tavg;
/* snapast parameters; configurable in menuconfig */
#define SNAPCAST_SERVER_USE_MDNS CONFIG_SNAPSERVER_USE_MDNS
#if !SNAPCAST_SERVER_USE_MDNS
#define SNAPCAST_SERVER_HOST CONFIG_SNAPSERVER_HOST
#define SNAPCAST_SERVER_PORT CONFIG_SNAPSERVER_PORT
#endif
#define SNAPCAST_CLIENT_NAME CONFIG_SNAPCLIENT_NAME
#define SNAPCAST_USE_SOFT_VOL CONFIG_SNAPCLIENT_USE_SOFT_VOL
/* Logging tag */
static const char *TAG = "SC";
// static QueueHandle_t playerChunkQueueHandle = NULL;
SemaphoreHandle_t timeSyncSemaphoreHandle = NULL;
SemaphoreHandle_t idCounterSemaphoreHandle = NULL;
#if CONFIG_USE_DSP_PROCESSOR
#if CONFIG_SNAPCLIENT_DSP_FLOW_STEREO
dspFlows_t dspFlow = dspfStereo;
#endif
#if CONFIG_SNAPCLIENT_DSP_FLOW_BASSBOOST
dspFlows_t dspFlow = dspfBassBoost;
#endif
#if CONFIG_SNAPCLIENT_DSP_FLOW_BIAMP
dspFlows_t dspFlow = dspfBiamp;
#endif
#if CONFIG_SNAPCLIENT_DSP_FLOW_BASS_TREBLE_EQ
dspFlows_t dspFlow = dspfEQBassTreble;
#endif
#endif
typedef struct audioDACdata_s {
bool mute;
int volume;
bool enabled;
} audioDACdata_t;
static audioDACdata_t audioDAC_data;
static QueueHandle_t audioDACQHdl = NULL;
static SemaphoreHandle_t audioDACSemaphore = NULL;
typedef struct decoderData_s {
uint32_t type; // should be SNAPCAST_MESSAGE_CODEC_HEADER
// or SNAPCAST_MESSAGE_WIRE_CHUNK
uint8_t *inData;
tv_t timestamp;
uint8_t *outData;
uint32_t bytes;
} decoderData_t;
void time_sync_msg_cb(void *args);
static char base_message_serialized[BASE_MESSAGE_SIZE];
static const esp_timer_create_args_t tSyncArgs = {
.callback = &time_sync_msg_cb,
.dispatch_method = ESP_TIMER_TASK,
.name = "tSyncMsg",
.skip_unhandled_events = false};
struct netconn *lwipNetconn;
static int id_counter = 0;
static OpusDecoder *opusDecoder = NULL;
static decoderData_t decoderChunk = {
.type = SNAPCAST_MESSAGE_INVALID,
.inData = NULL,
.timestamp = {0, 0},
.outData = NULL,
.bytes = 0,
};
static decoderData_t pcmChunk = {
.type = SNAPCAST_MESSAGE_INVALID,
.inData = NULL,
.timestamp = {0, 0},
.outData = NULL,
.bytes = 0,
};
/**
*
*/
void time_sync_msg_cb(void *args) {
base_message_t base_message_tx;
// struct timeval now;
int64_t now;
int rc1;
uint8_t *p_pkt = (uint8_t *)malloc(BASE_MESSAGE_SIZE + TIME_MESSAGE_SIZE);
if (p_pkt == NULL) {
ESP_LOGW(
TAG,
"%s: Failed to get memory for time sync message. Skipping this round.",
__func__);
return;
}
memset(p_pkt, 0, BASE_MESSAGE_SIZE + TIME_MESSAGE_SIZE);
base_message_tx.type = SNAPCAST_MESSAGE_TIME;
xSemaphoreTake(idCounterSemaphoreHandle, portMAX_DELAY);
base_message_tx.id = id_counter++;
xSemaphoreGive(idCounterSemaphoreHandle);
base_message_tx.refersTo = 0;
base_message_tx.received.sec = 0;
base_message_tx.received.usec = 0;
now = esp_timer_get_time();
base_message_tx.sent.sec = now / 1000000;
base_message_tx.sent.usec = now - base_message_tx.sent.sec * 1000000;
base_message_tx.size = TIME_MESSAGE_SIZE;
rc1 = base_message_serialize(&base_message_tx, (char *)&p_pkt[0],
BASE_MESSAGE_SIZE);
if (rc1) {
ESP_LOGE(TAG, "Failed to serialize base message for time");
return;
}
rc1 = netconn_write(lwipNetconn, p_pkt, BASE_MESSAGE_SIZE + TIME_MESSAGE_SIZE,
NETCONN_NOCOPY);
if (rc1 != ERR_OK) {
ESP_LOGW(TAG, "error writing timesync msg");
return;
}
free(p_pkt);
// ESP_LOGI(TAG, "%s: sent time sync message, %u", __func__,
// base_message_tx.id);
}
/**
*
*/
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;
// decoderData_t *flacData;
(void)scSet;
// xQueueReceive(decoderReadQHdl, &flacData, portMAX_DELAY);
// if (xQueueReceive(decoderReadQHdl, &flacData, pdMS_TO_TICKS(100)))
if (decoderChunk.inData) {
// ESP_LOGI(TAG, "in flac read cb %ld %p", flacData->bytes,
// flacData->inData);
if (decoderChunk.bytes <= 0) {
// free_flac_data(flacData);
return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
}
isCachedChunk = false;
// if (flacData->inData == NULL) {
// free_flac_data(flacData);
//
// return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
// }
if (decoderChunk.bytes <= *bytes) {
memcpy(buffer, decoderChunk.inData, decoderChunk.bytes);
*bytes = decoderChunk.bytes;
// ESP_LOGW(TAG, "read all flac inData %d", *bytes);
free(decoderChunk.inData);
decoderChunk.inData = NULL;
decoderChunk.bytes = 0;
} else {
memcpy(buffer, decoderChunk.inData, *bytes);
memmove(decoderChunk.inData, decoderChunk.inData + *bytes,
decoderChunk.bytes - *bytes);
decoderChunk.bytes -= *bytes;
decoderChunk.inData =
(uint8_t *)realloc(decoderChunk.inData, decoderChunk.bytes);
// ESP_LOGW(TAG, "didn't read all flac inData %d", *bytes);
// flacData->inData += *bytes;
// flacData->bytes -= *bytes;
}
// free_flac_data(flacData);
// xQueueSend (flacReadQHdl, &flacData, portMAX_DELAY);
// xSemaphoreGive(decoderReadSemaphore);
// ESP_LOGE(TAG, "%s: data processed", __func__);
return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
} else {
return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
}
}
/**
*
*/
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;
size_t bytes = frame->header.blocksize * frame->header.channels *
frame->header.bits_per_sample / 8;
(void)decoder;
if (isCachedChunk) {
cachedBlocks += frame->header.blocksize;
}
// ESP_LOGI(TAG, "in flac write cb %ld %d, pcmChunk.bytes %ld",
// frame->header.blocksize, bytes, pcmChunk.bytes);
if (frame->header.channels != scSet->ch) {
ESP_LOGE(TAG,
"ERROR: frame header reports different channel count %ld than "
"previous metadata block %d",
frame->header.channels, scSet->ch);
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
if (frame->header.bits_per_sample != scSet->bits) {
ESP_LOGE(TAG,
"ERROR: frame header reports different bps %ld than previous "
"metadata block %d",
frame->header.bits_per_sample, scSet->bits);
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
if (buffer[0] == NULL) {
ESP_LOGE(TAG, "ERROR: buffer [0] is NULL");
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
if (buffer[1] == NULL) {
ESP_LOGE(TAG, "ERROR: buffer [1] is NULL");
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
uint8_t * pcmData;
do {
pcmData = (uint8_t *)realloc(pcmChunk.outData, pcmChunk.bytes + bytes);
if (!pcmData) {
ESP_LOGW(TAG, "%s, failed to allocate PCM chunk payload, try again", __func__);
vTaskDelay(pdMS_TO_TICKS(5));
//return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
} while(!pcmData);
pcmChunk.outData = pcmData;
for (i = 0; i < frame->header.blocksize; i++) {
// write little endian
pcmChunk.outData[pcmChunk.bytes + 4 * i] = (uint8_t)(buffer[0][i]);
pcmChunk.outData[pcmChunk.bytes + 4 * i + 1] = (uint8_t)(buffer[0][i] >> 8);
pcmChunk.outData[pcmChunk.bytes + 4 * i + 2] = (uint8_t)(buffer[1][i]);
pcmChunk.outData[pcmChunk.bytes + 4 * i + 3] = (uint8_t)(buffer[1][i] >> 8);
}
pcmChunk.bytes += bytes;
scSet->chkInFrames = frame->header.blocksize;
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}
/**
*
*/
void metadata_callback(const FLAC__StreamDecoder *decoder,
const FLAC__StreamMetadata *metadata,
void *client_data) {
snapcastSetting_t *scSet = (snapcastSetting_t *)client_data;
(void)decoder;
if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
// ESP_LOGI(TAG, "in flac meta cb");
// save for later
scSet->sr = metadata->data.stream_info.sample_rate;
scSet->ch = metadata->data.stream_info.channels;
scSet->bits = metadata->data.stream_info.bits_per_sample;
ESP_LOGI(TAG, "fLaC sampleformat: %ld:%d:%d", scSet->sr, scSet->bits,
scSet->ch);
// ESP_LOGE(TAG, "%s: data processed", __func__);
}
}
/**
*
*/
void error_callback(const FLAC__StreamDecoder *decoder,
FLAC__StreamDecoderErrorStatus status, void *client_data) {
(void)decoder, (void)client_data;
ESP_LOGE(TAG, "Got error callback: %s\n",
FLAC__StreamDecoderErrorStatusString[status]);
}
/**
*
*/
void init_snapcast(QueueHandle_t audioQHdl) {
audioDACQHdl = audioQHdl;
audioDACSemaphore = xSemaphoreCreateMutex();
audioDAC_data.mute = true;
audioDAC_data.volume = -1;
audioDAC_data.enabled = false;
}
/**
*
*/
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);
}
/**
*
*/
static void http_get_task(void *pvParameters) {
char *start;
base_message_t base_message_rx;
hello_message_t hello_message;
wire_chunk_message_t wire_chnk = {{0, 0}, 0, NULL};
char *hello_message_serialized = NULL;
int result;
int64_t now, trx, tdif, ttx;
time_message_t time_message_rx = {{0, 0}};
client_info_t clientInfo = {0, 0};
int64_t tmpDiffToServer;
int64_t lastTimeSync = 0;
esp_timer_handle_t timeSyncMessageTimer = NULL;
esp_err_t err = 0;
server_settings_message_t server_settings_message;
bool received_header = false;
mdns_result_t *r;
codec_type_t codec = NONE;
snapcastSetting_t scSet;
pcm_chunk_message_t *pcmData = NULL;
uint16_t remotePort = 0;
int rc1 = ERR_OK, rc2 = ERR_OK;
struct netbuf *firstNetBuf = NULL;
uint16_t len;
uint64_t timeout = FAST_SYNC_LATENCY_BUF;
char *codecString = NULL;
char *codecPayload = NULL;
char *serverSettingsString = NULL;
int connected_interface = -1;
esp_netif_t *netif = NULL;
// create a timer to send time sync messages every x µs
esp_timer_create(&tSyncArgs, &timeSyncMessageTimer);
idCounterSemaphoreHandle = xSemaphoreCreateMutex();
if (idCounterSemaphoreHandle == NULL) {
ESP_LOGE(TAG, "can't create id Counter Semaphore");
return;
}
#if CONFIG_SNAPCLIENT_USE_MDNS
ESP_LOGI(TAG, "Enable mdns");
mdns_init();
#endif
while (1) {
// do some house keeping
{
esp_timer_stop(timeSyncMessageTimer);
connected_interface = -1;
received_header = false;
timeout = FAST_SYNC_LATENCY_BUF;
xSemaphoreTake(idCounterSemaphoreHandle, portMAX_DELAY);
id_counter = 0;
xSemaphoreGive(idCounterSemaphoreHandle);
if (opusDecoder != NULL) {
opus_decoder_destroy(opusDecoder);
opusDecoder = NULL;
}
if (flacDecoder != NULL) {
FLAC__stream_decoder_finish(flacDecoder);
FLAC__stream_decoder_delete(flacDecoder);
flacDecoder = NULL;
}
if (decoderChunk.inData) {
free(decoderChunk.inData);
decoderChunk.inData = NULL;
}
if (decoderChunk.outData) {
free(decoderChunk.outData);
decoderChunk.outData = NULL;
}
if (codecString) {
free(codecString);
codecString = NULL;
}
if (codecPayload) {
free(codecPayload);
codecPayload = NULL;
}
if (codecPayload) {
free(serverSettingsString);
serverSettingsString = NULL;
}
if (lwipNetconn != NULL) {
netconn_delete(lwipNetconn);
lwipNetconn = NULL;
}
}
ESP_LOGI(TAG, "Wait for network connection");
#if CONFIG_SNAPCLIENT_USE_INTERNAL_ETHERNET || \
CONFIG_SNAPCLIENT_USE_SPI_ETHERNET
esp_netif_t *eth_netif =
network_get_netif_from_desc(NETWORK_INTERFACE_DESC_ETH);
#endif
esp_netif_t *sta_netif =
network_get_netif_from_desc(NETWORK_INTERFACE_DESC_STA);
while (1) {
#if CONFIG_SNAPCLIENT_USE_INTERNAL_ETHERNET || \
CONFIG_SNAPCLIENT_USE_SPI_ETHERNET
bool ethUp = network_is_netif_up(eth_netif);
if (ethUp) {
netif = eth_netif;
break;
}
#endif
bool staUp = network_is_netif_up(sta_netif);
if (staUp) {
netif = sta_netif;
break;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
#if SNAPCAST_SERVER_USE_MDNS
// Find snapcast server
// Connect to first snapcast server found
r = NULL;
err = 0;
while (!r || err) {
ESP_LOGI(TAG, "Lookup snapcast service on network");
esp_err_t err = mdns_query_ptr("_snapcast", "_tcp", 3000, 20, &r);
if (err) {
ESP_LOGE(TAG, "Query Failed");
vTaskDelay(pdMS_TO_TICKS(1000));
}
if (!r) {
ESP_LOGW(TAG, "No results found!");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
ESP_LOGI(TAG, "\n~~~~~~~~~~ MDNS Query success ~~~~~~~~~~");
mdns_print_results(r);
ESP_LOGI(TAG, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
ip_addr_t remote_ip;
mdns_result_t *re = r;
while (re) {
mdns_ip_addr_t *a = re->addr;
#if CONFIG_SNAPCLIENT_CONNECT_IPV6
if (a->addr.type == IPADDR_TYPE_V6) {
netif = re->esp_netif;
break;
}
// TODO: fall back to IPv4 if no IPv6 was available
#else
if (a->addr.type == IPADDR_TYPE_V4) {
netif = re->esp_netif;
break;
}
#endif
re = re->next;
}
if (!re) {
mdns_query_results_free(r);
ESP_LOGW(TAG, "didn't find any valid IP in MDNS query");
continue;
}
ip_addr_copy(remote_ip, re->addr->addr);
remotePort = r->port;
mdns_query_results_free(r);
ESP_LOGI(TAG, "Found %s:%d", ipaddr_ntoa(&remote_ip), remotePort);
#else
ip_addr_t remote_ip;
if (ipaddr_aton(SNAPCAST_SERVER_HOST, &remote_ip) == 0) {
ESP_LOGE(TAG, "can't convert static server adress to numeric");
continue;
}
remotePort = SNAPCAST_SERVER_PORT;
ESP_LOGI(TAG, "try connecting to static configuration %s:%d",
ipaddr_ntoa(&remote_ip), remotePort);
#endif
if (remote_ip.type == IPADDR_TYPE_V4) {
lwipNetconn = netconn_new(NETCONN_TCP);
ESP_LOGV(TAG, "netconn using IPv4");
} else if (remote_ip.type == IPADDR_TYPE_V6) {
lwipNetconn = netconn_new(NETCONN_TCP_IPV6);
ESP_LOGV(TAG, "netconn using IPv6");
} else {
ESP_LOGW(TAG, "remote IP has unsupported IP type");
continue;
}
if (lwipNetconn == NULL) {
ESP_LOGE(TAG, "can't create netconn");
continue;
}
#define USE_INTERFACE_BIND
#ifdef USE_INTERFACE_BIND // use interface to bind connection
uint8_t netifIdx = esp_netif_get_netif_impl_index(netif);
rc1 = netconn_bind_if(lwipNetconn, netifIdx);
if (rc1 != ERR_OK) {
ESP_LOGE(TAG, "can't bind interface %s", network_get_ifkey(netif));
}
#else // use IP to bind connection
if (remote_ip.type == IPADDR_TYPE_V4) {
// rc1 = netconn_bind(lwipNetconn, &ipAddr, 0);
rc1 = netconn_bind(lwipNetconn, IP4_ADDR_ANY, 0);
} else {
rc1 = netconn_bind(lwipNetconn, IP6_ADDR_ANY, 0);
}
if (rc1 != ERR_OK) {
ESP_LOGE(TAG, "can't bind local IP");
}
#endif
rc2 = netconn_connect(lwipNetconn, &remote_ip, remotePort);
if (rc2 != ERR_OK) {
ESP_LOGE(TAG, "can't connect to remote %s:%d, err %d",
ipaddr_ntoa(&remote_ip), remotePort, rc2);
#if !SNAPCAST_SERVER_USE_MDNS
vTaskDelay(pdMS_TO_TICKS(1000));
#endif
}
if (rc1 != ERR_OK || rc2 != ERR_OK) {
netconn_close(lwipNetconn);
netconn_delete(lwipNetconn);
lwipNetconn = NULL;
continue;
}
ESP_LOGI(TAG, "netconn connected using %s", network_get_ifkey(netif));
if (reset_latency_buffer() < 0) {
ESP_LOGE(TAG,
"reset_diff_buffer: couldn't reset median filter long. STOP");
return;
}
uint8_t base_mac[6];
#if CONFIG_SNAPCLIENT_USE_INTERNAL_ETHERNET || \
CONFIG_SNAPCLIENT_USE_SPI_ETHERNET
// Get MAC address for Eth Interface
char eth_mac_address[18];
esp_read_mac(base_mac, ESP_MAC_ETH);
sprintf(eth_mac_address, "%02X:%02X:%02X:%02X:%02X:%02X", base_mac[0],
base_mac[1], base_mac[2], base_mac[3], base_mac[4], base_mac[5]);
ESP_LOGI(TAG, "eth mac: %s", eth_mac_address);
#endif
// Get MAC address for WiFi station
char mac_address[18];
esp_read_mac(base_mac, ESP_MAC_WIFI_STA);
sprintf(mac_address, "%02X:%02X:%02X:%02X:%02X:%02X", base_mac[0],
base_mac[1], base_mac[2], base_mac[3], base_mac[4], base_mac[5]);
ESP_LOGI(TAG, "sta mac: %s", mac_address);
now = esp_timer_get_time();
// init base message
base_message_rx.type = SNAPCAST_MESSAGE_HELLO;
xSemaphoreTake(idCounterSemaphoreHandle, portMAX_DELAY);
base_message_rx.id = id_counter++;
xSemaphoreGive(idCounterSemaphoreHandle);
base_message_rx.refersTo = 0x0000;
base_message_rx.sent.sec = now / 1000000;
base_message_rx.sent.usec = now - base_message_rx.sent.sec * 1000000;
base_message_rx.received.sec = 0;
base_message_rx.received.usec = 0;
base_message_rx.size = 0x00000000;
// init hello message
hello_message.mac = mac_address;
hello_message.hostname = SNAPCAST_CLIENT_NAME;
hello_message.version = (char *)VERSION_STRING;
hello_message.client_name = "libsnapcast";
hello_message.os = "esp32";
hello_message.arch = "xtensa";
hello_message.instance = 1;
hello_message.id = mac_address;
hello_message.protocol_version = 2;
if (hello_message_serialized == NULL) {
hello_message_serialized = hello_message_serialize(
&hello_message, (size_t *)&(base_message_rx.size));
if (!hello_message_serialized) {
ESP_LOGE(TAG, "Failed to serialize hello message");
return;
}
}
result = base_message_serialize(&base_message_rx, base_message_serialized,
BASE_MESSAGE_SIZE);
if (result) {
ESP_LOGE(TAG, "Failed to serialize base message");
return;
}
rc1 = netconn_write(lwipNetconn, base_message_serialized, BASE_MESSAGE_SIZE,
NETCONN_NOCOPY);
if (rc1 != ERR_OK) {
ESP_LOGE(TAG, "netconn failed to send base message");
continue;
}
rc1 = netconn_write(lwipNetconn, hello_message_serialized,
base_message_rx.size, NETCONN_NOCOPY);
if (rc1 != ERR_OK) {
ESP_LOGE(TAG, "netconn failed to send hello message");
continue;
}
ESP_LOGI(TAG, "netconn sent hello message");
free(hello_message_serialized);
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.volume = 0;
scSet.muted = true;
uint64_t startTime, endTime;
// size_t currentPos = 0;
size_t typedMsgCurrentPos = 0;
uint32_t typedMsgLen = 0;
uint32_t offset = 0;
uint32_t payloadOffset = 0;
uint32_t tmpData = 0;
int32_t payloadDataShift = 0;
#define BASE_MESSAGE_STATE 0
#define TYPED_MESSAGE_STATE 1
// 0 ... base message, 1 ... typed message
uint32_t state = BASE_MESSAGE_STATE;
uint32_t internalState = 0;
firstNetBuf = NULL;
while (1) {
rc2 = netconn_recv(lwipNetconn, &firstNetBuf);
if (rc2 != ERR_OK) {
ESP_LOGE(TAG, "netconn err %d", rc2);
if (rc2 == ERR_CONN) {
netconn_close(lwipNetconn);
// restart and try to reconnect
break;
}
if (firstNetBuf != NULL) {
netbuf_delete(firstNetBuf);
firstNetBuf = NULL;
}
continue;
}
#if CONFIG_SNAPCLIENT_USE_INTERNAL_ETHERNET || \
CONFIG_SNAPCLIENT_USE_SPI_ETHERNET
if (scSet.muted) {
esp_netif_t *eth_netif =
network_get_netif_from_desc(NETWORK_INTERFACE_DESC_ETH);
if (netif != eth_netif) {
bool ethUp = network_is_netif_up(eth_netif);
if (ethUp) {
netconn_close(lwipNetconn);
if (firstNetBuf != NULL) {
netbuf_delete(firstNetBuf);
firstNetBuf = NULL;
}
// restart and try to reconnect using preferred interface ETH
break;
}
}
}
#endif
// now parse the data
netbuf_first(firstNetBuf);
do {
// currentPos = 0;
rc1 = netbuf_data(firstNetBuf, (void **)&start, &len);
if (rc1 == ERR_OK) {
// ESP_LOGI (TAG, "netconn rx, data len: %d, %d",
// len, netbuf_len(firstNetBuf));
} else {
ESP_LOGE(TAG, "netconn rx, couldn't get data");
continue;
}
while (len > 0) {
rc1 = ERR_OK; // probably not necessary
switch (state) {
// decode base message
case BASE_MESSAGE_STATE: {
switch (internalState) {
case 0:
base_message_rx.type = *start & 0xFF;
internalState++;
break;
case 1:
base_message_rx.type |= (*start & 0xFF) << 8;
internalState++;
break;
case 2:
base_message_rx.id = *start & 0xFF;
internalState++;
break;
case 3:
base_message_rx.id |= (*start & 0xFF) << 8;
internalState++;
break;
case 4:
base_message_rx.refersTo = *start & 0xFF;
internalState++;
break;
case 5:
base_message_rx.refersTo |= (*start & 0xFF) << 8;
internalState++;
break;
case 6:
base_message_rx.sent.sec = *start & 0xFF;
internalState++;
break;
case 7:
base_message_rx.sent.sec |= (*start & 0xFF) << 8;
internalState++;
break;
case 8:
base_message_rx.sent.sec |= (*start & 0xFF) << 16;
internalState++;
break;
case 9:
base_message_rx.sent.sec |= (*start & 0xFF) << 24;
internalState++;
break;
case 10:
base_message_rx.sent.usec = *start & 0xFF;
internalState++;
break;
case 11:
base_message_rx.sent.usec |= (*start & 0xFF) << 8;
internalState++;
break;
case 12:
base_message_rx.sent.usec |= (*start & 0xFF) << 16;
internalState++;
break;
case 13:
base_message_rx.sent.usec |= (*start & 0xFF) << 24;
internalState++;
break;
case 14:
base_message_rx.received.sec = *start & 0xFF;
internalState++;
break;
case 15:
base_message_rx.received.sec |= (*start & 0xFF) << 8;
internalState++;
break;
case 16:
base_message_rx.received.sec |= (*start & 0xFF) << 16;
internalState++;
break;
case 17:
base_message_rx.received.sec |= (*start & 0xFF) << 24;
internalState++;
break;
case 18:
base_message_rx.received.usec = *start & 0xFF;
internalState++;
break;
case 19:
base_message_rx.received.usec |= (*start & 0xFF) << 8;
internalState++;
break;
case 20:
base_message_rx.received.usec |= (*start & 0xFF) << 16;
internalState++;
break;
case 21:
base_message_rx.received.usec |= (*start & 0xFF) << 24;
internalState++;
break;
case 22:
base_message_rx.size = *start & 0xFF;
internalState++;
break;
case 23:
base_message_rx.size |= (*start & 0xFF) << 8;
internalState++;
break;
case 24:
base_message_rx.size |= (*start & 0xFF) << 16;
internalState++;
break;
case 25:
base_message_rx.size |= (*start & 0xFF) << 24;
internalState = 0;
now = esp_timer_get_time();
base_message_rx.received.sec = now / 1000000;
base_message_rx.received.usec =
now - base_message_rx.received.sec * 1000000;
typedMsgCurrentPos = 0;
// ESP_LOGI(TAG, "BM type %d ts %ld.%ld, refers to %u",
// base_message_rx.type,
// base_message_rx.received.sec,
// base_message_rx.received.usec,
// base_message_rx.refersTo);
// ESP_LOGI(TAG,"%u, %ld.%ld", base_message_rx.type,
// base_message_rx.received.sec,
// base_message_rx.received.usec);
// ESP_LOGI(TAG,"%u, %llu", base_message_rx.type,
// 1000000ULL *
// (uint64_t)base_message_rx.received.sec
// +
// (uint64_t)base_message_rx.received.usec);
state = TYPED_MESSAGE_STATE;
break;
}
// currentPos++;++;
len--;
start++;
break;
}
// decode typed message
case TYPED_MESSAGE_STATE: {
switch (base_message_rx.type) {
case SNAPCAST_MESSAGE_WIRE_CHUNK: {
switch (internalState) {
case 0: {
wire_chnk.timestamp.sec = *start & 0xFF;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 1: {
wire_chnk.timestamp.sec |= (*start & 0xFF) << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 2: {
wire_chnk.timestamp.sec |= (*start & 0xFF) << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 3: {
wire_chnk.timestamp.sec |= (*start & 0xFF) << 24;
// ESP_LOGI(TAG,
// "wire chunk time sec: %d",
// wire_chnk.timestamp.sec);
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 4: {
wire_chnk.timestamp.usec = (*start & 0xFF);
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 5: {
wire_chnk.timestamp.usec |= (*start & 0xFF) << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 6: {
wire_chnk.timestamp.usec |= (*start & 0xFF) << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 7: {
wire_chnk.timestamp.usec |= (*start & 0xFF) << 24;
// ESP_LOGI(TAG,
// "wire chunk time usec: %d",
// wire_chnk.timestamp.usec);
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 8: {
wire_chnk.size = (*start & 0xFF);
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 9: {
wire_chnk.size |= (*start & 0xFF) << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 10: {
wire_chnk.size |= (*start & 0xFF) << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 11: {
wire_chnk.size |= (*start & 0xFF) << 24;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
// TODO: we could use wire chunk directly maybe?
decoderChunk.bytes = wire_chnk.size;
while (!decoderChunk.inData) {
decoderChunk.inData =
(uint8_t *)malloc(decoderChunk.bytes);
if (!decoderChunk.inData) {
ESP_LOGW(TAG,
"malloc decoderChunk.inData failed, wait "
"1ms and try again");
vTaskDelay(pdMS_TO_TICKS(1));
}
}
payloadOffset = 0;
#if 0
ESP_LOGI(TAG, "chunk with size: %u, at time %ld.%ld",
wire_chnk.size,
wire_chnk.timestamp.sec,
wire_chnk.timestamp.usec);
#endif
if (len == 0) {
break;
}
}
case 12: {
size_t tmp_size;
if ((base_message_rx.size - typedMsgCurrentPos) <= len) {
tmp_size = base_message_rx.size - typedMsgCurrentPos;
} else {
tmp_size = len;
}
if (received_header == true) {
switch (codec) {
case OPUS:
case FLAC: {
memcpy(&decoderChunk.inData[payloadOffset], start,
tmp_size);
payloadOffset += tmp_size;
decoderChunk.outData = NULL;
decoderChunk.type = SNAPCAST_MESSAGE_WIRE_CHUNK;
break;
}
case PCM: {
size_t _tmp = tmp_size;
offset = 0;
if (pcmData == NULL) {
if (allocate_pcm_chunk_memory(
&pcmData, wire_chnk.size) < 0) {
pcmData = NULL;
}
tmpData = 0;
payloadDataShift = 3;
payloadOffset = 0;
}
while (_tmp--) {
tmpData |= ((uint32_t)start[offset++]
<< (8 * payloadDataShift));
payloadDataShift--;
if (payloadDataShift < 0) {
payloadDataShift = 3;
if ((pcmData) && (pcmData->fragment->payload)) {
volatile uint32_t *sample;
uint8_t dummy1;
uint32_t dummy2 = 0;
// TODO: find a more
// clever way to do this,
// best would be to
// actually store it the
// right way in the first
// place
dummy1 = tmpData >> 24;
dummy2 |= (uint32_t)dummy1 << 16;
dummy1 = tmpData >> 16;
dummy2 |= (uint32_t)dummy1 << 24;
dummy1 = tmpData >> 8;
dummy2 |= (uint32_t)dummy1 << 0;
dummy1 = tmpData >> 0;
dummy2 |= (uint32_t)dummy1 << 8;
tmpData = dummy2;
sample = (volatile uint32_t *)(&(
pcmData->fragment
->payload[payloadOffset]));
*sample = (volatile uint32_t)tmpData;
payloadOffset += 4;
}
tmpData = 0;
}
}
break;
}
default: {
ESP_LOGE(TAG, "Decoder (1) not supported");
return;
break;
}
}
}
typedMsgCurrentPos += tmp_size;
start += tmp_size;
// currentPos += tmp_size;
len -= tmp_size;
if (typedMsgCurrentPos >= base_message_rx.size) {
if (received_header == true) {
switch (codec) {
case OPUS: {
int frame_size = -1;
int samples_per_frame;
opus_int16 *audio = NULL;
samples_per_frame =
opus_packet_get_samples_per_frame(
decoderChunk.inData, scSet.sr);
if (samples_per_frame < 0) {
ESP_LOGE(TAG,
"couldn't get samples per frame count "
"of packet");
}
scSet.chkInFrames = samples_per_frame;
// ESP_LOGW(TAG, "%d, %llu, %llu",
// samples_per_frame, 1000000ULL *
// samples_per_frame / scSet.sr,
// 1000000ULL *
// wire_chnk.timestamp.sec +
// wire_chnk.timestamp.usec);
// ESP_LOGW(TAG, "got OPUS decoded chunk size: %ld
// " "frames from encoded chunk with size %d,
// allocated audio buffer %d", scSet.chkInFrames,
// wire_chnk.size, samples_per_frame);
size_t bytes;
do {
bytes = samples_per_frame *
(scSet.ch * scSet.bits >> 3);
while ((audio = (opus_int16 *)realloc(
audio, bytes)) == NULL) {
ESP_LOGE(TAG,
"couldn't realloc memory for OPUS "
"audio %d",
bytes);
vTaskDelay(pdMS_TO_TICKS(1));
}
frame_size = opus_decode(
opusDecoder, decoderChunk.inData,
decoderChunk.bytes, (opus_int16 *)audio,
samples_per_frame, 0);
samples_per_frame <<= 1;
} while (frame_size < 0);
free(decoderChunk.inData);
decoderChunk.inData = NULL;
pcm_chunk_message_t *new_pcmChunk = NULL;
// ESP_LOGW(TAG, "OPUS decode: %d", frame_size);
if (allocate_pcm_chunk_memory(&new_pcmChunk,
bytes) < 0) {
pcmData = NULL;
} else {
new_pcmChunk->timestamp = wire_chnk.timestamp;
if (new_pcmChunk->fragment->payload) {
volatile uint32_t *sample;
uint32_t tmpData;
uint32_t cnt = 0;
for (int i = 0; i < bytes; i += 4) {
sample = (volatile uint32_t *)(&(
new_pcmChunk->fragment->payload[i]));
tmpData = (((uint32_t)audio[cnt] << 16) &
0xFFFF0000) |
(((uint32_t)audio[cnt + 1] << 0) &
0x0000FFFF);
*sample = (volatile uint32_t)tmpData;
cnt += 2;
}
}
free(audio);
audio = NULL;
#if CONFIG_USE_DSP_PROCESSOR
if (new_pcmChunk->fragment->payload) {
dsp_processor_worker((void *)new_pcmChunk,
(void *)&scSet);
}
#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?");
return;
}
break;
}
case FLAC: {
isCachedChunk = true;
cachedBlocks = 0;
while (decoderChunk.bytes > 0) {
if (FLAC__stream_decoder_process_single(
flacDecoder) == 0) {
ESP_LOGE(
TAG,
"%s: FLAC__stream_decoder_process_single "
"failed",
__func__);
// TODO: should insert some abort condition?
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// alternating chunk sizes need time stamp repair
if ((cachedBlocks > 0) && (scSet.sr != 0)) {
uint64_t diffUs =
1000000ULL * cachedBlocks / scSet.sr;
uint64_t timestamp =
1000000ULL * wire_chnk.timestamp.sec +
wire_chnk.timestamp.usec;
timestamp = timestamp - diffUs;
wire_chnk.timestamp.sec =
timestamp / 1000000ULL;
wire_chnk.timestamp.usec =
timestamp % 1000000ULL;
}
pcm_chunk_message_t *new_pcmChunk;
int32_t ret = allocate_pcm_chunk_memory(
&new_pcmChunk, pcmChunk.bytes);
scSet.chkInFrames =
FLAC__stream_decoder_get_blocksize(
flacDecoder);
// ESP_LOGE (TAG, "block size: %ld",
// scSet.chkInFrames * scSet.bits / 8 * scSet.ch);
// ESP_LOGI(TAG, "new_pcmChunk with size %ld",
// new_pcmChunk->totalSize);
if (ret == 0) {
pcm_chunk_fragment_t *fragment =
new_pcmChunk->fragment;
uint32_t fragmentCnt = 0;
if (fragment->payload != NULL) {
uint32_t frames =
pcmChunk.bytes /
(scSet.ch * (scSet.bits / 8));
for (int i = 0; i < frames; i++) {
// TODO: for now fragmented payload is not
// supported and the whole chunk is expected
// to be in the first fragment
uint32_t tmpData;
memcpy(&tmpData,
&pcmChunk.outData[fragmentCnt],
(scSet.ch * (scSet.bits / 8)));
if (fragment != NULL) {
volatile uint32_t *test =
(volatile uint32_t *)(&(
fragment->payload[fragmentCnt]));
*test = (volatile uint32_t)tmpData;
}
fragmentCnt +=
(scSet.ch * (scSet.bits / 8));
if (fragmentCnt >= fragment->size) {
fragmentCnt = 0;
fragment = fragment->nextFragment;
}
}
}
new_pcmChunk->timestamp = wire_chnk.timestamp;
#if CONFIG_USE_DSP_PROCESSOR
if (new_pcmChunk->fragment->payload) {
dsp_processor_worker((void *)new_pcmChunk,
(void *)&scSet);
}
#endif
insert_pcm_chunk(new_pcmChunk);
}
free(pcmChunk.outData);
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?");
return;
}
break;
}
case PCM: {
size_t decodedSize = wire_chnk.size;
// ESP_LOGW(TAG, "got PCM chunk,"
// "typedMsgCurrentPos %d",
// typedMsgCurrentPos);
if (pcmData) {
pcmData->timestamp = wire_chnk.timestamp;
}
scSet.chkInFrames =
decodedSize /
((size_t)scSet.ch * (size_t)(scSet.bits / 8));
// ESP_LOGW(TAG,
// "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?");
return;
}
#if CONFIG_USE_DSP_PROCESSOR
if ((pcmData) && (pcmData->fragment->payload)) {
dsp_processor_worker((void *)pcmData,
(void *)&scSet);
}
#endif
if (pcmData) {
insert_pcm_chunk(pcmData);
}
pcmData = NULL;
free(decoderChunk.inData);
decoderChunk.inData = NULL;
break;
}
default: {
ESP_LOGE(TAG,
"Decoder (2) not "
"supported");
return;
break;
}
}
}
state = BASE_MESSAGE_STATE;
internalState = 0;
typedMsgCurrentPos = 0;
}
break;
}
default: {
ESP_LOGE(TAG,
"wire chunk decoder "
"shouldn't get here");
break;
}
}
break;
}
case SNAPCAST_MESSAGE_CODEC_HEADER: {
switch (internalState) {
case 0: {
received_header = false;
typedMsgLen = *start & 0xFF;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 1: {
typedMsgLen |= (*start & 0xFF) << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 2: {
typedMsgLen |= (*start & 0xFF) << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 3: {
typedMsgLen |= (*start & 0xFF) << 24;
if (codecString) {
free(codecString);
codecString = NULL;
}
codecString =
malloc(typedMsgLen + 1); // allocate memory for
// codec string
if (codecString == NULL) {
ESP_LOGE(TAG,
"couldn't get memory "
"for codec string");
return;
}
offset = 0;
// ESP_LOGI(TAG,
// "codec header string is %d long",
// typedMsgLen);
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 4: {
if (len >= typedMsgLen) {
memcpy(&codecString[offset], start, typedMsgLen);
offset += typedMsgLen;
typedMsgCurrentPos += typedMsgLen;
start += typedMsgLen;
// currentPos += typedMsgLen;
len -= typedMsgLen;
} else {
memcpy(&codecString[offset], start, typedMsgLen);
offset += len;
typedMsgCurrentPos += len;
start += len;
// currentPos += len;
len -= len;
}
if (offset == typedMsgLen) {
// NULL terminate string
codecString[typedMsgLen] = 0;
// ESP_LOGI (TAG, "got codec string: %s", tmp);
if (strcmp(codecString, "opus") == 0) {
codec = OPUS;
} else if (strcmp(codecString, "flac") == 0) {
codec = FLAC;
} else if (strcmp(codecString, "pcm") == 0) {
codec = PCM;
} else {
codec = NONE;
ESP_LOGI(TAG, "Codec : %s not supported",
codecString);
ESP_LOGI(TAG,
"Change encoder codec to "
"opus, flac or pcm in "
"/etc/snapserver.conf on "
"server");
return;
}
free(codecString);
codecString = NULL;
internalState++;
}
if (len == 0) {
break;
}
}
case 5: {
typedMsgLen = *start & 0xFF;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 6: {
typedMsgLen |= (*start & 0xFF) << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 7: {
typedMsgLen |= (*start & 0xFF) << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 8: {
typedMsgLen |= (*start & 0xFF) << 24;
if (codecPayload) {
free(codecPayload);
codecPayload = NULL;
}
codecPayload = malloc(typedMsgLen); // allocate memory
// for codec payload
if (codecPayload == NULL) {
ESP_LOGE(TAG,
"couldn't get memory "
"for codec payload");
return;
}
offset = 0;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 9: {
if (len >= typedMsgLen) {
memcpy(&codecPayload[offset], start, typedMsgLen);
offset += typedMsgLen;
typedMsgCurrentPos += typedMsgLen;
start += typedMsgLen;
// currentPos += typedMsgLen;
len -= typedMsgLen;
} else {
memcpy(&codecPayload[offset], start, len);
offset += len;
typedMsgCurrentPos += len;
start += len;
// currentPos += len;
len -= len;
}
if (offset == typedMsgLen) {
// first ensure everything is set up
// correctly and resources are
// available
if (flacDecoder != NULL) {
FLAC__stream_decoder_finish(flacDecoder);
FLAC__stream_decoder_delete(flacDecoder);
flacDecoder = NULL;
}
if (opusDecoder != NULL) {
opus_decoder_destroy(opusDecoder);
opusDecoder = NULL;
}
if (codec == OPUS) {
uint16_t channels;
uint32_t rate;
uint16_t bits;
memcpy(&rate, codecPayload + 4, sizeof(rate));
memcpy(&bits, codecPayload + 8, sizeof(bits));
memcpy(&channels, codecPayload + 10,
sizeof(channels));
scSet.codec = codec;
scSet.bits = bits;
scSet.ch = channels;
scSet.sr = rate;
ESP_LOGI(TAG, "Opus sample format: %ld:%d:%d\n", rate,
bits, channels);
int error = 0;
opusDecoder =
opus_decoder_create(scSet.sr, scSet.ch, &error);
if (error != 0) {
ESP_LOGI(TAG, "Failed to init opus coder");
return;
}
ESP_LOGI(TAG, "Initialized opus Decoder: %d", error);
} else if (codec == FLAC) {
decoderChunk.bytes = typedMsgLen;
do {
decoderChunk.inData =
(uint8_t *)malloc(decoderChunk.bytes);
vTaskDelay(pdMS_TO_TICKS(1));
} while (decoderChunk.inData == NULL);
memcpy(decoderChunk.inData, codecPayload,
typedMsgLen);
decoderChunk.outData = NULL;
decoderChunk.type = SNAPCAST_MESSAGE_CODEC_HEADER;
flacDecoder = FLAC__stream_decoder_new();
if (flacDecoder == NULL) {
ESP_LOGE(TAG, "Failed to init flac decoder");
return;
}
FLAC__StreamDecoderInitStatus init_status =
FLAC__stream_decoder_init_stream(
flacDecoder, read_callback, NULL, NULL, NULL,
NULL, write_callback, metadata_callback,
error_callback, &scSet);
if (init_status !=
FLAC__STREAM_DECODER_INIT_STATUS_OK) {
ESP_LOGE(TAG, "ERROR: initializing decoder: %s\n",
FLAC__StreamDecoderInitStatusString
[init_status]);
return;
}
FLAC__stream_decoder_process_until_end_of_metadata(
flacDecoder);
// ESP_LOGI(TAG, "%s: processed codec header",
// __func__);
} else if (codec == PCM) {
uint16_t channels;
uint32_t rate;
uint16_t bits;
memcpy(&channels, codecPayload + 22,
sizeof(channels));
memcpy(&rate, codecPayload + 24, sizeof(rate));
memcpy(&bits, codecPayload + 34, sizeof(bits));
scSet.codec = codec;
scSet.bits = bits;
scSet.ch = channels;
scSet.sr = rate;
ESP_LOGI(TAG, "pcm sampleformat: %ld:%d:%d", scSet.sr,
scSet.bits, scSet.ch);
} else {
ESP_LOGE(TAG,
"codec header decoder "
"shouldn't get here after "
"codec string was detected");
return;
}
free(codecPayload);
codecPayload = NULL;
if (player_send_snapcast_setting(&scSet) != pdPASS) {
ESP_LOGE(TAG,
"Failed to notify sync task. "
"Did you init player?");
return;
}
// ESP_LOGI(TAG, "done codec header msg");
state = BASE_MESSAGE_STATE;
internalState = 0;
received_header = true;
esp_timer_stop(timeSyncMessageTimer);
if (!esp_timer_is_active(timeSyncMessageTimer)) {
esp_timer_start_periodic(timeSyncMessageTimer,
timeout);
}
}
break;
}
default: {
ESP_LOGE(TAG,
"codec header decoder "
"shouldn't get here");
break;
}
}
break;
}
case SNAPCAST_MESSAGE_SERVER_SETTINGS: {
switch (internalState) {
case 0: {
typedMsgLen = *start & 0xFF;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 1: {
typedMsgLen |= (*start & 0xFF) << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 2: {
typedMsgLen |= (*start & 0xFF) << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 3: {
typedMsgLen |= (*start & 0xFF) << 24;
// ESP_LOGI(TAG,"server settings string is %lu"
// " long", typedMsgLen);
// now get some memory for server settings
// string
serverSettingsString = malloc(typedMsgLen + 1);
if (serverSettingsString == NULL) {
ESP_LOGE(TAG,
"couldn't get memory for "
"server settings string");
}
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
offset = 0;
if (len == 0) {
break;
}
}
case 4: {
size_t tmpSize =
base_message_rx.size - typedMsgCurrentPos;
if (len > 0) {
if (tmpSize < len) {
if (serverSettingsString) {
memcpy(&serverSettingsString[offset], start,
tmpSize);
}
offset += tmpSize;
start += tmpSize;
// currentPos += tmpSize; // will be
// incremented by 1
// later so -1 here
typedMsgCurrentPos += tmpSize;
len -= tmpSize;
} else {
if (serverSettingsString) {
memcpy(&serverSettingsString[offset], start, len);
}
offset += len;
start += len;
// currentPos += len; // will be incremented
// by 1 later so -1
// here
typedMsgCurrentPos += len;
len = 0;
}
}
if (typedMsgCurrentPos >= base_message_rx.size) {
if (serverSettingsString) {
// ESP_LOGI(TAG, "done server settings %lu/%lu",
// offset,
// typedMsgLen);
// NULL terminate string
serverSettingsString[typedMsgLen] = 0;
// ESP_LOGI(TAG, "got string: %s",
// serverSettingsString);
result = server_settings_message_deserialize(
&server_settings_message, serverSettingsString);
if (result) {
ESP_LOGE(TAG,
"Failed to read server "
"settings: %d",
result);
} else {
// 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);
ESP_LOGI(TAG, "Mute: %d",
server_settings_message.muted);
ESP_LOGI(TAG, "Setting volume: %ld",
server_settings_message.volume);
}
// Volume setting using ADF HAL
// abstraction
if (scSet.muted != server_settings_message.muted) {
#if SNAPCAST_USE_SOFT_VOL
if (server_settings_message.muted) {
dsp_processor_set_volome(0.0);
} else {
dsp_processor_set_volome(
(double)server_settings_message.volume / 100);
}
#endif
audio_set_mute(server_settings_message.muted);
}
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);
}
#else
audio_set_volume(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;
if (player_send_snapcast_setting(&scSet) != pdPASS) {
ESP_LOGE(TAG,
"Failed to notify sync task. "
"Did you init player?");
return;
}
free(serverSettingsString);
serverSettingsString = NULL;
}
state = BASE_MESSAGE_STATE;
internalState = 0;
typedMsgCurrentPos = 0;
}
break;
}
default: {
ESP_LOGE(TAG,
"server settings decoder "
"shouldn't get here");
break;
}
}
break;
}
// case SNAPCAST_MESSAGE_STREAM_TAGS: {
// size_t tmpSize = base_message_rx.size -
// typedMsgCurrentPos;
//
// if (tmpSize < len) {
// start += tmpSize;
// // currentPos += tmpSize;
// typedMsgCurrentPos += tmpSize;
// len -= tmpSize;
// } else {
// start += len;
// // currentPos += len;
//
// typedMsgCurrentPos += len;
// len = 0;
// }
//
// if (typedMsgCurrentPos >=
// base_message_rx.size) {
// // ESP_LOGI(TAG,
// // "done stream tags with length %d %d
// %d",
// // base_message_rx.size, currentPos,
// // tmpSize);
//
// typedMsgCurrentPos = 0;
// // currentPos = 0;
//
// state = BASE_MESSAGE_STATE;
// internalState = 0;
// }
//
// break;
// }
case SNAPCAST_MESSAGE_TIME: {
switch (internalState) {
case 0: {
time_message_rx.latency.sec = *start;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 1: {
time_message_rx.latency.sec |= (int32_t)*start << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 2: {
time_message_rx.latency.sec |= (int32_t)*start << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 3: {
time_message_rx.latency.sec |= (int32_t)*start << 24;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 4: {
time_message_rx.latency.usec = *start;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 5: {
time_message_rx.latency.usec |= (int32_t)*start << 8;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 6: {
time_message_rx.latency.usec |= (int32_t)*start << 16;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
internalState++;
if (len == 0) {
break;
}
}
case 7: {
time_message_rx.latency.usec |= (int32_t)*start << 24;
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
if (typedMsgCurrentPos >= base_message_rx.size) {
// ESP_LOGI(TAG, "done time message");
typedMsgCurrentPos = 0;
state = BASE_MESSAGE_STATE;
internalState = 0;
trx =
(int64_t)base_message_rx.received.sec * 1000000LL +
(int64_t)base_message_rx.received.usec;
ttx = (int64_t)base_message_rx.sent.sec * 1000000LL +
(int64_t)base_message_rx.sent.usec;
tdif = trx - ttx;
trx = (int64_t)time_message_rx.latency.sec * 1000000LL +
(int64_t)time_message_rx.latency.usec;
tmpDiffToServer = (trx - tdif) / 2;
int64_t diff;
// clear diffBuffer if last update is
// older than a minute
diff = now - lastTimeSync;
if (diff > 60000000LL) {
ESP_LOGW(TAG,
"Last time sync older "
"than a minute. "
"Clearing time buffer");
reset_latency_buffer();
timeout = FAST_SYNC_LATENCY_BUF;
esp_timer_stop(timeSyncMessageTimer);
if (received_header == true) {
if (!esp_timer_is_active(timeSyncMessageTimer)) {
esp_timer_start_periodic(timeSyncMessageTimer,
timeout);
}
}
}
player_latency_insert(tmpDiffToServer);
// ESP_LOGI(TAG, "Current latency:%lld:",
// tmpDiffToServer);
// store current time
lastTimeSync = now;
if (received_header == true) {
if (!esp_timer_is_active(timeSyncMessageTimer)) {
esp_timer_start_periodic(timeSyncMessageTimer,
timeout);
}
bool is_full = false;
latency_buffer_full(&is_full, portMAX_DELAY);
if ((is_full == true) &&
(timeout < NORMAL_SYNC_LATENCY_BUF)) {
timeout = NORMAL_SYNC_LATENCY_BUF;
ESP_LOGI(TAG, "latency buffer full");
if (esp_timer_is_active(timeSyncMessageTimer)) {
esp_timer_stop(timeSyncMessageTimer);
}
esp_timer_start_periodic(timeSyncMessageTimer,
timeout);
} else if ((is_full == false) &&
(timeout > FAST_SYNC_LATENCY_BUF)) {
timeout = FAST_SYNC_LATENCY_BUF;
ESP_LOGI(TAG, "latency buffer not full");
if (esp_timer_is_active(timeSyncMessageTimer)) {
esp_timer_stop(timeSyncMessageTimer);
}
esp_timer_start_periodic(timeSyncMessageTimer,
timeout);
}
}
} else {
ESP_LOGE(TAG,
"error time message, this "
"shouldn't happen! %d %ld",
typedMsgCurrentPos, base_message_rx.size);
typedMsgCurrentPos = 0;
state = BASE_MESSAGE_STATE;
internalState = 0;
}
break;
}
default: {
ESP_LOGE(TAG,
"time message decoder shouldn't "
"get here %d %ld %ld",
typedMsgCurrentPos, base_message_rx.size,
internalState);
break;
}
}
break;
}
default: {
typedMsgCurrentPos++;
start++;
// currentPos++;
len--;
if (typedMsgCurrentPos >= base_message_rx.size) {
ESP_LOGI(TAG, "done unknown typed message %d",
base_message_rx.type);
state = BASE_MESSAGE_STATE;
internalState = 0;
typedMsgCurrentPos = 0;
}
break;
}
}
break;
}
default: {
break;
}
}
if (rc1 != ERR_OK) {
break;
}
}
} while (netbuf_next(firstNetBuf) >= 0);
netbuf_delete(firstNetBuf);
if (rc1 != ERR_OK) {
ESP_LOGE(TAG, "Data error, closing netconn");
netconn_close(lwipNetconn);
break;
}
}
}
}
/**
*
*/
void app_main(void) {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
esp_log_level_set("*", ESP_LOG_INFO);
// if enabled these cause a timer srv stack overflow
esp_log_level_set("HEADPHONE", ESP_LOG_NONE);
esp_log_level_set("gpio", ESP_LOG_WARN);
esp_log_level_set("uart", ESP_LOG_WARN);
// esp_log_level_set("i2s_std", ESP_LOG_DEBUG);
// esp_log_level_set("i2s_common", ESP_LOG_DEBUG);
esp_log_level_set("wifi", ESP_LOG_WARN);
esp_log_level_set("wifi_init", ESP_LOG_WARN);
#if CONFIG_SNAPCLIENT_USE_INTERNAL_ETHERNET || \
CONFIG_SNAPCLIENT_USE_SPI_ETHERNET
// clang-format off
// nINT/REFCLKO Function Select Configuration Strap
// • When nINTSEL is floated or pulled to
// VDD2A, nINT is selected for operation on the
// nINT/REFCLKO pin (default).
// • When nINTSEL is pulled low to VSS, REF-
// CLKO is selected for operation on the nINT/
// REFCLKO pin.
//
// LAN8720 doesn't stop REFCLK while in reset, so we leave the
// strap floated. It is connected to IO0 on ESP32 so we get nINT
// function with a HIGH pin value, which is also perfect during boot.
// Before initializing LAN8720 (which resets the PHY) we pull the
// strap low and this results in REFCLK enabled which is needed
// for MAC unit.
//
// clang-format on
gpio_config_t cfg = {.pin_bit_mask = BIT64(GPIO_NUM_5),
.mode = GPIO_MODE_DEF_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_ENABLE,
.intr_type = GPIO_INTR_DISABLE};
gpio_config(&cfg);
#endif
board_i2s_pin_t pin_config0;
get_i2s_pins(I2S_NUM_0, &pin_config0);
#if CONFIG_AUDIO_BOARD_CUSTOM && CONFIG_DAC_ADAU1961
// some codecs need i2s mclk for initialization
i2s_chan_handle_t tx_chan;
i2s_chan_config_t tx_chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 2,
.dma_frame_num = 128,
.auto_clear = true,
};
ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_chan, NULL));
i2s_std_clk_config_t i2s_clkcfg = {
.sample_rate_hz = 44100,
.clk_src = I2S_CLK_SRC_APLL,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
};
i2s_std_config_t tx_std_cfg = {
.clk_cfg = i2s_clkcfg,
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_STEREO),
.gpio_cfg =
{
.mclk = pin_config0
.mck_io_num, // some codecs may require mclk signal,
// this example doesn't need it
.bclk = pin_config0.bck_io_num,
.ws = pin_config0.ws_io_num,
.dout = pin_config0.data_out_num,
.din = pin_config0.data_in_num,
.invert_flags =
{
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_chan, &tx_std_cfg));
i2s_channel_enable(tx_chan);
#endif
ESP_LOGI(TAG, "Start codec chip");
audio_board_handle_t board_handle = audio_board_init();
if (board_handle) {
ESP_LOGI(TAG, "Audio board_init done");
} else {
ESP_LOGE(TAG,
"Audio board couldn't be initialized. Check menuconfig if project "
"is configured right or check your wiring!");
vTaskDelay(portMAX_DELAY);
}
audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_DECODE,
AUDIO_HAL_CTRL_STOP);
audio_hal_set_mute(board_handle->audio_hal,
true); // ensure no noise is sent after firmware crash
#if CONFIG_AUDIO_BOARD_CUSTOM && CONFIG_DAC_ADAU1961
if (tx_chan) {
i2s_channel_disable(tx_chan);
i2s_del_channel(tx_chan);
tx_chan = NULL;
}
#endif
// ESP_LOGI(TAG, "init player");
i2s_std_gpio_config_t i2s_pin_config0 = {
.mclk = pin_config0.mck_io_num,
.bclk = pin_config0.bck_io_num,
.ws = pin_config0.ws_io_num,
.dout = pin_config0.data_out_num,
.din = pin_config0.data_in_num,
.invert_flags =
{
#if CONFIG_INVERT_MCLK_LEVEL
.mclk_inv = true,
#else
.mclk_inv = false,
#endif
#if CONFIG_INVERT_BCLK_LEVEL
.bclk_inv = true,
#else
.bclk_inv = false,
#endif
#if CONFIG_INVERT_WORD_SELECT_LEVEL
.ws_inv = true,
#else
.ws_inv = false,
#endif
},
};
QueueHandle_t audioQHdl = xQueueCreate(1, sizeof(audioDACdata_t));
init_snapcast(audioQHdl);
init_player(i2s_pin_config0, I2S_NUM_0);
// ensure there is no noise from DAC
{
gpio_config_t gpioCfg = {
.pin_bit_mask =
BIT64(pin_config0.mck_io_num) | BIT64(pin_config0.data_out_num) |
BIT64(pin_config0.bck_io_num) | BIT64(pin_config0.ws_io_num),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&gpioCfg);
gpio_set_level(pin_config0.mck_io_num, 0);
gpio_set_level(pin_config0.data_out_num, 0);
gpio_set_level(pin_config0.bck_io_num, 0);
gpio_set_level(pin_config0.ws_io_num, 0);
}
/*
#if CONFIG_SNAPCLIENT_USE_INTERNAL_ETHERNET || \
CONFIG_SNAPCLIENT_USE_SPI_ETHERNET
eth_init();
// pass "WIFI_STA_DEF", "WIFI_AP_DEF", "ETH_DEF"
init_http_server_task("ETH_DEF");
#else
// Enable and setup WIFI in station mode and connect to Access point setup
in
// menu config or set up provisioning mode settable in menuconfig
wifi_init();
ESP_LOGI(TAG, "Connected to AP");
// http server for control operations and user interface
// pass "WIFI_STA_DEF", "WIFI_AP_DEF", "ETH_DEF"
init_http_server_task("WIFI_STA_DEF");
#endif
*/
network_if_init();
init_http_server_task();
// Enable websocket server
// ESP_LOGI(TAG, "Setup ws server");
// websocket_if_start();
net_mdns_register("snapclient");
#ifdef CONFIG_SNAPCLIENT_SNTP_ENABLE
set_time_from_sntp();
#endif
#if CONFIG_USE_DSP_PROCESSOR
dsp_processor_init();
#endif
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);
// while (1) {
// // audio_event_iface_msg_t msg;
// vTaskDelay(portMAX_DELAY); //(pdMS_TO_TICKS(5000));
//
// // ma120_read_error(0x20);
//
// esp_err_t ret = 0; // audio_event_iface_listen(evt, &msg,
// portMAX_DELAY); if (ret != ESP_OK) {
// ESP_LOGE(TAG, "[ * ] Event interface error : %d", ret);
// continue;
// }
// }
audioDACdata_t dac_data;
audioDACdata_t dac_data_old = {
.mute = true,
.volume = -1,
.enabled = false,
};
#if 1
esp_pm_config_t pmConfig = {80 , 240, true};
esp_pm_configure(&pmConfig);
#endif
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;
}
}
}