diff --git a/README.md b/README.md index c2785f9b..999c4f47 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -m ESP32" nvs_set autoexec u8 -v 1 +4/ set bluetooth & airplaysink name (if not set in menuconfig) + +nvs_set bt_sink_name str -v "" +nvs_set airplay_sink_name str -v "" + The "join" and "squeezelite" commands can also be typed at the prompt to start manually. Use "help" to see the list. The squeezelite options are very similar to the regular Linux ones. Differences are : diff --git a/components/airplay/airplay_sink.c b/components/airplay/airplay_sink.c new file mode 100644 index 00000000..8566d08c --- /dev/null +++ b/components/airplay/airplay_sink.c @@ -0,0 +1,65 @@ +#include +#include +#include +#include + +#include "mdns.h" +#include "nvs.h" +#include "tcpip_adapter.h" +#include "esp_log.h" +#include "esp_console.h" +#include "esp_pthread.h" +#include "esp_system.h" +#include "freertos/timers.h" +#include "airplay_sink.h" + +#include "trace.h" + +static const char * TAG = "platform"; +extern char current_namespace[]; + +void airplay_sink_init(void) { + const char *hostname; + char *airplay_name, sink_name[32] = CONFIG_AIRPLAY_NAME; + nvs_handle nvs; + + tcpip_adapter_get_hostname(TCPIP_ADAPTER_IF_STA, &hostname); + + //initialize mDNS + ESP_ERROR_CHECK( mdns_init() ); + ESP_ERROR_CHECK( mdns_hostname_set(hostname) ); + + //structure with TXT records + mdns_txt_item_t serviceTxtData[] = { + {"am", "esp32"}, + {"tp", "UDP"}, + {"sm","false"}, + {"sv","false"}, + {"ek","1"}, + {"et","0,1"}, + {"md","0,1,2"}, + {"cn","0,1"}, + {"ch","2"}, + {"ss","16"}, + {"sr","44100"}, + {"vn","3"}, + {"txtvers","1"}, + }; + + if (nvs_open(current_namespace, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = 31; + nvs_get_str(nvs, "airplay_sink_name", sink_name, &len); + nvs_close(nvs); + } + + // AirPlay wants mDNS name to be MAC@name + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + asprintf(&airplay_name, "%02X%02X%02X%02X%02X%02X@%s", mac[3], mac[4], mac[5], mac[3], mac[4], mac[5], sink_name); + + ESP_LOGI(TAG, "mdns hostname set to: [%s] with servicename %s", hostname, sink_name); + + //initialize service + ESP_ERROR_CHECK( mdns_service_add(airplay_name, "_raop", "_tcp", 6000, serviceTxtData, sizeof(serviceTxtData) / sizeof(mdns_txt_item_t)) ); + free(airplay_name); +} diff --git a/components/airplay/airplay_sink.h b/components/airplay/airplay_sink.h new file mode 100644 index 00000000..d6ba163b --- /dev/null +++ b/components/airplay/airplay_sink.h @@ -0,0 +1,22 @@ +/* + 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 __AIRPLAY_SINK_H__ +#define __AIRPLAY_SINK_H__ + +#include + +//typedef enum { BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE, + //BT_SINK_RATE, BT_SINK_VOLUME, } bt_sink_cmd_t; + +/** + * @brief init sink mode (need to be provided) + */ +void airplay_sink_init(void); + +#endif /* __AIRPLAY_SINK_H__*/ \ No newline at end of file diff --git a/components/airplay/component.mk b/components/airplay/component.mk new file mode 100644 index 00000000..06578f47 --- /dev/null +++ b/components/airplay/component.mk @@ -0,0 +1,10 @@ +# +# Component Makefile +# +# This Makefile should, at the very least, just include $(SDK_PATH)/Makefile. By default, +# this will take the sources in the src/ directory, compile them and link them into +# lib(subdirectory_name).a in the build directory. This behaviour is entirely configurable, +# please read the SDK documents if you need to do this. +# + +CFLAGS += -I$(COMPONENT_PATH)/../tools diff --git a/components/driver_bt/bt_app_sink.c b/components/driver_bt/bt_app_sink.c index ef323f5a..b2d3536c 100644 --- a/components/driver_bt/bt_app_sink.c +++ b/components/driver_bt/bt_app_sink.c @@ -21,6 +21,7 @@ #include "esp_gap_bt_api.h" #include "esp_a2dp_api.h" #include "esp_avrc_api.h" +#include "nvs.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -39,9 +40,11 @@ #define BT_RC_CT_TAG "RCCT" #ifndef CONFIG_BT_SINK_NAME -#define CONFIG_BT_SINK_NAME "unavailable" +#define CONFIG_BT_SINK_NAME "default" #endif +extern char current_namespace[]; + /* event for handler "bt_av_hdl_stack_up */ enum { BT_APP_EVT_STACK_UP = 0, @@ -449,9 +452,17 @@ static void bt_av_hdl_stack_evt(uint16_t event, void *p_param) switch (event) { case BT_APP_EVT_STACK_UP: { /* set up device name */ - char *dev_name = CONFIG_BT_SINK_NAME; - esp_bt_dev_set_device_name(dev_name); - + nvs_handle nvs; + char dev_name[32] = CONFIG_BT_SINK_NAME; + + if (nvs_open(current_namespace, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = 31; + nvs_get_str(nvs, "bt_sink_name", dev_name, &len); + nvs_close(nvs); + } + + esp_bt_dev_set_device_name(dev_name); + esp_bt_gap_register_callback(bt_app_gap_cb); /* initialize AVRCP controller */ diff --git a/components/driver_bt/component.mk b/components/driver_bt/component.mk index 0c83faa9..e694d472 100644 --- a/components/driver_bt/component.mk +++ b/components/driver_bt/component.mk @@ -7,5 +7,6 @@ # please read the SDK documents if you need to do this. # -CFLAGS += -I$(COMPONENT_PATH)/../tools +CFLAGS += -I$(COMPONENT_PATH)/../tools + #CFLAGS += -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG diff --git a/components/squeezelite/component.mk b/components/squeezelite/component.mk index 1d9abe13..6452d246 100644 --- a/components/squeezelite/component.mk +++ b/components/squeezelite/component.mk @@ -13,7 +13,8 @@ CFLAGS += -O3 -DLINKALL -DLOOPBACK -DNO_FAAD -DRESAMPLE16 -DEMBEDDED -DTREMOR_ON -I$(COMPONENT_PATH)/../tools \ -I$(COMPONENT_PATH)/../codecs/inc/opus \ -I$(COMPONENT_PATH)/../codecs/inc/opusfile \ - -I$(COMPONENT_PATH)/../driver_bt + -I$(COMPONENT_PATH)/../driver_bt \ + -I$(COMPONENT_PATH)/../airplay # -I$(COMPONENT_PATH)/../codecs/inc/faad2 diff --git a/components/squeezelite/decode_bt.c b/components/squeezelite/decode_external.c similarity index 91% rename from components/squeezelite/decode_bt.c rename to components/squeezelite/decode_external.c index 273d81ae..6cf83fbc 100644 --- a/components/squeezelite/decode_bt.c +++ b/components/squeezelite/decode_external.c @@ -21,6 +21,7 @@ #include "squeezelite.h" #include "bt_app_sink.h" +#include "airplay_sink.h" #define LOCK_O mutex_lock(outputbuf->mutex) #define UNLOCK_O mutex_unlock(outputbuf->mutex) @@ -92,15 +93,17 @@ static void bt_sink_cmd_handler(bt_sink_cmd_t cmd, ...) switch(cmd) { case BT_SINK_CONNECTED: + output.external = true; output.state = OUTPUT_STOPPED; LOG_INFO("BT sink started"); break; case BT_SINK_DISCONNECTED: + output.external = false; output.state = OUTPUT_OFF; LOG_INFO("BT sink stopped"); break; case BT_SINK_PLAY: - output.state = OUTPUT_EXTERNAL; + output.state = OUTPUT_RUNNING; LOG_INFO("BT sink playing"); break; case BT_SINK_PAUSE: @@ -138,4 +141,12 @@ void register_other(void) { LOG_WARN("Cannot be a BT sink and source"); } #endif +#ifdef CONFIG_AIRPLAY_SINK + if (!strcasestr(output.device, "BT ")) { + airplay_sink_init(); + LOG_INFO("Initializing AirPlay sink"); + } else { + LOG_WARN("Cannot be an AirPlay sink and BT source"); + } +#endif } diff --git a/components/squeezelite/output_i2s.c b/components/squeezelite/output_i2s.c index 4e72ff3e..00cb6e22 100644 --- a/components/squeezelite/output_i2s.c +++ b/components/squeezelite/output_i2s.c @@ -417,7 +417,7 @@ static void *output_thread_i2s() { LOG_INFO("Output state is %d", output.state); if (output.state == OUTPUT_OFF) led_blink(LED_GREEN, 100, 2500); else if (output.state == OUTPUT_STOPPED) led_blink(LED_GREEN, 200, 1000); - else if (output.state >= OUTPUT_RUNNING) led_on(LED_GREEN); + else if (output.state == OUTPUT_RUNNING) led_on(LED_GREEN); } state = output.state; diff --git a/components/squeezelite/slimproto.c b/components/squeezelite/slimproto.c index e301d13e..d41befc4 100644 --- a/components/squeezelite/slimproto.c +++ b/components/squeezelite/slimproto.c @@ -371,6 +371,7 @@ static void process_strm(u8_t *pkt, int len) { sendSTAT("STMc", 0); sentSTMu = sentSTMo = sentSTMl = false; LOCK_O; + output.external = false; output.threshold = strm->output_threshold; output.next_replay_gain = unpackN(&strm->replay_gain); output.fade_mode = strm->transition_type - '0'; @@ -688,7 +689,7 @@ static void slimproto_run() { status.current_sample_rate = output.current_sample_rate; status.updated = output.updated; status.device_frames = output.device_frames; - + if (output.track_started) { _sendSTMs = true; output.track_started = false; @@ -703,7 +704,7 @@ static void slimproto_run() { if (_start_output && (output.state == OUTPUT_STOPPED || output.state == OUTPUT_OFF)) { output.state = OUTPUT_BUFFER; } - if (output.state == OUTPUT_RUNNING && !sentSTMu && status.output_full == 0 && status.stream_state <= DISCONNECT && + if (!output.external && output.state == OUTPUT_RUNNING && !sentSTMu && status.output_full == 0 && status.stream_state <= DISCONNECT && _decode_state == DECODE_STOPPED) { _sendSTMu = true; @@ -721,7 +722,7 @@ static void slimproto_run() { output.state = OUTPUT_OFF; LOG_DEBUG("output timeout"); } - if (output.state == OUTPUT_RUNNING && now - status.last > 1000) { + if (!output.external && output.state == OUTPUT_RUNNING && now - status.last > 1000) { _sendSTMt = true; status.last = now; } diff --git a/components/squeezelite/squeezelite.h b/components/squeezelite/squeezelite.h index b51247a3..73cc363d 100644 --- a/components/squeezelite/squeezelite.h +++ b/components/squeezelite/squeezelite.h @@ -634,7 +634,7 @@ bool resample_init(char *opt); // output.c output_alsa.c output_pa.c output_pack.c typedef enum { OUTPUT_OFF = -1, OUTPUT_STOPPED = 0, OUTPUT_BUFFER, OUTPUT_RUNNING, - OUTPUT_PAUSE_FRAMES, OUTPUT_SKIP_FRAMES, OUTPUT_START_AT, OUTPUT_EXTERNAL } output_state; + OUTPUT_PAUSE_FRAMES, OUTPUT_SKIP_FRAMES, OUTPUT_START_AT } output_state; #if DSD typedef enum { PCM, DOP, DSD_U8, DSD_U16_LE, DSD_U32_LE, DSD_U16_BE, DSD_U32_BE, DOP_S24_LE, DOP_S24_3LE } dsd_format; @@ -654,6 +654,7 @@ struct outputstate { output_state state; output_format format; const char *device; + bool external; #if ALSA unsigned buffer; unsigned period; diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index b009ab04..e6c29b69 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -229,7 +229,7 @@ menu "Squeezelite-ESP32" config BT_SINK_NAME depends on BT_SINK string "Name of Bluetooth A2DP device" - default "ESP32" + default "ESP32-BT" help This is the name of the bluetooth speaker that will be broadcasted config BT_SINK_PIN @@ -237,8 +237,20 @@ menu "Squeezelite-ESP32" int "Bluetooth PIN code" default 1234 config AIRPLAY_SINK - bool "AirPlay receiver (not availabe now)" - default n + bool "AirPlay receiver" + default y + config AIRPLAY_NAME + depends on AIRPLAY_SINK + string "Name of AirPlay device" + default "ESP32-AirPlay" + help + This is the name of the AirPlay speaker that will be broadcasted + config AIRPLAY_PORT + depends on AIRPLAY_SINK + string "AirPlay listening port" + default 5000 + help + AirPlay service listening port endmenu endmenu \ No newline at end of file