diff --git a/build-scripts/PureS3-sdkconfig.defaults b/build-scripts/PureS3-sdkconfig.defaults index 477a79423..1449daad2 100644 --- a/build-scripts/PureS3-sdkconfig.defaults +++ b/build-scripts/PureS3-sdkconfig.defaults @@ -927,7 +927,7 @@ CONFIG_LWIP_SO_REUSE=y CONFIG_LWIP_SO_REUSE_RXTOALL=y # CONFIG_LWIP_SO_RCVBUF is not set # CONFIG_LWIP_NETBUF_RECVINFO is not set -# CONFIG_LWIP_IP4_FRAG is not set +# CONFIG_LWIP_IP4_FRAG=y # CONFIG_LWIP_IP6_FRAG is not set CONFIG_LWIP_IP4_REASSEMBLY=y CONFIG_LWIP_IP6_REASSEMBLY=y diff --git a/build-scripts/tembed-sdconfig.defaults b/build-scripts/tembed-sdconfig.defaults index 8415f5f68..a26314e91 100644 --- a/build-scripts/tembed-sdconfig.defaults +++ b/build-scripts/tembed-sdconfig.defaults @@ -168,6 +168,8 @@ CONFIG_TARGET_LOCKED=y # # Factory Configuration # +CONFIG_ADC_CONFIG="model=ES7210,bck=47,ws=21,di=14,mck=48,i2c=64,sda=18,scl=8" +CONFIG_ADC_LOCKED=y CONFIG_AUDIO_CONTROLS="" CONFIG_BAT_CONFIG="channel=3,scale=6.5,cells=1,atten=3" # CONFIG_BAT_LOCKED is not set @@ -228,6 +230,7 @@ CONFIG_A2DP_CONNECT_TIMEOUT_MS=1000 # CONFIG_BT_SINK is not set # CONFIG_AIRPLAY_SINK is not set # CONFIG_CSPOT_SINK is not set +CONFIG_ADC_SINK=y # end of Audio Input # @@ -934,7 +937,7 @@ CONFIG_LWIP_SO_REUSE=y CONFIG_LWIP_SO_REUSE_RXTOALL=y # CONFIG_LWIP_SO_RCVBUF is not set # CONFIG_LWIP_NETBUF_RECVINFO is not set -# CONFIG_LWIP_IP4_FRAG is not set +CONFIG_LWIP_IP4_FRAG=y # CONFIG_LWIP_IP6_FRAG is not set CONFIG_LWIP_IP4_REASSEMBLY=y CONFIG_LWIP_IP6_REASSEMBLY=y diff --git a/components/adc/CMakeLists.txt b/components/adc/CMakeLists.txt new file mode 100644 index 000000000..43477c041 --- /dev/null +++ b/components/adc/CMakeLists.txt @@ -0,0 +1,5 @@ + +idf_component_register(SRC_DIRS . + INCLUDE_DIRS . + PRIV_REQUIRES freertos pthread squeezelite services codecs tools display wifi-manager +) \ No newline at end of file diff --git a/components/adc/adc_sink.c b/components/adc/adc_sink.c new file mode 100644 index 000000000..cf43beb34 --- /dev/null +++ b/components/adc/adc_sink.c @@ -0,0 +1,123 @@ +#include +#include +#include +#include + +#include "nvs.h" +#include "esp_netif.h" +#include "esp_log.h" +#include "esp_console.h" +#include "esp_pthread.h" +#include "esp_system.h" +#include "freertos/timers.h" +#include "platform_config.h" +#include "input_i2s.h" +//#include "audio_controls.h" +#include "display.h" +#include "accessors.h" +#include "network_services.h" + +static EXT_RAM_ATTR struct adc_cb_s { + adc_cmd_vcb_t cmd; + adc_data_cb_t data; +} adc_cbs; + +static const char TAG[] = "adc_sink"; + +static struct adc_ctx_s *adc_i2s; +static adc_cmd_vcb_t cmd_handler_chain; + +void adc_linein_start(uint16_t sample_rate) { + ESP_LOGI(TAG, "ADC Start (setup)"); + adc_cmd(adc_i2s, ADC_SETUP, sample_rate); + ESP_LOGI(TAG, "ADC Start (play)"); + adc_cmd(adc_i2s, ADC_PLAY, NULL); + ESP_LOGI(TAG, "ADC Start (done)"); + +} + +/**************************************************************************************** + * Command handler + */ +static bool cmd_handler(adc_event_t event, ...) { + va_list args; + + va_start(args, event); + + // handle audio event and stop if forbidden + if (!cmd_handler_chain(event, args)) { + va_end(args); + return false; + } + + // now handle events for display + switch(event) { + case ADC_SETUP: + ESP_LOGI(TAG, "ADC Setup"); + displayer_control(DISPLAYER_ACTIVATE, "ADC INPUT ACTIVE", true); + displayer_artwork(NULL); + break; + case ADC_PLAY: + ESP_LOGI(TAG, "ADC Play"); + // control it internally + //displayer_control(DISPLAYER_TIMER_RUN); + displayer_control(DISPLAYER_SHUTDOWN); // hand back to lms + break; + case ADC_STALLED: + ESP_LOGI(TAG, "ADC Stalled"); + adc_abort(adc_i2s); + displayer_control(DISPLAYER_SHUTDOWN); + break; + + default: + ESP_LOGI(TAG, "ADC Unknown: %d", event); + break; + } + + va_end(args); + + return true; +} + +/**************************************************************************************** + * Airplay sink de-initialization + */ +void adc_sink_deinit(void) { + adc_delete(adc_i2s); + ESP_LOGI(TAG, "deinit ADC"); +} + +/**************************************************************************************** + * ADC sink startup + */ +static void adc_sink_start(nm_state_t state_id, int sub_state) { + const char *hostname; + + cmd_handler_chain = adc_cbs.cmd; + network_get_hostname(&hostname); + + ESP_LOGI(TAG, "starting ADC on host %s", hostname); + + adc_i2s = adc_create(cmd_handler, adc_cbs.data); +} + +/**************************************************************************************** + * ADC sink initialization + */ +void adc_sink_init(adc_cmd_vcb_t cmd_cb, adc_data_cb_t data_cb) { + adc_cbs.cmd = cmd_cb; + adc_cbs.data = data_cb; + + network_register_state_callback(NETWORK_WIFI_ACTIVE_STATE, WIFI_CONNECTED_STATE, "adc_sink_start", adc_sink_start); + network_register_state_callback(NETWORK_ETH_ACTIVE_STATE, ETH_ACTIVE_CONNECTED_STATE, "adc_sink_start", adc_sink_start); +} + +/**************************************************************************************** + * ADC forced disconnection + */ +void adc_disconnect(void) { + ESP_LOGI(TAG, "forced disconnection"); + displayer_control(DISPLAYER_SHUTDOWN); + adc_cmd(adc_i2s, ADC_CLOSE, NULL); +} + diff --git a/components/adc/adc_sink.h b/components/adc/adc_sink.h new file mode 100644 index 000000000..11344bc17 --- /dev/null +++ b/components/adc/adc_sink.h @@ -0,0 +1,39 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#ifndef ADC_SINK_H +#define ADC_SINK_H + +#include +#include + + +typedef enum { ADC_SETUP, ADC_STREAM, ADC_PLAY, ADC_FLUSH, ADC_STOP, ADC_STALLED, + ADC_TOGGLE, ADC_CLOSE, ADC_METADATA, ADC_GAIN } adc_event_t ; // , ADC_VOLUME_UP, ADC_VOLUME_DOWN, + +typedef bool (*adc_cmd_cb_t)(adc_event_t event, ...); +typedef bool (*adc_cmd_vcb_t)(adc_event_t event, va_list args); +typedef void (*adc_data_cb_t)(const uint8_t *data, size_t len); + +/** + * @brief init sink mode (need to be provided) + */ +void adc_sink_init(adc_cmd_vcb_t cmd_cb, adc_data_cb_t data_cb); + +/** + * @brief deinit sink mode (need to be provided) + */ +void adc_sink_deinit(void); + +/** + * @brief force disconnection + */ +void adc_disconnect(void); +void adc_linein_start(uint16_t sample_rate); + +#endif /* ADC_SINK_H*/ \ No newline at end of file diff --git a/components/adc/input_i2s.c b/components/adc/input_i2s.c new file mode 100644 index 000000000..d675780ac --- /dev/null +++ b/components/adc/input_i2s.c @@ -0,0 +1,434 @@ +/* + * + * (c) Wizmo 2023, + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + * + * Initalizes i2s input for loopback or streaming + * - if host and port is provided, streaming will start on boot + * - fmt specifies streaming format. currently RAW or WAVE @16khz single channel. + * - if i2s ws etc is porvided, will create new adac on channel 1, otherwise will use existing adac (and its i2c port if applicable). + * adac init should configure input stream and mixer (and will define mic /line-in config). lineinon and lineinoff added as commands. + * - if i2s and i2c is provided, will create port, otherwise use i2c_config + * - playback is controlled by lms for gain, loop back and displayer + */ + +#include + +#include "esp_pthread.h" +#include +//#include + +#include "input_i2s.h" +#include "gpio_exp.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_task.h" +#include +#include "freertos/FreeRTOS.h" +#include "freertos/timers.h" +#include "mbedtls/net_sockets.h" +#include "platform_config.h" +#include "adac.h" + +#include "lwip/err.h" +#include "lwip/sockets.h" +#include "lwip/sys.h" + +#define ADC_SAMPLE_RATE_HZ 16000 // default, use 16000 for Rhasspy / OpenWakeWord (OWW) +#define ADC_STREAM_FRAME_SIZE 2048 // Use 2048 for OWW (in bytes) +#define ADC_CHANNELS_OUT 1 // default, use 1 for OWW +#define ADC_CHANNELS_IN 2 +// TODO: Add 32bit support. +//#define BYTES_PER_FRAME 4 // 4 (2x16bit) or 8 (2x32bit) + +#define ADC_I2C_PORT 1 +#define ADC_I2S_CH I2S_NUM_1 + +#define ADC_STACK_SIZE (4*1024) + +#define SAFE_PTR_FREE(P) \ + do { \ + TimerHandle_t timer = xTimerCreate("cleanup", pdMS_TO_TICKS(10000), pdFALSE, P, _delayed_free); \ + xTimerStart(timer, portMAX_DELAY); \ + } while (0) +static void inline _delayed_free(TimerHandle_t xTimer) { + free(pvTimerGetTimerID(xTimer)); + xTimerDelete(xTimer, portMAX_DELAY); +} + + +const struct adac_s *aadc; + +typedef struct adc_ctx_s { + uint32_t host; + uint16_t port; + uint16_t channels; + int sock; // socket of listener + + bool running; // recieve i2s data + bool speaker; // send to dac + adac_src_e source; // use microphone over line in + bool stream; // stream over socket + + TaskHandle_t thread, joiner; + StaticTask_t *xTaskBuffer; + StackType_t xStack[ADC_STACK_SIZE] __attribute__ ((aligned (4))); + + bool abort; + uint16_t i2s_ch; + uint16_t sample_rate; + int16_t *input_buff; + int16_t *stream_buff; + size_t bytes_read; + adc_cmd_cb_t cmd_cb; + adc_data_cb_t data_cb; + void *owner; + +} adc_ctx_t; + +static const char TAG[] = "input_i2c"; +char title[20]; + +static void adc_thread(void *arg); + +/*----------------------------------------------------------------------------*/ +struct adc_ctx_s *adc_create(adc_cmd_cb_t cmd_cb, adc_data_cb_t data_cb) { + + struct adc_ctx_s *ctx = malloc(sizeof(struct adc_ctx_s)); + if (!ctx) return NULL; + + // make sure we have a clean context + memset(ctx, 0, sizeof(adc_ctx_t)); + + ctx->cmd_cb = cmd_cb; + ctx->data_cb = data_cb; + + // Load configration from NVS + char* config = config_alloc_get_str("adc_stream", NULL, " "); + ctx->sample_rate = ADC_SAMPLE_RATE_HZ; + PARSE_PARAM(config, "rate",'=', ctx->sample_rate); + char host[32]; + PARSE_PARAM_STR(config, "host", '=', host, 32); + ctx->host = inet_addr(host); + PARSE_PARAM(config, "port",'=', ctx->port); + ctx->channels = ADC_CHANNELS_OUT; + PARSE_PARAM(config, "ch",'=', ctx->channels); + PARSE_PARAM(config, "source",'=', ctx->source); + free(config); + + config = config_alloc_get_str("adc_config", NULL, CONFIG_ADC_CONFIG); + char model[32] = "\0"; + PARSE_PARAM_STR(config, "model", '=', model, 31); + if (model[0] != 0) { + ctx->i2s_ch = ADC_I2S_CH; + aadc = &dac_external; + } + + if (aadc) { + i2s_pin_config_t pin_config = {.bck_io_num=-1, .ws_io_num=-1, .data_out_num=-1, .data_in_num=-1, .mck_io_num=-1 }; + PARSE_PARAM(config, "bck",'=', pin_config.bck_io_num); + PARSE_PARAM(config, "ws",'=', pin_config.ws_io_num); + PARSE_PARAM(config, "do",'=', pin_config.data_out_num); + PARSE_PARAM(config, "di",'=', pin_config.data_in_num); + PARSE_PARAM(config, "mck",'=', pin_config.mck_io_num); + ESP_LOGI( TAG, "%s ADC using I2S ch %d bck:%u, ws:%u, di:%u (mlk:%u)", model, ADC_CHANNELS_IN, pin_config.bck_io_num, pin_config.ws_io_num, pin_config.data_in_num, pin_config.mck_io_num); + + i2s_config_t i2s_config = { + .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), + .sample_rate = ctx->sample_rate, + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .channel_format = (ADC_CHANNELS_IN==2 ? I2S_CHANNEL_FMT_RIGHT_LEFT : I2S_CHANNEL_FMT_ONLY_LEFT), + .communication_format = I2S_COMM_FORMAT_STAND_I2S, + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = 4, //8, + .dma_buf_len = 128, //64, + .use_apll = true, + .tx_desc_auto_clear = true, + .fixed_mclk = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .bits_per_chan = I2S_BITS_PER_CHAN_16BIT, +#ifndef CONFIG_IDF_TARGET_ESP32 + .chan_mask = (i2s_channel_t)(I2S_TDM_ACTIVE_CH0 | I2S_TDM_ACTIVE_CH1), +#endif + }; + + uint32_t res = ESP_OK; + bool mck_required = false; + res = aadc->init(config, ADC_I2C_PORT, &i2s_config, &mck_required) ? ESP_OK : ESP_FAIL; + if (res != ESP_OK) { + ESP_LOGE(TAG, "Error initializing i2c on ADC"); + } +#ifdef CONFIG_IDF_TARGET_ESP32 +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) + int mck_io_num = strcasestr(dac_config, "mck") || mck_required ? 0 : -1; + PARSE_PARAM(dac_config, "mck", '=', mck_io_num); + + ESP_LOGI(TAG, "configuring MCLK on GPIO %d", mck_io_num); + + if (mck_io_num == GPIO_NUM_0) { + PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1); + WRITE_PERI_REG(PIN_CTRL, CONFIG_DAC_I2S_NUM == I2S_NUM_0 ? 0xFFF0 : 0xFFFF); + } else if (mck_io_num == GPIO_NUM_1) { + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3); + WRITE_PERI_REG(PIN_CTRL, CONFIG_DAC_I2S_NUM == I2S_NUM_0 ? 0xF0F0 : 0xF0FF); + } else if (mck_io_num == GPIO_NUM_2) { + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2); + WRITE_PERI_REG(PIN_CTRL, CONFIG_DAC_I2S_NUM == I2S_NUM_0 ? 0xFF00 : 0xFF0F); + } else { + LOG_WARN("invalid MCK gpio %d", mck_io_num); + } +#else + if (mck_required && pin_config.mck_io_num == -1) pin_config.mck_io_num = 0; + ESP_LOGI(TAG, "configuring MCLK on GPIO %d", pin_config.mck_io_num); +#endif +#endif + ESP_LOGI( TAG, "Initializing ADC I2S with rate: %u, bits per sample: %u, buffer frames: %u, number of buffers: %u", + i2s_config.sample_rate, i2s_config.bits_per_sample, i2s_config.dma_buf_count, i2s_config.dma_buf_len) ; + + /* Start I2s for read */ + i2s_driver_install(ADC_I2S_CH, &i2s_config, 0, NULL); + i2s_set_pin(ADC_I2S_CH, &pin_config); + i2s_zero_dma_buffer(ADC_I2S_CH); + + } else { + // No dedicated ADC chip + ESP_LOGI( TAG, "ADC sharing not currently available"); + } + + if (ctx->host && ctx->port) { + /* Initilaize output stream */ + int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sock < 0) { + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + free(ctx); + return NULL; + } + + int enable = 1; + int err = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + } + + //set to non-blocking mode + int flags = fcntl(sock, F_GETFL, 0); + err = fcntl(sock, F_SETFL, flags | O_NONBLOCK); + //err = ioctl(sock, FIONBIO, &enable); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + free(ctx); + return NULL; + } + + ctx->sock = sock; + + ESP_LOGI(TAG, "Configured ADC stream %s:%d fmt:%s src:%u", host, ctx->port, "WAVE", ctx->source); + } + + ctx->running = true; + ctx->cmd_cb(ADC_SETUP, ctx->sample_rate); + + ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ctx->thread = xTaskCreateStaticPinnedToCore( (TaskFunction_t) adc_thread, "ADC", ADC_STACK_SIZE, ctx, + ESP_TASK_PRIO_MIN + 2, ctx->xStack, ctx->xTaskBuffer, CONFIG_PTHREAD_TASK_CORE_DEFAULT); + + ctx->stream = true; + + return ctx; +} + +/*----------------------------------------------------------------------------*/ +void adc_abort(struct adc_ctx_s *ctx) { + ESP_LOGI(TAG, "[%p]: aborting ADC session at next select() wakeup", ctx); + ctx->abort = true; +} + +/*----------------------------------------------------------------------------*/ +void adc_delete(struct adc_ctx_s *ctx) { + // ADD I2S CLEAN UP NAN_CONCAT_HELPER + ESP_LOGI(TAG, "[%p]: deleting ADC session", ctx); + + if (!ctx) return; + + // then the task + ctx->joiner = xTaskGetCurrentTaskHandle(); + ctx->running = false; + + // brute-force exit of accept() + shutdown(ctx->sock, SHUT_RDWR); + closesocket(ctx->sock); + + // wait to make sure LWIP if scheduled (avoid issue with NotifyTake) + vTaskDelay(100 / portTICK_PERIOD_MS); + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + vTaskDelete(ctx->thread); + SAFE_PTR_FREE(ctx->xTaskBuffer); + + free(ctx); +} + +/*----------------------------------------------------------------------------*/ +bool adc_cmd(struct adc_ctx_s *ctx, adc_event_t event, void *param) { + + ESP_LOGI(TAG, "ADC_cmd %d", event ); + + bool success = false; + + // first notify the remote controller (if any) + switch(event) { + case ADC_SETUP: + ESP_LOGI(TAG, "ADC Setup"); + ctx->stream = true; + ctx->cmd_cb(ADC_SETUP, ctx->sample_rate); + break; + case ADC_PLAY: + ESP_LOGI(TAG, "ADC Play"); + ctx->speaker = true; + ESP_LOGI(TAG, "About to start play cb"); + ctx->cmd_cb(ADC_PLAY); + break; + case ADC_STREAM: + ESP_LOGI(TAG, "ADC Stream"); + ctx->stream = true; + break; + case ADC_STOP: + ESP_LOGI(TAG, "ADC Stop"); + ctx->speaker = false; + break; + case ADC_CLOSE: + ESP_LOGI(TAG, "ADC Close"); + ctx->speaker = false; + break; + default: + ESP_LOGI(TAG, "ADC Unknown: %d", event); + break; + } + + return success; +} + +#define WAVE_HEADER_SIZE 44 +/*---------------------------------------------------------------------------- + Fills in the header based on sample size +*/ +static uint16_t generate_wav_header(char* wav_header, uint32_t wav_size, uint32_t sample_rate, uint16_t channels){ + + // See this for reference: http://soundfile.sapp.org/doc/WaveFormat/ + uint32_t file_size = wav_size + WAVE_HEADER_SIZE - 8; + uint32_t byte_rate = sample_rate * 16 * channels / 8; + + const char set_wav_header[] = { + 'R','I','F','F', // ChunkID + file_size, file_size >> 8, file_size >> 16, file_size >> 24, // ChunkSize + 'W','A','V','E', // Format + 'f','m','t',' ', // Subchunk1ID + 0x10, 0x00, 0x00, 0x00, // Subchunk1Size (16 for PCM) + 0x01, 0x00, // AudioFormat (1 for PCM) + channels, channels >> 8, // NumChannels + sample_rate, sample_rate >> 8, sample_rate >> 16, sample_rate >> 24, // SampleRate + byte_rate, byte_rate >> 8, byte_rate >> 16, byte_rate >> 24, // ByteRate + 0x02, 0x00, // BlockAlign + 0x10, 0x00, // BitsPerSample (16 bits) + 'd','a','t','a', // Subchunk2ID + wav_size, wav_size >> 8, wav_size >> 16, wav_size >> 24, // Subchunk2Size + }; + + memcpy(wav_header, set_wav_header, sizeof(set_wav_header)); + + return WAVE_HEADER_SIZE; +} +/*---------------------------------------------------------------------------- + Converts stereo data to mono and adds to wave file + TODO: higher bit rate output needs encoding (shine/libflac?) +*/ +static uint16_t encode_wav_data(int16_t* dst, int16_t* src, uint32_t offset, size_t size, uint16_t channels) { + int s = sizeof(short); + int p = offset/s; // start of frame data + + if (ADC_CHANNELS_IN == channels) { + memcpy(&dst[offset], src, size*s); + } else { + for (int i=0; i < size; i+=2) { + dst[p++] = (src[i] / 2) + (src[i+1] / 2); + } + } + return p*s; +} + +/*----------------------------------------------------------------------------*/ +static void adc_thread(void *arg) { + // THIS IS THE ORIGIONAL CODE + adc_ctx_t *ctx = (adc_ctx_t*) arg; + + size_t buffer_size_bytes = ADC_STREAM_FRAME_SIZE * ADC_CHANNELS_IN / ctx->channels; + size_t buffer_size = buffer_size_bytes / sizeof(short); + ctx->input_buff = (int16_t *)malloc(buffer_size_bytes); + if (ctx->input_buff == NULL) { + ESP_LOGE(TAG, "Memory Allocation Failed!"); + free(ctx); + return; + } + char *buf_ptr_read = (char *)ctx->input_buff; + + // TODO: This initiates buffer based on pre-configured sample rate and channels + // When sharing DAC chip, we need to match sample_rates, so this will need to be dynamic + size_t stream_size_bytes = ADC_STREAM_FRAME_SIZE; + ctx->stream_buff = (int16_t *)malloc(WAVE_HEADER_SIZE + stream_size_bytes); + if (ctx->stream_buff == NULL) { + ESP_LOGE(TAG, "Stream buffer Memory Allocation Failed!"); + ctx->stream = false; + } + char *buf_ptr_stream = (char *)ctx->stream_buff; + int stream_byte_ptr = generate_wav_header(buf_ptr_stream, stream_size_bytes, ctx->sample_rate, ctx->channels); + + struct sockaddr_in dest_addr; + dest_addr.sin_addr.s_addr = ctx->host; + dest_addr.sin_family = AF_INET; + dest_addr.sin_port = htons(ctx->port); + + ESP_LOGI(TAG, "Initialized ADC stream: rate:%u (%ums), chnls:%u frames:%u" , ctx->sample_rate, buffer_size * 1000 / (ctx->sample_rate * ADC_CHANNELS_IN), ctx->channels, stream_size_bytes); + + int stream_err = 0; + + while (ctx->running) { + + // TODO: This does not work with shared chip! Blocks when selecting new track and creates errors (but recovers) + // issues associated with resetting i2s and sample_rate requirements. + if (i2s_read(ctx->i2s_ch, buf_ptr_read, buffer_size_bytes, &ctx->bytes_read, portMAX_DELAY) == ESP_OK) { + if (ctx->speaker && ctx->bytes_read > 0) + { + ctx->data_cb((const uint8_t*) ctx->input_buff, ctx->bytes_read); + //ESP_LOGD(TAG, "Sent Bytes %d to DAC",ctx->bytes_read); + } + + if (ctx->sock && ctx->stream && ctx->bytes_read == buffer_size_bytes) + { + stream_byte_ptr = encode_wav_data(ctx->stream_buff, ctx->input_buff, (size_t)stream_byte_ptr, buffer_size, ctx->channels); + + //ESP_LOGI(TAG, "%d,%db %u Read, Sending bytes to Stream %s:%d",ctx->bytes_read, stream_byte_ptr, buffer_size, inet_ntoa(dest_addr.sin_addr), htons (dest_addr.sin_port)); + int err = sendto(ctx->sock, buf_ptr_stream, (size_t)stream_byte_ptr, 0, (struct sockaddr *) &dest_addr, sizeof(dest_addr)); + if (stream_byte_ptr >= stream_size_bytes) { + if (err < 0 && stream_err++ > 10) { + ESP_LOGE(TAG, "Multiple errors occurred during sending: errno %d. Check 'CONFIG_LWIP_IP4_FRAG=y'", errno); + stream_err = 0; + //const TickType_t xDelay = 50 / portTICK_PERIOD_MS; + //vTaskDelay(xDelay); + } else { + stream_err = 0; + stream_byte_ptr = WAVE_HEADER_SIZE; + } + } + } + } else { + ESP_LOGI(TAG, "Read Failed!"); + } + } + + if (ctx->sock != -1) closesocket(ctx->sock); + + xTaskNotifyGive(ctx->joiner); + vTaskSuspend(NULL); +} + diff --git a/components/adc/input_i2s.h b/components/adc/input_i2s.h new file mode 100644 index 000000000..0ba304937 --- /dev/null +++ b/components/adc/input_i2s.h @@ -0,0 +1,16 @@ +/* + * (c) Philippe 2020, philippe_44@outlook.com + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + * + */ + +#pragma once + +#include "adc_sink.h" + +struct adc_ctx_s* adc_create(adc_cmd_cb_t cmd_cb, adc_data_cb_t data_cb); +void adc_delete(struct adc_ctx_s *ctx); +void adc_abort(struct adc_ctx_s *ctx); +bool adc_cmd(struct adc_ctx_s *ctx, adc_event_t event, void *param); diff --git a/components/adc/test/udp_test_server.py b/components/adc/test/udp_test_server.py new file mode 100644 index 000000000..21aaa943e --- /dev/null +++ b/components/adc/test/udp_test_server.py @@ -0,0 +1,88 @@ +import socket +import io +import wave +import pyaudio +import numpy as np + + +localIP = "0.0.0.0" +localPort = 12203 +MAX_BYTES = 4096 # 2092 +RHASSPY_FRAMES = 1024 +OWW_FRAMES = 1280 # 80 ms window @ 16 kHz = 1280 frames +BAR_SIZE = 50 +BAR_MAX = 0xffff / 2 + +msgFromServer = "Hello Back!" +bytesToSend = str.encode(msgFromServer) + +# Create a datagram socket +UDPServerSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) +print("created socket ", UDPServerSocket) + +# Bind to address and ip +UDPServerSocket.bind((localIP, localPort)) +print("bind ", localIP, localPort) + +player = pyaudio.PyAudio() +address = "" +rate = 16000 +channels = 1 +i = 0 +stream = player.open(format = 8, channels = channels, rate = rate, output = True) + +print("UDP server up and listening") +# Listen for incoming datagrams + +while(True): + + message, addr = UDPServerSocket.recvfrom(MAX_BYTES) + audio = wave.open(io.BytesIO(message)) + + bytesReceived = len(message) + flow = "\x1b[A" + substr = message[:4] + if substr == b'RIFF': + + audio = wave.open(io.BytesIO(message)) + + wav_fmt = player.get_format_from_width(audio.getsampwidth()) + wav_ch = audio.getnchannels(); + wav_rate = audio.getframerate() + + if (address != addr or wav_ch != channels or wav_rate != rate): + address = addr + print ("Client IP Address:{}".format(address)) + print + + if stream.is_active: + stream.close() + rate = wav_rate + channels = wav_ch + stream = player.open(format = wav_fmt, channels = channels, rate = rate , output = True) + + # Read message in chunks + data = audio.readframes(RHASSPY_FRAMES) + + int_data = np.frombuffer(data, dtype=np.int16) + volume = np.average(np.abs(int_data)) + percent = min(BAR_SIZE, int(BAR_SIZE * volume / BAR_MAX)) + sampleData = "[{}{}]".format('#' * percent, '_' * (BAR_SIZE-percent)) + #sampleData = ":".join("{:02x}".format(c) for c in data[:40]) + #sampleData = ":".join("{:06d}".format(c) for c in int_data[:20]) + wav_fmt_str = "type:{},channels:{},rate:{},frames{}".format(wav_fmt,audio.getnchannels(),audio.getframerate(), audio.getnframes()) + print(flow + "Frame {},{} ({},{},{})".format(wav_fmt_str, sampleData, bytesReceived, i, int(volume))) + + # Play the sound by writing the audio message to the stream + while data != b'': + stream.write(data) + i+=1 + data = audio.readframes(RHASSPY_FRAMES) + + else: + sampleData = ":".join("{:02x}".format(c) for c in message[:40]) + print(flow + "{}-({}) {}".format(i, bytesReceived, sampleData)) + +# Close and terminate the stream +stream.close() +player.terminate() \ No newline at end of file diff --git a/components/platform_console/app_squeezelite/cmd_squeezelite.c b/components/platform_console/app_squeezelite/cmd_squeezelite.c index 8d44efb88..a82b6adca 100644 --- a/components/platform_console/app_squeezelite/cmd_squeezelite.c +++ b/components/platform_console/app_squeezelite/cmd_squeezelite.c @@ -60,6 +60,7 @@ extern void register_i2cdectect(void); extern void register_i2cget(void); extern void register_i2cset(void); extern void register_i2cdump(void); +extern void register_adc_config(void); extern cJSON * get_gpio_list_handler(bool refresh); @@ -72,10 +73,13 @@ void register_optional_cmd(void) { register_rotary_config(); register_ledvu_config(); #ifdef CONFIG_CSPOT_SINK - register_cspot_config(); + /?register_cspot_config(); #endif #ifdef CONFIG_IDF_TARGET_ESP32 register_bt_source_config(); +#endif +#ifdef CONFIG_ADC_SINK + register_adc_config(); #endif register_i2c_config(); register_i2cdectect(); diff --git a/components/platform_console/cmd_config.c b/components/platform_console/cmd_config.c index 822ea2b4d..56728c6ab 100644 --- a/components/platform_console/cmd_config.c +++ b/components/platform_console/cmd_config.c @@ -26,6 +26,7 @@ #include "cmd_system.h" const char * desc_squeezelite ="Squeezelite Options"; const char * desc_dac= "DAC Options"; +const char * desc_adc= "ADC Options"; const char * desc_cspotc= "Spotify (cSpot) Options"; const char * desc_preset= "Preset Options"; const char * desc_spdif= "SPDIF Options"; @@ -97,6 +98,25 @@ static struct { struct arg_lit *clear; struct arg_end *end; } i2s_args; +#if CONFIG_ADC_SINK +static struct { +#ifndef CONFIG_ADC_LOCKED + struct arg_str *model_name; + struct arg_int *clock; + struct arg_int *wordselect; + struct arg_int *data; + struct arg_int *mclk; + struct arg_int *dac_sda; + struct arg_int *dac_scl; + struct arg_int *dac_i2c; +#endif + struct arg_int *rate; + struct arg_str *host; + struct arg_int *port; + struct arg_lit *clear; + struct arg_end *end; +} adc_args; +#endif static struct { struct arg_str *model_config; struct arg_end *end; @@ -521,10 +541,11 @@ static int do_spdif_cmd(int argc, char **argv){ return 1; } if(nerrors >0){ - arg_print_errors(f,spdif_args.end,desc_dac); + arg_print_errors(f,spdif_args.end,desc_spdif); fclose(f); return 1; } + i2s_dac_pin.pin.data_in_num = -1; nerrors+=is_output_gpio(spdif_args.clock, f, &i2s_dac_pin.pin.bck_io_num, true); nerrors+=is_output_gpio(spdif_args.wordselect, f, &i2s_dac_pin.pin.ws_io_num, true); nerrors+=is_output_gpio(spdif_args.data, f, &i2s_dac_pin.pin.data_out_num, true); @@ -763,6 +784,7 @@ static int do_i2s_cmd(int argc, char **argv) else { strncpy(i2s_dac_pin.model,i2s_args.model_name->sval[0],sizeof(i2s_dac_pin.model)); i2s_dac_pin.model[sizeof(i2s_dac_pin.model) - 1] = '\0'; + i2s_dac_pin.pin.data_in_num = -1; nerrors += is_output_gpio(i2s_args.clock, f, &i2s_dac_pin.pin.bck_io_num, true); nerrors += is_output_gpio(i2s_args.wordselect, f, &i2s_dac_pin.pin.ws_io_num, true); nerrors += is_output_gpio(i2s_args.data, f, &i2s_dac_pin.pin.data_out_num, true); @@ -796,7 +818,85 @@ static int do_i2s_cmd(int argc, char **argv) #endif return (nerrors==0 && err==ESP_OK)?0:1; } +#if CONFIG_ADC_SINK +static int do_adc_cmd(int argc, char **argv) +{ + i2s_platform_config_t i2s_dac_pin = { + .i2c_addr = -1, + .sda= -1, + .scl = -1, + .mute_gpio = -1, + .mute_level = -1 + }; + adcout_struct_t adcout; + + ESP_LOGD(TAG,"Processing adc command %s with %d parameters",argv[0],argc); + esp_err_t err=ESP_OK; + int nerrors = arg_parse(argc, argv,(void **)&adc_args); + if (adc_args.clear->count) { + cmd_send_messaging(argv[0],MESSAGING_WARNING,"ADC config cleared\n"); + config_set_value(NVS_TYPE_STR, "adc_config", CONFIG_ADC_CONFIG); + return 0; + } + char *buf = NULL; + size_t buf_size = 0; + FILE *f = system_open_memstream(argv[0],&buf, &buf_size); + if (f == NULL) { + return 1; + } + if(nerrors >0){ + ESP_LOGE(TAG,"do_adc_cmd: %d errors parsing arguments",nerrors); + arg_print_errors(f,adc_args.end,desc_adc); + } + else { +#ifndef CONFIG_ADC_LOCKED + strncpy(i2s_dac_pin.model,adc_args.model_name->sval[0],sizeof(i2s_dac_pin.model)); + i2s_dac_pin.model[sizeof(i2s_dac_pin.model) - 1] = '\0'; + i2s_dac_pin.pin.data_out_num = -1; + nerrors += is_output_gpio(adc_args.clock, f, &i2s_dac_pin.pin.bck_io_num, true); + nerrors += is_output_gpio(adc_args.wordselect, f, &i2s_dac_pin.pin.ws_io_num, true); + nerrors += is_output_gpio(adc_args.data, f, &i2s_dac_pin.pin.data_in_num, true); + nerrors += is_output_gpio(adc_args.mclk, f, &i2s_dac_pin.pin.mck_io_num, true); + + if (adc_args.dac_sda->count > 0 && adc_args.dac_sda->ival[0] >= 0) { + // if SDA specified, then SDA and SCL are both mandatory + nerrors += is_output_gpio(adc_args.dac_sda, f, &i2s_dac_pin.sda, false); + nerrors += is_output_gpio(adc_args.dac_scl, f, &i2s_dac_pin.scl, false); + } + if (adc_args.dac_sda->count == 0 && adc_args.dac_i2c->count > 0) { + fprintf(f, "warning: ignoring adc address, since dac i2c gpios config is incomplete\n"); + } else if (adc_args.dac_i2c->count > 0) { + i2s_dac_pin.i2c_addr = adc_args.dac_i2c->ival[0]; + } + if (!nerrors) { + fprintf(f, "Storing adc i2s parameters.\n"); + nerrors += (config_i2s_set(&i2s_dac_pin, "adc_config") != ESP_OK); + } +#endif + if (adc_args.rate->count > 0) { + adcout.rate= adc_args.rate->ival[0]; + } + strncpy(adcout.host,adc_args.host->sval[0],sizeof(adcout.host)); + adcout.host[sizeof(adcout.host) - 1] = '\0'; + if (adc_args.port->count > 0) { + adcout.port = adc_args.port->ival[0]; + } + if (!nerrors) { + fprintf(f, "Storing adc stream parameters.\n"); + nerrors += (config_adcout_set(&adcout) != ESP_OK); + } + } + if(!nerrors ){ + fprintf(f,"Done.\n"); + } + fflush (f); + cmd_send_messaging(argv[0],nerrors>0?MESSAGING_ERROR:MESSAGING_INFO,"%s", buf); + fclose(f); + FREE_AND_NULL(buf); + return (nerrors==0 && err==ESP_OK)?0:1; +} +#endif cJSON * known_model_cb(){ cJSON * values = cJSON_CreateObject(); @@ -870,6 +970,40 @@ cJSON * i2s_cb(){ #endif return values; } +#if CONFIG_ADC_SINK +cJSON * adc_cb(){ + cJSON * values = cJSON_CreateObject(); + const adcout_struct_t *adcout=config_adcout_get(); +#ifndef CONFIG_ADC_LOCKED + const i2s_platform_config_t *i2s_conf=config_adc_get(); + cJSON_AddNumberToObject(values,adc_args.clock->hdr.longopts,i2s_conf->pin.bck_io_num); + cJSON_AddNumberToObject(values,adc_args.wordselect->hdr.longopts,i2s_conf->pin.ws_io_num); + cJSON_AddNumberToObject(values,adc_args.data->hdr.longopts,i2s_conf->pin.data_in_num); + if(i2s_conf->pin.mck_io_num>=0 ) { + cJSON_AddNumberToObject(values,adc_args.mclk->hdr.longopts,i2s_conf->pin.mck_io_num); + } + if(i2s_conf->sda>=0 ) { + cJSON_AddNumberToObject(values,adc_args.dac_sda->hdr.longopts,i2s_conf->sda); + cJSON_AddNumberToObject(values,adc_args.dac_scl->hdr.longopts,i2s_conf->scl); + cJSON_AddNumberToObject(values,adc_args.dac_i2c->hdr.longopts,i2s_conf->i2c_addr); + } + if(strlen(i2s_conf->model)>0){ + cJSON_AddStringToObject(values,adc_args.model_name->hdr.longopts,i2s_conf->model); + } + else { + cJSON_AddStringToObject(values,adc_args.model_name->hdr.longopts,"I2S"); + } +#endif + cJSON_AddNumberToObject(values,adc_args.rate->hdr.longopts,adcout->rate); + cJSON_AddNumberToObject(values,adc_args.port->hdr.longopts,adcout->port); + if(strlen(adcout->host)>0){ + cJSON_AddStringToObject(values,adc_args.host->hdr.longopts,adcout->host); + } + + return values; +} +#endif + cJSON * spdif_cb(){ cJSON * values = cJSON_CreateObject(); #ifndef CONFIG_SPDIF_LOCKED @@ -1175,7 +1309,7 @@ static char * get_log_level_options(const char * longname){ // loop through dac_set and concatenate model name separated with | static char * get_dac_list(){ - const char * EXTRA_MODEL_NAMES = "ES8388|I2S"; + const char * EXTRA_MODEL_NAMES = "ES8388|ES7210|I2S"; char * dac_list=NULL; size_t total_len=0; for(int i=0;dac_set[i];i++){ @@ -1393,7 +1527,35 @@ void register_i2s_config(void){ cmd_to_json_with_cb(&cmd,&i2s_cb); ESP_ERROR_CHECK(esp_console_cmd_register(&cmd)); } +#if CONFIG_ADC_SINK +void register_adc_config(void){ +#ifndef CONFIG_ADC_LOCKED + adc_args.model_name = arg_str0(NULL,"model_name",STR_OR_BLANK(get_dac_list()),"DAC Model Name [default: I2S]"); + adc_args.clock = arg_int1(NULL,"clock","","Clock GPIO pin (e.g. 33)"); + adc_args.wordselect = arg_int1(NULL,"wordselect","","Word Select GPIO pin (e.g. 25)"); + adc_args.data = arg_int1(NULL,"data","","Data In GPIO pin (e.g. 32)"); + adc_args.mclk = arg_int0(NULL,"mclk","","Master Clock GPIO pin (if required)"); + adc_args.dac_sda = arg_int0(NULL,"dac_sda", "", "I2C SDA GPIO pin (e.g. 27)"); + adc_args.dac_scl = arg_int0(NULL,"dac_scl", "", "I2C SCL GPIO pin (e.g. 26)"); + adc_args.dac_i2c = arg_int0(NULL,"dac_i2c", "", "I2C device address pin (e.g. 106)"); +#endif + adc_args.rate = arg_int0(NULL,"rate","","Sample Rate of output stream (use 16000 for Rhasspy)"); + adc_args.host = arg_str0(NULL,"host","","Output stream destination address (use 0.0.0.0 for multi-cast)"); + adc_args.port = arg_int0(NULL,"port","","Output stream port number"); + adc_args.clear = arg_lit0(NULL, "clear", "Clear configuration (factory)"); + adc_args.end = arg_end(6); + const esp_console_cmd_t cmd = { + .command = CFG_TYPE_HW("adc"), + .help = desc_adc, + .hint = NULL, + .func = &do_adc_cmd, + .argtable = &adc_args + }; + cmd_to_json_with_cb(&cmd,&adc_cb); + ESP_ERROR_CHECK(esp_console_cmd_register(&cmd)); +} +#endif void register_bt_source_config(void){ #if CONFIG_BT_ENABLED bt_source_args.sink_name= arg_str1("n","sink_name", "name","Bluetooth audio device name. (BT Out)"); diff --git a/components/platform_console/cmd_system.c b/components/platform_console/cmd_system.c index d528864bb..aeb1a26ac 100644 --- a/components/platform_console/cmd_system.c +++ b/components/platform_console/cmd_system.c @@ -56,6 +56,9 @@ EXT_RAM_ATTR static struct { #endif #if CONFIG_AIRPLAY_SINK struct arg_lit *airplay; + #endif + #if CONFIG_ADC_SINK + struct arg_lit *adc; #endif struct arg_str *telnet; @@ -688,6 +691,9 @@ static int do_set_services(int argc, char **argv) #if CONFIG_CSPOT_SINK nerrors += enable_disable(f,"enable_cspot",set_services_args.cspot); #endif + #if CONFIG_ADC_SINK + nerrors += enable_disable(f,"enable_adc",set_services_args.adc); + #endif if(set_services_args.telnet->count>0){ if(strcasecmp(set_services_args.telnet->sval[0],"Disabled") == 0){ @@ -735,6 +741,9 @@ cJSON * set_services_cb(){ #if CONFIG_CSPOT_SINK console_set_bool_parameter(values,"enable_cspot",set_services_args.cspot); #endif + #if CONFIG_ADC_SINK + console_set_bool_parameter(values,"enable_adc",set_services_args.adc); + #endif #if WITH_TASKS_INFO console_set_bool_parameter(values,"stats",set_services_args.stats); #endif @@ -764,6 +773,9 @@ static void register_set_services(){ #if CONFIG_BT_ENABLED set_services_args.btspeaker = arg_lit0(NULL, "BT_Speaker", "Bluetooth Speaker"); #endif + #if CONFIG_ADC_SINK + set_services_args.adc = arg_lit0(NULL, "adc", "ADC (Line-in / Microphone)"); + #endif set_services_args.telnet= arg_str0("t", "telnet","Disabled|Telnet Only|Telnet and Serial","Telnet server (use only for troubleshooting)"); #if WITH_TASKS_INFO set_services_args.stats= arg_lit0(NULL, "stats", "System Statistics (use only for troubleshooting)"); diff --git a/components/services/accessors.c b/components/services/accessors.c index e83a89786..0c067374c 100644 --- a/components/services/accessors.c +++ b/components/services/accessors.c @@ -63,6 +63,9 @@ static void set_i2s_pin(char *config, i2s_pin_config_t *pin_config) { PARSE_PARAM(config, "bck", '=', pin_config->bck_io_num); PARSE_PARAM(config, "ws", '=', pin_config->ws_io_num); PARSE_PARAM(config, "do", '=', pin_config->data_out_num); +#if CONFIG_ADC_SINK + PARSE_PARAM(config, "di", '=', pin_config->data_in_num); +#endif #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) pin_config->mck_io_num = strcasestr(config, "mck") ? 0 : -1; PARSE_PARAM(config, "mck", '=', pin_config->mck_io_num); @@ -172,6 +175,18 @@ const i2s_platform_config_t * config_dac_get(){ free(spdif_config); return &i2s_dac_config; } +#if CONFIG_ADC_SINK +/**************************************************************************************** + * Get adc config structure + */ +const i2s_platform_config_t * config_adc_get( ){ + char * adc_config = config_alloc_get_str("adc_config", NULL, CONFIG_SPDIF_CONFIG); + static EXT_RAM_ATTR i2s_platform_config_t i2s_dac_config; + memcpy(&i2s_dac_config, config_i2s_get_from_str(adc_config), sizeof(i2s_dac_config)); + free(adc_config); + return &i2s_dac_config; +} +#endif /**************************************************************************************** * Get ethernet config structure @@ -294,7 +309,26 @@ esp_err_t config_ledvu_set(ledvu_struct_t * config){ FREE_AND_NULL(config_buffer); return err; } - +#if CONFIG_ADC_SINK +esp_err_t config_adcout_set(adcout_struct_t * adcout){ + int buffer_size=512; + esp_err_t err=ESP_OK; + char * config_buffer=malloc_init_external(buffer_size); + if(config_buffer) { + snprintf(config_buffer,buffer_size,"rate=%u,host=%s,port=%u",adcout->rate, adcout->host, adcout->port); + log_send_messaging(MESSAGING_INFO,"Updating adc output configuration to %s",config_buffer); + err = config_set_value(NVS_TYPE_STR, "adc_stream", config_buffer); + if(err!=ESP_OK){ + log_send_messaging(MESSAGING_ERROR,"Error: %s",esp_err_to_name(err)); + } + } + else { + err = ESP_ERR_NO_MEM; + } + FREE_AND_NULL(config_buffer); + return err; +} +#endif /**************************************************************************************** * */ @@ -360,7 +394,21 @@ esp_err_t config_i2s_set(const i2s_platform_config_t * config, const char * nvs_ char * config_buffer=malloc_init_external(buffer_size); char * config_buffer2=malloc_init_external(buffer_size); if(config_buffer && config_buffer2) { - snprintf(config_buffer,buffer_size,"model=%s,bck=%u,ws=%u,do=%u",config->model,config->pin.bck_io_num,config->pin.ws_io_num,config->pin.data_out_num); + snprintf(config_buffer,buffer_size,"model=%s,bck=%u,ws=%u",config->model,config->pin.bck_io_num,config->pin.ws_io_num); + if(config->pin.data_out_num>=0){ + snprintf(config_buffer2,buffer_size,"%s,do=%u",config_buffer,config->pin.data_out_num); + strcpy(config_buffer,config_buffer2); + } + if(config->pin.data_in_num>=0){ + snprintf(config_buffer2,buffer_size,"%s,di=%u",config_buffer,config->pin.data_in_num); + strcpy(config_buffer,config_buffer2); + } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + if(config->pin.mck_io_num>=0){ + snprintf(config_buffer2,buffer_size,"%s,mck=%u",config_buffer,config->pin.mck_io_num); + strcpy(config_buffer,config_buffer2); + } +#endif if(config->mute_gpio>=0){ snprintf(config_buffer2,buffer_size,"%s,mute=%u:%u",config_buffer,config->mute_gpio,config->mute_level); strcpy(config_buffer,config_buffer2); @@ -733,7 +781,23 @@ const ledvu_struct_t * config_ledvu_get() { } return &ledvu; } +#if CONFIG_ADC_SINK +/**************************************************************************************** + * + */ +const adcout_struct_t * config_adcout_get() { + static adcout_struct_t adcout={ .rate = 16000, .host = "", .port=0 }; + char *config = config_alloc_get_default(NVS_TYPE_STR, "adc_stream", NULL, 0); + if (config && *config) { + PARSE_PARAM(config, "rate", '=', adcout.rate); + PARSE_PARAM_STR(config, "host", '=', adcout.host, 32); + PARSE_PARAM(config, "port", '=', adcout.port); + free(config); + } + return &adcout; +} +#endif /**************************************************************************************** * */ @@ -793,7 +857,7 @@ cJSON * get_GPIO_nvs_list(cJSON * list) { */ cJSON * get_DAC_GPIO(cJSON * list){ #ifdef CONFIG_DAC_LOCKED - bool fixed = true; + bool fixed = CONFIG_DAC_LOCKED; #else bool fixed = false; #endif @@ -816,13 +880,46 @@ cJSON * get_DAC_GPIO(cJSON * list){ } return llist; } +#if CONFIG_ADC_SINK +/**************************************************************************************** + * + */ +cJSON * get_ADC_GPIO(cJSON * list){ +#ifdef CONFIG_ADC_LOCKED + bool fixed = CONFIG_ADC_LOCKED; +#else + bool fixed = false; +#endif + cJSON * llist = list; + if(!llist){ + llist = cJSON_CreateArray(); + } + const i2s_platform_config_t * i2s_config= config_adc_get(); + if(i2s_config->pin.bck_io_num>=0){ + cJSON_AddItemToArray(llist,get_gpio_entry("bck","adc",i2s_config->pin.bck_io_num,fixed)); + cJSON_AddItemToArray(llist,get_gpio_entry("ws","adc",i2s_config->pin.ws_io_num,fixed)); + cJSON_AddItemToArray(llist,get_gpio_entry("di","adc",i2s_config->pin.data_in_num,fixed)); + if(i2s_config->pin.mck_io_num>=0){ + cJSON_AddItemToArray(llist,get_gpio_entry("mck","adc",i2s_config->pin.mck_io_num,fixed)); + } + if(i2s_config->sda>=0){ + cJSON_AddItemToArray(llist,get_gpio_entry("sda","adc",i2s_config->sda,fixed)); + cJSON_AddItemToArray(llist,get_gpio_entry("scl","adc",i2s_config->scl,fixed)); + } + if(i2s_config->mute_gpio>=0){ + cJSON_AddItemToArray(llist,get_gpio_entry("mute","adc",i2s_config->mute_gpio,fixed)); + } + } + return llist; +} +#endif /**************************************************************************************** * */ cJSON * get_Display_GPIO(cJSON * list){ #ifdef CONFIG_DISPLAY_LOCKED - bool fixed = true; + bool fixed = CONFIG_DISPLAY_LOCKED; #else bool fixed = false; #endif @@ -847,7 +944,7 @@ if(config->back >=0){ */ cJSON * get_I2C_GPIO(cJSON * list){ #ifdef CONFIG_I2C_LOCKED - bool fixed = true; + bool fixed = CONFIG_I2C_LOCKED; #else bool fixed = false; #endif @@ -869,7 +966,7 @@ cJSON * get_I2C_GPIO(cJSON * list){ */ cJSON * get_SPI_GPIO(cJSON * list){ #if CONFIG_SPI_LOCKED - bool fixed = true; + bool fixed = CONFIG_SPI_LOCKED; #else bool fixed = false; #endif @@ -939,7 +1036,7 @@ cJSON * get_eth_GPIO(cJSON * list){ */ cJSON * get_SPDIF_GPIO(cJSON * list){ #ifdef CONFIG_SPDIF_LOCKED - bool fixed = true; + bool fixed = CONFIG_SPDIF_LOCKED; #else bool fixed = false; #endif @@ -960,7 +1057,7 @@ cJSON * get_SPDIF_GPIO(cJSON * list){ cJSON * get_Rotary_GPIO(cJSON * list){ cJSON * llist = list?list:cJSON_CreateArray(); #ifdef CONFIG_ROTARY_ENCODER_LOCKED - bool fixed = true; + bool fixed = CONFIG_ROTARY_ENCODER_LOCKED; #else bool fixed = false; #endif @@ -977,7 +1074,7 @@ cJSON * get_Rotary_GPIO(cJSON * list){ cJSON * get_ledvu_GPIO(cJSON * list){ cJSON * llist = list?list:cJSON_CreateArray(); #ifdef CONFIG_LED_VU_LOCKED - bool fixed = true; + bool fixed = CONFIG_LED_VU_LOCKED; #else bool fixed = false; #endif @@ -1198,6 +1295,7 @@ cJSON * get_gpio_list_handler(bool refresh) { gpio_list=get_SPI_GPIO(gpio_list); gpio_list=get_I2C_GPIO(gpio_list); gpio_list=get_DAC_GPIO(gpio_list); + gpio_list=get_ADC_GPIO(gpio_list); gpio_list=get_ledvu_GPIO(gpio_list); gpio_list=get_psram_gpio_list(gpio_list); gpio_list=get_eth_GPIO(gpio_list); diff --git a/components/services/accessors.h b/components/services/accessors.h index 7fb312a03..e1529d250 100644 --- a/components/services/accessors.h +++ b/components/services/accessors.h @@ -94,7 +94,13 @@ typedef struct { char seq[10]; int scale; } ledvu_struct_t; - +#if CONFIG_ADC_SINK +typedef struct { + int rate; + char host[32]; + int port; +} adcout_struct_t; +#endif typedef struct { bool fixed; char * name; @@ -125,4 +131,9 @@ bool are_statistics_enabled(); const rotary_struct_t * config_rotary_get(); esp_err_t config_rotary_set(rotary_struct_t * rotary); const ledvu_struct_t * config_ledvu_get(); -esp_err_t config_ledvu_set(ledvu_struct_t * ledvu); \ No newline at end of file +esp_err_t config_ledvu_set(ledvu_struct_t * ledvu); +#if CONFIG_ADC_SINK +const i2s_platform_config_t * config_adc_get( ); +const adcout_struct_t * config_adcout_get(); +esp_err_t config_adcout_set(adcout_struct_t * adcout); +#endif \ No newline at end of file diff --git a/components/squeezelite/CMakeLists.txt b/components/squeezelite/CMakeLists.txt index ede572b4e..c2b7014fc 100644 --- a/components/squeezelite/CMakeLists.txt +++ b/components/squeezelite/CMakeLists.txt @@ -29,7 +29,8 @@ idf_component_register( SRC_DIRS . external ac101 tas57xx wm8978 _override ${cspot_required} ${airplay_required} - ${bt_requires} + ${bt_requires} + adc EMBED_FILES vu_s.data arrow.data ) diff --git a/components/squeezelite/ac101/ac101.c b/components/squeezelite/ac101/ac101.c index bc64378f5..1ec185005 100644 --- a/components/squeezelite/ac101/ac101.c +++ b/components/squeezelite/ac101/ac101.c @@ -37,7 +37,6 @@ static const char TAG[] = "AC101"; #define SPKOUT_EN ((1 << 9) | (1 << 11) | (1 << 7) | (1 << 5)) #define EAROUT_EN ((1 << 11) | (1 << 12) | (1 << 13)) -#define BIN(a,b,c,d) 0b##a##b##c##d #define min(a,b) (((a) < (b)) ? (a) : (b)) #define max(a,b) (((a) > (b)) ? (a) : (b)) diff --git a/components/squeezelite/adac.h b/components/squeezelite/adac.h index 1ab7790e7..f32d7b954 100644 --- a/components/squeezelite/adac.h +++ b/components/squeezelite/adac.h @@ -14,6 +14,9 @@ #include "driver/i2c.h" typedef enum { ADAC_ON = 0, ADAC_STANDBY, ADAC_OFF } adac_power_e; +typedef enum { ADAC_LOOPBACK, ADAC_LINEIN, ADAC_MIC } adac_src_e; + +#define BIN(a,b,c,d) 0b##a##b##c##d struct adac_s { char *model; diff --git a/components/squeezelite/decode_external.c b/components/squeezelite/decode_external.c index ba7269e7b..e1a82446c 100644 --- a/components/squeezelite/decode_external.c +++ b/components/squeezelite/decode_external.c @@ -16,6 +16,7 @@ #endif #include "platform_config.h" #include "squeezelite.h" +#include "slimproto.h" #if CONFIG_BT_SINK @@ -47,6 +48,12 @@ static EXT_RAM_ATTR struct { } raop_sync; #endif +#if CONFIG_ADC_SINK +#include "adc_sink.h" +static bool enable_adc; +#endif + + static enum { SINK_RUNNING, SINK_ABORT, SINK_DISCARD } sink_state; #define LOCK_O mutex_lock(outputbuf->mutex) @@ -54,7 +61,7 @@ static enum { SINK_RUNNING, SINK_ABORT, SINK_DISCARD } sink_state; #define LOCK_D mutex_lock(decode.mutex); #define UNLOCK_D mutex_unlock(decode.mutex); -enum { DECODE_BT = 1, DECODE_RAOP, DECODE_CSPOT }; +enum { DECODE_BT = 1, DECODE_RAOP, DECODE_CSPOT, DECODE_ADC }; extern struct outputstate output; extern struct decodestate decode; @@ -274,7 +281,6 @@ static bool raop_sink_cmd_handler(raop_event_t event, va_list args) raop_sync.win = SYNC_WIN_SLOW; LOG_INFO("switching to slow sync mode %u", raop_sync.win); } - break; } case RAOP_SETUP: { @@ -442,6 +448,99 @@ static bool cspot_cmd_handler(cspot_event_t cmd, va_list args) } #endif +/**************************************************************************************** + * adc sink data handler + */ +#if CONFIG_ADC_SINK +static uint32_t adc_data_handler(const uint8_t *data, uint32_t len) { + return sink_data_handler(data, len, 0); +} + +/**************************************************************************************** + * adc sink command handler + */ +static bool adc_cmd_handler(adc_event_t cmd, va_list args) +{ + LOG_SDEBUG("ADC cmd begin: extern %d - output %d, frames %d, threshold: %d, state %d", output.external, output.current_sample_rate, output.frames_played, output.threshold, output.state); + + // don't LOCK_O as there is always a chance that LMS takes control later anyway + if (cmd != ADC_SETUP && output.external != DECODE_ADC && output.state > OUTPUT_STOPPED) { + LOG_WARN("Cannot use %s while other apps are controlling player %d", "ADC", output.external); + return false; + } + + LOCK_D; + //if (cmd != ADC_GAIN) LOCK_O; disabled as it stops here using ac101 tests + + switch(cmd) { + case ADC_SETUP: + LOG_INFO("ADC Setup"); + output.state = OUTPUT_STOPPED; + output.current_sample_rate = output.next_sample_rate = va_arg(args, u32_t); + output.external = DECODE_ADC; + output.frames_played = 0; + // in 1/10 of seconds + output.threshold = 25; + sink_state = SINK_ABORT; + _buf_flush(outputbuf); + if (decode.state != DECODE_STOPPED) decode.state = DECODE_ERROR; + break; + case ADC_CLOSE: + LOG_INFO("ADC Close"); + _buf_flush(outputbuf); + sink_state = SINK_ABORT; + output.external = 0; + output.state = OUTPUT_STOPPED; + output.stop_time = gettime_ms(); + break; + case ADC_PLAY: + LOG_INFO("ADC Play"); + sink_state = SINK_RUNNING; + output.state = OUTPUT_RUNNING; + output.external = DECODE_ADC; + break; + case ADC_STOP: + LOG_INFO("ADC Stop"); + output.state = OUTPUT_STOPPED; + output.stop_time = gettime_ms(); + break; + default: + LOG_INFO("ADC unknown: event:%d", cmd); + break; + } + + //if (cmd != ADC_GAIN) UNLOCK_O; + UNLOCK_D; + + LOG_SDEBUG("ADC cmd end: extern %d - output %d, frames %d, threshold: %d, state %d", output.external, output.current_sample_rate, output.frames_played, output.threshold, output.state); + + return true; +} + + +static bool (*slimp_handler_chain)(u8_t *data, int len); + +static bool adc_slimp_handler(u8_t *data, int len){ + bool res = true; + + if (!strncmp((char*) data, "audp", 4)) { + struct audp_packet *pkt = (struct audp_packet*) data; + // 0 = start? + adc_linein_start(output.current_sample_rate); + LOG_INFO("got AUDP %02x", pkt->config); + LOG_SDEBUG("ADC audp: output %d, frames %d, threshold: %d, state %d external %d", output.current_sample_rate, output.frames_played, output.threshold, output.state, output.external); + } else { + res = false; + } + + // chain protocol handlers (bitwise or is fine) + if (*slimp_handler_chain) res |= (*slimp_handler_chain)(data, len); + + return res; +} +#endif + + /**************************************************************************************** * We provide the generic codec register option */ @@ -479,6 +578,20 @@ void register_external(void) { } } #endif + +#if CONFIG_ADC_SINK + if ((p = config_alloc_get(NVS_TYPE_STR, "enable_adc")) != NULL) { + enable_adc = strcmp(p,"1") == 0 || strcasecmp(p,"y") == 0; + free(p); + if (enable_adc){ + adc_sink_init(adc_cmd_handler, adc_data_handler); + LOG_INFO("Initializing ADC sink"); + + slimp_handler_chain = slimp_handler; + slimp_handler = adc_slimp_handler; + } + } +#endif } void deregister_external(void) { @@ -501,6 +614,13 @@ void deregister_external(void) { cspot_sink_deinit(); } #endif + +#if CONFIG_ADC_SINK + if (enable_adc){ + LOG_INFO("Stopping ADC sink"); + adc_sink_deinit(); + } +#endif } void decode_restore(int external) { @@ -519,6 +639,11 @@ void decode_restore(int external) { case DECODE_CSPOT: cspot_disconnect(); break; +#endif +#if CONFIG_ADC_SINK + case DECODE_ADC: + adc_disconnect(); + break; #endif } } diff --git a/components/squeezelite/external/dac_external.c b/components/squeezelite/external/dac_external.c index 08f182340..6686001b1 100644 --- a/components/squeezelite/external/dac_external.c +++ b/components/squeezelite/external/dac_external.c @@ -51,6 +51,17 @@ static const struct { {\"reg\":26,\"val\":0}, {\"reg\":27,\"val\":0}, {\"reg\":25,\"val\":50}, {\"reg\":38,\"val\":0}, \ {\"reg\":39,\"val\":184}, {\"reg\":42,\"val\":184}, {\"reg\":46,\"val\":30}, {\"reg\":47,\"val\":30}, \ {\"reg\":48,\"val\":30}, {\"reg\":49,\"val\":30}, {\"reg\":2,\"val\":170}]}" }, + { "es7210", true, + "{\"init\":[ \ + {\"reg\":0,\"val\":255}, {\"reg\":0,\"val\":65}, \ + {\"reg\":1,\"val\":31}, {\"reg\":9,\"val\":48}, {\"reg\":10,\"val\":48}, \ + {\"reg\":64,\"val\":195}, {\"reg\":65,\"val\":112}, {\"reg\":66,\"val\":112}, \ + {\"reg\":7,\"val\":32}, {\"reg\":2,\"val\":193}, {\"reg\":4,\"val\":1}, {\"reg\":5,\"val\":0}, \ + {\"reg\":1,\"val\":0}, {\"reg\":17,\"val\":96}, {\"reg\":18,\"val\":0}, {\"reg\":2,\"val\":193}, \ + {\"reg\":7,\"val\":32}, {\"reg\":4,\"val\":1}, {\"reg\":5,\"val\":0}, {\"reg\":6,\"val\":0}, \ + {\"reg\":71,\"val\":0}, {\"reg\":72,\"val\":0}, {\"reg\":73,\"val\":0}, {\"reg\":74,\"val\":0}, \ + {\"reg\":75,\"val\":0}, {\"reg\":67,\"val\":30}, {\"reg\":75,\"val\":0}, {\"reg\":68,\"val\":30}, \ + {\"reg\":76,\"val\":0}, {\"reg\":69,\"val\":16}, {\"reg\":76,\"val\":0}, {\"reg\":70,\"val\":16}]}" }, { NULL, false, NULL } }; diff --git a/components/squeezelite/output_i2s.c b/components/squeezelite/output_i2s.c index 2b2b9dbed..94f4325ec 100644 --- a/components/squeezelite/output_i2s.c +++ b/components/squeezelite/output_i2s.c @@ -225,6 +225,9 @@ static void set_i2s_pin(char *config, i2s_pin_config_t *pin_config) { PARSE_PARAM(config, "bck", '=', pin_config->bck_io_num); PARSE_PARAM(config, "ws", '=', pin_config->ws_io_num); PARSE_PARAM(config, "do", '=', pin_config->data_out_num); +#if CONFIG_ADC_SINK + PARSE_PARAM(config, "di", '=', pin_config->data_in_num); +#endif #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) pin_config->mck_io_num = strcasestr(config, "mck") ? 0 : -1; PARSE_PARAM(config, "mck", '=', pin_config->mck_io_num); @@ -327,6 +330,9 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch res |= i2s_set_pin(CONFIG_DAC_I2S_NUM, &i2s_spdif_pin); LOG_INFO("SPDIF using I2S bck:%d, ws:%d, do:%d", i2s_spdif_pin.bck_io_num, i2s_spdif_pin.ws_io_num, i2s_spdif_pin.data_out_num); } else { +#if CONFIG_ADC_SINK + i2s_config.mode |= I2S_MODE_RX; +#endif i2s_config.sample_rate = output.current_sample_rate; i2s_config.bits_per_sample = BYTES_PER_FRAME * 8 / 2; // Counted in frames (but i2s allocates a buffer <= 4092 bytes) diff --git a/components/squeezelite/slimproto.c b/components/squeezelite/slimproto.c index f053684ed..117112633 100644 --- a/components/squeezelite/slimproto.c +++ b/components/squeezelite/slimproto.c @@ -447,7 +447,7 @@ static void process_aude(u8_t *pkt, int len) { #endif LOCK_O; - if (!aude->enable_spdif && output.state != OUTPUT_OFF) { + if (!aude->enable_spdif && output.state != OUTPUT_OFF && !output.external) { output.state = OUTPUT_OFF; } if (aude->enable_spdif && output.state == OUTPUT_OFF && !output.idle_to) { diff --git a/components/squeezelite/slimproto.h b/components/squeezelite/slimproto.h index 56e79ff56..370cc67c2 100644 --- a/components/squeezelite/slimproto.h +++ b/components/squeezelite/slimproto.h @@ -184,6 +184,11 @@ struct audo_packet { u8_t config; }; +struct audp_packet { + char opcode[4]; + u8_t config; +}; + #ifndef SUN #pragma pack(pop) #else diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 4fd220822..af6094200 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -67,6 +67,16 @@ menu "Squeezelite-ESP32" menu "Factory Configuration" + config ADC_CONFIG + string "adc_config" + default "" + help + Configuration of adc linein/microphone stream + config ADC_LOCKED + bool "ADC is Locked" + depends on ADC_CONFIG != "" + default y + config AUDIO_CONTROLS string "audio_control_set" default "[{\"gpio\":32, \"pull\":true, \"long_press\":1000, \"normal\":{\"pressed\":\"ACTRLS_VOLDOWN\"}, \"longpress\":{\"pressed\":\"ACTRLS_PREV\"}}, {\"gpio\":19, \"pull\":true, \"long_press\":1000, \"normal\":{\"pressed\":\"ACTRLS_VOLUP\"}, \"longpress\":{\"pressed\":\"ACTRLS_NEXT\"}}, {\"gpio\":12, \"pull\":true, \"long_press\":1000, \"normal\":{\"pressed\":\"ACTRLS_TOGGLE\"},\"longpress\":{\"pressed\":\"ACTRLS_POWER\"}}]" if MUSE @@ -345,6 +355,11 @@ menu "Squeezelite-ESP32" default y help Enable Spotify connect using CSpot + config ADC_SINK + bool "ADC receiver" + default y + help + Enable Microphone or Line in test endmenu menu "Command Sets"