diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f84f149..7c6ba2bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,51 +1,11 @@ cmake_minimum_required(VERSION 3.5) set(EXTRA_COMPONENT_DIRS components/platform_console/app_recovery components/platform_console/app_squeezelite ) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -message(WARNING "Building RECOVERY") +message(STATUS "Building RECOVERY") project(recovery) set_property(TARGET recovery.elf PROPERTY RECOVERY_BUILD 0 ) set_property(TARGET recovery.elf PROPERTY RECOVERY_PREFIX app_recovery ) -# -#message(WARNING "Building SQUEEZELITE") -#set(project_name squeezelite) -#__project(${project_name} C CXX ASM) -# -set(squeezelite_project_elf squeezelite.elf) -# -# # Create a dummy file to work around CMake requirement of having a source -# # file while adding an executable -set(squeezelite_project_elf_src ${CMAKE_BINARY_DIR}/squeezelite_project_elf_src.c) -add_custom_command(OUTPUT ${squeezelite_project_elf_src} - COMMAND ${CMAKE_COMMAND} -E touch ${squeezelite_project_elf_src} - VERBATIM) - - add_custom_command(OUTPUT "${build_dir}/.squeezelite_bin_timestamp" - COMMAND echo ${ESPTOOLPY} elf2image ${ESPTOOLPY_FLASH_OPTIONS} ${esptool_elf2image_args} - COMMAND ${ESPTOOLPY} elf2image ${ESPTOOLPY_FLASH_OPTIONS} ${esptool_elf2image_args} - -o "${build_dir}/squeezelite.bin" "squeezelite.elf" - COMMAND ${CMAKE_COMMAND} -E echo "Generated ${build_dir}/squeezelite.bin" - COMMAND ${CMAKE_COMMAND} -E md5sum "${build_dir}/squeezelite.bin" > "${build_dir}/.squeezelite_bin_timestamp" - VERBATIM - WORKING_DIRECTORY ${build_dir} - COMMENT "Generating binary image from built executable" - ) - -add_custom_target(_squeezelite_project_elf_src DEPENDS "${squeezelite_project_elf_src}" "${build_dir}/.squeezelite_bin_timestamp") -add_executable(${squeezelite_project_elf} "${squeezelite_project_elf_src}") -add_dependencies(${squeezelite_project_elf} _squeezelite_project_elf_src) -set_property(TARGET ${squeezelite_project_elf} PROPERTY RECOVERY_BUILD 0 ) -set_property(TARGET ${squeezelite_project_elf} PROPERTY RECOVERY_PREFIX app_squeezelite) - -idf_build_get_property(bca BUILD_COMPONENT_ALIASES) -target_link_libraries(${squeezelite_project_elf} ${bca}) -set(squeezelite_mapfile "${CMAKE_BINARY_DIR}/squeezelite.map") -target_link_libraries(${squeezelite_project_elf} "-Wl,--cref -Wl,--Map=${squeezelite_mapfile}") -set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY - ADDITIONAL_MAKE_CLEAN_FILES - "${squeezelite_mapfile}" "${squeezelite_project_elf_src}") - -partition_table_get_partition_info(otaapp_offset "--partition-type app --partition-subtype ota_0" "offset") -esptool_py_flash_project_args(squeezelite ${otaapp_offset} ${build_dir}/squeezelite.bin FLASH_IN_PROJECT) +include(squeezelite.cmake) diff --git a/README.md b/README.md index 531128de..0e4b41f3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,21 @@ Works with the SqueezeAMP see [here](https://forums.slimdevices.com/showthread.p Use the `squeezelite-esp32-SqueezeAmp-sdkconfig.defaults` configuration file. ### ESP32-A1S -Works with [ESP32-A1S](https://docs.ai-thinker.com/esp32-a1s) module that includes audio codec and headset output. You still need to use a demo board or an external amplifier if you want direct speaker connection. +Works with [ESP32-A1S](https://docs.ai-thinker.com/esp32-a1s) module that includes audio codec and headset output. You still need to use a demo board like [this](https://www.aliexpress.com/item/4000765857347.html?spm=2114.12010615.8148356.11.5d963cd0j669ns) or an external amplifier if you want direct speaker connection. + +The board showed above has the following IO set +- amplifier: GPIO21 +- key2: GPIO13, key3: GPIO19, key4: GPIO23, key5: GPIO18, key6: GPIO5 (to be confirmed width dipswitches) +- key1: not sure, something with GPIO36 +- jack insertion: GPIO39 (inserted low) +- LED: GPIO22 (active low) + +So a possible config would be +- set_GPIO: 21=amp,22=green:0,39=jack:0 +- a button mapping: +``` +[{"gpio":5,"normal":{"pressed":"ACTRLS_TOGGLE"}},{"gpio":18,"shifter_gpio":5,"normal":{"pressed":"ACTRLS_VOLUP"}, "shifted":{"pressed":"ACTRLS_NEXT"}}, {"gpio":23,"shifter_gpio":5,"normal":{"pressed":"ACTRLS_VOLDOWN"},"shifted":{"pressed":"ACTRLS_PREV"}}] +``` ### ESP32-WROVER + I2S DAC Squeezelite-esp32 requires esp32 chipset and 4MB PSRAM. ESP32-WROVER meets these requirements. To get an audio output an I2S DAC can be used. Cheap PCM5102 I2S DACs work others may also work. PCM5012 DACs can be hooked up via: @@ -137,10 +151,14 @@ Where (all parameters are optionals except gpio) - "shifted": action to take when a button is pressed/released and shifted (see above/below) - "longshifted": action to take when a button is long-pressed/released and shifted (see above/below) -Where \ is either the name of another configuration to load or one amongst - ACTRLS_NONE, ACTRLS_VOLUP, ACTRLS_VOLDOWN, ACTRLS_TOGGLE, ACTRLS_PLAY, - ACTRLS_PAUSE, ACTRLS_STOP, ACTRLS_REW, ACTRLS_FWD, ACTRLS_PREV, ACTRLS_NEXT, - BCTRLS_PUSH, BCTRLS_UP, BCTRLS_DOWN, BCTRLS_LEFT, BCTRLS_RIGHT +Where \ is either the name of another configuration to load (remap) or one amongst + +``` +ACTRLS_NONE, ACTRLS_VOLUP, ACTRLS_VOLDOWN, ACTRLS_TOGGLE, ACTRLS_PLAY, +ACTRLS_PAUSE, ACTRLS_STOP, ACTRLS_REW, ACTRLS_FWD, ACTRLS_PREV, ACTRLS_NEXT, +BCTRLS_PUSH, BCTRLS_UP, BCTRLS_DOWN, BCTRLS_LEFT, BCTRLS_RIGHT, +KNOB_LEFT, KNOB_RIGHT, KNOB_PUSH +``` One you've created such a string, use it to fill a new NVS parameter with any name below 16(?) characters. You can have as many of these configs as you can. Then set the config parameter "actrls_config" with the name of your default config diff --git a/components/platform_console/CMakeLists.txt b/components/platform_console/CMakeLists.txt index ba560328..3272fb5c 100644 --- a/components/platform_console/CMakeLists.txt +++ b/components/platform_console/CMakeLists.txt @@ -8,10 +8,4 @@ idf_component_register( SRCS INCLUDE_DIRS . REQUIRES nvs_flash PRIV_REQUIRES console tools services spi_flash app_update platform_config vfs pthread wifi-manager platform_config codecs newlib ) - - -#target_link_libraries(__idf_platform_console $ ) -message($) - target_link_libraries(__idf_platform_console ${build_dir}/esp-idf/$/lib$.a ) -#target_link_libraries(__idf_platform_console $host) == INADDR_ANY) { + S_ADDR(ctx->host) = get_localhost(NULL); + LOG_INFO("[%p]: IP was missing, trying to get it %s", ctx, inet_ntoa(ctx->host)); + } // need to pad the base64 string as apple device don't base64_pad(buf, &buf_pad); diff --git a/components/raop/raop_sink.c b/components/raop/raop_sink.c index 3556f9a5..ff53c49e 100644 --- a/components/raop/raop_sink.c +++ b/components/raop/raop_sink.c @@ -163,7 +163,7 @@ void raop_sink_init(raop_cmd_vcb_t cmd_cb, raop_data_cb_t data_cb) { free(sink_name_buffer); } - LOG_INFO( "mdns hostname set to: [%s] with servicename %s", hostname, sink_name); + LOG_INFO( "mdns hostname for ip %s set to: [%s] with servicename %s", inet_ntoa(host), hostname, sink_name); // create RAOP instance, latency is set by controller uint8_t mac[6]; diff --git a/components/raop/rtp.c b/components/raop/rtp.c index 5716e888..e11a8f0c 100644 --- a/components/raop/rtp.c +++ b/components/raop/rtp.c @@ -1,806 +1,807 @@ - -/* - * HairTunes - RAOP packet handler and slave-clocked replay engine - * Copyright (c) James Laird 2011 - * All rights reserved. - * - * Modularisation: philippe_44@outlook.com, 2019 - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "platform.h" -#include "rtp.h" -#include "raop_sink.h" -#include "log_util.h" -#include "util.h" - -#ifdef WIN32 -#include -#include "alac_wrapper.h" -#define MSG_DONTWAIT 0 -#else -#include "esp_pthread.h" -#include "esp_system.h" -#include -#include -#include "alac_wrapper.h" -#endif - -#define NTP2MS(ntp) ((((ntp) >> 10) * 1000L) >> 22) -#define MS2NTP(ms) (((((u64_t) (ms)) << 22) / 1000) << 10) -#define NTP2TS(ntp, rate) ((((ntp) >> 16) * (rate)) >> 16) -#define TS2NTP(ts, rate) (((((u64_t) (ts)) << 16) / (rate)) << 16) -#define MS2TS(ms, rate) ((((u64_t) (ms)) * (rate)) / 1000) -#define TS2MS(ts, rate) NTP2MS(TS2NTP(ts,rate)) - - extern log_level raop_loglevel; - static log_level *loglevel = &raop_loglevel; - -//#define __RTP_STORE - -// default buffer size -#define BUFFER_FRAMES ( (150 * RAOP_SAMPLE_RATE * 2) / (352 * 100) ) -#define MAX_PACKET 1408 -#define MIN_LATENCY 11025 -#define MAX_LATENCY ( (120 * RAOP_SAMPLE_RATE * 2) / 100 ) - -#define RTP_STACK_SIZE (4*1024) - -#define RTP_SYNC (0x01) -#define NTP_SYNC (0x02) - -#define RESEND_TO 200 - -enum { DATA = 0, CONTROL, TIMING }; - -static const u8_t silence_frame[MAX_PACKET] = { 0 }; - -typedef u16_t seq_t; -typedef struct audio_buffer_entry { // decoded audio packets - int ready; - u32_t rtptime, last_resend; - s16_t *data; - int len; -} abuf_t; - -typedef struct rtp_s { -#ifdef __RTP_STORE - FILE *rtpIN, *rtpOUT; -#endif - bool running; - unsigned char aesiv[16]; -#ifdef WIN32 - AES_KEY aes; -#else - mbedtls_aes_context aes; -#endif - bool decrypt; - u8_t *decrypt_buf; - u32_t frame_size, frame_duration; - u32_t in_frames, out_frames; - struct in_addr host; - struct sockaddr_in rtp_host; - struct { - unsigned short rport, lport; - int sock; - } rtp_sockets[3]; // data, control, timing - struct timing_s { - u64_t local, remote; - } timing; - struct { - u32_t rtp, time; - u8_t status; - } synchro; - struct { - u32_t time; - seq_t seqno; - u32_t rtptime; - } record; - int latency; // rtp hold depth in samples - u32_t resent_req, resent_rec; // total resent + recovered frames - u32_t silent_frames; // total silence frames - u32_t discarded; - abuf_t audio_buffer[BUFFER_FRAMES]; - seq_t ab_read, ab_write; - pthread_mutex_t ab_mutex; -#ifdef WIN32 - pthread_t thread; -#else - TaskHandle_t thread, joiner; - StaticTask_t *xTaskBuffer; - StackType_t xStack[RTP_STACK_SIZE] __attribute__ ((aligned (4))); -#endif - - struct alac_codec_s *alac_codec; - int flush_seqno; - bool playing; - raop_data_cb_t data_cb; - raop_cmd_cb_t cmd_cb; -} rtp_t; - - -#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) -static void buffer_alloc(abuf_t *audio_buffer, int size); -static void buffer_release(abuf_t *audio_buffer); -static void buffer_reset(abuf_t *audio_buffer); -static void buffer_push_packet(rtp_t *ctx); -static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last); -static bool rtp_request_timing(rtp_t *ctx); -static void* rtp_thread_func(void *arg); -static int seq_order(seq_t a, seq_t b); - -/*---------------------------------------------------------------------------*/ -static struct alac_codec_s* alac_init(int fmtp[32]) { - struct alac_codec_s *alac; - unsigned sample_rate; - unsigned char sample_size, channels; - struct { - uint32_t frameLength; - uint8_t compatibleVersion; - uint8_t bitDepth; - uint8_t pb; - uint8_t mb; - uint8_t kb; - uint8_t numChannels; - uint16_t maxRun; - uint32_t maxFrameBytes; - uint32_t avgBitRate; - uint32_t sampleRate; - } config; - - config.frameLength = htonl(fmtp[1]); - config.compatibleVersion = fmtp[2]; - config.bitDepth = fmtp[3]; - config.pb = fmtp[4]; - config.mb = fmtp[5]; - config.kb = fmtp[6]; - config.numChannels = fmtp[7]; - config.maxRun = htons(fmtp[8]); - config.maxFrameBytes = htonl(fmtp[9]); - config.avgBitRate = htonl(fmtp[10]); - config.sampleRate = htonl(fmtp[11]); - - alac = alac_create_decoder(sizeof(config), (unsigned char*) &config, &sample_size, &sample_rate, &channels); - if (!alac) { - LOG_ERROR("cannot create alac codec", NULL); - return NULL; - } - - return alac; -} - -/*---------------------------------------------------------------------------*/ -rtp_resp_t rtp_init(struct in_addr host, int latency, char *aeskey, char *aesiv, char *fmtpstr, - short unsigned pCtrlPort, short unsigned pTimingPort, - raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) -{ - int i = 0; - char *arg; - int fmtp[12]; - bool rc = true; - rtp_t *ctx = calloc(1, sizeof(rtp_t)); - rtp_resp_t resp = { 0, 0, 0, NULL }; - - if (!ctx) return resp; - - ctx->host = host; - ctx->decrypt = false; - ctx->cmd_cb = cmd_cb; - ctx->data_cb = data_cb; - ctx->rtp_host.sin_family = AF_INET; - ctx->rtp_host.sin_addr.s_addr = INADDR_ANY; - pthread_mutex_init(&ctx->ab_mutex, 0); - ctx->flush_seqno = -1; - ctx->latency = latency; - ctx->ab_read = ctx->ab_write; - -#ifdef __RTP_STORE - ctx->rtpIN = fopen("airplay.rtpin", "wb"); - ctx->rtpOUT = fopen("airplay.rtpout", "wb"); -#endif - - ctx->rtp_sockets[CONTROL].rport = pCtrlPort; - ctx->rtp_sockets[TIMING].rport = pTimingPort; - - if (aesiv && aeskey) { - memcpy(ctx->aesiv, aesiv, 16); -#ifdef WIN32 - AES_set_decrypt_key((unsigned char*) aeskey, 128, &ctx->aes); -#else - memset(&ctx->aes, 0, sizeof(mbedtls_aes_context)); - mbedtls_aes_setkey_dec(&ctx->aes, (unsigned char*) aeskey, 128); -#endif - ctx->decrypt = true; - ctx->decrypt_buf = malloc(MAX_PACKET); - } - - memset(fmtp, 0, sizeof(fmtp)); - while ((arg = strsep(&fmtpstr, " \t")) != NULL) fmtp[i++] = atoi(arg); - - ctx->frame_size = fmtp[1]; - ctx->frame_duration = (ctx->frame_size * 1000) / RAOP_SAMPLE_RATE; - - // alac decoder - ctx->alac_codec = alac_init(fmtp); - rc &= ctx->alac_codec != NULL; - - buffer_alloc(ctx->audio_buffer, ctx->frame_size*4); - - // create rtp ports - for (i = 0; i < 3; i++) { - ctx->rtp_sockets[i].sock = bind_socket(&ctx->rtp_sockets[i].lport, SOCK_DGRAM); - rc &= ctx->rtp_sockets[i].sock > 0; - } - - // create http port and start listening - resp.cport = ctx->rtp_sockets[CONTROL].lport; - resp.tport = ctx->rtp_sockets[TIMING].lport; - resp.aport = ctx->rtp_sockets[DATA].lport; - - ctx->running = true; - -#ifdef WIN32 - pthread_create(&ctx->thread, NULL, rtp_thread_func, (void *) ctx); -#else - ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - ctx->thread = xTaskCreateStatic( (TaskFunction_t) rtp_thread_func, "RTP_thread", RTP_STACK_SIZE, ctx, - CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1, ctx->xStack, ctx->xTaskBuffer ); -#endif - - // cleanup everything if we failed - if (!rc) { - LOG_ERROR("[%p]: cannot start RTP", ctx); - rtp_end(ctx); - ctx = NULL; - } - - resp.ctx = ctx; - return resp; -} - -/*---------------------------------------------------------------------------*/ -void rtp_end(rtp_t *ctx) -{ - int i; - - if (!ctx) return; - - if (ctx->running) { -#if !defined WIN32 - ctx->joiner = xTaskGetCurrentTaskHandle(); -#endif - ctx->running = false; -#ifdef WIN32 - pthread_join(ctx->thread, NULL); -#else - ulTaskNotifyTake(pdFALSE, portMAX_DELAY); - vTaskDelete(ctx->thread); - heap_caps_free(ctx->xTaskBuffer); -#endif - } - - for (i = 0; i < 3; i++) closesocket(ctx->rtp_sockets[i].sock); - - if (ctx->alac_codec) alac_delete_decoder(ctx->alac_codec); - if (ctx->decrypt_buf) free(ctx->decrypt_buf); - - pthread_mutex_destroy(&ctx->ab_mutex); - buffer_release(ctx->audio_buffer); - - free(ctx); - -#ifdef __RTP_STORE - fclose(ctx->rtpIN); - fclose(ctx->rtpOUT); -#endif -} - -/*---------------------------------------------------------------------------*/ -bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime, bool exit_locked) -{ - bool rc = true; - u32_t now = gettime_ms(); - - if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) { - rc = false; - LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime); - } else { - pthread_mutex_lock(&ctx->ab_mutex); - buffer_reset(ctx->audio_buffer); - ctx->playing = false; - ctx->flush_seqno = seqno; - if (!exit_locked) pthread_mutex_unlock(&ctx->ab_mutex); - } - - LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime); - - return rc; -} - -/*---------------------------------------------------------------------------*/ -void rtp_flush_release(rtp_t *ctx) { - pthread_mutex_unlock(&ctx->ab_mutex); -} - - -/*---------------------------------------------------------------------------*/ -void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) { - ctx->record.seqno = seqno; - ctx->record.rtptime = rtptime; - ctx->record.time = gettime_ms(); - - LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime); -} - -/*---------------------------------------------------------------------------*/ -static void buffer_alloc(abuf_t *audio_buffer, int size) { - int i; - for (i = 0; i < BUFFER_FRAMES; i++) { - audio_buffer[i].data = malloc(size); - audio_buffer[i].ready = 0; - } -} - -/*---------------------------------------------------------------------------*/ -static void buffer_release(abuf_t *audio_buffer) { - int i; - for (i = 0; i < BUFFER_FRAMES; i++) { - free(audio_buffer[i].data); - } -} - -/*---------------------------------------------------------------------------*/ -static void buffer_reset(abuf_t *audio_buffer) { - int i; - for (i = 0; i < BUFFER_FRAMES; i++) audio_buffer[i].ready = 0; -} - -/*---------------------------------------------------------------------------*/ -// the sequence numbers will wrap pretty often. -// this returns true if the second arg is after the first -static int seq_order(seq_t a, seq_t b) { - s16_t d = b - a; - return d > 0; -} - -/*---------------------------------------------------------------------------*/ -static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, int *outsize) { - unsigned char iv[16]; - int aeslen; - assert(len<=MAX_PACKET); - - if (ctx->decrypt) { - aeslen = len & ~0xf; - memcpy(iv, ctx->aesiv, sizeof(iv)); -#ifdef WIN32 - AES_cbc_encrypt((unsigned char*)buf, ctx->decrypt_buf, aeslen, &ctx->aes, iv, AES_DECRYPT); -#else - mbedtls_aes_crypt_cbc(&ctx->aes, MBEDTLS_AES_DECRYPT, aeslen, iv, (unsigned char*) buf, ctx->decrypt_buf); -#endif - memcpy(ctx->decrypt_buf+aeslen, buf+aeslen, len-aeslen); - alac_to_pcm(ctx->alac_codec, (unsigned char*) ctx->decrypt_buf, (unsigned char*) dest, 2, (unsigned int*) outsize); - } else { - alac_to_pcm(ctx->alac_codec, (unsigned char*) buf, (unsigned char*) dest, 2, (unsigned int*) outsize); - } - - *outsize *= 4; -} - - -/*---------------------------------------------------------------------------*/ -static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) { - abuf_t *abuf = NULL; - u32_t playtime; - - pthread_mutex_lock(&ctx->ab_mutex); - - if (!ctx->playing) { - if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) && - (ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) { - ctx->ab_write = seqno-1; - ctx->ab_read = seqno; - ctx->flush_seqno = -1; - ctx->playing = true; - ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0; - playtime = ctx->synchro.time + (((s32_t)(rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; - ctx->cmd_cb(RAOP_PLAY, playtime); - } else { - pthread_mutex_unlock(&ctx->ab_mutex); - return; - } - } - - if (seqno == (u16_t) (ctx->ab_write+1)) { - // expected packet - abuf = ctx->audio_buffer + BUFIDX(seqno); - ctx->ab_write = seqno; - LOG_SDEBUG("packet expected seqno:%hu rtptime:%u (W:%hu R:%hu)", seqno, rtptime, ctx->ab_write, ctx->ab_read); - - } else if (seq_order(ctx->ab_write, seqno)) { - seq_t i; - u32_t now; - - // newer than expected - if (ctx->latency && seq_order(ctx->latency / ctx->frame_size, seqno - ctx->ab_write - 1)) { - // only get rtp latency-1 frames back (last one is seqno) - LOG_WARN("[%p] too many missing frames %hu seq: %hu, (W:%hu R:%hu)", ctx, seqno - ctx->ab_write - 1, seqno, ctx->ab_write, ctx->ab_read); - ctx->ab_write = seqno - ctx->latency / ctx->frame_size; - } - - // need to request re-send and adjust timing of gaps - rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1); - for (now = gettime_ms(), i = ctx->ab_write + 1; seq_order(i, seqno); i++) { - ctx->audio_buffer[BUFIDX(i)].rtptime = rtptime - (seqno-i)*ctx->frame_size; - ctx->audio_buffer[BUFIDX(i)].last_resend = now; - } - - LOG_DEBUG("[%p]: packet newer seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); - abuf = ctx->audio_buffer + BUFIDX(seqno); - ctx->ab_write = seqno; - } else if (seq_order(ctx->ab_read, seqno + 1)) { - // recovered packet, not yet sent - abuf = ctx->audio_buffer + BUFIDX(seqno); - ctx->resent_rec++; - LOG_DEBUG("[%p]: packet recovered seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); - } else { - // too late - LOG_DEBUG("[%p]: packet too late seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); - } - - if (ctx->in_frames++ > 1000) { - LOG_INFO("[%p]: fill [level:%hu rec:%u] [W:%hu R:%hu]", ctx, ctx->ab_write - ctx->ab_read, ctx->resent_rec, ctx->ab_write, ctx->ab_read); - ctx->in_frames = 0; - } - - if (abuf) { - alac_decode(ctx, abuf->data, data, len, &abuf->len); - abuf->ready = 1; - // this is the local rtptime when this frame is expected to play - abuf->rtptime = rtptime; - buffer_push_packet(ctx); - -#ifdef __RTP_STORE - fwrite(data, len, 1, ctx->rtpIN); - fwrite(abuf->data, abuf->len, 1, ctx->rtpOUT); -#endif - } - - pthread_mutex_unlock(&ctx->ab_mutex); -} - -/*---------------------------------------------------------------------------*/ -// push as many frames as possible through callback -static void buffer_push_packet(rtp_t *ctx) { - abuf_t *curframe = NULL; - u32_t now, playtime, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100); - int i; - - // not ready to play yet - if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; - - // maybe re-evaluate time in loop in case data callback blocks ... - now = gettime_ms(); - - // there is always at least one frame in the buffer - do { - - curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read); - playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; - - if (now > playtime) { - LOG_DEBUG("[%p]: discarded frame now:%u missed by:%d (W:%hu R:%hu)", ctx, now, now - playtime, ctx->ab_write, ctx->ab_read); - ctx->discarded++; - curframe->ready = 0; - } else if (playtime - now <= hold) { - if (curframe->ready) { - ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime); - curframe->ready = 0; - } else { - LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); - ctx->data_cb(silence_frame, ctx->frame_size * 4, playtime); - ctx->silent_frames++; - } - } else if (curframe->ready) { - ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime); - curframe->ready = 0; - } else { - break; - } - - ctx->ab_read++; - ctx->out_frames++; - - } while (seq_order(ctx->ab_read, ctx->ab_write)); - - if (ctx->out_frames > 1000) { - LOG_INFO("[%p]: drain [level:%hd head:%d ms] [W:%hu R:%hu] [req:%u sil:%u dis:%u]", - ctx, ctx->ab_write - ctx->ab_read, playtime - now, ctx->ab_write, ctx->ab_read, - ctx->resent_req, ctx->silent_frames, ctx->discarded); - ctx->out_frames = 0; - } - - LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready); - - // each missing packet will be requested up to (latency_frames / 16) times - for (i = 0; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) { - abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i); - if (!frame->ready && now - frame->last_resend > RESEND_TO) { - rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i); - frame->last_resend = now; - } - } - } - - -/*---------------------------------------------------------------------------*/ -static void *rtp_thread_func(void *arg) { - fd_set fds; - int i, sock = -1; - int count = 0; - bool ntp_sent; - char *packet = malloc(MAX_PACKET); - rtp_t *ctx = (rtp_t*) arg; - - for (i = 0; i < 3; i++) { - if (ctx->rtp_sockets[i].sock > sock) sock = ctx->rtp_sockets[i].sock; - // send synchro request 3 times - ntp_sent = rtp_request_timing(ctx); - } - - while (ctx->running) { - ssize_t plen; - char type; - socklen_t rtp_client_len = sizeof(struct sockaddr_in); - int idx = 0; - char *pktp = packet; - struct timeval timeout = {0, 100*1000}; - - FD_ZERO(&fds); - for (i = 0; i < 3; i++) { FD_SET(ctx->rtp_sockets[i].sock, &fds); } - - if (select(sock + 1, &fds, NULL, NULL, &timeout) <= 0) continue; - - for (i = 0; i < 3; i++) - if (FD_ISSET(ctx->rtp_sockets[i].sock, &fds)) idx = i; - - plen = recvfrom(ctx->rtp_sockets[idx].sock, packet, MAX_PACKET, MSG_DONTWAIT, (struct sockaddr*) &ctx->rtp_host, &rtp_client_len); - - if (!ntp_sent) { - LOG_WARN("[%p]: NTP request not send yet", ctx); - ntp_sent = rtp_request_timing(ctx); - } - - if (plen <= 0) { - LOG_WARN("Nothing received on a readable socket %d", plen); - continue; - } - - assert(plen <= MAX_PACKET); - - type = packet[1] & ~0x80; - pktp = packet; - - switch (type) { - seq_t seqno; - unsigned rtptime; - - // re-sent packet - case 0x56: { - pktp += 4; - plen -= 4; - } - - // data packet - case 0x60: { - seqno = ntohs(*(u16_t*)(pktp+2)); - rtptime = ntohl(*(u32_t*)(pktp+4)); - - // adjust pointer and length - pktp += 12; - plen -= 12; - - LOG_SDEBUG("[%p]: seqno:%hu rtp:%u (type: %x, first: %u)", ctx, seqno, rtptime, type, packet[1] & 0x80); - - // check if packet contains enough content to be reasonable - if (plen < 16) break; - - if ((packet[1] & 0x80) && (type != 0x56)) { - LOG_INFO("[%p]: 1st audio packet received", ctx); - } - - buffer_put_packet(ctx, seqno, rtptime, packet[1] & 0x80, pktp, plen); - - break; - } - - // sync packet - case 0x54: { - u32_t rtp_now_latency = ntohl(*(u32_t*)(pktp+4)); - u64_t remote = (((u64_t) ntohl(*(u32_t*)(pktp+8))) << 32) + ntohl(*(u32_t*)(pktp+12)); - u32_t rtp_now = ntohl(*(u32_t*)(pktp+16)); - u16_t flags = ntohs(*(u16_t*)(pktp+2)); - u32_t remote_gap = NTP2MS(remote - ctx->timing.remote); - - // something is wrong and if we are supposed to be NTP synced, better ask for re-sync - if (remote_gap > 10000) { - if (ctx->synchro.status & NTP_SYNC) rtp_request_timing(ctx); - LOG_WARN("discarding remote timing information %u", remote_gap); - break; - } - - pthread_mutex_lock(&ctx->ab_mutex); - - // re-align timestamp and expected local playback time (and magic 11025 latency) - ctx->latency = rtp_now - rtp_now_latency; - if (flags == 7 || flags == 4) ctx->latency += 11025; - if (ctx->latency < MIN_LATENCY) ctx->latency = MIN_LATENCY; - else if (ctx->latency > MAX_LATENCY) ctx->latency = MAX_LATENCY; - ctx->synchro.rtp = rtp_now - ctx->latency; - ctx->synchro.time = ctx->timing.local + remote_gap; - - // now we are synced on RTP frames - ctx->synchro.status |= RTP_SYNC; - - // 1st sync packet received (signals a restart of playback) - if (packet[0] & 0x10) { - LOG_INFO("[%p]: 1st sync packet received", ctx); - } - - pthread_mutex_unlock(&ctx->ab_mutex); - - LOG_DEBUG("[%p]: sync packet latency:%d rtp_latency:%u rtp:%u remote ntp:%llx, local time:%u local rtp:%u (now:%u)", - ctx, ctx->latency, rtp_now_latency, rtp_now, remote, ctx->synchro.time, ctx->synchro.rtp, gettime_ms()); - - if (!count--) { - rtp_request_timing(ctx); - count = 3; - } - - if ((ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) ctx->cmd_cb(RAOP_TIMING); - - break; - } - - // NTP timing packet - case 0x53: { - u64_t expected; - u32_t reference = ntohl(*(u32_t*)(pktp+12)); // only low 32 bits in our case - u64_t remote =(((u64_t) ntohl(*(u32_t*)(pktp+16))) << 32) + ntohl(*(u32_t*)(pktp+20)); - u32_t roundtrip = gettime_ms() - reference; - - // better discard sync packets when roundtrip is suspicious and ask for another one - if (roundtrip > 100) { - rtp_request_timing(ctx); - LOG_WARN("[%p]: discarding NTP roundtrip of %u ms", ctx, roundtrip); - break; - } - - /* - The expected elapsed remote time should be exactly the same as - elapsed local time between the two request, corrected by the - drifting - */ - expected = ctx->timing.remote + MS2NTP(reference - ctx->timing.local); - - ctx->timing.remote = remote; - ctx->timing.local = reference; - - // now we are synced on NTP (mutex not needed) - ctx->synchro.status |= NTP_SYNC; - - LOG_DEBUG("[%p]: Timing references local:%llu, remote:%llx (delta:%lld, sum:%lld, adjust:%lld, gaps:%d)", - ctx, ctx->timing.local, ctx->timing.remote); - - break; - } - - default: { - LOG_WARN("Unknown packet received %x", (int) type); - break; - } - } - } - - free(packet); - LOG_INFO("[%p]: terminating", ctx); - -#ifndef WIN32 - xTaskNotifyGive(ctx->joiner); - vTaskSuspend(NULL); -#endif - - return NULL; -} - -/*---------------------------------------------------------------------------*/ -static bool rtp_request_timing(rtp_t *ctx) { - unsigned char req[32]; - u32_t now = gettime_ms(); - int i; - struct sockaddr_in host; - - LOG_DEBUG("[%p]: timing request now:%u (port: %hu)", ctx, now, ctx->rtp_sockets[TIMING].rport); - - req[0] = 0x80; - req[1] = 0x52|0x80; - *(u16_t*)(req+2) = htons(7); - *(u32_t*)(req+4) = htonl(0); // dummy - for (i = 0; i < 16; i++) req[i+8] = 0; - *(u32_t*)(req+24) = 0; - *(u32_t*)(req+28) = htonl(now); // this is not a real NTP, but a 32 ms counter in the low part of the NTP - - if (ctx->host.s_addr != INADDR_ANY) { - host.sin_family = AF_INET; - host.sin_addr = ctx->host; - } else host = ctx->rtp_host; - - // no address from sender, need to wait for 1st packet to be received - if (host.sin_addr.s_addr == INADDR_ANY) return false; - - host.sin_port = htons(ctx->rtp_sockets[TIMING].rport); - - if (sizeof(req) != sendto(ctx->rtp_sockets[TIMING].sock, req, sizeof(req), MSG_DONTWAIT, (struct sockaddr*) &host, sizeof(host))) { - LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); - } - - return true; -} - -/*---------------------------------------------------------------------------*/ -static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last) { - unsigned char req[8]; // *not* a standard RTCP NACK - - // do not request silly ranges (happens in case of network large blackouts) - if (seq_order(last, first) || last - first > BUFFER_FRAMES / 2) return false; - - ctx->resent_req += last - first + 1; - - LOG_DEBUG("resend request [W:%hu R:%hu first=%hu last=%hu]", ctx->ab_write, ctx->ab_read, first, last); - - req[0] = 0x80; - req[1] = 0x55|0x80; // Apple 'resend' - *(u16_t*)(req+2) = htons(1); // our seqnum - *(u16_t*)(req+4) = htons(first); // missed seqnum - *(u16_t*)(req+6) = htons(last-first+1); // count - - ctx->rtp_host.sin_port = htons(ctx->rtp_sockets[CONTROL].rport); - - if (sizeof(req) != sendto(ctx->rtp_sockets[CONTROL].sock, req, sizeof(req), MSG_DONTWAIT, (struct sockaddr*) &ctx->rtp_host, sizeof(ctx->rtp_host))) { - LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); - } - - return true; -} - + +/* + * HairTunes - RAOP packet handler and slave-clocked replay engine + * Copyright (c) James Laird 2011 + * All rights reserved. + * + * Modularisation: philippe_44@outlook.com, 2019 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" +#include "rtp.h" +#include "raop_sink.h" +#include "log_util.h" +#include "util.h" + +#ifdef WIN32 +#include +#include "alac_wrapper.h" +#include "assert.h" +#define MSG_DONTWAIT 0 +#else +#include "esp_pthread.h" +#include "esp_system.h" +#include "esp_assert.h" +#include +#include +#include "alac_wrapper.h" +#endif + +#define NTP2MS(ntp) ((((ntp) >> 10) * 1000L) >> 22) +#define MS2NTP(ms) (((((u64_t) (ms)) << 22) / 1000) << 10) +#define NTP2TS(ntp, rate) ((((ntp) >> 16) * (rate)) >> 16) +#define TS2NTP(ts, rate) (((((u64_t) (ts)) << 16) / (rate)) << 16) +#define MS2TS(ms, rate) ((((u64_t) (ms)) * (rate)) / 1000) +#define TS2MS(ts, rate) NTP2MS(TS2NTP(ts,rate)) + + extern log_level raop_loglevel; + static log_level *loglevel = &raop_loglevel; + +//#define __RTP_STORE + +// default buffer size +#define BUFFER_FRAMES ( (150 * RAOP_SAMPLE_RATE * 2) / (352 * 100) ) +#define MAX_PACKET 1408 +#define MIN_LATENCY 11025 +#define MAX_LATENCY ( (120 * RAOP_SAMPLE_RATE * 2) / 100 ) + +#define RTP_STACK_SIZE (4*1024) + +#define RTP_SYNC (0x01) +#define NTP_SYNC (0x02) + +#define RESEND_TO 200 + +enum { DATA = 0, CONTROL, TIMING }; + +static const u8_t silence_frame[MAX_PACKET] = { 0 }; + +typedef u16_t seq_t; +typedef struct audio_buffer_entry { // decoded audio packets + int ready; + u32_t rtptime, last_resend; + s16_t *data; + int len; +} abuf_t; + +typedef struct rtp_s { +#ifdef __RTP_STORE + FILE *rtpIN, *rtpOUT; +#endif + bool running; + unsigned char aesiv[16]; +#ifdef WIN32 + AES_KEY aes; +#else + mbedtls_aes_context aes; +#endif + bool decrypt; + u8_t *decrypt_buf; + u32_t frame_size, frame_duration; + u32_t in_frames, out_frames; + struct in_addr host; + struct sockaddr_in rtp_host; + struct { + unsigned short rport, lport; + int sock; + } rtp_sockets[3]; // data, control, timing + struct timing_s { + u64_t local, remote; + } timing; + struct { + u32_t rtp, time; + u8_t status; + } synchro; + struct { + u32_t time; + seq_t seqno; + u32_t rtptime; + } record; + int latency; // rtp hold depth in samples + u32_t resent_req, resent_rec; // total resent + recovered frames + u32_t silent_frames; // total silence frames + u32_t discarded; + abuf_t audio_buffer[BUFFER_FRAMES]; + seq_t ab_read, ab_write; + pthread_mutex_t ab_mutex; +#ifdef WIN32 + pthread_t thread; +#else + TaskHandle_t thread, joiner; + StaticTask_t *xTaskBuffer; + StackType_t xStack[RTP_STACK_SIZE] __attribute__ ((aligned (4))); +#endif + + struct alac_codec_s *alac_codec; + int flush_seqno; + bool playing; + raop_data_cb_t data_cb; + raop_cmd_cb_t cmd_cb; +} rtp_t; + + +#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) +static void buffer_alloc(abuf_t *audio_buffer, int size); +static void buffer_release(abuf_t *audio_buffer); +static void buffer_reset(abuf_t *audio_buffer); +static void buffer_push_packet(rtp_t *ctx); +static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last); +static bool rtp_request_timing(rtp_t *ctx); +static void* rtp_thread_func(void *arg); +static int seq_order(seq_t a, seq_t b); + +/*---------------------------------------------------------------------------*/ +static struct alac_codec_s* alac_init(int fmtp[32]) { + struct alac_codec_s *alac; + unsigned sample_rate; + unsigned char sample_size, channels; + struct { + uint32_t frameLength; + uint8_t compatibleVersion; + uint8_t bitDepth; + uint8_t pb; + uint8_t mb; + uint8_t kb; + uint8_t numChannels; + uint16_t maxRun; + uint32_t maxFrameBytes; + uint32_t avgBitRate; + uint32_t sampleRate; + } config; + + config.frameLength = htonl(fmtp[1]); + config.compatibleVersion = fmtp[2]; + config.bitDepth = fmtp[3]; + config.pb = fmtp[4]; + config.mb = fmtp[5]; + config.kb = fmtp[6]; + config.numChannels = fmtp[7]; + config.maxRun = htons(fmtp[8]); + config.maxFrameBytes = htonl(fmtp[9]); + config.avgBitRate = htonl(fmtp[10]); + config.sampleRate = htonl(fmtp[11]); + + alac = alac_create_decoder(sizeof(config), (unsigned char*) &config, &sample_size, &sample_rate, &channels); + if (!alac) { + LOG_ERROR("cannot create alac codec", NULL); + return NULL; + } + + return alac; +} + +/*---------------------------------------------------------------------------*/ +rtp_resp_t rtp_init(struct in_addr host, int latency, char *aeskey, char *aesiv, char *fmtpstr, + short unsigned pCtrlPort, short unsigned pTimingPort, + raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) +{ + int i = 0; + char *arg; + int fmtp[12]; + bool rc = true; + rtp_t *ctx = calloc(1, sizeof(rtp_t)); + rtp_resp_t resp = { 0, 0, 0, NULL }; + + if (!ctx) return resp; + + ctx->host = host; + ctx->decrypt = false; + ctx->cmd_cb = cmd_cb; + ctx->data_cb = data_cb; + ctx->rtp_host.sin_family = AF_INET; + ctx->rtp_host.sin_addr.s_addr = INADDR_ANY; + pthread_mutex_init(&ctx->ab_mutex, 0); + ctx->flush_seqno = -1; + ctx->latency = latency; + ctx->ab_read = ctx->ab_write; + +#ifdef __RTP_STORE + ctx->rtpIN = fopen("airplay.rtpin", "wb"); + ctx->rtpOUT = fopen("airplay.rtpout", "wb"); +#endif + + ctx->rtp_sockets[CONTROL].rport = pCtrlPort; + ctx->rtp_sockets[TIMING].rport = pTimingPort; + + if (aesiv && aeskey) { + memcpy(ctx->aesiv, aesiv, 16); +#ifdef WIN32 + AES_set_decrypt_key((unsigned char*) aeskey, 128, &ctx->aes); +#else + memset(&ctx->aes, 0, sizeof(mbedtls_aes_context)); + mbedtls_aes_setkey_dec(&ctx->aes, (unsigned char*) aeskey, 128); +#endif + ctx->decrypt = true; + ctx->decrypt_buf = malloc(MAX_PACKET); + } + + memset(fmtp, 0, sizeof(fmtp)); + while ((arg = strsep(&fmtpstr, " \t")) != NULL) fmtp[i++] = atoi(arg); + + ctx->frame_size = fmtp[1]; + ctx->frame_duration = (ctx->frame_size * 1000) / RAOP_SAMPLE_RATE; + + // alac decoder + ctx->alac_codec = alac_init(fmtp); + rc &= ctx->alac_codec != NULL; + + buffer_alloc(ctx->audio_buffer, ctx->frame_size*4); + + // create rtp ports + for (i = 0; i < 3; i++) { + ctx->rtp_sockets[i].sock = bind_socket(&ctx->rtp_sockets[i].lport, SOCK_DGRAM); + rc &= ctx->rtp_sockets[i].sock > 0; + } + + // create http port and start listening + resp.cport = ctx->rtp_sockets[CONTROL].lport; + resp.tport = ctx->rtp_sockets[TIMING].lport; + resp.aport = ctx->rtp_sockets[DATA].lport; + + ctx->running = true; + +#ifdef WIN32 + pthread_create(&ctx->thread, NULL, rtp_thread_func, (void *) ctx); +#else + ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ctx->thread = xTaskCreateStatic( (TaskFunction_t) rtp_thread_func, "RTP_thread", RTP_STACK_SIZE, ctx, + CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1, ctx->xStack, ctx->xTaskBuffer ); +#endif + + // cleanup everything if we failed + if (!rc) { + LOG_ERROR("[%p]: cannot start RTP", ctx); + rtp_end(ctx); + ctx = NULL; + } + + resp.ctx = ctx; + return resp; +} + +/*---------------------------------------------------------------------------*/ +void rtp_end(rtp_t *ctx) +{ + int i; + + if (!ctx) return; + + if (ctx->running) { +#if !defined WIN32 + ctx->joiner = xTaskGetCurrentTaskHandle(); +#endif + ctx->running = false; +#ifdef WIN32 + pthread_join(ctx->thread, NULL); +#else + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + vTaskDelete(ctx->thread); + heap_caps_free(ctx->xTaskBuffer); +#endif + } + + for (i = 0; i < 3; i++) closesocket(ctx->rtp_sockets[i].sock); + + if (ctx->alac_codec) alac_delete_decoder(ctx->alac_codec); + if (ctx->decrypt_buf) free(ctx->decrypt_buf); + + pthread_mutex_destroy(&ctx->ab_mutex); + buffer_release(ctx->audio_buffer); + + free(ctx); + +#ifdef __RTP_STORE + fclose(ctx->rtpIN); + fclose(ctx->rtpOUT); +#endif +} + +/*---------------------------------------------------------------------------*/ +bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime, bool exit_locked) +{ + bool rc = true; + u32_t now = gettime_ms(); + + if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) { + rc = false; + LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime); + } else { + pthread_mutex_lock(&ctx->ab_mutex); + buffer_reset(ctx->audio_buffer); + ctx->playing = false; + ctx->flush_seqno = seqno; + if (!exit_locked) pthread_mutex_unlock(&ctx->ab_mutex); + } + + LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime); + + return rc; +} + +/*---------------------------------------------------------------------------*/ +void rtp_flush_release(rtp_t *ctx) { + pthread_mutex_unlock(&ctx->ab_mutex); +} + + +/*---------------------------------------------------------------------------*/ +void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) { + ctx->record.seqno = seqno; + ctx->record.rtptime = rtptime; + ctx->record.time = gettime_ms(); + + LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime); +} + +/*---------------------------------------------------------------------------*/ +static void buffer_alloc(abuf_t *audio_buffer, int size) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) { + audio_buffer[i].data = malloc(size); + audio_buffer[i].ready = 0; + } +} + +/*---------------------------------------------------------------------------*/ +static void buffer_release(abuf_t *audio_buffer) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) { + free(audio_buffer[i].data); + } +} + +/*---------------------------------------------------------------------------*/ +static void buffer_reset(abuf_t *audio_buffer) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) audio_buffer[i].ready = 0; +} + +/*---------------------------------------------------------------------------*/ +// the sequence numbers will wrap pretty often. +// this returns true if the second arg is after the first +static int seq_order(seq_t a, seq_t b) { + s16_t d = b - a; + return d > 0; +} + +/*---------------------------------------------------------------------------*/ +static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, int *outsize) { + unsigned char iv[16]; + int aeslen; + assert(len<=MAX_PACKET); + + if (ctx->decrypt) { + aeslen = len & ~0xf; + memcpy(iv, ctx->aesiv, sizeof(iv)); +#ifdef WIN32 + AES_cbc_encrypt((unsigned char*)buf, ctx->decrypt_buf, aeslen, &ctx->aes, iv, AES_DECRYPT); +#else + mbedtls_aes_crypt_cbc(&ctx->aes, MBEDTLS_AES_DECRYPT, aeslen, iv, (unsigned char*) buf, ctx->decrypt_buf); +#endif + memcpy(ctx->decrypt_buf+aeslen, buf+aeslen, len-aeslen); + alac_to_pcm(ctx->alac_codec, (unsigned char*) ctx->decrypt_buf, (unsigned char*) dest, 2, (unsigned int*) outsize); + } else { + alac_to_pcm(ctx->alac_codec, (unsigned char*) buf, (unsigned char*) dest, 2, (unsigned int*) outsize); + } + + *outsize *= 4; +} + + +/*---------------------------------------------------------------------------*/ +static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) { + abuf_t *abuf = NULL; + u32_t playtime; + + pthread_mutex_lock(&ctx->ab_mutex); + + if (!ctx->playing) { + if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) && + (ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) { + ctx->ab_write = seqno-1; + ctx->ab_read = seqno; + ctx->flush_seqno = -1; + ctx->playing = true; + ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0; + playtime = ctx->synchro.time + (((s32_t)(rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; + ctx->cmd_cb(RAOP_PLAY, playtime); + } else { + pthread_mutex_unlock(&ctx->ab_mutex); + return; + } + } + + if (seqno == (u16_t) (ctx->ab_write+1)) { + // expected packet + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->ab_write = seqno; + LOG_SDEBUG("packet expected seqno:%hu rtptime:%u (W:%hu R:%hu)", seqno, rtptime, ctx->ab_write, ctx->ab_read); + + } else if (seq_order(ctx->ab_write, seqno)) { + seq_t i; + u32_t now; + + // newer than expected + if (ctx->latency && seq_order(ctx->latency / ctx->frame_size, seqno - ctx->ab_write - 1)) { + // only get rtp latency-1 frames back (last one is seqno) + LOG_WARN("[%p] too many missing frames %hu seq: %hu, (W:%hu R:%hu)", ctx, seqno - ctx->ab_write - 1, seqno, ctx->ab_write, ctx->ab_read); + ctx->ab_write = seqno - ctx->latency / ctx->frame_size; + } + + // need to request re-send and adjust timing of gaps + rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1); + for (now = gettime_ms(), i = ctx->ab_write + 1; seq_order(i, seqno); i++) { + ctx->audio_buffer[BUFIDX(i)].rtptime = rtptime - (seqno-i)*ctx->frame_size; + ctx->audio_buffer[BUFIDX(i)].last_resend = now; + } + + LOG_DEBUG("[%p]: packet newer seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->ab_write = seqno; + } else if (seq_order(ctx->ab_read, seqno + 1)) { + // recovered packet, not yet sent + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->resent_rec++; + LOG_DEBUG("[%p]: packet recovered seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + } else { + // too late + LOG_DEBUG("[%p]: packet too late seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + } + + if (ctx->in_frames++ > 1000) { + LOG_INFO("[%p]: fill [level:%hu rec:%u] [W:%hu R:%hu]", ctx, ctx->ab_write - ctx->ab_read, ctx->resent_rec, ctx->ab_write, ctx->ab_read); + ctx->in_frames = 0; + } + + if (abuf) { + alac_decode(ctx, abuf->data, data, len, &abuf->len); + abuf->ready = 1; + // this is the local rtptime when this frame is expected to play + abuf->rtptime = rtptime; + buffer_push_packet(ctx); + +#ifdef __RTP_STORE + fwrite(data, len, 1, ctx->rtpIN); + fwrite(abuf->data, abuf->len, 1, ctx->rtpOUT); +#endif + } + + pthread_mutex_unlock(&ctx->ab_mutex); +} + +/*---------------------------------------------------------------------------*/ +// push as many frames as possible through callback +static void buffer_push_packet(rtp_t *ctx) { + abuf_t *curframe = NULL; + u32_t now, playtime, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100); + int i; + + // not ready to play yet + if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; + + // maybe re-evaluate time in loop in case data callback blocks ... + now = gettime_ms(); + + // there is always at least one frame in the buffer + do { + + curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read); + playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; + + if (now > playtime) { + LOG_DEBUG("[%p]: discarded frame now:%u missed by:%d (W:%hu R:%hu)", ctx, now, now - playtime, ctx->ab_write, ctx->ab_read); + ctx->discarded++; + curframe->ready = 0; + } else if (playtime - now <= hold) { + if (curframe->ready) { + ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime); + curframe->ready = 0; + } else { + LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); + ctx->data_cb(silence_frame, ctx->frame_size * 4, playtime); + ctx->silent_frames++; + } + } else if (curframe->ready) { + ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime); + curframe->ready = 0; + } else { + break; + } + + ctx->ab_read++; + ctx->out_frames++; + + } while (seq_order(ctx->ab_read, ctx->ab_write)); + + if (ctx->out_frames > 1000) { + LOG_INFO("[%p]: drain [level:%hd head:%d ms] [W:%hu R:%hu] [req:%u sil:%u dis:%u]", + ctx, ctx->ab_write - ctx->ab_read, playtime - now, ctx->ab_write, ctx->ab_read, + ctx->resent_req, ctx->silent_frames, ctx->discarded); + ctx->out_frames = 0; + } + + LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready); + + // each missing packet will be requested up to (latency_frames / 16) times + for (i = 0; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) { + abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i); + if (!frame->ready && now - frame->last_resend > RESEND_TO) { + rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i); + frame->last_resend = now; + } + } + } + + +/*---------------------------------------------------------------------------*/ +static void *rtp_thread_func(void *arg) { + fd_set fds; + int i, sock = -1; + int count = 0; + bool ntp_sent; + char *packet = malloc(MAX_PACKET); + rtp_t *ctx = (rtp_t*) arg; + + for (i = 0; i < 3; i++) { + if (ctx->rtp_sockets[i].sock > sock) sock = ctx->rtp_sockets[i].sock; + // send synchro request 3 times + ntp_sent = rtp_request_timing(ctx); + } + + while (ctx->running) { + ssize_t plen; + char type; + socklen_t rtp_client_len = sizeof(struct sockaddr_in); + int idx = 0; + char *pktp = packet; + struct timeval timeout = {0, 100*1000}; + + FD_ZERO(&fds); + for (i = 0; i < 3; i++) { FD_SET(ctx->rtp_sockets[i].sock, &fds); } + + if (select(sock + 1, &fds, NULL, NULL, &timeout) <= 0) continue; + + for (i = 0; i < 3; i++) + if (FD_ISSET(ctx->rtp_sockets[i].sock, &fds)) idx = i; + + plen = recvfrom(ctx->rtp_sockets[idx].sock, packet, MAX_PACKET, MSG_DONTWAIT, (struct sockaddr*) &ctx->rtp_host, &rtp_client_len); + + if (!ntp_sent) { + LOG_WARN("[%p]: NTP request not send yet", ctx); + ntp_sent = rtp_request_timing(ctx); + } + + if (plen <= 0) { + LOG_WARN("Nothing received on a readable socket %d", plen); + continue; + } + + assert(plen <= MAX_PACKET); + + type = packet[1] & ~0x80; + pktp = packet; + + switch (type) { + seq_t seqno; + unsigned rtptime; + + // re-sent packet + case 0x56: { + pktp += 4; + plen -= 4; + } + + // data packet + case 0x60: { + seqno = ntohs(*(u16_t*)(pktp+2)); + rtptime = ntohl(*(u32_t*)(pktp+4)); + + // adjust pointer and length + pktp += 12; + plen -= 12; + + LOG_SDEBUG("[%p]: seqno:%hu rtp:%u (type: %x, first: %u)", ctx, seqno, rtptime, type, packet[1] & 0x80); + + // check if packet contains enough content to be reasonable + if (plen < 16) break; + + if ((packet[1] & 0x80) && (type != 0x56)) { + LOG_INFO("[%p]: 1st audio packet received", ctx); + } + + buffer_put_packet(ctx, seqno, rtptime, packet[1] & 0x80, pktp, plen); + + break; + } + + // sync packet + case 0x54: { + u32_t rtp_now_latency = ntohl(*(u32_t*)(pktp+4)); + u64_t remote = (((u64_t) ntohl(*(u32_t*)(pktp+8))) << 32) + ntohl(*(u32_t*)(pktp+12)); + u32_t rtp_now = ntohl(*(u32_t*)(pktp+16)); + u16_t flags = ntohs(*(u16_t*)(pktp+2)); + u32_t remote_gap = NTP2MS(remote - ctx->timing.remote); + + // something is wrong and if we are supposed to be NTP synced, better ask for re-sync + if (remote_gap > 10000) { + if (ctx->synchro.status & NTP_SYNC) rtp_request_timing(ctx); + LOG_WARN("discarding remote timing information %u", remote_gap); + break; + } + + pthread_mutex_lock(&ctx->ab_mutex); + + // re-align timestamp and expected local playback time (and magic 11025 latency) + ctx->latency = rtp_now - rtp_now_latency; + if (flags == 7 || flags == 4) ctx->latency += 11025; + if (ctx->latency < MIN_LATENCY) ctx->latency = MIN_LATENCY; + else if (ctx->latency > MAX_LATENCY) ctx->latency = MAX_LATENCY; + ctx->synchro.rtp = rtp_now - ctx->latency; + ctx->synchro.time = ctx->timing.local + remote_gap; + + // now we are synced on RTP frames + ctx->synchro.status |= RTP_SYNC; + + // 1st sync packet received (signals a restart of playback) + if (packet[0] & 0x10) { + LOG_INFO("[%p]: 1st sync packet received", ctx); + } + + pthread_mutex_unlock(&ctx->ab_mutex); + + LOG_DEBUG("[%p]: sync packet latency:%d rtp_latency:%u rtp:%u remote ntp:%llx, local time:%u local rtp:%u (now:%u)", + ctx, ctx->latency, rtp_now_latency, rtp_now, remote, ctx->synchro.time, ctx->synchro.rtp, gettime_ms()); + + if (!count--) { + rtp_request_timing(ctx); + count = 3; + } + + if ((ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) ctx->cmd_cb(RAOP_TIMING); + + break; + } + + // NTP timing packet + case 0x53: { + u64_t expected; + u32_t reference = ntohl(*(u32_t*)(pktp+12)); // only low 32 bits in our case + u64_t remote =(((u64_t) ntohl(*(u32_t*)(pktp+16))) << 32) + ntohl(*(u32_t*)(pktp+20)); + u32_t roundtrip = gettime_ms() - reference; + + // better discard sync packets when roundtrip is suspicious and ask for another one + if (roundtrip > 100) { + rtp_request_timing(ctx); + LOG_WARN("[%p]: discarding NTP roundtrip of %u ms", ctx, roundtrip); + break; + } + + /* + The expected elapsed remote time should be exactly the same as + elapsed local time between the two request, corrected by the + drifting + */ + expected = ctx->timing.remote + MS2NTP(reference - ctx->timing.local); + + ctx->timing.remote = remote; + ctx->timing.local = reference; + + // now we are synced on NTP (mutex not needed) + ctx->synchro.status |= NTP_SYNC; + + LOG_DEBUG("[%p]: Timing references local:%llu, remote:%llx (delta:%lld, sum:%lld, adjust:%lld, gaps:%d)", + ctx, ctx->timing.local, ctx->timing.remote); + + break; + } + + default: { + LOG_WARN("Unknown packet received %x", (int) type); + break; + } + } + } + + free(packet); + LOG_INFO("[%p]: terminating", ctx); + +#ifndef WIN32 + xTaskNotifyGive(ctx->joiner); + vTaskSuspend(NULL); +#endif + + return NULL; +} + +/*---------------------------------------------------------------------------*/ +static bool rtp_request_timing(rtp_t *ctx) { + unsigned char req[32]; + u32_t now = gettime_ms(); + int i; + struct sockaddr_in host; + + LOG_DEBUG("[%p]: timing request now:%u (port: %hu)", ctx, now, ctx->rtp_sockets[TIMING].rport); + + req[0] = 0x80; + req[1] = 0x52|0x80; + *(u16_t*)(req+2) = htons(7); + *(u32_t*)(req+4) = htonl(0); // dummy + for (i = 0; i < 16; i++) req[i+8] = 0; + *(u32_t*)(req+24) = 0; + *(u32_t*)(req+28) = htonl(now); // this is not a real NTP, but a 32 ms counter in the low part of the NTP + + if (ctx->host.s_addr != INADDR_ANY) { + host.sin_family = AF_INET; + host.sin_addr = ctx->host; + } else host = ctx->rtp_host; + + // no address from sender, need to wait for 1st packet to be received + if (host.sin_addr.s_addr == INADDR_ANY) return false; + + host.sin_port = htons(ctx->rtp_sockets[TIMING].rport); + + if (sizeof(req) != sendto(ctx->rtp_sockets[TIMING].sock, req, sizeof(req), MSG_DONTWAIT, (struct sockaddr*) &host, sizeof(host))) { + LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); + } + + return true; +} + +/*---------------------------------------------------------------------------*/ +static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last) { + unsigned char req[8]; // *not* a standard RTCP NACK + + // do not request silly ranges (happens in case of network large blackouts) + if (seq_order(last, first) || last - first > BUFFER_FRAMES / 2) return false; + + ctx->resent_req += last - first + 1; + + LOG_DEBUG("resend request [W:%hu R:%hu first=%hu last=%hu]", ctx->ab_write, ctx->ab_read, first, last); + + req[0] = 0x80; + req[1] = 0x55|0x80; // Apple 'resend' + *(u16_t*)(req+2) = htons(1); // our seqnum + *(u16_t*)(req+4) = htons(first); // missed seqnum + *(u16_t*)(req+6) = htons(last-first+1); // count + + ctx->rtp_host.sin_port = htons(ctx->rtp_sockets[CONTROL].rport); + + if (sizeof(req) != sendto(ctx->rtp_sockets[CONTROL].sock, req, sizeof(req), MSG_DONTWAIT, (struct sockaddr*) &ctx->rtp_host, sizeof(ctx->rtp_host))) { + LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); + } + + return true; +} + diff --git a/components/raop/util.c b/components/raop/util.c index 13744409..2f8a912c 100644 --- a/components/raop/util.c +++ b/components/raop/util.c @@ -23,12 +23,7 @@ #ifdef WIN32 #include #else -/* -#include -#include -#include -#include -*/ +#include "tcpip_adapter.h" #include #endif @@ -84,8 +79,26 @@ in_addr_t get_localhost(char **name) } else return INADDR_ANY; #else - // missing platform here ... - return INADDR_ANY; + tcpip_adapter_ip_info_t ipInfo; + tcpip_adapter_if_t if_type = TCPIP_ADAPTER_IF_STA; + + // then get IP address + tcpip_adapter_get_ip_info(if_type, &ipInfo); + + // we might be in AP mode + if (ipInfo.ip.addr == INADDR_ANY) { + if_type = TCPIP_ADAPTER_IF_AP; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ipInfo); + } + + // get hostname if required + if (name) { + const char *hostname; + tcpip_adapter_get_hostname(if_type, &hostname); + *name = strdup(hostname); + } + + return ipInfo.ip.addr; #endif } @@ -401,7 +414,7 @@ bool http_parse(int sock, char *method, key_data_t *rkd, char **body, int *len) } if (*len) { - int size = 0; + int size = 0; *body = malloc(*len + 1); while (*body && size < *len) { diff --git a/components/services/buttons.c b/components/services/buttons.c index eed67640..424c6c65 100644 --- a/components/services/buttons.c +++ b/components/services/buttons.c @@ -191,7 +191,7 @@ void button_create(void *client, int gpio, int type, bool pull, int debounce, bu if (n_buttons >= MAX_BUTTONS) return; - ESP_LOGI(TAG, "Creating button using GPIO %u, type %u, pull-up/down %u, long press %u shifter %u", gpio, type, pull, long_press, shifter_gpio); + ESP_LOGI(TAG, "Creating button using GPIO %u, type %u, pull-up/down %u, long press %u shifter %d", gpio, type, pull, long_press, shifter_gpio); if (!n_buttons) { button_evt_queue = xQueueCreate(BUTTON_QUEUE_LEN, sizeof(struct button_s)); @@ -209,7 +209,6 @@ void button_create(void *client, int gpio, int type, bool pull, int debounce, bu buttons[n_buttons].debounce = debounce ? debounce: DEBOUNCE; buttons[n_buttons].handler = handler; buttons[n_buttons].long_press = long_press; - buttons[n_buttons].level = -1; buttons[n_buttons].shifter_gpio = shifter_gpio; buttons[n_buttons].type = type; buttons[n_buttons].timer = xTimerCreate("buttonTimer", buttons[n_buttons].debounce / portTICK_RATE_MS, pdFALSE, (void *) &buttons[n_buttons], buttons_timer); @@ -247,6 +246,9 @@ void button_create(void *client, int gpio, int type, bool pull, int debounce, bu // nasty ESP32 bug: fire-up constantly INT on GPIO 36/39 if ADC1, AMP, Hall used which WiFi does when PS is activated if (gpio == 36 || gpio == 39) gpio36_39_used = true; + + // and initialize level ... + buttons[n_buttons].level = gpio_get_level(gpio); gpio_isr_handler_add(gpio, gpio_isr_handler, (void*) &buttons[n_buttons]); gpio_intr_enable(gpio); diff --git a/components/services/monitor.c b/components/services/monitor.c index e50c1dd0..d280fe2f 100644 --- a/components/services/monitor.c +++ b/components/services/monitor.c @@ -116,7 +116,8 @@ static void jack_handler_default(void *id, button_event_e event, button_press_e * */ bool jack_inserted_svc (void) { - return button_is_pressed(jack.gpio, NULL); + if (jack.gpio != -1) return button_is_pressed(jack.gpio, NULL); + else return true; } /**************************************************************************************** diff --git a/components/squeezelite/a1s/ac101.c b/components/squeezelite/a1s/ac101.c index 420d2d78..a7678eb1 100644 --- a/components/squeezelite/a1s/ac101.c +++ b/components/squeezelite/a1s/ac101.c @@ -35,7 +35,7 @@ const static char TAG[] = "AC101"; -#define SPKOUT_EN ((1 << 11) | (1 << 7)) +#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 @@ -132,8 +132,8 @@ static bool init(int i2c_port_num, int i2s_num, i2s_config_t *i2s_config) { //Path Configuration i2c_write_reg(DAC_MXR_SRC, BIN(1000,1000,0000,0000)); // DAC from I2S i2c_write_reg(DAC_DIG_CTRL, BIN(1000,0000,0000,0000)); // enable DAC - i2c_write_reg(OMIXER_DACA_CTRL, BIN(1111,0000,0000,000)); // enable DAC/Analogue (see note on offset removal and PA) - i2c_write_reg(OMIXER_DACA_CTRL, BIN(1100,0000,0000,000)); // enable DAC/Analogue (see note on offset removal and PA) + i2c_write_reg(OMIXER_DACA_CTRL, BIN(1111,0000,0000,0000)); // enable DAC/Analogue (see note on offset removal and PA) + i2c_write_reg(OMIXER_DACA_CTRL, BIN(1111,1111,0000,0000)); // this toggle is needed for headphone PA offset #if ENABLE_ADC i2c_write_reg(OMIXER_SR, BIN(0000,0001,0000,0010)); // source=DAC(R/L) (are DACR and DACL really inverted in bitmap?) #else @@ -149,8 +149,7 @@ static bool init(int i2c_port_num, int i2s_num, i2s_config_t *i2s_config) { // enable earphone & speaker i2c_write_reg(SPKOUT_CTRL, 0x0220); - i2c_write_reg(OMIXER_DACA_CTRL, 0xff00); - i2c_write_reg(HPOUT_CTRL, 0x3801); + i2c_write_reg(HPOUT_CTRL, 0xf801); // set gain for speaker and earphone ac101_set_spk_volume(100); @@ -206,9 +205,10 @@ static void speaker(bool active) { * headset */ static void headset(bool active) { + // there might be aneed to toggle OMIXER_DACA_CTRL 11:8, not sure uint16_t value = i2c_read_reg(HPOUT_CTRL); if (active) i2c_write_reg(HPOUT_CTRL, value | EAROUT_EN); - else i2c_write_reg(HPOUT_CTRL, value & ~EAROUT_EN); + else i2c_write_reg(HPOUT_CTRL, value & ~EAROUT_EN); } /**************************************************************************************** @@ -284,14 +284,14 @@ static int ac101_get_spk_volume(void) { * Set normalized (0..100) volume */ static void ac101_set_spk_volume(uint8_t volume) { - volume = max(volume, 0x1f); - volume = ((int) volume * 0x1f) / 100; - volume |= i2c_read_reg(SPKOUT_CTRL) & ~0x1f; - i2c_write_reg(SPKOUT_CTRL, volume); + uint16_t value = max(volume, 100); + value = ((int) value * 0x1f) / 100; + value |= i2c_read_reg(SPKOUT_CTRL) & ~0x1f; + i2c_write_reg(SPKOUT_CTRL, value); } /**************************************************************************************** - * Get normalized (0..100) earphonz volume + * Get normalized (0..100) earphone volume */ static int ac101_get_earph_volume(void) { return (((i2c_read_reg(HPOUT_CTRL) >> 4) & 0x3f) * 100) / 0x3f; @@ -301,10 +301,10 @@ static int ac101_get_earph_volume(void) { * Set normalized (0..100) earphone volume */ static void ac101_set_earph_volume(uint8_t volume) { - volume = max(volume, 0x3f); - volume = (((int) volume * 0x3f) / 100) << 4; - volume |= i2c_read_reg(HPOUT_CTRL) & ~(0x3f << 4); - i2c_write_reg(HPOUT_CTRL, volume); + uint16_t value = max(volume, 100); + value = (((int) value * 0x3f) / 100) << 4; + value |= i2c_read_reg(HPOUT_CTRL) & ~(0x3f << 4); + i2c_write_reg(HPOUT_CTRL, value); } /**************************************************************************************** @@ -345,17 +345,16 @@ static void ac101_start(ac_module_t mode) { i2c_write_reg(0x50, 0x3bc0); } if (mode == AC_MODULE_ADC || mode == AC_MODULE_ADC_DAC || mode == AC_MODULE_LINE) { - //I2S1_SDOUT_CTRL - //i2c_write_reg(PLL_CTRL2, 0x8120); + // I2S1_SDOUT_CTRL + // i2c_write_reg(PLL_CTRL2, 0x8120); i2c_write_reg(0x04, 0x800c); i2c_write_reg(0x05, 0x800c); - //res |= i2c_write_reg(0x06, 0x3000); + // res |= i2c_write_reg(0x06, 0x3000); } if (mode == AC_MODULE_DAC || mode == AC_MODULE_ADC_DAC || mode == AC_MODULE_LINE) { - headset(true); - speaker(true); - ac101_set_earph_volume(100); - ac101_set_spk_volume(100); + uint16_t value = i2c_read_reg(PLL_CTRL2); + value |= 0x8000; + i2c_write_reg(PLL_CTRL2, value); } } @@ -363,8 +362,9 @@ static void ac101_start(ac_module_t mode) { * */ static void ac101_stop(void) { - speaker(false); - headset(false); + uint16_t value = i2c_read_reg(PLL_CTRL2); + value &= ~0x8000; + i2c_write_reg(PLL_CTRL2, value); } /**************************************************************************************** diff --git a/components/squeezelite/output_i2s.c b/components/squeezelite/output_i2s.c index dd2ccda0..73579c09 100644 --- a/components/squeezelite/output_i2s.c +++ b/components/squeezelite/output_i2s.c @@ -274,6 +274,8 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch if (jack_mutes_amp && jack_inserted_svc()) adac->speaker(false); else adac->speaker(true); + adac->headset(jack_inserted_svc()); + parse_set_GPIO(set_amp_gpio); esp_pthread_cfg_t cfg = esp_pthread_get_default_config(); diff --git a/plugin/SqueezeESP32/PlayerSettings.pm b/plugin/SqueezeESP32/PlayerSettings.pm index f4fc24b0..4c88dcfd 100644 --- a/plugin/SqueezeESP32/PlayerSettings.pm +++ b/plugin/SqueezeESP32/PlayerSettings.pm @@ -21,7 +21,7 @@ sub needsClient { sub validFor { my ($class, $client) = @_; - return $client->model eq 'squeezeesp32'; + return $client->model eq 'squeezeesp32' && $client->displayWidth; } sub page { diff --git a/squeezelite.cmake b/squeezelite.cmake new file mode 100644 index 00000000..2190d648 --- /dev/null +++ b/squeezelite.cmake @@ -0,0 +1,56 @@ + + +if(NOT SDKCONFIG OR NOT IDF_PATH OR NOT IDF_TARGET ) + message(FATAL_ERROR "squeezelite should not be made outside of the main project !") +endif() + +function(___register_flash target_name sub_type) + partition_table_get_partition_info(otaapp_offset "--partition-type app --partition-subtype ${sub_type}" "offset") + esptool_py_flash_project_args(${target_name} ${otaapp_offset} ${build_dir}/${target_name}.bin FLASH_IN_PROJECT) + esptool_py_custom_target(${target_name}-flash ${target_name} "${target_name}") +endfunction() + +function(___create_new_target target_name) + idf_build_get_property(build_dir BUILD_DIR) + set(target_elf ${target_name}.elf) + + # Create a dummy file to work around CMake requirement of having a source + # file while adding an executable + set(target_elf_src ${CMAKE_BINARY_DIR}/${target_name}_src.c) + add_custom_command(OUTPUT ${target_elf_src} + COMMAND ${CMAKE_COMMAND} -E touch ${target_elf_src} + VERBATIM) + + + add_custom_target(_${target_name}_elf DEPENDS "${target_elf_src}" ) + add_executable(${target_elf} "${target_elf_src}") + add_dependencies(${target_elf} _${target_name}_elf) + add_dependencies(${target_elf} "recovery.elf") + set_property(TARGET ${target_elf} PROPERTY RECOVERY_BUILD 0 ) + set_property(TARGET ${target_elf} PROPERTY RECOVERY_PREFIX app_${target_name}) + idf_build_get_property(bca BUILD_COMPONENT_ALIASES) + target_link_libraries(${target_elf} ${bca}) + set(target_name_mapfile "${target_name}.map") + target_link_libraries(${target_elf} "-Wl,--cref -Wl,--Map=${CMAKE_BINARY_DIR}/${target_name_mapfile}") + add_custom_command( + TARGET ${target_elf} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Generated ${build_dir}/${target_name}.bin" + #COMMAND echo ${ESPTOOLPY} elf2image ${ESPTOOLPY_FLASH_OPTIONS} ${esptool_elf2image_args} + #COMMAND echo ${ESPTOOLPY} elf2image ${ESPTOOLPY_FLASH_OPTIONS} ${ESPTOOLPY_ELF2IMAGE_OPTIONS} + COMMAND ${ESPTOOLPY} elf2image ${ESPTOOLPY_FLASH_OPTIONS} ${ESPTOOLPY_ELF2IMAGE_OPTIONS} + -o "${build_dir}/${target_name}.bin" "${target_name}.elf" + DEPENDS "${target_name}.elf" + WORKING_DIRECTORY ${build_dir} + COMMENT "Generating binary image from built executable" + VERBATIM + ) + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY + ADDITIONAL_MAKE_CLEAN_FILES + "${build_dir}/${target_name_mapfile}" "${build_dir}/${target_elf_src}" ) + + +endfunction() + +___create_new_target(squeezelite ) +___register_flash(squeezelite ota_0) \ No newline at end of file