From 9b5bdd8cb47f52fc6dba51bca98089f7e44b018e Mon Sep 17 00:00:00 2001 From: philippe44 Date: Sun, 19 May 2019 00:11:01 -0700 Subject: [PATCH] initial commit --- CMakeLists.txt | 5 + Kconfig.projbuild | 68 ++++ buffer.c | 115 ++++++ component.mk | 8 + decode.c | 296 ++++++++++++++ esp32.c | 12 + main.c | 837 +++++++++++++++++++++++++++++++++++++++ output.c | 450 +++++++++++++++++++++ output_dac.c | 154 ++++++++ output_pack.c | 365 +++++++++++++++++ pcm.c | 438 +++++++++++++++++++++ scan.c | 148 +++++++ slimproto.c | 976 ++++++++++++++++++++++++++++++++++++++++++++++ slimproto.h | 185 +++++++++ squeezelite.h | 786 +++++++++++++++++++++++++++++++++++++ stream.c | 590 ++++++++++++++++++++++++++++ utils.c | 563 ++++++++++++++++++++++++++ 17 files changed, 5996 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 Kconfig.projbuild create mode 100644 buffer.c create mode 100644 component.mk create mode 100644 decode.c create mode 100644 esp32.c create mode 100644 main.c create mode 100644 output.c create mode 100644 output_dac.c create mode 100644 output_pack.c create mode 100644 pcm.c create mode 100644 scan.c create mode 100644 slimproto.c create mode 100644 slimproto.h create mode 100644 squeezelite.h create mode 100644 stream.c create mode 100644 utils.c diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..e6be577f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.5) +set(COMPONENT_SRCS "scan.c") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +register_component() diff --git a/Kconfig.projbuild b/Kconfig.projbuild new file mode 100644 index 00000000..b3fc5d73 --- /dev/null +++ b/Kconfig.projbuild @@ -0,0 +1,68 @@ +menu "Example Configuration" + + config WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + + config WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + + choice SCAN_METHOD + prompt "scan method" + default WIFI_FAST_SCAN + help + scan method for the esp32 to use + + config WIFI_FAST_SCAN + bool "fast" + config WIFI_ALL_CHANNEL_SCAN + bool "all" + endchoice + + choice SORT_METHOD + prompt "sort method" + default WIFI_CONNECT_AP_BY_SIGNAL + help + sort method for the esp32 to use + + config WIFI_CONNECT_AP_BY_SIGNAL + bool "rssi" + config WIFI_CONNECT_AP_BY_SECURITY + bool "authmode" + endchoice + + config FAST_SCAN_THRESHOLD + bool "fast scan threshold" + default y + help + wifi fast scan threshold + + config FAST_SCAN_MINIMUM_SIGNAL + int "fast scan minimum rssi" + depends on FAST_SCAN_THRESHOLD + range -127 0 + default -127 + help + rssi is use to measure the signal + + choice FAST_SCAN_WEAKEST_AUTHMODE + prompt "fast scan weakest authmode" + depends on FAST_SCAN_THRESHOLD + default EXAMPLE_OPEN + + config EXAMPLE_OPEN + bool "open" + config EXAMPLE_WEP + bool "wep" + config EXAMPLE_WPA + bool "wpa" + config EXAMPLE_WPA2 + bool "wpa2" + endchoice + +endmenu diff --git a/buffer.c b/buffer.c new file mode 100644 index 00000000..ed71d811 --- /dev/null +++ b/buffer.c @@ -0,0 +1,115 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// fifo bufffers + +#define _GNU_SOURCE + +#include "squeezelite.h" + +// _* called with muxtex locked + +inline unsigned _buf_used(struct buffer *buf) { + return buf->writep >= buf->readp ? buf->writep - buf->readp : buf->size - (buf->readp - buf->writep); +} + +unsigned _buf_space(struct buffer *buf) { + return buf->size - _buf_used(buf) - 1; // reduce by one as full same as empty otherwise +} + +unsigned _buf_cont_read(struct buffer *buf) { + return buf->writep >= buf->readp ? buf->writep - buf->readp : buf->wrap - buf->readp; +} + +unsigned _buf_cont_write(struct buffer *buf) { + return buf->writep >= buf->readp ? buf->wrap - buf->writep : buf->readp - buf->writep; +} + +void _buf_inc_readp(struct buffer *buf, unsigned by) { + buf->readp += by; + if (buf->readp >= buf->wrap) { + buf->readp -= buf->size; + } +} + +void _buf_inc_writep(struct buffer *buf, unsigned by) { + buf->writep += by; + if (buf->writep >= buf->wrap) { + buf->writep -= buf->size; + } +} + +void buf_flush(struct buffer *buf) { + mutex_lock(buf->mutex); + buf->readp = buf->buf; + buf->writep = buf->buf; + mutex_unlock(buf->mutex); +} + +// adjust buffer to multiple of mod bytes so reading in multiple always wraps on frame boundary +void buf_adjust(struct buffer *buf, size_t mod) { + size_t size; + mutex_lock(buf->mutex); + size = ((unsigned)(buf->base_size / mod)) * mod; + buf->readp = buf->buf; + buf->writep = buf->buf; + buf->wrap = buf->buf + size; + buf->size = size; + mutex_unlock(buf->mutex); +} + +// called with mutex locked to resize, does not retain contents, reverts to original size if fails +void _buf_resize(struct buffer *buf, size_t size) { + free(buf->buf); + buf->buf = malloc(size); + if (!buf->buf) { + size = buf->size; + buf->buf= malloc(size); + if (!buf->buf) { + size = 0; + } + } + buf->readp = buf->buf; + buf->writep = buf->buf; + buf->wrap = buf->buf + size; + buf->size = size; + buf->base_size = size; +} + +void buf_init(struct buffer *buf, size_t size) { + buf->buf = malloc(size); + buf->readp = buf->buf; + buf->writep = buf->buf; + buf->wrap = buf->buf + size; + buf->size = size; + buf->base_size = size; + mutex_create_p(buf->mutex); +} + +void buf_destroy(struct buffer *buf) { + if (buf->buf) { + free(buf->buf); + buf->buf = NULL; + buf->size = 0; + buf->base_size = 0; + mutex_destroy(buf->mutex); + } +} diff --git a/component.mk b/component.mk new file mode 100644 index 00000000..2b7bec8d --- /dev/null +++ b/component.mk @@ -0,0 +1,8 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) +CFLAGS += -DPOSIX -DLINKALL -DLOOPBACK -DDACAUDIO + + + diff --git a/decode.c b/decode.c new file mode 100644 index 00000000..62cdd87a --- /dev/null +++ b/decode.c @@ -0,0 +1,296 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// decode thread + +#include "squeezelite.h" + +log_level loglevel; + +extern struct buffer *streambuf; +extern struct buffer *outputbuf; +extern struct streamstate stream; +extern struct outputstate output; +extern struct processstate process; + +struct decodestate decode; +struct codec *codecs[MAX_CODECS]; +struct codec *codec; +static bool running = true; + +#define LOCK_S mutex_lock(streambuf->mutex) +#define UNLOCK_S mutex_unlock(streambuf->mutex) +#define LOCK_O mutex_lock(outputbuf->mutex) +#define UNLOCK_O mutex_unlock(outputbuf->mutex) +#define LOCK_D mutex_lock(decode.mutex); +#define UNLOCK_D mutex_unlock(decode.mutex); + +#if PROCESS +#define IF_DIRECT(x) if (decode.direct) { x } +#define IF_PROCESS(x) if (!decode.direct) { x } +#define MAY_PROCESS(x) { x } +#else +#define IF_DIRECT(x) { x } +#define IF_PROCESS(x) +#define MAY_PROCESS(x) +#endif + +static void *decode_thread() { + + while (running) { + size_t bytes, space, min_space; + bool toend; + bool ran = false; + + LOCK_S; + bytes = _buf_used(streambuf); + toend = (stream.state <= DISCONNECT); + UNLOCK_S; + LOCK_O; + space = _buf_space(outputbuf); + UNLOCK_O; + + LOCK_D; + + if (decode.state == DECODE_RUNNING && codec) { + + LOG_SDEBUG("streambuf bytes: %u outputbuf space: %u", bytes, space); + + IF_DIRECT( + min_space = codec->min_space; + ); + IF_PROCESS( + min_space = process.max_out_frames * BYTES_PER_FRAME; + ); + + if (space > min_space && (bytes > codec->min_read_bytes || toend)) { + + decode.state = codec->decode(); + + IF_PROCESS( + if (process.in_frames) { + process_samples(); + } + + if (decode.state == DECODE_COMPLETE) { + process_drain(); + } + ); + + if (decode.state != DECODE_RUNNING) { + + LOG_INFO("decode %s", decode.state == DECODE_COMPLETE ? "complete" : "error"); + + LOCK_O; + if (output.fade_mode) _checkfade(false); + UNLOCK_O; + + wake_controller(); + } + + ran = true; + } + } + + UNLOCK_D; + + if (!ran) { + usleep(100000); + } + } + + return 0; +} + +static void sort_codecs(int pry, struct codec* ptr) { + static int priority[MAX_CODECS]; + int i, tpry; + struct codec* tptr; + + for (i = 0; i < MAX_CODECS; i++) { + if (!codecs[i]) { + codecs[i] = ptr; + priority[i] = pry; + return; + } + if (pry < priority[i]) { + tptr = codecs[i]; + codecs[i] = ptr; + ptr = tptr; + tpry = priority[i]; + priority[i] = pry; + pry = tpry; + } + } +} + +static thread_type thread; + +void decode_init(log_level level, const char *include_codecs, const char *exclude_codecs) { + int i; + char* order_codecs = NULL; + + loglevel = level; + + LOG_INFO("init decode"); + + // register codecs + // dsf,dff,alc,wma,wmap,wmal,aac,spt,ogg,ogf,flc,aif,pcm,mp3 + i = 0; +/* +#if DSD + if (!strstr(exclude_codecs, "dsd") && (!include_codecs || (order_codecs = strstr(include_codecs, "dsd")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_dsd()); +#endif +#if FFMPEG + if (!strstr(exclude_codecs, "alac") && (!include_codecs || (order_codecs = strstr(include_codecs, "alac")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_ff("alc")); + if (!strstr(exclude_codecs, "wma") && (!include_codecs || (order_codecs = strstr(include_codecs, "wma")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_ff("wma")); +#endif +#ifndef NO_FAAD + if (!strstr(exclude_codecs, "aac") && (!include_codecs || (order_codecs = strstr(include_codecs, "aac")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_faad()); +#endif + if (!strstr(exclude_codecs, "ogg") && (!include_codecs || (order_codecs = strstr(include_codecs, "ogg")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_vorbis()); + if (!strstr(exclude_codecs, "flac") && (!include_codecs || (order_codecs = strstr(include_codecs, "flac")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_flac()); +*/ + if (!strstr(exclude_codecs, "pcm") && (!include_codecs || (order_codecs = strstr(include_codecs, "pcm")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_pcm()); +/* + // try mad then mpg for mp3 unless command line option passed + if (!(strstr(exclude_codecs, "mp3") || strstr(exclude_codecs, "mad")) && + (!include_codecs || (order_codecs = strstr(include_codecs, "mp3")) || (order_codecs = strstr(include_codecs, "mad")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_mad()); + else if (!(strstr(exclude_codecs, "mp3") || strstr(exclude_codecs, "mpg")) && + (!include_codecs || (order_codecs = strstr(include_codecs, "mp3")) || (order_codecs = strstr(include_codecs, "mpg")))) + sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_mpg()); +*/ + + LOG_DEBUG("include codecs: %s exclude codecs: %s", include_codecs ? include_codecs : "", exclude_codecs); + + mutex_create(decode.mutex); + +#if LINUX || OSX || FREEBSD || POSIX + pthread_attr_t attr; + pthread_attr_init(&attr); +#ifdef PTHREAD_STACK_MIN + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + DECODE_THREAD_STACK_SIZE); +#endif + pthread_create(&thread, &attr, decode_thread, NULL); + pthread_attr_destroy(&attr); +#endif +#if WIN + thread = CreateThread(NULL, DECODE_THREAD_STACK_SIZE, (LPTHREAD_START_ROUTINE)&decode_thread, NULL, 0, NULL); +#endif + + decode.new_stream = true; + decode.state = DECODE_STOPPED; + + MAY_PROCESS( + decode.direct = true; + decode.process = false; + ); +} + +void decode_close(void) { + LOG_INFO("close decode"); + LOCK_D; + if (codec) { + codec->close(); + codec = NULL; + } + running = false; + UNLOCK_D; +#if LINUX || OSX || FREEBSD + pthread_join(thread, NULL); +#endif + mutex_destroy(decode.mutex); +} + +void decode_flush(void) { + LOG_INFO("decode flush"); + LOCK_D; + decode.state = DECODE_STOPPED; + IF_PROCESS( + process_flush(); + ); + UNLOCK_D; +} + +unsigned decode_newstream(unsigned sample_rate, unsigned supported_rates[]) { + + // called with O locked to get sample rate for potentially processed output stream + // release O mutex during process_newstream as it can take some time + + MAY_PROCESS( + if (decode.process) { + UNLOCK_O; + sample_rate = process_newstream(&decode.direct, sample_rate, supported_rates); + LOCK_O; + } + ); + + return sample_rate; +} + +void codec_open(u8_t format, u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness) { + int i; + + LOG_INFO("codec open: '%c'", format); + + LOCK_D; + + decode.new_stream = true; + decode.state = DECODE_STOPPED; + + MAY_PROCESS( + decode.direct = true; // potentially changed within codec when processing enabled + ); + + // find the required codec + for (i = 0; i < MAX_CODECS; ++i) { + + if (codecs[i] && codecs[i]->id == format) { + + if (codec && codec != codecs[i]) { + LOG_INFO("closing codec: '%c'", codec->id); + codec->close(); + } + + codec = codecs[i]; + + codec->open(sample_size, sample_rate, channels, endianness); + + decode.state = DECODE_READY; + + UNLOCK_D; + return; + } + } + + UNLOCK_D; + + LOG_ERROR("codec not found"); +} + diff --git a/esp32.c b/esp32.c new file mode 100644 index 00000000..57dc38c3 --- /dev/null +++ b/esp32.c @@ -0,0 +1,12 @@ +#include + +#include "esp_system.h" +#include "squeezelite.h" + +void get_mac(u8_t mac[]) { + esp_read_mac(mac, ESP_MAC_WIFI_STA); +} + +_sig_func_ptr signal(int sig, _sig_func_ptr func) { + return NULL; +} diff --git a/main.c b/main.c new file mode 100644 index 00000000..953fa5ae --- /dev/null +++ b/main.c @@ -0,0 +1,837 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Additions (c) Paul Hermann, 2015-2017 under the same license terms + * -Control of Raspberry pi GPIO for amplifier power + * -Launch script on power status change from LMS + */ + +#include "squeezelite.h" + +#include + +#define TITLE "Squeezelite " VERSION ", Copyright 2012-2015 Adrian Smith, 2015-2019 Ralph Irving." + +#define CODECS_BASE "flac,pcm,mp3,ogg" +#if NO_FAAD +#define CODECS_AAC "" +#else +#define CODECS_AAC ",aac" +#endif +#if FFMPEG +#define CODECS_FF ",wma,alac" +#else +#define CODECS_FF "" +#endif +#if DSD +#define CODECS_DSD ",dsd" +#else +#define CODECS_DSD "" +#endif +#define CODECS_MP3 " (mad,mpg for specific mp3 codec)" + +#define CODECS CODECS_BASE CODECS_AAC CODECS_FF CODECS_DSD CODECS_MP3 + +static void usage(const char *argv0) { + printf(TITLE " See -t for license terms\n" + "Usage: %s [options]\n" + " -s [:]\tConnect to specified server, otherwise uses autodiscovery to find server\n" + " -o \tSpecify output device, default \"default\", - = output to stdout\n" + " -l \t\t\tList output devices\n" +#if ALSA + " -a :

::\tSpecify ALSA params to open output device, b = buffer time in ms or size in bytes, p = period count or size in bytes, f sample format (16|24|24_3|32), m = use mmap (0|1)\n" +#endif +#if PORTAUDIO +#if PA18API + " -a :\tSpecify output target 4 byte frames per buffer, number of buffers\n" +#elif OSX && !defined(OSXPPC) + " -a :\t\tSpecify Portaudio params to open output device, l = target latency in ms, r = allow OSX to resample (0|1)\n" +#elif WIN + " -a :\t\tSpecify Portaudio params to open output device, l = target latency in ms, e = use exclusive mode for WASAPI (0|1)\n" +#else + " -a \t\tSpecify Portaudio params to open output device, l = target latency in ms\n" +#endif +#endif + " -a \t\tSpecify sample format (16|24|32) of output file when using -o - to output samples to stdout (interleaved little endian only)\n" + " -b :\tSpecify internal Stream and Output buffer sizes in Kbytes\n" + " -c ,\tRestrict codecs to those specified, otherwise load all available codecs; known codecs: " CODECS "\n" + " \t\t\tCodecs reported to LMS in order listed, allowing codec priority refinement.\n" + " -C \t\tClose output device when idle after timeout seconds, default is to keep it open while player is 'on'\n" +#if !IR + " -d =\tSet logging level, logs: all|slimproto|stream|decode|output, level: info|debug|sdebug\n" +#else + " -d =\tSet logging level, logs: all|slimproto|stream|decode|output|ir, level: info|debug|sdebug\n" +#endif +#if defined(GPIO) && defined(RPI) + " -G :\tSpecify the BCM GPIO# to use for Amp Power Relay and if the output should be Active High or Low\n" +#endif + " -e ,\tExplicitly exclude native support of one or more codecs; known codecs: " CODECS "\n" + " -f \t\tWrite debug to logfile\n" +#if IR + " -i []\tEnable lirc remote control support (lirc config file ~/.lircrc used if filename not specified)\n" +#endif + " -m \t\tSet mac address, format: ab:cd:ef:12:34:56\n" + " -M \tSet the squeezelite player model name sent to the server (default: " MODEL_NAME_STRING ")\n" + " -n \t\tSet the player name\n" + " -N \t\tStore player name in filename to allow server defined name changes to be shared between servers (not supported with -n)\n" + " -W\t\t\tRead wave and aiff format from header, ignore server parameters\n" +#if ALSA + " -p \t\tSet real time priority of output thread (1-99)\n" +#endif +#if LINUX || FREEBSD || SUN + " -P \t\tStore the process id (PID) in filename\n" +#endif + " -r [:]\tSample rates supported, allows output to be off when squeezelite is started; rates = |-|,,; delay = optional delay switching rates in ms\n" +#if GPIO + " -S \tAbsolute path to script to launch on power commands from LMS\n" +#endif +#if RESAMPLE + " -R -u [params]\tResample, params = ::::::,\n" + " \t\t\t recipe = (v|h|m|l|q)(L|I|M)(s) [E|X], E = exception - resample only if native rate not supported, X = async - resample to max rate for device, otherwise to max sync rate\n" + " \t\t\t flags = num in hex,\n" + " \t\t\t attenuation = attenuation in dB to apply (default is -1db if not explicitly set),\n" + " \t\t\t precision = number of bits precision (NB. HQ = 20. VHQ = 28),\n" + " \t\t\t passband_end = number in percent (0dB pt. bandwidth to preserve. nyquist = 100%%),\n" + " \t\t\t stopband_start = number in percent (Aliasing/imaging control. > passband_end),\n" + " \t\t\t phase_response = 0-100 (0 = minimum / 50 = linear / 100 = maximum)\n" +#endif +#if DSD +#if ALSA + " -D [delay][:format]\tOutput device supports DSD, delay = optional delay switching between PCM and DSD in ms\n" + " \t\t\t format = dop (default if not specified), u8, u16le, u16be, u32le or u32be.\n" +#else + " -D [delay]\t\tOutput device supports DSD over PCM (DoP), delay = optional delay switching between PCM and DoP in ms\n" +#endif +#endif +#if VISEXPORT + " -v \t\t\tVisualiser support\n" +#endif +# if ALSA + " -O \tSpecify mixer device, defaults to 'output device'\n" + " -L \t\t\tList volume controls for output device\n" + " -U \t\tUnmute ALSA control and set to full volume (not supported with -V)\n" + " -V \t\tUse ALSA control for volume adjustment, otherwise use software volume adjustment\n" + " -X \t\t\tUse linear volume adjustments instead of in terms of dB (only for hardware volume control)\n" +#endif +#if LINUX || FREEBSD || SUN + " -z \t\t\tDaemonize\n" +#endif +#if RESAMPLE + " -Z \t\tReport rate to server in helo as the maximum sample rate we can support\n" +#endif + " -t \t\t\tLicense terms\n" + " -? \t\t\tDisplay this help text\n" + "\n" + "Build options:" +#if SUN + " SOLARIS" +#elif LINUX + " LINUX" +#endif +#if WIN + " WIN" +#endif +#if OSX + " OSX" +#endif +#if OSXPPC + "PPC" +#endif +#if FREEBSD + " FREEBSD" +#endif +#if ALSA + " ALSA" +#endif +#if PORTAUDIO + " PORTAUDIO" +#if PA18API + "18" +#endif +#endif +#if EVENTFD + " EVENTFD" +#endif +#if SELFPIPE + " SELFPIPE" +#endif +#if LOOPBACK + " LOOPBACK" +#endif +#if WINEVENT + " WINEVENT" +#endif +#if RESAMPLE_MP + " RESAMPLE_MP" +#else +#if RESAMPLE + " RESAMPLE" +#endif +#endif +#if FFMPEG + " FFMPEG" +#endif +#if NO_FAAD + " NO_FAAD" +#endif +#if VISEXPORT + " VISEXPORT" +#endif +#if IR + " IR" +#endif +#if GPIO + " GPIO" +#endif +#if RPI + " RPI" +#endif +#if DSD + " DSD" +#endif +#if USE_SSL + " SSL" +#endif +#if LINKALL + " LINKALL" +#endif +#if STATUSHACK + " STATUSHACK" +#endif + "\n\n", + argv0); +} + +static void license(void) { + printf(TITLE "\n\n" + "This program is free software: you can redistribute it and/or modify\n" + "it under the terms of the GNU General Public License as published by\n" + "the Free Software Foundation, either version 3 of the License, or\n" + "(at your option) any later version.\n\n" + "This program is distributed in the hope that it will be useful,\n" + "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + "GNU General Public License for more details.\n\n" + "You should have received a copy of the GNU General Public License\n" + "along with this program. If not, see .\n" +#if DSD + "\nContains dsd2pcm library Copyright 2009, 2011 Sebastian Gesemann which\n" + "is subject to its own license.\n" + "\nContains the Daphile Project full dsd patch Copyright 2013-2017 Daphile,\n" + "which is subject to its own license.\n" +#endif + "\nOption to allow server side upsampling for PCM streams (-W) from\n" + "squeezelite-R2 (c) Marco Curti 2015, marcoc1712@gmail.com.\n" +#if GPIO + "\nAdditions (c) Paul Hermann, 2015, 2017 under the same license terms\n" + "- Launch a script on power status change\n" + "- Control of Raspberry pi GPIO for amplifier power\n" +#endif +#if RPI + "\nContains wiringpi GPIO Interface library Copyright (c) 2012-2017\n" + "Gordon Henderson, which is subject to its own license.\n" +#endif +#if FFMPEG + "\nThis software uses libraries from the FFmpeg project under\n" + "the LGPLv2.1 and its source can be downloaded from\n" + "\n" +#endif + "\n" + ); +} + +static void sighandler(int signum) { + slimproto_stop(); + + // remove ourselves in case above does not work, second SIGINT will cause non gracefull shutdown + signal(signum, SIG_DFL); +} + +int main(int argc, char **argv) { + char *server = NULL; + char *output_device = "default"; + char *include_codecs = NULL; + char *exclude_codecs = ""; + char *name = NULL; + char *namefile = NULL; + char *modelname = NULL; + extern bool pcm_check_header; + extern bool user_rates; + char *logfile = NULL; + u8_t mac[6]; + unsigned stream_buf_size = STREAMBUF_SIZE; + unsigned output_buf_size = 0; // set later + unsigned rates[MAX_SUPPORTED_SAMPLERATES] = { 0 }; + unsigned rate_delay = 0; + char *resample = NULL; + char *output_params = NULL; + unsigned idle = 0; +#if LINUX || FREEBSD || SUN + bool daemonize = false; + char *pidfile = NULL; + FILE *pidfp = NULL; +#endif +#if ALSA + unsigned rt_priority = OUTPUT_RT_PRIORITY; + char *mixer_device = output_device; + char *output_mixer = NULL; + bool output_mixer_unmute = false; + bool linear_volume = false; +#endif +#if DSD + unsigned dsd_delay = 0; + dsd_format dsd_outfmt = PCM; +#endif +#if VISEXPORT + bool visexport = false; +#endif +#if IR + char *lircrc = NULL; +#endif + + log_level log_output = lWARN; + log_level log_stream = lWARN; + log_level log_decode = lWARN; + log_level log_slimproto = lWARN; +#if IR + log_level log_ir = lWARN; +#endif + + int maxSampleRate = 0; + + char *optarg = NULL; + int optind = 1; + int i; + +#define MAXCMDLINE 512 + char cmdline[MAXCMDLINE] = ""; + + get_mac(mac); + + for (i = 0; i < argc && (strlen(argv[i]) + strlen(cmdline) + 2 < MAXCMDLINE); i++) { + strcat(cmdline, argv[i]); + strcat(cmdline, " "); + } + + while (optind < argc && strlen(argv[optind]) >= 2 && argv[optind][0] == '-') { + char *opt = argv[optind] + 1; + if (strstr("oabcCdefmMnNpPrs" +#if ALSA + "UVO" +#endif +/* + * only allow '-Z ' override of maxSampleRate + * reported by client if built with the capability to resample! + */ +#if RESAMPLE + "Z" +#endif + , opt) && optind < argc - 1) { + optarg = argv[optind + 1]; + optind += 2; + } else if (strstr("ltz?W" +#if ALSA + "LX" +#endif +#if RESAMPLE + "uR" +#endif +#if DSD + "D" +#endif +#if VISEXPORT + "v" +#endif +#if IR + "i" +#endif +#if defined(GPIO) && defined(RPI) + "G" +#endif +#if GPIO + "S" +#endif + + , opt)) { + optarg = NULL; + optind += 1; + } else { + fprintf(stderr, "\nOption error: -%s\n\n", opt); + usage(argv[0]); + exit(1); + } + + switch (opt[0]) { + case 'o': + output_device = optarg; +#if ALSA + mixer_device = optarg; +#endif + break; + case 'a': + output_params = optarg; + break; + case 'b': + { + char *s = next_param(optarg, ':'); + char *o = next_param(NULL, ':'); + if (s) stream_buf_size = atoi(s) * 1024; + if (o) output_buf_size = atoi(o) * 1024; + } + break; + case 'c': + include_codecs = optarg; + break; + case 'C': + if (atoi(optarg) > 0) { + idle = atoi(optarg) * 1000; + } + break; + case 'e': + exclude_codecs = optarg; + break; + case 'd': + { + char *l = strtok(optarg, "="); + char *v = strtok(NULL, "="); + log_level new = lWARN; + if (l && v) { + if (!strcmp(v, "info")) new = lINFO; + if (!strcmp(v, "debug")) new = lDEBUG; + if (!strcmp(v, "sdebug")) new = lSDEBUG; + if (!strcmp(l, "all") || !strcmp(l, "slimproto")) log_slimproto = new; + if (!strcmp(l, "all") || !strcmp(l, "stream")) log_stream = new; + if (!strcmp(l, "all") || !strcmp(l, "decode")) log_decode = new; + if (!strcmp(l, "all") || !strcmp(l, "output")) log_output = new; +#if IR + if (!strcmp(l, "all") || !strcmp(l, "ir")) log_ir = new; +#endif + } else { + fprintf(stderr, "\nDebug settings error: -d %s\n\n", optarg); + usage(argv[0]); + exit(1); + } + } + break; + case 'f': + logfile = optarg; + break; + case 'm': + { + int byte = 0; + char *tmp; + if (!strncmp(optarg, "00:04:20", 8)) { + LOG_ERROR("ignoring mac address from hardware player range 00:04:20:**:**:**"); + } else { + char *t = strtok(optarg, ":"); + while (t && byte < 6) { + mac[byte++] = (u8_t)strtoul(t, &tmp, 16); + t = strtok(NULL, ":"); + } + } + } + break; + case 'M': + modelname = optarg; + break; + case 'r': + { + char *rstr = next_param(optarg, ':'); + char *dstr = next_param(NULL, ':'); + if (rstr && strstr(rstr, ",")) { + // parse sample rates and sort them + char *r = next_param(rstr, ','); + unsigned tmp[MAX_SUPPORTED_SAMPLERATES] = { 0 }; + int i, j; + int last = 999999; + for (i = 0; r && i < MAX_SUPPORTED_SAMPLERATES; ++i) { + tmp[i] = atoi(r); + r = next_param(NULL, ','); + } + for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) { + int largest = 0; + for (j = 0; j < MAX_SUPPORTED_SAMPLERATES; ++j) { + if (tmp[j] > largest && tmp[j] < last) { + largest = tmp[j]; + } + } + rates[i] = last = largest; + } + } else if (rstr) { + // optstr is - or , extract rates from test rates within this range + unsigned ref[] TEST_RATES; + char *str1 = next_param(rstr, '-'); + char *str2 = next_param(NULL, '-'); + unsigned max = str2 ? atoi(str2) : (str1 ? atoi(str1) : ref[0]); + unsigned min = str1 && str2 ? atoi(str1) : 0; + unsigned tmp; + int i, j; + if (max < min) { tmp = max; max = min; min = tmp; } + rates[0] = max; + for (i = 0, j = 1; i < MAX_SUPPORTED_SAMPLERATES; ++i) { + if (ref[i] < rates[j-1] && ref[i] >= min) { + rates[j++] = ref[i]; + } + } + } + if (dstr) { + rate_delay = atoi(dstr); + } + if (rates[0]) { + user_rates = true; + } + } + break; + case 's': + server = optarg; + break; + case 'n': + name = optarg; + break; + case 'N': + namefile = optarg; + break; + case 'W': + pcm_check_header = true; + break; +#if ALSA + case 'p': + rt_priority = atoi(optarg); + if (rt_priority > 99 || rt_priority < 1) { + fprintf(stderr, "\nError: invalid priority: %s\n\n", optarg); + usage(argv[0]); + exit(1); + } + break; +#endif +#if LINUX || FREEBSD || SUN + case 'P': + pidfile = optarg; + break; +#endif +#ifndef DACAUDIO + case 'l': + list_devices(); + exit(0); + break; +#endif +#if RESAMPLE + case 'u': + case 'R': + if (optind < argc && argv[optind] && argv[optind][0] != '-') { + resample = argv[optind++]; + } else { + resample = ""; + } + break; + case 'Z': + maxSampleRate = atoi(optarg); + break; +#endif +#if DSD + case 'D': + dsd_outfmt = DOP; + if (optind < argc && argv[optind] && argv[optind][0] != '-') { + char *dstr = next_param(argv[optind++], ':'); + char *fstr = next_param(NULL, ':'); + dsd_delay = dstr ? atoi(dstr) : 0; + if (fstr) { + if (!strcmp(fstr, "dop")) dsd_outfmt = DOP; + if (!strcmp(fstr, "u8")) dsd_outfmt = DSD_U8; + if (!strcmp(fstr, "u16le")) dsd_outfmt = DSD_U16_LE; + if (!strcmp(fstr, "u32le")) dsd_outfmt = DSD_U32_LE; + if (!strcmp(fstr, "u16be")) dsd_outfmt = DSD_U16_BE; + if (!strcmp(fstr, "u32be")) dsd_outfmt = DSD_U32_BE; + if (!strcmp(fstr, "dop24")) dsd_outfmt = DOP_S24_LE; + if (!strcmp(fstr, "dop24_3")) dsd_outfmt = DOP_S24_3LE; + } + } + break; +#endif +#if VISEXPORT + case 'v': + visexport = true; + break; +#endif +#if ALSA + case 'O': + mixer_device = optarg; + break; + case 'L': + list_mixers(mixer_device); + exit(0); + break; + case 'X': + linear_volume = true; + break; + case 'U': + output_mixer_unmute = true; + case 'V': + if (output_mixer) { + fprintf(stderr, "-U and -V option should not be used at same time\n"); + exit(1); + } + output_mixer = optarg; + break; +#endif +#if IR + case 'i': + if (optind < argc && argv[optind] && argv[optind][0] != '-') { + lircrc = argv[optind++]; + } else { + lircrc = "~/.lircrc"; // liblirc_client will expand ~/ + } + break; +#endif +#if defined(GPIO) && defined(RPI) + case 'G': + if (power_script != NULL){ + fprintf(stderr, "-G and -S options cannot be used together \n\n" ); + usage(argv[0]); + exit(1); + } + if (optind < argc && argv[optind] && argv[optind][0] != '-') { + char *gp = next_param(argv[optind++], ':'); + char *go = next_param (NULL, ':'); + gpio_pin = atoi(gp); + if (go != NULL){ + if ((strcmp(go, "H")==0)|(strcmp(go, "h")==0)){ + gpio_active_low=false; + }else if((strcmp(go, "L")==0)|(strcmp(go, "l")==0)){ + gpio_active_low=true; + }else{ + fprintf(stderr,"Must set output to be active High or Low i.e. -G18:H or -G18:L\n"); + usage(argv[0]); + exit(1); + } + }else{ + fprintf(stderr,"-G Option Error\n"); + usage(argv[0]); + exit(1); + } + gpio_active = true; + relay(0); + + } else { + fprintf(stderr, "Error in GPIO Pin assignment.\n"); + usage(argv[0]); + exit(1); + } + break; +#endif +#if GPIO + case 'S': + if (gpio_active){ + fprintf(stderr, "-G and -S options cannot be used together \n\n" ); + usage(argv[0]); + exit(1); + } + if (optind < argc && argv[optind] && argv[optind][0] != '-') { + power_script = argv[optind++]; + if( access( power_script, R_OK|X_OK ) == -1 ) { + // file doesn't exist + fprintf(stderr, "Script %s, not found\n\n", argv[optind-1]); + usage(argv[0]); + exit(1); + } + } else { + fprintf(stderr, "No Script Name Given.\n\n"); + usage(argv[0]); + exit(1); + } + relay_script(0); + break; +#endif +#if LINUX || FREEBSD || SUN + case 'z': + daemonize = true; +#if SUN + init_daemonize(); +#endif /* SUN */ + break; +#endif + case 't': + license(); + exit(0); + case '?': + usage(argv[0]); + exit(0); + default: + fprintf(stderr, "Arg error: %s\n", argv[optind]); + break; + } + } + + // warn if command line includes something which isn't parsed + if (optind < argc) { + fprintf(stderr, "\nError: command line argument error\n\n"); + usage(argv[0]); + exit(1); + } + + signal(SIGINT, sighandler); + signal(SIGTERM, sighandler); +#if defined(SIGQUIT) + signal(SIGQUIT, sighandler); +#endif +#if defined(SIGHUP) + signal(SIGHUP, sighandler); +#endif + +#if USE_SSL && !LINKALL + ssl_loaded = load_ssl_symbols(); +#endif + + // set the output buffer size if not specified on the command line, take account of resampling + if (!output_buf_size) { + output_buf_size = OUTPUTBUF_SIZE; + if (resample) { + unsigned scale = 8; + if (rates[0]) { + scale = rates[0] / 44100; + if (scale > 8) scale = 8; + if (scale < 1) scale = 1; + } + output_buf_size *= scale; + } + } + + if (logfile) { + if (!freopen(logfile, "a", stderr)) { + fprintf(stderr, "error opening logfile %s: %s\n", logfile, strerror(errno)); + } else { + if (log_output >= lINFO || log_stream >= lINFO || log_decode >= lINFO || log_slimproto >= lINFO) { + fprintf(stderr, "\n%s\n", cmdline); + } + } + } + +#if LINUX || FREEBSD || SUN + if (pidfile) { + if (!(pidfp = fopen(pidfile, "w")) ) { + fprintf(stderr, "Error opening pidfile %s: %s\n", pidfile, strerror(errno)); + exit(1); + } + pidfile = realpath(pidfile, NULL); // daemonize will change cwd + } + + if (daemonize) { + if (daemon(0, logfile ? 1 : 0)) { + fprintf(stderr, "error daemonizing: %s\n", strerror(errno)); + } + } + + if (pidfp) { + fprintf(pidfp, "%d\n", (int) getpid()); + fclose(pidfp); + } +#endif + +#if WIN + winsock_init(); +#endif + + stream_init(log_stream, stream_buf_size); + +#if DACAUDIO + output_init_dac(log_output, output_buf_size, output_params, rates, rate_delay, idle); +#else + if (!strcmp(output_device, "-")) { + output_init_stdout(log_output, output_buf_size, output_params, rates, rate_delay); + } else { +#if ALSA + output_init_alsa(log_output, output_device, output_buf_size, output_params, rates, rate_delay, rt_priority, idle, mixer_device, output_mixer, + output_mixer_unmute, linear_volume); +#endif +#if PORTAUDIO + output_init_pa(log_output, output_device, output_buf_size, output_params, rates, rate_delay, idle); +#endif + } +#endif + +#if DSD + dsd_init(dsd_outfmt, dsd_delay); +#endif + +#if VISEXPORT + if (visexport) { + output_vis_init(log_output, mac); + } +#endif + + decode_init(log_decode, include_codecs, exclude_codecs); + +#if RESAMPLE + if (resample) { + process_init(resample); + } +#endif + +#if IR + if (lircrc) { + ir_init(log_ir, lircrc); + } +#endif + + if (name && namefile) { + fprintf(stderr, "-n and -N option should not be used at same time\n"); + exit(1); + } + + slimproto(log_slimproto, server, mac, name, namefile, modelname, maxSampleRate); + + decode_close(); + stream_close(); + +#if DACAUDIO + output_close_dac(); +#else + if (!strcmp(output_device, "-")) { + output_close_stdout(); + } else { +#if ALSA + output_close_alsa(); +#endif +#if PORTAUDIO + output_close_pa(); +#endif + } +#endif + +#if IR + ir_close(); +#endif + +#if WIN + winsock_close(); +#endif + +#if LINUX || FREEBSD || SUN + if (pidfile) { + unlink(pidfile); + free(pidfile); + } +#endif + +#if USE_SSL && !LINKALL + free_ssl_symbols(); +#endif + + exit(0); +} diff --git a/output.c b/output.c new file mode 100644 index 00000000..65b54c7e --- /dev/null +++ b/output.c @@ -0,0 +1,450 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Common output function + +#include "squeezelite.h" + +static log_level loglevel; + +struct outputstate output; + +static struct buffer buf; + +struct buffer *outputbuf = &buf; + +u8_t *silencebuf; +#if DSD +u8_t *silencebuf_dsd; +#endif + +bool user_rates = false; + +#define LOCK mutex_lock(outputbuf->mutex) +#define UNLOCK mutex_unlock(outputbuf->mutex) + +// functions starting _* are called with mutex locked + +frames_t _output_frames(frames_t avail) { + + frames_t frames, size; + bool silence; + + s32_t cross_gain_in = 0, cross_gain_out = 0; s32_t *cross_ptr = NULL; + + s32_t gainL = output.current_replay_gain ? gain(output.gainL, output.current_replay_gain) : output.gainL; + s32_t gainR = output.current_replay_gain ? gain(output.gainR, output.current_replay_gain) : output.gainR; + + if (output.invert) { gainL = -gainL; gainR = -gainR; } + + frames = _buf_used(outputbuf) / BYTES_PER_FRAME; + silence = false; + + // start when threshold met + if (output.state == OUTPUT_BUFFER && frames > output.threshold * output.next_sample_rate / 100 && frames > output.start_frames) { + output.state = OUTPUT_RUNNING; + LOG_INFO("start buffer frames: %u", frames); + wake_controller(); + } + + // skip ahead - consume outputbuf but play nothing + if (output.state == OUTPUT_SKIP_FRAMES) { + if (frames > 0) { + frames_t skip = min(frames, output.skip_frames); + LOG_INFO("skip %u of %u frames", skip, output.skip_frames); + frames -= skip; + output.frames_played += skip; + while (skip > 0) { + frames_t cont_frames = min(skip, _buf_cont_read(outputbuf) / BYTES_PER_FRAME); + skip -= cont_frames; + _buf_inc_readp(outputbuf, cont_frames * BYTES_PER_FRAME); + } + } + output.state = OUTPUT_RUNNING; + } + + // pause frames - play silence for required frames + if (output.state == OUTPUT_PAUSE_FRAMES) { + LOG_INFO("pause %u frames", output.pause_frames); + if (output.pause_frames == 0) { + output.state = OUTPUT_RUNNING; + } else { + silence = true; + frames = min(avail, output.pause_frames); + frames = min(frames, MAX_SILENCE_FRAMES); + output.pause_frames -= frames; + } + } + + // start at - play silence until jiffies reached + if (output.state == OUTPUT_START_AT) { + u32_t now = gettime_ms(); + if (now >= output.start_at || output.start_at > now + 10000) { + output.state = OUTPUT_RUNNING; + } else { + u32_t delta_frames = (output.start_at - now) * output.current_sample_rate / 1000; + silence = true; + frames = min(avail, delta_frames); + frames = min(frames, MAX_SILENCE_FRAMES); + } + } + + // play silence if buffering or no frames + if (output.state <= OUTPUT_BUFFER || frames == 0) { + silence = true; + frames = min(avail, MAX_SILENCE_FRAMES); + } + + LOG_SDEBUG("avail: %d frames: %d silence: %d", avail, frames, silence); + frames = min(frames, avail); + size = frames; + + while (size > 0) { + frames_t out_frames; + frames_t cont_frames = _buf_cont_read(outputbuf) / BYTES_PER_FRAME; + int wrote; + + if (output.track_start && !silence) { + if (output.track_start == outputbuf->readp) { + unsigned delay = 0; + if (output.current_sample_rate != output.next_sample_rate) { + delay = output.rate_delay; + } + IF_DSD( + if (output.outfmt != output.next_fmt) { + delay = output.dsd_delay; + } + ) + frames -= size; + // add silence delay in two halves, before and after track start on rate or pcm-dop change + if (delay) { + output.state = OUTPUT_PAUSE_FRAMES; + if (!output.delay_active) { + output.pause_frames = output.current_sample_rate * delay / 2000; + output.delay_active = true; // first delay - don't process track start + break; + } else { + output.pause_frames = output.next_sample_rate * delay / 2000; + output.delay_active = false; // second delay - process track start + } + } + LOG_INFO("track start sample rate: %u replay_gain: %u", output.next_sample_rate, output.next_replay_gain); + output.frames_played = 0; + output.track_started = true; + output.track_start_time = gettime_ms(); + output.current_sample_rate = output.next_sample_rate; + IF_DSD( + output.outfmt = output.next_fmt; + ) + if (output.fade == FADE_INACTIVE || output.fade_mode != FADE_CROSSFADE) { + output.current_replay_gain = output.next_replay_gain; + } + output.track_start = NULL; + break; + } else if (output.track_start > outputbuf->readp) { + // reduce cont_frames so we find the next track start at beginning of next chunk + cont_frames = min(cont_frames, (output.track_start - outputbuf->readp) / BYTES_PER_FRAME); + } + } + + IF_DSD( + if (output.outfmt != PCM) { + gainL = gainR = FIXED_ONE; + } + ) + + if (output.fade && !silence) { + if (output.fade == FADE_DUE) { + if (output.fade_start == outputbuf->readp) { + LOG_INFO("fade start reached"); + output.fade = FADE_ACTIVE; + } else if (output.fade_start > outputbuf->readp) { + cont_frames = min(cont_frames, (output.fade_start - outputbuf->readp) / BYTES_PER_FRAME); + } + } + if (output.fade == FADE_ACTIVE) { + // find position within fade + frames_t cur_f = outputbuf->readp >= output.fade_start ? (outputbuf->readp - output.fade_start) / BYTES_PER_FRAME : + (outputbuf->readp + outputbuf->size - output.fade_start) / BYTES_PER_FRAME; + frames_t dur_f = output.fade_end >= output.fade_start ? (output.fade_end - output.fade_start) / BYTES_PER_FRAME : + (output.fade_end + outputbuf->size - output.fade_start) / BYTES_PER_FRAME; + if (cur_f >= dur_f) { + if (output.fade_mode == FADE_INOUT && output.fade_dir == FADE_DOWN) { + LOG_INFO("fade down complete, starting fade up"); + output.fade_dir = FADE_UP; + output.fade_start = outputbuf->readp; + output.fade_end = outputbuf->readp + dur_f * BYTES_PER_FRAME; + if (output.fade_end >= outputbuf->wrap) { + output.fade_end -= outputbuf->size; + } + cur_f = 0; + } else if (output.fade_mode == FADE_CROSSFADE) { + LOG_INFO("crossfade complete"); + if (_buf_used(outputbuf) >= dur_f * BYTES_PER_FRAME) { + _buf_inc_readp(outputbuf, dur_f * BYTES_PER_FRAME); + LOG_INFO("skipped crossfaded start"); + } else { + LOG_WARN("unable to skip crossfaded start"); + } + output.fade = FADE_INACTIVE; + output.current_replay_gain = output.next_replay_gain; + } else { + LOG_INFO("fade complete"); + output.fade = FADE_INACTIVE; + } + } + // if fade in progress set fade gain, ensure cont_frames reduced so we get to end of fade at start of chunk + if (output.fade) { + if (output.fade_end > outputbuf->readp) { + cont_frames = min(cont_frames, (output.fade_end - outputbuf->readp) / BYTES_PER_FRAME); + } + if (output.fade_dir == FADE_UP || output.fade_dir == FADE_DOWN) { + // fade in, in-out, out handled via altering standard gain + s32_t fade_gain; + if (output.fade_dir == FADE_DOWN) { + cur_f = dur_f - cur_f; + } + fade_gain = to_gain((float)cur_f / (float)dur_f); + gainL = gain(gainL, fade_gain); + gainR = gain(gainR, fade_gain); + if (output.invert) { gainL = -gainL; gainR = -gainR; } + } + if (output.fade_dir == FADE_CROSS) { + // cross fade requires special treatment - performed later based on these values + // support different replay gain for old and new track by retaining old value until crossfade completes + if (_buf_used(outputbuf) / BYTES_PER_FRAME > dur_f + size) { + cross_gain_in = to_gain((float)cur_f / (float)dur_f); + cross_gain_out = FIXED_ONE - cross_gain_in; + if (output.current_replay_gain) { + cross_gain_out = gain(cross_gain_out, output.current_replay_gain); + } + if (output.next_replay_gain) { + cross_gain_in = gain(cross_gain_in, output.next_replay_gain); + } + gainL = output.gainL; + gainR = output.gainR; + if (output.invert) { gainL = -gainL; gainR = -gainR; } + cross_ptr = (s32_t *)(output.fade_end + cur_f * BYTES_PER_FRAME); + } else { + LOG_INFO("unable to continue crossfade - too few samples"); + output.fade = FADE_INACTIVE; + } + } + } + } + } + + out_frames = !silence ? min(size, cont_frames) : size; + + wrote = output.write_cb(out_frames, silence, gainL, gainR, cross_gain_in, cross_gain_out, &cross_ptr); + + if (wrote <= 0) { + frames -= size; + break; + } else { + out_frames = (frames_t)wrote; + } + + size -= out_frames; + + _vis_export(outputbuf, &output, out_frames, silence); + + if (!silence) { + _buf_inc_readp(outputbuf, out_frames * BYTES_PER_FRAME); + output.frames_played += out_frames; + } + } + + LOG_SDEBUG("wrote %u frames", frames); + + return frames; +} + +void _checkfade(bool start) { + frames_t bytes; + + LOG_INFO("fade mode: %u duration: %u %s", output.fade_mode, output.fade_secs, start ? "track-start" : "track-end"); + + bytes = output.next_sample_rate * BYTES_PER_FRAME * output.fade_secs; + if (output.fade_mode == FADE_INOUT) { + /* align on a frame boundary */ + bytes = ((bytes / 2) / BYTES_PER_FRAME) * BYTES_PER_FRAME; + } + + if (start && (output.fade_mode == FADE_IN || (output.fade_mode == FADE_INOUT && _buf_used(outputbuf) == 0))) { + bytes = min(bytes, outputbuf->size - BYTES_PER_FRAME); // shorter than full buffer otherwise start and end align + LOG_INFO("fade IN: %u frames", bytes / BYTES_PER_FRAME); + output.fade = FADE_DUE; + output.fade_dir = FADE_UP; + output.fade_start = outputbuf->writep; + output.fade_end = output.fade_start + bytes; + if (output.fade_end >= outputbuf->wrap) { + output.fade_end -= outputbuf->size; + } + } + + if (!start && (output.fade_mode == FADE_OUT || output.fade_mode == FADE_INOUT)) { + bytes = min(_buf_used(outputbuf), bytes); + LOG_INFO("fade %s: %u frames", output.fade_mode == FADE_INOUT ? "IN-OUT" : "OUT", bytes / BYTES_PER_FRAME); + output.fade = FADE_DUE; + output.fade_dir = FADE_DOWN; + output.fade_start = outputbuf->writep - bytes; + if (output.fade_start < outputbuf->buf) { + output.fade_start += outputbuf->size; + } + output.fade_end = outputbuf->writep; + } + + if (start && output.fade_mode == FADE_CROSSFADE) { + if (_buf_used(outputbuf) != 0) { + if (output.next_sample_rate != output.current_sample_rate) { + LOG_INFO("crossfade disabled as sample rates differ"); + return; + } + bytes = min(bytes, _buf_used(outputbuf)); // max of current remaining samples from previous track + bytes = min(bytes, (frames_t)(outputbuf->size * 0.9)); // max of 90% of outputbuf as we consume additional buffer during crossfade + LOG_INFO("CROSSFADE: %u frames", bytes / BYTES_PER_FRAME); + output.fade = FADE_DUE; + output.fade_dir = FADE_CROSS; + output.fade_start = outputbuf->writep - bytes; + if (output.fade_start < outputbuf->buf) { + output.fade_start += outputbuf->size; + } + output.fade_end = outputbuf->writep; + output.track_start = output.fade_start; + } else if (outputbuf->size == OUTPUTBUF_SIZE && outputbuf->readp == outputbuf->buf) { + // if default setting used and nothing in buffer attempt to resize to provide full crossfade support + LOG_INFO("resize outputbuf for crossfade"); + _buf_resize(outputbuf, OUTPUTBUF_SIZE_CROSSFADE); +#if LINUX || FREEBSD + touch_memory(outputbuf->buf, outputbuf->size); +#endif + } + } +} + +void output_init_common(log_level level, const char *device, unsigned output_buf_size, unsigned rates[], unsigned idle) { + unsigned i; + + loglevel = level; + + output_buf_size = output_buf_size - (output_buf_size % BYTES_PER_FRAME); + LOG_DEBUG("outputbuf size: %u", output_buf_size); + + buf_init(outputbuf, output_buf_size); + if (!outputbuf->buf) { + LOG_ERROR("unable to malloc output buffer"); + exit(0); + } + + silencebuf = malloc(MAX_SILENCE_FRAMES * BYTES_PER_FRAME); + if (!silencebuf) { + LOG_ERROR("unable to malloc silence buffer"); + exit(0); + } + memset(silencebuf, 0, MAX_SILENCE_FRAMES * BYTES_PER_FRAME); + + IF_DSD( + silencebuf_dsd = malloc(MAX_SILENCE_FRAMES * BYTES_PER_FRAME); + if (!silencebuf_dsd) { + LOG_ERROR("unable to malloc silence dsd buffer"); + exit(0); + } + dsd_silence_frames((u32_t *)silencebuf_dsd, MAX_SILENCE_FRAMES); + ) + + LOG_DEBUG("idle timeout: %u", idle); + + output.state = idle ? OUTPUT_OFF: OUTPUT_STOPPED; + output.device = device; + output.fade = FADE_INACTIVE; + output.invert = false; + output.error_opening = false; + output.idle_to = (u32_t) idle; + + /* Skip test_open for stdout, set default sample rates */ + if ( output.device[0] == '-' ) { + for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) { + output.supported_rates[i] = rates[i]; + } + } +#ifndef DACAUDIO + else { + if (!test_open(output.device, output.supported_rates, user_rates)) { + LOG_ERROR("unable to open output device: %s", output.device); + exit(0); + } + } +#endif + + if (user_rates) { + for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) { + output.supported_rates[i] = rates[i]; + } + } + + // set initial sample rate, preferring 44100 + for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) { + if (output.supported_rates[i] == 44100) { + output.default_sample_rate = 44100; + break; + } + } + if (!output.default_sample_rate) { + output.default_sample_rate = output.supported_rates[0]; + } + + output.current_sample_rate = output.default_sample_rate; + + if (loglevel >= lINFO) { + char rates_buf[10 * MAX_SUPPORTED_SAMPLERATES] = ""; + for (i = 0; output.supported_rates[i]; ++i) { + char s[10]; + sprintf(s, "%d ", output.supported_rates[i]); + strcat(rates_buf, s); + } + LOG_INFO("supported rates: %s", rates_buf); + } +} + +void output_close_common(void) { + buf_destroy(outputbuf); + free(silencebuf); + IF_DSD( + free(silencebuf_dsd); + ) +} + +void output_flush(void) { + LOG_INFO("flush output buffer"); + buf_flush(outputbuf); + LOCK; + output.fade = FADE_INACTIVE; + if (output.state != OUTPUT_OFF) { + output.state = OUTPUT_STOPPED; + if (output.error_opening) { + output.current_sample_rate = output.default_sample_rate; + } + output.delay_active = false; + } + output.frames_played = 0; + UNLOCK; +} diff --git a/output_dac.c b/output_dac.c new file mode 100644 index 00000000..ac304a5a --- /dev/null +++ b/output_dac.c @@ -0,0 +1,154 @@ + +#include "squeezelite.h" + +#include + +static log_level loglevel; + +static bool running = true; + +extern struct outputstate output; +extern struct buffer *outputbuf; + +#define LOCK mutex_lock(outputbuf->mutex) +#define UNLOCK mutex_unlock(outputbuf->mutex) + +#define FRAME_BLOCK MAX_SILENCE_FRAMES + +extern u8_t *silencebuf; + +// buffer to hold output data so we can block on writing outside of output lock, allocated on init +static u8_t *buf; +static unsigned buffill; +static int bytes_per_frame; +static thread_type thread; + +static int _dac_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR, + s32_t cross_gain_in, s32_t cross_gain_out, s32_t **cross_ptr); +static void *output_thread(); + +void set_volume(unsigned left, unsigned right) {} + +void output_init_dac(log_level level, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle) { + loglevel = level; + + LOG_INFO("init output DAC"); + + buf = malloc(FRAME_BLOCK * BYTES_PER_FRAME); + if (!buf) { + LOG_ERROR("unable to malloc buf"); + return; + } + buffill = 0; + + memset(&output, 0, sizeof(output)); + + output.format = S32_LE; + output.start_frames = FRAME_BLOCK * 2; + output.write_cb = &_dac_write_frames; + output.rate_delay = rate_delay; + + if (params) { + if (!strcmp(params, "32")) output.format = S32_LE; + if (!strcmp(params, "24")) output.format = S24_3LE; + if (!strcmp(params, "16")) output.format = S16_LE; + } + + // ensure output rate is specified to avoid test open + if (!rates[0]) { + rates[0] = 44100; + } + + output_init_common(level, "-", output_buf_size, rates, 0); + +#if LINUX || OSX || FREEBSD || POSIX + pthread_attr_t attr; + pthread_attr_init(&attr); +#ifdef PTHREAD_STACK_MIN + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + OUTPUT_THREAD_STACK_SIZE); +#endif + pthread_create(&thread, &attr, output_thread, NULL); + pthread_attr_destroy(&attr); +#endif +#if WIN + thread = CreateThread(NULL, OUTPUT_THREAD_STACK_SIZE, (LPTHREAD_START_ROUTINE)&output_thread, NULL, 0, NULL); +#endif +} + +void output_close_dac(void) { + LOG_INFO("close output"); + + LOCK; + running = false; + UNLOCK; + + free(buf); + + output_close_common(); +} + +static int _dac_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR, + s32_t cross_gain_in, s32_t cross_gain_out, s32_t **cross_ptr) { + + u8_t *obuf; + + if (!silence) { + + if (output.fade == FADE_ACTIVE && output.fade_dir == FADE_CROSS && *cross_ptr) { + _apply_cross(outputbuf, out_frames, cross_gain_in, cross_gain_out, cross_ptr); + } + + obuf = outputbuf->readp; + + } else { + + obuf = silencebuf; + } + + _scale_and_pack_frames(buf + buffill * bytes_per_frame, (s32_t *)(void *)obuf, out_frames, gainL, gainR, output.format); + + buffill += out_frames; + + return (int)out_frames; +} + +static void *output_thread() { + + LOCK; + + switch (output.format) { + case S32_LE: + bytes_per_frame = 4 * 2; break; + case S24_3LE: + bytes_per_frame = 3 * 2; break; + case S16_LE: + bytes_per_frame = 2 * 2; break; + default: + bytes_per_frame = 4 * 2; break; + break; + } + + UNLOCK; + + while (running) { + + LOCK; + + output.device_frames = 0; + output.updated = gettime_ms(); + output.frames_played_dmp = output.frames_played; + + _output_frames(FRAME_BLOCK); + + UNLOCK; + + if (buffill) { + //fwrite(buf, bytes_per_frame, buffill, stdout); + buffill = 0; + } + + } + + return 0; +} + diff --git a/output_pack.c b/output_pack.c new file mode 100644 index 00000000..bbffc577 --- /dev/null +++ b/output_pack.c @@ -0,0 +1,365 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Scale and pack functions + +#include "squeezelite.h" + +#define MAX_SCALESAMPLE 0x7fffffffffffLL +#define MIN_SCALESAMPLE -MAX_SCALESAMPLE + +// inlining these on windows prevents them being linkable... +#if !WIN +inline +#endif +s32_t gain(s32_t gain, s32_t sample) { + s64_t res = (s64_t)gain * (s64_t)sample; + if (res > MAX_SCALESAMPLE) res = MAX_SCALESAMPLE; + if (res < MIN_SCALESAMPLE) res = MIN_SCALESAMPLE; + return (s32_t) (res >> 16); +} +#if !WIN +inline +#endif +s32_t to_gain(float f) { + return (s32_t)(f * 65536.0F); +} + +void _scale_and_pack_frames(void *outputptr, s32_t *inputptr, frames_t cnt, s32_t gainL, s32_t gainR, output_format format) { + switch(format) { +#if DSD + case U32_LE: + { +#if SL_LITTLE_ENDIAN + memcpy(outputptr, inputptr, cnt * BYTES_PER_FRAME); +#else + u32_t *optr = (u32_t *)(void *)outputptr; + while (cnt--) { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = + (lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 | + (lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24; + *(optr++) = + (rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 | + (rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24; + } +#endif + } + break; + case U32_BE: + { +#if SL_LITTLE_ENDIAN + u32_t *optr = (u32_t *)(void *)outputptr; + while (cnt--) { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = + (lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 | + (lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24; + *(optr++) = + (rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 | + (rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24; + } +#else + memcpy(outputptr, inputptr, cnt * BYTES_PER_FRAME); +#endif + } + break; + case U16_LE: + { + u32_t *optr = (u32_t *)(void *)outputptr; +#if SL_LITTLE_ENDIAN + while (cnt--) { + *(optr++) = (*(inputptr) >> 16 & 0x0000ffff) | (*(inputptr + 1) & 0xffff0000); + inputptr += 2; + } +#else + while (cnt--) { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = + (lsample & 0x00ff0000) << 8 | (lsample & 0xff000000) >> 8 | + (rsample & 0x00ff0000) >> 8 | (rsample & 0xff000000) >> 24; + } +#endif + } + break; + case U16_BE: + { + u32_t *optr = (u32_t *)(void *)outputptr; +#if SL_LITTLE_ENDIAN + while (cnt--) { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = + (lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 | + (rsample & 0xff000000) >> 8 | (rsample & 0x00ff0000) << 8; + } +#else + while (cnt--) { + *(optr++) = (*(inputptr) & 0xffff0000) | (*(inputptr + 1) >> 16 & 0x0000ffff); + inputptr += 2; + } +#endif + } + break; + case U8: + { + u16_t *optr = (u16_t *)(void *)outputptr; +#if SL_LITTLE_ENDIAN + while (cnt--) { + *(optr++) = (u16_t)((*(inputptr) >> 24 & 0x000000ff) | (*(inputptr + 1) >> 16 & 0x0000ff00)); + inputptr += 2; + } +#else + while (cnt--) { + *(optr++) = (u16_t)((*(inputptr) >> 16 & 0x0000ff00) | (*(inputptr + 1) >> 24 & 0x000000ff)); + inputptr += 2; + } +#endif + } + break; +#endif + case S16_LE: + { + u32_t *optr = (u32_t *)(void *)outputptr; +#if SL_LITTLE_ENDIAN + if (gainL == FIXED_ONE && gainR == FIXED_ONE) { + while (cnt--) { + *(optr++) = (*(inputptr) >> 16 & 0x0000ffff) | (*(inputptr + 1) & 0xffff0000); + inputptr += 2; + } + } else { + while (cnt--) { + *(optr++) = (gain(gainL, *(inputptr)) >> 16 & 0x0000ffff) | (gain(gainR, *(inputptr+1)) & 0xffff0000); + inputptr += 2; + } + } +#else + if (gainL == FIXED_ONE && gainR == FIXED_ONE) { + while (cnt--) { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = + (lsample & 0x00ff0000) << 8 | (lsample & 0xff000000) >> 8 | + (rsample & 0x00ff0000) >> 8 | (rsample & 0xff000000) >> 24; + } + } else { + while (cnt--) { + s32_t lsample = gain(gainL, *(inputptr++)); + s32_t rsample = gain(gainR, *(inputptr++)); + *(optr++) = + (lsample & 0x00ff0000) << 8 | (lsample & 0xff000000) >> 8 | + (rsample & 0x00ff0000) >> 8 | (rsample & 0xff000000) >> 24; + } + } +#endif + } + break; + case S24_LE: + { + u32_t *optr = (u32_t *)(void *)outputptr; +#if SL_LITTLE_ENDIAN + if (gainL == FIXED_ONE && gainR == FIXED_ONE) { + while (cnt--) { + *(optr++) = *(inputptr++) >> 8; + *(optr++) = *(inputptr++) >> 8; + } + } else { + while (cnt--) { + *(optr++) = gain(gainL, *(inputptr++)) >> 8; + *(optr++) = gain(gainR, *(inputptr++)) >> 8; + } + } +#else + if (gainL == FIXED_ONE && gainR == FIXED_ONE) { + while (cnt--) { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = + (lsample & 0xff000000) >> 16 | (lsample & 0x00ff0000) | (lsample & 0x0000ff00 << 16); + *(optr++) = + (rsample & 0xff000000) >> 16 | (rsample & 0x00ff0000) | (rsample & 0x0000ff00 << 16); + } + } else { + while (cnt--) { + s32_t lsample = gain(gainL, *(inputptr++)); + s32_t rsample = gain(gainR, *(inputptr++)); + *(optr++) = + (lsample & 0xff000000) >> 16 | (lsample & 0x00ff0000) | (lsample & 0x0000ff00 << 16); + *(optr++) = + (rsample & 0xff000000) >> 16 | (rsample & 0x00ff0000) | (rsample & 0x0000ff00 << 16); + } + } +#endif + } + break; + case S24_3LE: + { + u8_t *optr = (u8_t *)(void *)outputptr; + if (gainL == FIXED_ONE && gainR == FIXED_ONE) { + while (cnt) { + // attempt to do 32 bit memory accesses - move 2 frames at once: 16 bytes -> 12 bytes + // falls through to exception case when not aligned or if less than 2 frames to move + if (((uintptr_t)optr & 0x3) == 0 && cnt >= 2) { + u32_t *o_ptr = (u32_t *)(void *)optr; + while (cnt >= 2) { + s32_t l1 = *(inputptr++); s32_t r1 = *(inputptr++); + s32_t l2 = *(inputptr++); s32_t r2 = *(inputptr++); +#if SL_LITTLE_ENDIAN + *(o_ptr++) = (l1 & 0xffffff00) >> 8 | (r1 & 0x0000ff00) << 16; + *(o_ptr++) = (r1 & 0xffff0000) >> 16 | (l2 & 0x00ffff00) << 8; + *(o_ptr++) = (l2 & 0xff000000) >> 24 | (r2 & 0xffffff00); +#else + *(o_ptr++) = (l1 & 0x0000ff00) << 16 | (l1 & 0x00ff0000) | (l1 & 0xff000000) >> 16 | + (r1 & 0x0000ff00) >> 8; + *(o_ptr++) = (r1 & 0x00ff0000) << 8 | (r1 & 0xff000000) >> 8 | (l2 & 0x0000ff00) | + (l2 & 0x00ff0000) >> 16; + *(o_ptr++) = (l2 & 0xff000000) | (r2 & 0x0000ff00) << 8 | (r2 & 0x00ff0000) >> 8 | + (r2 & 0xff000000) >> 24; +#endif + optr += 12; + cnt -= 2; + } + } else { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = (lsample & 0x0000ff00) >> 8; + *(optr++) = (lsample & 0x00ff0000) >> 16; + *(optr++) = (lsample & 0xff000000) >> 24; + *(optr++) = (rsample & 0x0000ff00) >> 8; + *(optr++) = (rsample & 0x00ff0000) >> 16; + *(optr++) = (rsample & 0xff000000) >> 24; + cnt--; + } + } + } else { + while (cnt) { + // attempt to do 32 bit memory accesses - move 2 frames at once: 16 bytes -> 12 bytes + // falls through to exception case when not aligned or if less than 2 frames to move + if (((uintptr_t)optr & 0x3) == 0 && cnt >= 2) { + u32_t *o_ptr = (u32_t *)(void *)optr; + while (cnt >= 2) { + s32_t l1 = gain(gainL, *(inputptr++)); s32_t r1 = gain(gainR, *(inputptr++)); + s32_t l2 = gain(gainL, *(inputptr++)); s32_t r2 = gain(gainR, *(inputptr++)); +#if SL_LITTLE_ENDIAN + *(o_ptr++) = (l1 & 0xffffff00) >> 8 | (r1 & 0x0000ff00) << 16; + *(o_ptr++) = (r1 & 0xffff0000) >> 16 | (l2 & 0x00ffff00) << 8; + *(o_ptr++) = (l2 & 0xff000000) >> 24 | (r2 & 0xffffff00); +#else + *(o_ptr++) = (l1 & 0x0000ff00) << 16 | (l1 & 0x00ff0000) | (l1 & 0xff000000) >> 16 | + (r1 & 0x0000ff00) >> 8; + *(o_ptr++) = (r1 & 0x00ff0000) << 8 | (r1 & 0xff000000) >> 8 | (l2 & 0x0000ff00) | + (l2 & 0x00ff0000) >> 16; + *(o_ptr++) = (l2 & 0xff000000) | (r2 & 0x0000ff00) << 8 | (r2 & 0x00ff0000) >> 8 | + (r2 & 0xff000000) >> 24; +#endif + optr += 12; + cnt -= 2; + } + } else { + s32_t lsample = gain(gainL, *(inputptr++)); + s32_t rsample = gain(gainR, *(inputptr++)); + *(optr++) = (lsample & 0x0000ff00) >> 8; + *(optr++) = (lsample & 0x00ff0000) >> 16; + *(optr++) = (lsample & 0xff000000) >> 24; + *(optr++) = (rsample & 0x0000ff00) >> 8; + *(optr++) = (rsample & 0x00ff0000) >> 16; + *(optr++) = (rsample & 0xff000000) >> 24; + cnt--; + } + } + } + } + break; + case S32_LE: + { + u32_t *optr = (u32_t *)(void *)outputptr; +#if SL_LITTLE_ENDIAN + if (gainL == FIXED_ONE && gainR == FIXED_ONE) { + memcpy(outputptr, inputptr, cnt * BYTES_PER_FRAME); + } else { + while (cnt--) { + *(optr++) = gain(gainL, *(inputptr++)); + *(optr++) = gain(gainR, *(inputptr++)); + } + } +#else + if (gainL == FIXED_ONE && gainR == FIXED_ONE) { + while (cnt--) { + s32_t lsample = *(inputptr++); + s32_t rsample = *(inputptr++); + *(optr++) = + (lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 | + (lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24; + *(optr++) = + (rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 | + (rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24; + } + } else { + while (cnt--) { + s32_t lsample = gain(gainL, *(inputptr++)); + s32_t rsample = gain(gainR, *(inputptr++)); + *(optr++) = + (lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 | + (lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24; + *(optr++) = + (rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 | + (rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24; + } + } +#endif + } + break; + default: + break; + } +} + +#if !WIN +inline +#endif +void _apply_cross(struct buffer *outputbuf, frames_t out_frames, s32_t cross_gain_in, s32_t cross_gain_out, s32_t **cross_ptr) { + s32_t *ptr = (s32_t *)(void *)outputbuf->readp; + frames_t count = out_frames * 2; + while (count--) { + if (*cross_ptr > (s32_t *)outputbuf->wrap) { + *cross_ptr -= outputbuf->size / BYTES_PER_FRAME * 2; + } + *ptr = gain(cross_gain_out, *ptr) + gain(cross_gain_in, **cross_ptr); + ptr++; (*cross_ptr)++; + } +} + +#if !WIN +inline +#endif +void _apply_gain(struct buffer *outputbuf, frames_t count, s32_t gainL, s32_t gainR) { + s32_t *ptrL = (s32_t *)(void *)outputbuf->readp; + s32_t *ptrR = (s32_t *)(void *)outputbuf->readp + 1; + while (count--) { + *ptrL = gain(gainL, *ptrL); + *ptrR = gain(gainR, *ptrR); + ptrL += 2; + ptrR += 2; + } +} diff --git a/pcm.c b/pcm.c new file mode 100644 index 00000000..9d5fd26f --- /dev/null +++ b/pcm.c @@ -0,0 +1,438 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "squeezelite.h" + +extern log_level loglevel; + +extern struct buffer *streambuf; +extern struct buffer *outputbuf; +extern struct streamstate stream; +extern struct outputstate output; +extern struct decodestate decode; +extern struct processstate process; + +bool pcm_check_header = false; + +#define LOCK_S mutex_lock(streambuf->mutex) +#define UNLOCK_S mutex_unlock(streambuf->mutex) +#define LOCK_O mutex_lock(outputbuf->mutex) +#define UNLOCK_O mutex_unlock(outputbuf->mutex) +#if PROCESS +#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex) +#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex) +#define LOCK_O_not_direct if (!decode.direct) mutex_lock(outputbuf->mutex) +#define UNLOCK_O_not_direct if (!decode.direct) mutex_unlock(outputbuf->mutex) +#define IF_DIRECT(x) if (decode.direct) { x } +#define IF_PROCESS(x) if (!decode.direct) { x } +#else +#define LOCK_O_direct mutex_lock(outputbuf->mutex) +#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex) +#define LOCK_O_not_direct +#define UNLOCK_O_not_direct +#define IF_DIRECT(x) { x } +#define IF_PROCESS(x) +#endif + +#define MAX_DECODE_FRAMES 4096 + +static u32_t sample_rates[] = { + 11025, 22050, 32000, 44100, 48000, 8000, 12000, 16000, 24000, 96000, 88200, 176400, 192000, 352800, 384000, 705600, 768000 +}; + +static u32_t sample_rate; +static u32_t sample_size; +static u32_t channels; +static bool bigendian; +static bool limit; +static u32_t audio_left; +static u32_t bytes_per_frame; + +typedef enum { UNKNOWN = 0, WAVE, AIFF } header_format; + +static void _check_header(void) { + u8_t *ptr = streambuf->readp; + unsigned bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + header_format format = UNKNOWN; + + // simple parsing of wav and aiff headers and get to samples + + if (bytes > 12) { + if (!memcmp(ptr, "RIFF", 4) && !memcmp(ptr+8, "WAVE", 4)) { + LOG_INFO("WAVE"); + format = WAVE; + } else if (!memcmp(ptr, "FORM", 4) && (!memcmp(ptr+8, "AIFF", 4) || !memcmp(ptr+8, "AIFC", 4))) { + LOG_INFO("AIFF"); + format = AIFF; + } + } + + if (format != UNKNOWN) { + ptr += 12; + bytes -= 12; + + while (bytes >= 8) { + char id[5]; + unsigned len; + memcpy(id, ptr, 4); + id[4] = '\0'; + + if (format == WAVE) { + len = *(ptr+4) | *(ptr+5) << 8 | *(ptr+6) << 16| *(ptr+7) << 24; + } else { + len = *(ptr+4) << 24 | *(ptr+5) << 16 | *(ptr+6) << 8 | *(ptr+7); + } + + LOG_INFO("header: %s len: %d", id, len); + + if (format == WAVE && !memcmp(ptr, "data", 4)) { + ptr += 8; + _buf_inc_readp(streambuf, ptr - streambuf->readp); + audio_left = len; + + if ((audio_left == 0xFFFFFFFF) || (audio_left == 0x7FFFEFFC)) { + LOG_INFO("wav audio size unknown: %u", audio_left); + limit = false; + } else { + LOG_INFO("wav audio size: %u", audio_left); + limit = true; + } + return; + } + + if (format == AIFF && !memcmp(ptr, "SSND", 4) && bytes >= 16) { + unsigned offset = *(ptr+8) << 24 | *(ptr+9) << 16 | *(ptr+10) << 8 | *(ptr+11); + // following 4 bytes is blocksize - ignored + ptr += 8 + 8; + _buf_inc_readp(streambuf, ptr + offset - streambuf->readp); + + // Reading from an upsampled stream, length could be wrong. + // Only use length in header for files. + if (stream.state == STREAMING_FILE) { + audio_left = len - 8 - offset; + LOG_INFO("aif audio size: %u", audio_left); + limit = true; + } + return; + } + + if (format == WAVE && !memcmp(ptr, "fmt ", 4) && bytes >= 24) { + // override the server parsed values with our own + channels = *(ptr+10) | *(ptr+11) << 8; + sample_rate = *(ptr+12) | *(ptr+13) << 8 | *(ptr+14) << 16 | *(ptr+15) << 24; + sample_size = (*(ptr+22) | *(ptr+23) << 8) / 8; + bigendian = 0; + LOG_INFO("pcm size: %u rate: %u chan: %u bigendian: %u", sample_size, sample_rate, channels, bigendian); + } + + if (format == AIFF && !memcmp(ptr, "COMM", 4) && bytes >= 26) { + int exponent; + // override the server parsed values with our own + channels = *(ptr+8) << 8 | *(ptr+9); + sample_size = (*(ptr+14) << 8 | *(ptr+15)) / 8; + bigendian = 1; + // sample rate is encoded as IEEE 80 bit extended format + // make some assumptions to simplify processing - only use first 32 bits of mantissa + exponent = ((*(ptr+16) & 0x7f) << 8 | *(ptr+17)) - 16383 - 31; + sample_rate = *(ptr+18) << 24 | *(ptr+19) << 16 | *(ptr+20) << 8 | *(ptr+21); + while (exponent < 0) { sample_rate >>= 1; ++exponent; } + while (exponent > 0) { sample_rate <<= 1; --exponent; } + LOG_INFO("pcm size: %u rate: %u chan: %u bigendian: %u", sample_size, sample_rate, channels, bigendian); + } + + if (bytes >= len + 8) { + ptr += len + 8; + bytes -= (len + 8); + } else { + LOG_WARN("run out of data"); + return; + } + } + + } else { + LOG_WARN("unknown format - can't parse header"); + } +} + +static decode_state pcm_decode(void) { + unsigned bytes, in, out; + frames_t frames, count; + u32_t *optr; + u8_t *iptr; + u8_t tmp[16]; + + LOCK_S; + + if ( decode.new_stream && ( ( stream.state == STREAMING_FILE ) || pcm_check_header ) ) { + _check_header(); + } + + LOCK_O_direct; + + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + + IF_DIRECT( + out = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME; + ); + IF_PROCESS( + out = process.max_in_frames; + ); + + if ((stream.state <= DISCONNECT && bytes == 0) || (limit && audio_left == 0)) { + UNLOCK_O_direct; + UNLOCK_S; + return DECODE_COMPLETE; + } + + if (decode.new_stream) { + LOG_INFO("setting track_start"); + LOCK_O_not_direct; + output.track_start = outputbuf->writep; + decode.new_stream = false; +#if DSD + if (sample_size == 3 && + is_stream_dop(((u8_t *)streambuf->readp) + (bigendian?0:2), + ((u8_t *)streambuf->readp) + (bigendian?0:2) + sample_size, + sample_size * channels, bytes / (sample_size * channels))) { + LOG_INFO("file contains DOP"); + if (output.dsdfmt == DOP_S24_LE || output.dsdfmt == DOP_S24_3LE) + output.next_fmt = output.dsdfmt; + else + output.next_fmt = DOP; + output.next_sample_rate = sample_rate; + output.fade = FADE_INACTIVE; + } else { + output.next_sample_rate = decode_newstream(sample_rate, output.supported_rates); + output.next_fmt = PCM; + if (output.fade_mode) _checkfade(true); + } +#else + output.next_sample_rate = decode_newstream(sample_rate, output.supported_rates); + if (output.fade_mode) _checkfade(true); +#endif + UNLOCK_O_not_direct; + IF_PROCESS( + out = process.max_in_frames; + ); + bytes_per_frame = channels * sample_size; + } + + IF_DIRECT( + optr = (u32_t *)outputbuf->writep; + ); + IF_PROCESS( + optr = (u32_t *)process.inbuf; + ); + iptr = (u8_t *)streambuf->readp; + + in = bytes / bytes_per_frame; + + // handle frame wrapping round end of streambuf + // - only need if resizing of streambuf does not avoid this, could occur in localfile case + if (in == 0 && bytes > 0 && _buf_used(streambuf) >= bytes_per_frame) { + memcpy(tmp, iptr, bytes); + memcpy(tmp + bytes, streambuf->buf, bytes_per_frame - bytes); + iptr = tmp; + in = 1; + } + + frames = min(in, out); + frames = min(frames, MAX_DECODE_FRAMES); + + if (limit && frames * bytes_per_frame > audio_left) { + LOG_INFO("reached end of audio"); + frames = audio_left / bytes_per_frame; + } + + count = frames * channels; + + if (channels == 2) { + if (sample_size == 1) { + while (count--) { + *optr++ = *iptr++ << 24; + } + } else if (sample_size == 2) { + if (bigendian) { + while (count--) { + *optr++ = *(iptr) << 24 | *(iptr+1) << 16; + iptr += 2; + } + } else { + while (count--) { + *optr++ = *(iptr) << 16 | *(iptr+1) << 24; + iptr += 2; + } + } + } else if (sample_size == 3) { + if (bigendian) { + while (count--) { + *optr++ = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8; + iptr += 3; + } + } else { + while (count--) { + *optr++ = *(iptr) << 8 | *(iptr+1) << 16 | *(iptr+2) << 24; + iptr += 3; + } + } + } else if (sample_size == 4) { + if (bigendian) { + while (count--) { + *optr++ = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8 | *(iptr+3); + iptr += 4; + } + } else { + while (count--) { + *optr++ = *(iptr) | *(iptr+1) << 8 | *(iptr+2) << 16 | *(iptr+3) << 24; + iptr += 4; + } + } + } + } else if (channels == 1) { + if (sample_size == 1) { + while (count--) { + *optr = *iptr++ << 24; + *(optr+1) = *optr; + optr += 2; + } + } else if (sample_size == 2) { + if (bigendian) { + while (count--) { + *optr = *(iptr) << 24 | *(iptr+1) << 16; + *(optr+1) = *optr; + iptr += 2; + optr += 2; + } + } else { + while (count--) { + *optr = *(iptr) << 16 | *(iptr+1) << 24; + *(optr+1) = *optr; + iptr += 2; + optr += 2; + } + } + } else if (sample_size == 3) { + if (bigendian) { + while (count--) { + *optr = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8; + *(optr+1) = *optr; + iptr += 3; + optr += 2; + } + } else { + while (count--) { + *optr = *(iptr) << 8 | *(iptr+1) << 16 | *(iptr+2) << 24; + *(optr+1) = *optr; + iptr += 3; + optr += 2; + } + } + } else if (sample_size == 4) { + if (bigendian) { + while (count--) { + *optr++ = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8 | *(iptr+3); + *(optr+1) = *optr; + iptr += 4; + optr += 2; + } + } else { + while (count--) { + *optr++ = *(iptr) | *(iptr+1) << 8 | *(iptr+2) << 16 | *(iptr+3) << 24; + *(optr+1) = *optr; + iptr += 4; + optr += 2; + } + } + } + } else { + LOG_ERROR("unsupported channels"); + } + + LOG_SDEBUG("decoded %u frames", frames); + + _buf_inc_readp(streambuf, frames * bytes_per_frame); + + if (limit) { + audio_left -= frames * bytes_per_frame; + } + + IF_DIRECT( + _buf_inc_writep(outputbuf, frames * BYTES_PER_FRAME); + ); + IF_PROCESS( + process.in_frames = frames; + ); + + UNLOCK_O_direct; + UNLOCK_S; + + return DECODE_RUNNING; +} + +static void pcm_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + sample_size = size - '0' + 1; + sample_rate = sample_rates[rate - '0']; + channels = chan - '0'; + bigendian = (endianness == '0'); + limit = false; + + LOG_INFO("pcm size: %u rate: %u chan: %u bigendian: %u", sample_size, sample_rate, channels, bigendian); + buf_adjust(streambuf, sample_size * channels); +} + +static void pcm_close(void) { + buf_adjust(streambuf, 1); +} + +struct codec *register_pcm(void) { + if ( pcm_check_header ) + { + static struct codec ret = { + 'p', // id + "wav,aif,pcm", // types + 4096, // min read + 102400, // min space + pcm_open, // open + pcm_close, // close + pcm_decode, // decode + }; + + LOG_INFO("using pcm to decode wav,aif,pcm"); + return &ret; + } + else + { + static struct codec ret = { + 'p', // id + "aif,pcm", // types + 4096, // min read + 102400, // min space + pcm_open, // open + pcm_close, // close + pcm_decode, // decode + }; + + LOG_INFO("using pcm to decode aif,pcm"); + return &ret; + } + + return NULL; +} diff --git a/scan.c b/scan.c new file mode 100644 index 00000000..f9a75341 --- /dev/null +++ b/scan.c @@ -0,0 +1,148 @@ +/* Scan Example + + 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. +*/ + +/* + This example shows how to use the All Channel Scan or Fast Scan to connect + to a Wi-Fi network. + + In the Fast Scan mode, the scan will stop as soon as the first network matching + the SSID is found. In this mode, an application can set threshold for the + authentication mode and the Signal strength. Networks that do not meet the + threshold requirements will be ignored. + + In the All Channel Scan mode, the scan will end only after all the channels + are scanned, and connection will start with the best network. The networks + can be sorted based on Authentication Mode or Signal Strength. The priority + for the Authentication mode is: WPA2 > WPA > WEP > Open +*/ +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "sys/socket.h" +#include "string.h" + +/*Set the SSID and Password via "make menuconfig"*/ +#define DEFAULT_SSID CONFIG_WIFI_SSID +#define DEFAULT_PWD CONFIG_WIFI_PASSWORD + +#if CONFIG_WIFI_ALL_CHANNEL_SCAN +#define DEFAULT_SCAN_METHOD WIFI_ALL_CHANNEL_SCAN +#elif CONFIG_WIFI_FAST_SCAN +#define DEFAULT_SCAN_METHOD WIFI_FAST_SCAN +#else +#define DEFAULT_SCAN_METHOD WIFI_FAST_SCAN +#endif /*CONFIG_SCAN_METHOD*/ + +#if CONFIG_WIFI_CONNECT_AP_BY_SIGNAL +#define DEFAULT_SORT_METHOD WIFI_CONNECT_AP_BY_SIGNAL +#elif CONFIG_WIFI_CONNECT_AP_BY_SECURITY +#define DEFAULT_SORT_METHOD WIFI_CONNECT_AP_BY_SECURITY +#else +#define DEFAULT_SORT_METHOD WIFI_CONNECT_AP_BY_SIGNAL +#endif /*CONFIG_SORT_METHOD*/ + +#if CONFIG_FAST_SCAN_THRESHOLD +#define DEFAULT_RSSI CONFIG_FAST_SCAN_MINIMUM_SIGNAL +#if CONFIG_EXAMPLE_OPEN +#define DEFAULT_AUTHMODE WIFI_AUTH_OPEN +#elif CONFIG_EXAMPLE_WEP +#define DEFAULT_AUTHMODE WIFI_AUTH_WEP +#elif CONFIG_EXAMPLE_WPA +#define DEFAULT_AUTHMODE WIFI_AUTH_WPA_PSK +#elif CONFIG_EXAMPLE_WPA2 +#define DEFAULT_AUTHMODE WIFI_AUTH_WPA2_PSK +#else +#define DEFAULT_AUTHMODE WIFI_AUTH_OPEN +#endif +#else +#define DEFAULT_RSSI -127 +#define DEFAULT_AUTHMODE WIFI_AUTH_OPEN +#endif /*CONFIG_FAST_SCAN_THRESHOLD*/ + +static const char *TAG = "scan"; + +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + esp_wifi_connect(); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "got ip: %s", ip4addr_ntoa(&event->ip_info.ip)); + } +} + + +/* Initialize Wi-Fi as sta and set scan method */ +static void wifi_scan(void) +{ + tcpip_adapter_init(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL)); + + wifi_config_t wifi_config = { + .sta = { + .ssid = DEFAULT_SSID, + .password = DEFAULT_PWD, + .scan_method = DEFAULT_SCAN_METHOD, + .sort_method = DEFAULT_SORT_METHOD, + .threshold.rssi = DEFAULT_RSSI, + .threshold.authmode = DEFAULT_AUTHMODE, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); +} + +int main(int argc, char**argv); + +void app_main() +{ + int i; + char **argv, *_argv[] = { + "squeezelite-esp32", + "-n", + "ESP32", + "-d", + "all=info", + "-d", + "slimproto=debug", + "-b", + "128:2000", + }; + + // can't do strtok on FLASH strings + argv = malloc(sizeof(_argv)); + for (i = 0; i < sizeof(_argv)/sizeof(char*); i++) { + argv[i] = strdup(_argv[i]); + } + + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK( ret ); + + wifi_scan(); + + main(sizeof(_argv)/sizeof(char*), argv); +} diff --git a/slimproto.c b/slimproto.c new file mode 100644 index 00000000..4d26796b --- /dev/null +++ b/slimproto.c @@ -0,0 +1,976 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Additions (c) Paul Hermann, 2015-2017 under the same license terms + * -Control of Raspberry pi GPIO for amplifier power + * -Launch script on power status change from LMS + */ + +#include "squeezelite.h" +#include "slimproto.h" + +static log_level loglevel; + +#define SQUEEZENETWORK "mysqueezebox.com:3483" + +#define PORT 3483 + +#define MAXBUF 4096 + +#if SL_LITTLE_ENDIAN +#define LOCAL_PLAYER_IP 0x0100007f // 127.0.0.1 +#define LOCAL_PLAYER_PORT 0x9b0d // 3483 +#else +#define LOCAL_PLAYER_IP 0x7f000001 // 127.0.0.1 +#define LOCAL_PLAYER_PORT 0x0d9b // 3483 +#endif + +static sockfd sock = -1; +static in_addr_t slimproto_ip = 0; + +extern struct buffer *streambuf; +extern struct buffer *outputbuf; + +extern struct streamstate stream; +extern struct outputstate output; +extern struct decodestate decode; + +extern struct codec *codecs[]; +#if IR +extern struct irstate ir; +#endif + +event_event wake_e; + +#define LOCK_S mutex_lock(streambuf->mutex) +#define UNLOCK_S mutex_unlock(streambuf->mutex) +#define LOCK_O mutex_lock(outputbuf->mutex) +#define UNLOCK_O mutex_unlock(outputbuf->mutex) +#define LOCK_D mutex_lock(decode.mutex) +#define UNLOCK_D mutex_unlock(decode.mutex) +#if IR +#define LOCK_I mutex_lock(ir.mutex) +#define UNLOCK_I mutex_unlock(ir.mutex) +#endif + +static struct { + u32_t updated; + u32_t stream_start; + u32_t stream_full; + u32_t stream_size; + u64_t stream_bytes; + u32_t output_full; + u32_t output_size; + u32_t frames_played; + u32_t device_frames; + u32_t current_sample_rate; + u32_t last; + stream_state stream_state; +} status; + +int autostart; +bool sentSTMu, sentSTMo, sentSTMl; +u32_t new_server; +char *new_server_cap; +#define PLAYER_NAME_LEN 64 +char player_name[PLAYER_NAME_LEN + 1] = ""; +const char *name_file = NULL; + +void send_packet(u8_t *packet, size_t len) { + u8_t *ptr = packet; + unsigned try = 0; + ssize_t n; + + while (len) { + n = send(sock, ptr, len, MSG_NOSIGNAL); + if (n <= 0) { + if (n < 0 && last_error() == ERROR_WOULDBLOCK && try < 10) { + LOG_DEBUG("retrying (%d) writing to socket", ++try); + usleep(1000); + continue; + } + LOG_INFO("failed writing to socket: %s", strerror(last_error())); + return; + } + ptr += n; + len -= n; + } +} + +static void sendHELO(bool reconnect, const char *fixed_cap, const char *var_cap, u8_t mac[6]) { + #define BASE_CAP "Model=squeezelite,AccuratePlayPoints=1,HasDigitalOut=1,HasPolarityInversion=1,Firmware=" VERSION + #define SSL_CAP "CanHTTPS=1" + const char *base_cap; + struct HELO_packet pkt; + +#if USE_SSL +#if !LINKALL + if (ssl_loaded) base_cap = SSL_CAP "," BASE_CAP; + else base_cap = BASE_CAP; +#endif + base_cap = SSL_CAP "," BASE_CAP; +#else + base_cap = BASE_CAP; +#endif + + memset(&pkt, 0, sizeof(pkt)); + memcpy(&pkt.opcode, "HELO", 4); + pkt.length = htonl(sizeof(struct HELO_packet) - 8 + strlen(base_cap) + strlen(fixed_cap) + strlen(var_cap)); + pkt.deviceid = 12; // squeezeplay + pkt.revision = 0; + packn(&pkt.wlan_channellist, reconnect ? 0x4000 : 0x0000); + packN(&pkt.bytes_received_H, (u64_t)status.stream_bytes >> 32); + packN(&pkt.bytes_received_L, (u64_t)status.stream_bytes & 0xffffffff); + memcpy(pkt.mac, mac, 6); + + LOG_INFO("mac: %02x:%02x:%02x:%02x:%02x:%02x", pkt.mac[0], pkt.mac[1], pkt.mac[2], pkt.mac[3], pkt.mac[4], pkt.mac[5]); + + LOG_INFO("cap: %s%s%s", base_cap, fixed_cap, var_cap); + + send_packet((u8_t *)&pkt, sizeof(pkt)); + send_packet((u8_t *)base_cap, strlen(base_cap)); + send_packet((u8_t *)fixed_cap, strlen(fixed_cap)); + send_packet((u8_t *)var_cap, strlen(var_cap)); +} + +static void sendSTAT(const char *event, u32_t server_timestamp) { + struct STAT_packet pkt; + u32_t now = gettime_ms(); + u32_t ms_played; + + if (status.current_sample_rate && status.frames_played && status.frames_played > status.device_frames) { + ms_played = (u32_t)(((u64_t)(status.frames_played - status.device_frames) * (u64_t)1000) / (u64_t)status.current_sample_rate); +#ifndef STATUSHACK + if (now > status.updated) ms_played += (now - status.updated); +#endif + LOG_SDEBUG("ms_played: %u (frames_played: %u device_frames: %u)", ms_played, status.frames_played, status.device_frames); + } else if (status.frames_played && now > status.stream_start) { + ms_played = now - status.stream_start; + LOG_SDEBUG("ms_played: %u using elapsed time (frames_played: %u device_frames: %u)", ms_played, status.frames_played, status.device_frames); + } else { + LOG_SDEBUG("ms_played: 0"); + ms_played = 0; + } + + memset(&pkt, 0, sizeof(struct STAT_packet)); + memcpy(&pkt.opcode, "STAT", 4); + pkt.length = htonl(sizeof(struct STAT_packet) - 8); + memcpy(&pkt.event, event, 4); + // num_crlf + // mas_initialized; mas_mode; + packN(&pkt.stream_buffer_fullness, status.stream_full); + packN(&pkt.stream_buffer_size, status.stream_size); + packN(&pkt.bytes_received_H, (u64_t)status.stream_bytes >> 32); + packN(&pkt.bytes_received_L, (u64_t)status.stream_bytes & 0xffffffff); + pkt.signal_strength = 0xffff; + packN(&pkt.jiffies, now); + packN(&pkt.output_buffer_size, status.output_size); + packN(&pkt.output_buffer_fullness, status.output_full); + packN(&pkt.elapsed_seconds, ms_played / 1000); + // voltage; + packN(&pkt.elapsed_milliseconds, ms_played); + pkt.server_timestamp = server_timestamp; // keep this is server format - don't unpack/pack + // error_code; + + LOG_DEBUG("STAT: %s", event); + + if (loglevel == lSDEBUG) { + LOG_SDEBUG("received bytesL: %u streambuf: %u outputbuf: %u calc elapsed: %u real elapsed: %u (diff: %d) device: %u delay: %d", + (u32_t)status.stream_bytes, status.stream_full, status.output_full, ms_played, now - status.stream_start, + ms_played - now + status.stream_start, status.device_frames * 1000 / status.current_sample_rate, now - status.updated); + } + + send_packet((u8_t *)&pkt, sizeof(pkt)); +} + +static void sendDSCO(disconnect_code disconnect) { + struct DSCO_packet pkt; + + memset(&pkt, 0, sizeof(pkt)); + memcpy(&pkt.opcode, "DSCO", 4); + pkt.length = htonl(sizeof(pkt) - 8); + pkt.reason = disconnect & 0xFF; + + LOG_DEBUG("DSCO: %d", disconnect); + + send_packet((u8_t *)&pkt, sizeof(pkt)); +} + +static void sendRESP(const char *header, size_t len) { + struct RESP_header pkt_header; + + memset(&pkt_header, 0, sizeof(pkt_header)); + memcpy(&pkt_header.opcode, "RESP", 4); + pkt_header.length = htonl(sizeof(pkt_header) + len - 8); + + LOG_DEBUG("RESP"); + + send_packet((u8_t *)&pkt_header, sizeof(pkt_header)); + send_packet((u8_t *)header, len); +} + +static void sendMETA(const char *meta, size_t len) { + struct META_header pkt_header; + + memset(&pkt_header, 0, sizeof(pkt_header)); + memcpy(&pkt_header.opcode, "META", 4); + pkt_header.length = htonl(sizeof(pkt_header) + len - 8); + + LOG_DEBUG("META"); + + send_packet((u8_t *)&pkt_header, sizeof(pkt_header)); + send_packet((u8_t *)meta, len); +} + +static void sendSETDName(const char *name) { + struct SETD_header pkt_header; + + memset(&pkt_header, 0, sizeof(pkt_header)); + memcpy(&pkt_header.opcode, "SETD", 4); + + pkt_header.id = 0; // id 0 is playername S:P:Squeezebox2 + pkt_header.length = htonl(sizeof(pkt_header) + strlen(name) + 1 - 8); + + LOG_DEBUG("set playername: %s", name); + + send_packet((u8_t *)&pkt_header, sizeof(pkt_header)); + send_packet((u8_t *)name, strlen(name) + 1); +} + +#if IR +void sendIR(u32_t code, u32_t ts) { + struct IR_packet pkt; + + memset(&pkt, 0, sizeof(pkt)); + memcpy(&pkt.opcode, "IR ", 4); + pkt.length = htonl(sizeof(pkt) - 8); + + packN(&pkt.jiffies, ts); + pkt.ir_code = htonl(code); + + LOG_DEBUG("IR: ir code: 0x%x ts: %u", code, ts); + + send_packet((u8_t *)&pkt, sizeof(pkt)); +} +#endif + +static void process_strm(u8_t *pkt, int len) { + struct strm_packet *strm = (struct strm_packet *)pkt; + + LOG_DEBUG("strm command %c", strm->command); + + switch(strm->command) { + case 't': + sendSTAT("STMt", strm->replay_gain); // STMt replay_gain is no longer used to track latency, but support it + break; + case 'q': + decode_flush(); + output_flush(); + status.frames_played = 0; + stream_disconnect(); + sendSTAT("STMf", 0); + buf_flush(streambuf); + break; + case 'f': + decode_flush(); + output_flush(); + status.frames_played = 0; + if (stream_disconnect()) { + sendSTAT("STMf", 0); + } + buf_flush(streambuf); + break; + case 'p': + { + unsigned interval = unpackN(&strm->replay_gain); + LOCK_O; + output.pause_frames = interval * status.current_sample_rate / 1000; + if (interval) { + output.state = OUTPUT_PAUSE_FRAMES; + } else { + output.state = OUTPUT_STOPPED; + output.stop_time = gettime_ms(); + } + UNLOCK_O; + if (!interval) sendSTAT("STMp", 0); + LOG_DEBUG("pause interval: %u", interval); + } + break; + case 'a': + { + unsigned interval = unpackN(&strm->replay_gain); + LOCK_O; + output.skip_frames = interval * status.current_sample_rate / 1000; + output.state = OUTPUT_SKIP_FRAMES; + UNLOCK_O; + LOG_DEBUG("skip ahead interval: %u", interval); + } + break; + case 'u': + { + unsigned jiffies = unpackN(&strm->replay_gain); + LOCK_O; + output.state = jiffies ? OUTPUT_START_AT : OUTPUT_RUNNING; + output.start_at = jiffies; +#ifdef STATUSHACK + status.frames_played = output.frames_played; +#endif + UNLOCK_O; + + LOG_DEBUG("unpause at: %u now: %u", jiffies, gettime_ms()); + sendSTAT("STMr", 0); + } + break; + case 's': + { + unsigned header_len = len - sizeof(struct strm_packet); + char *header = (char *)(pkt + sizeof(struct strm_packet)); + in_addr_t ip = (in_addr_t)strm->server_ip; // keep in network byte order + u16_t port = strm->server_port; // keep in network byte order + if (ip == 0) ip = slimproto_ip; + + LOG_DEBUG("strm s autostart: %c transition period: %u transition type: %u codec: %c", + strm->autostart, strm->transition_period, strm->transition_type - '0', strm->format); + + autostart = strm->autostart - '0'; + + sendSTAT("STMf", 0); + if (header_len > MAX_HEADER -1) { + LOG_WARN("header too long: %u", header_len); + break; + } + if (strm->format != '?') { + codec_open(strm->format, strm->pcm_sample_size, strm->pcm_sample_rate, strm->pcm_channels, strm->pcm_endianness); + } else if (autostart >= 2) { + // extension to slimproto to allow server to detect codec from response header and send back in codc message + LOG_DEBUG("streaming unknown codec"); + } else { + LOG_WARN("unknown codec requires autostart >= 2"); + break; + } + if (ip == LOCAL_PLAYER_IP && port == LOCAL_PLAYER_PORT) { + // extension to slimproto for LocalPlayer - header is filename not http header, don't expect cont + stream_file(header, header_len, strm->threshold * 1024); + autostart -= 2; + } else { + stream_sock(ip, port, header, header_len, strm->threshold * 1024, autostart >= 2); + } + sendSTAT("STMc", 0); + sentSTMu = sentSTMo = sentSTMl = false; + LOCK_O; + output.threshold = strm->output_threshold; + output.next_replay_gain = unpackN(&strm->replay_gain); + output.fade_mode = strm->transition_type - '0'; + output.fade_secs = strm->transition_period; + output.invert = (strm->flags & 0x03) == 0x03; + LOG_DEBUG("set fade mode: %u", output.fade_mode); + UNLOCK_O; + } + break; + default: + LOG_WARN("unhandled strm %c", strm->command); + break; + } +} + +static void process_cont(u8_t *pkt, int len) { + struct cont_packet *cont = (struct cont_packet *)pkt; + cont->metaint = unpackN(&cont->metaint); + + LOG_DEBUG("cont metaint: %u loop: %u", cont->metaint, cont->loop); + + if (autostart > 1) { + autostart -= 2; + LOCK_S; + if (stream.state == STREAMING_WAIT) { + stream.state = STREAMING_BUFFERING; + stream.meta_interval = stream.meta_next = cont->metaint; + } + UNLOCK_S; + wake_controller(); + } +} + +static void process_codc(u8_t *pkt, int len) { + struct codc_packet *codc = (struct codc_packet *)pkt; + + LOG_DEBUG("codc: %c", codc->format); + codec_open(codc->format, codc->pcm_sample_size, codc->pcm_sample_rate, codc->pcm_channels, codc->pcm_endianness); +} + +static void process_aude(u8_t *pkt, int len) { + struct aude_packet *aude = (struct aude_packet *)pkt; + + LOG_DEBUG("enable spdif: %d dac: %d", aude->enable_spdif, aude->enable_dac); + + LOCK_O; + if (!aude->enable_spdif && output.state != OUTPUT_OFF) { + output.state = OUTPUT_OFF; + } + if (aude->enable_spdif && output.state == OUTPUT_OFF && !output.idle_to) { + output.state = OUTPUT_STOPPED; + output.stop_time = gettime_ms(); + } + UNLOCK_O; +} + +static void process_audg(u8_t *pkt, int len) { + struct audg_packet *audg = (struct audg_packet *)pkt; + audg->gainL = unpackN(&audg->gainL); + audg->gainR = unpackN(&audg->gainR); + + LOG_DEBUG("audg gainL: %u gainR: %u adjust: %u", audg->gainL, audg->gainR, audg->adjust); + + set_volume(audg->adjust ? audg->gainL : FIXED_ONE, audg->adjust ? audg->gainR : FIXED_ONE); +} + +static void process_setd(u8_t *pkt, int len) { + struct setd_packet *setd = (struct setd_packet *)pkt; + + // handle player name query and change + if (setd->id == 0) { + if (len == 5) { + if (strlen(player_name)) { + sendSETDName(player_name); + } + } else if (len > 5) { + strncpy(player_name, setd->data, PLAYER_NAME_LEN); + player_name[PLAYER_NAME_LEN] = '\0'; + LOG_INFO("set name: %s", setd->data); + // confirm change to server + sendSETDName(setd->data); + // write name to name_file if -N option set + if (name_file) { + FILE *fp = fopen(name_file, "w"); + if (fp) { + LOG_INFO("storing name in %s", name_file); + fputs(player_name, fp); + fclose(fp); + } else { + LOG_WARN("unable to store new name in %s", name_file); + } + } + } + } +} + +#define SYNC_CAP ",SyncgroupID=" +#define SYNC_CAP_LEN 13 + +static void process_serv(u8_t *pkt, int len) { + struct serv_packet *serv = (struct serv_packet *)pkt; + + unsigned slimproto_port = 0; + char squeezeserver[] = SQUEEZENETWORK; + + if(pkt[4] == 0 && pkt[5] == 0 && pkt[6] == 0 && pkt[7] == 1) { + server_addr(squeezeserver, &new_server, &slimproto_port); + } else { + new_server = serv->server_ip; + } + + LOG_INFO("switch server"); + + if (len - sizeof(struct serv_packet) == 10) { + if (!new_server_cap) { + new_server_cap = malloc(SYNC_CAP_LEN + 10 + 1); + } + new_server_cap[0] = '\0'; + strcat(new_server_cap, SYNC_CAP); + strncat(new_server_cap, (const char *)(pkt + sizeof(struct serv_packet)), 10); + } else { + if (new_server_cap) { + free(new_server_cap); + new_server_cap = NULL; + } + } +} + +struct handler { + char opcode[5]; + void (*handler)(u8_t *, int); +}; + +static struct handler handlers[] = { + { "strm", process_strm }, + { "cont", process_cont }, + { "codc", process_codc }, + { "aude", process_aude }, + { "audg", process_audg }, + { "setd", process_setd }, + { "serv", process_serv }, + { "", NULL }, +}; + +static void process(u8_t *pack, int len) { + struct handler *h = handlers; + while (h->handler && strncmp((char *)pack, h->opcode, 4)) { h++; } + + if (h->handler) { + LOG_DEBUG("%s", h->opcode); + h->handler(pack, len); + } else { + pack[4] = '\0'; + LOG_WARN("unhandled %s", (char *)pack); + } +} + +static bool running; + +static void slimproto_run() { + static u8_t buffer[MAXBUF]; + int expect = 0; + int got = 0; + u32_t now; + static u32_t last = 0; + event_handle ehandles[2]; + int timeouts = 0; + + set_readwake_handles(ehandles, sock, wake_e); + + while (running && !new_server) { + + bool wake = false; + event_type ev; + + if ((ev = wait_readwake(ehandles, 1000)) != EVENT_TIMEOUT) { + + if (ev == EVENT_READ) { + + if (expect > 0) { + int n = recv(sock, buffer + got, expect, 0); + if (n <= 0) { + if (n < 0 && last_error() == ERROR_WOULDBLOCK) { + continue; + } + LOG_INFO("error reading from socket: %s", n ? strerror(last_error()) : "closed"); + return; + } + expect -= n; + got += n; + if (expect == 0) { + process(buffer, got); + got = 0; + } + } else if (expect == 0) { + int n = recv(sock, buffer + got, 2 - got, 0); + if (n <= 0) { + if (n < 0 && last_error() == ERROR_WOULDBLOCK) { + continue; + } + LOG_INFO("error reading from socket: %s", n ? strerror(last_error()) : "closed"); + return; + } + got += n; + if (got == 2) { + expect = buffer[0] << 8 | buffer[1]; // length pack 'n' + got = 0; + if (expect > MAXBUF) { + LOG_ERROR("FATAL: slimproto packet too big: %d > %d", expect, MAXBUF); + return; + } + } + } else { + LOG_ERROR("FATAL: negative expect"); + return; + } + + } + + if (ev == EVENT_WAKE) { + wake = true; + } + + timeouts = 0; + + } else if (++timeouts > 35) { + + // expect message from server every 5 seconds, but 30 seconds on mysb.com so timeout after 35 seconds + LOG_INFO("No messages from server - connection dead"); + return; + } + + // update playback state when woken or every 100ms + now = gettime_ms(); + + if (wake || now - last > 100 || last > now) { + bool _sendSTMs = false; + bool _sendDSCO = false; + bool _sendRESP = false; + bool _sendMETA = false; + bool _sendSTMd = false; + bool _sendSTMt = false; + bool _sendSTMl = false; + bool _sendSTMu = false; + bool _sendSTMo = false; + bool _sendSTMn = false; + bool _stream_disconnect = false; + bool _start_output = false; + decode_state _decode_state; + disconnect_code disconnect_code; + static char header[MAX_HEADER]; + size_t header_len = 0; +#if IR + bool _sendIR = false; + u32_t ir_code, ir_ts; +#endif + last = now; + + + LOCK_S; + status.stream_full = _buf_used(streambuf); + status.stream_size = streambuf->size; + status.stream_bytes = stream.bytes; + status.stream_state = stream.state; + + if (stream.state == DISCONNECT) { + disconnect_code = stream.disconnect; + stream.state = STOPPED; + _sendDSCO = true; + } + if (!stream.sent_headers && + (stream.state == STREAMING_HTTP || stream.state == STREAMING_WAIT || stream.state == STREAMING_BUFFERING)) { + header_len = stream.header_len; + memcpy(header, stream.header, header_len); + _sendRESP = true; + stream.sent_headers = true; + } + if (stream.meta_send) { + header_len = stream.header_len; + memcpy(header, stream.header, header_len); + _sendMETA = true; + stream.meta_send = false; + } + UNLOCK_S; + + LOCK_D; + if ((status.stream_state == STREAMING_HTTP || status.stream_state == STREAMING_FILE || + (status.stream_state == DISCONNECT && stream.disconnect == DISCONNECT_OK)) && + !sentSTMl && decode.state == DECODE_READY) { + if (autostart == 0) { + decode.state = DECODE_RUNNING; + _sendSTMl = true; + sentSTMl = true; + } else if (autostart == 1) { + decode.state = DECODE_RUNNING; + _start_output = true; + } + // autostart 2 and 3 require cont to be received first + } + if (decode.state == DECODE_COMPLETE || decode.state == DECODE_ERROR) { + if (decode.state == DECODE_COMPLETE) _sendSTMd = true; + if (decode.state == DECODE_ERROR) _sendSTMn = true; + decode.state = DECODE_STOPPED; + if (status.stream_state == STREAMING_HTTP || status.stream_state == STREAMING_FILE) { + _stream_disconnect = true; + } + } + _decode_state = decode.state; + UNLOCK_D; + + LOCK_O; + status.output_full = _buf_used(outputbuf); + status.output_size = outputbuf->size; + status.frames_played = output.frames_played_dmp; + 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; + status.stream_start = output.track_start_time; +#ifdef STATUSHACK + status.frames_played = output.frames_played; +#endif + } +#if PORTAUDIO + if (output.pa_reopen) { + _pa_open(); + output.pa_reopen = false; + } +#endif + 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 && + _decode_state == DECODE_STOPPED) { + + _sendSTMu = true; + sentSTMu = true; + LOG_DEBUG("output underrun"); + output.state = OUTPUT_STOPPED; + output.stop_time = now; + } + if (output.state == OUTPUT_RUNNING && !sentSTMo && status.output_full == 0 && status.stream_state == STREAMING_HTTP) { + + _sendSTMo = true; + sentSTMo = true; + } + if (output.state == OUTPUT_STOPPED && output.idle_to && (now - output.stop_time > output.idle_to)) { + output.state = OUTPUT_OFF; + LOG_DEBUG("output timeout"); + } + if (output.state == OUTPUT_RUNNING && now - status.last > 1000) { + _sendSTMt = true; + status.last = now; + } + UNLOCK_O; + +#if IR + LOCK_I; + if (ir.code) { + _sendIR = true; + ir_code = ir.code; + ir_ts = ir.ts; + ir.code = 0; + } + UNLOCK_I; +#endif + + if (_stream_disconnect) stream_disconnect(); + + // send packets once locks released as packet sending can block + if (_sendDSCO) sendDSCO(disconnect_code); + if (_sendSTMs) sendSTAT("STMs", 0); + if (_sendSTMd) sendSTAT("STMd", 0); + if (_sendSTMt) sendSTAT("STMt", 0); + if (_sendSTMl) sendSTAT("STMl", 0); + if (_sendSTMu) sendSTAT("STMu", 0); + if (_sendSTMo) sendSTAT("STMo", 0); + if (_sendSTMn) sendSTAT("STMn", 0); + if (_sendRESP) sendRESP(header, header_len); + if (_sendMETA) sendMETA(header, header_len); +#if IR + if (_sendIR) sendIR(ir_code, ir_ts); +#endif + } + } +} + +// called from other threads to wake state machine above +void wake_controller(void) { + wake_signal(wake_e); +} + +in_addr_t discover_server(char *default_server) { + struct sockaddr_in d; + struct sockaddr_in s; + char *buf; + struct pollfd pollinfo; + unsigned port; + + int disc_sock = socket(AF_INET, SOCK_DGRAM, 0); + + socklen_t enable = 1; + setsockopt(disc_sock, SOL_SOCKET, SO_BROADCAST, (const void *)&enable, sizeof(enable)); + + buf = "e"; + + memset(&d, 0, sizeof(d)); + d.sin_family = AF_INET; + d.sin_port = htons(PORT); + d.sin_addr.s_addr = htonl(INADDR_BROADCAST); + + pollinfo.fd = disc_sock; + pollinfo.events = POLLIN; + + do { + + LOG_INFO("sending discovery"); + memset(&s, 0, sizeof(s)); + + if (sendto(disc_sock, buf, 1, 0, (struct sockaddr *)&d, sizeof(d)) < 0) { + LOG_INFO("error sending disovery"); + } + + if (poll(&pollinfo, 1, 5000) == 1) { + char readbuf[10]; + socklen_t slen = sizeof(s); + recvfrom(disc_sock, readbuf, 10, 0, (struct sockaddr *)&s, &slen); + LOG_INFO("got response from: %s:%d", inet_ntoa(s.sin_addr), ntohs(s.sin_port)); + } + + if (default_server) { + server_addr(default_server, &s.sin_addr.s_addr, &port); + } + + } while (s.sin_addr.s_addr == 0 && running); + + closesocket(disc_sock); + + return s.sin_addr.s_addr; +} + +#define FIXED_CAP_LEN 256 +#define VAR_CAP_LEN 128 + +void slimproto(log_level level, char *server, u8_t mac[6], const char *name, const char *namefile, const char *modelname, int maxSampleRate) { + struct sockaddr_in serv_addr; + static char fixed_cap[FIXED_CAP_LEN], var_cap[VAR_CAP_LEN] = ""; + bool reconnect = false; + unsigned failed_connect = 0; + unsigned slimproto_port = 0; + in_addr_t previous_server = 0; + int i; + + memset(&status, 0, sizeof(status)); + + wake_create(wake_e); + + loglevel = level; + running = true; + + if (server) { + server_addr(server, &slimproto_ip, &slimproto_port); + } + + if (!slimproto_ip) { + slimproto_ip = discover_server(server); + } + + if (!slimproto_port) { + slimproto_port = PORT; + } + + if (name) { + strncpy(player_name, name, PLAYER_NAME_LEN); + player_name[PLAYER_NAME_LEN] = '\0'; + } + + if (namefile) { + FILE *fp; + name_file = namefile; + fp = fopen(namefile, "r"); + if (fp) { + if (!fgets(player_name, PLAYER_NAME_LEN, fp)) { + player_name[PLAYER_NAME_LEN] = '\0'; + } else { + // strip any \n from fgets response + int len = strlen(player_name); + if (len > 0 && player_name[len - 1] == '\n') { + player_name[len - 1] = '\0'; + } + LOG_INFO("retrieved name %s from %s", player_name, name_file); + } + fclose(fp); + } + } + + if (!running) return; + + LOCK_O; + snprintf(fixed_cap, FIXED_CAP_LEN, ",ModelName=%s,MaxSampleRate=%u", modelname ? modelname : MODEL_NAME_STRING, + ((maxSampleRate > 0) ? maxSampleRate : output.supported_rates[0])); + + for (i = 0; i < MAX_CODECS; i++) { + if (codecs[i] && codecs[i]->id && strlen(fixed_cap) < FIXED_CAP_LEN - 10) { + strcat(fixed_cap, ","); + strcat(fixed_cap, codecs[i]->types); + } + } + UNLOCK_O; + + memset(&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = slimproto_ip; + serv_addr.sin_port = htons(slimproto_port); + + LOG_INFO("connecting to %s:%d", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port)); + + new_server = 0; + + while (running) { + + if (new_server) { + previous_server = slimproto_ip; + slimproto_ip = serv_addr.sin_addr.s_addr = new_server; + LOG_INFO("switching server to %s:%d", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port)); + new_server = 0; + reconnect = false; + } + + sock = socket(AF_INET, SOCK_STREAM, 0); + + set_nonblock(sock); + set_nosigpipe(sock); + + if (connect_timeout(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr), 5) != 0) { + + if (previous_server) { + slimproto_ip = serv_addr.sin_addr.s_addr = previous_server; + LOG_INFO("new server not reachable, reverting to previous server %s:%d", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port)); + } else { + LOG_INFO("unable to connect to server %u", failed_connect); + sleep(5); + } + + // rediscover server if it was not set at startup + if (!server && ++failed_connect > 5) { + slimproto_ip = serv_addr.sin_addr.s_addr = discover_server(NULL); + } + + } else { + + struct sockaddr_in our_addr; + socklen_t len; + + LOG_INFO("connected"); + + var_cap[0] = '\0'; + failed_connect = 0; + + // check if this is a local player now we are connected & signal to server via 'loc' format + // this requires LocalPlayer server plugin to enable direct file access + len = sizeof(our_addr); + getsockname(sock, (struct sockaddr *) &our_addr, &len); + + if (our_addr.sin_addr.s_addr == serv_addr.sin_addr.s_addr) { + LOG_INFO("local player"); + strcat(var_cap, ",loc"); + } + + // add on any capablity to be sent to the new server + if (new_server_cap) { + strcat(var_cap, new_server_cap); + free(new_server_cap); + new_server_cap = NULL; + } + + sendHELO(reconnect, fixed_cap, var_cap, mac); + + slimproto_run(); + + if (!reconnect) { + reconnect = true; + } + + usleep(100000); + } + + previous_server = 0; + + closesocket(sock); + } +} + +void slimproto_stop(void) { + LOG_INFO("slimproto stop"); + running = false; +} diff --git a/slimproto.h b/slimproto.h new file mode 100644 index 00000000..e4af8040 --- /dev/null +++ b/slimproto.h @@ -0,0 +1,185 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// packet formats for slimproto + +#ifndef SUN +#pragma pack(push, 1) +#else +#pragma pack(1) +#endif + +// from S:N:Slimproto _hello_handler +struct HELO_packet { + char opcode[4]; + u32_t length; + u8_t deviceid; + u8_t revision; + u8_t mac[6]; + u8_t uuid[16]; + u16_t wlan_channellist; + u32_t bytes_received_H, bytes_received_L; + char lang[2]; + // u8_t capabilities[]; +}; + +// S:N:Slimproto _stat_handler +struct STAT_packet { + char opcode[4]; + u32_t length; + u32_t event; + u8_t num_crlf; + u8_t mas_initialized; + u8_t mas_mode; + u32_t stream_buffer_size; + u32_t stream_buffer_fullness; + u32_t bytes_received_H; + u32_t bytes_received_L; + u16_t signal_strength; + u32_t jiffies; + u32_t output_buffer_size; + u32_t output_buffer_fullness; + u32_t elapsed_seconds; + u16_t voltage; + u32_t elapsed_milliseconds; + u32_t server_timestamp; + u16_t error_code; +}; + +// S:N:Slimproto _disco_handler +struct DSCO_packet { + char opcode[4]; + u32_t length; + u8_t reason; +}; + +// S:N:Slimproto _http_response_handler +struct RESP_header { + char opcode[4]; + u32_t length; + // char header[] - added in sendRESP +}; + +// S:N:Slimproto _http_metadata_handler +struct META_header { + char opcode[4]; + u32_t length; + // char metadata[] +}; + +// S:N:Slimproto _http_setting_handler +struct SETD_header { + char opcode[4]; + u32_t length; + u8_t id; + // data +}; + +#if IR +struct IR_packet { + char opcode[4]; + u32_t length; + u32_t jiffies; + u8_t format; // ignored by server + u8_t bits; // ignored by server + u32_t ir_code; +}; +#endif + +// from S:P:Squeezebox stream_s +struct strm_packet { + char opcode[4]; + char command; + u8_t autostart; + u8_t format; + u8_t pcm_sample_size; + u8_t pcm_sample_rate; + u8_t pcm_channels; + u8_t pcm_endianness; + u8_t threshold; + u8_t spdif_enable; + u8_t transition_period; + u8_t transition_type; + u8_t flags; + u8_t output_threshold; + u8_t slaves; + u32_t replay_gain; + u16_t server_port; + u32_t server_ip; + //char request_string[]; +}; + +// S:P:Squeezebox2 +struct aude_packet { + char opcode[4]; + u8_t enable_spdif; + u8_t enable_dac; +}; + +// S:P:Squeezebox2 +struct audg_packet { + char opcode[4]; + u32_t old_gainL; // unused + u32_t old_gainR; // unused + u8_t adjust; + u8_t preamp; // unused + u32_t gainL; + u32_t gainR; + // squence ids - unused +}; + +// S:P:Squeezebox2 +struct cont_packet { + char opcode[4]; + u32_t metaint; + u8_t loop; + // guids we don't use +}; + +// S:C:Commands +struct serv_packet { + char opcode[4]; + u32_t server_ip; + // possible sync group +}; + +// S:P:Squeezebox2 +struct setd_packet { + char opcode[4]; + u8_t id; + char data[]; +}; + +// codec open - this is an extension to slimproto to allow the server to read the header and then return decode params +struct codc_packet { + char opcode[4]; + u8_t format; + u8_t pcm_sample_size; + u8_t pcm_sample_rate; + u8_t pcm_channels; + u8_t pcm_endianness; +}; + +#ifndef SUN +#pragma pack(pop) +#else +#pragma pack() +#endif diff --git a/squeezelite.h b/squeezelite.h new file mode 100644 index 00000000..435fb347 --- /dev/null +++ b/squeezelite.h @@ -0,0 +1,786 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Additions (c) Paul Hermann, 2015-2017 under the same license terms + * -Control of Raspberry pi GPIO for amplifier power + * -Launch script on power status change from LMS + */ + +// make may define: PORTAUDIO, SELFPIPE, RESAMPLE, RESAMPLE_MP, VISEXPORT, GPIO, IR, DSD, LINKALL to influence build + +#define MAJOR_VERSION "1.9" +#define MINOR_VERSION "2" +#define MICRO_VERSION "1145" + +#if defined(CUSTOM_VERSION) +#define VERSION "v" MAJOR_VERSION "." MINOR_VERSION "-" MICRO_VERSION STR(CUSTOM_VERSION) +#else +#define VERSION "v" MAJOR_VERSION "." MINOR_VERSION "-" MICRO_VERSION +#endif + + +#if !defined(MODEL_NAME) +#define MODEL_NAME SqueezeLite +#endif + +#define QUOTE(name) #name +#define STR(macro) QUOTE(macro) +#define MODEL_NAME_STRING STR(MODEL_NAME) + +// build detection +#if defined(linux) +#define LINUX 1 +#define OSX 0 +#define WIN 0 +#define FREEBSD 0 +#elif defined (__APPLE__) +#define LINUX 0 +#define OSX 1 +#define WIN 0 +#define FREEBSD 0 +#elif defined (_MSC_VER) +#define LINUX 0 +#define OSX 0 +#define WIN 1 +#define FREEBSD 0 +#elif defined(__FreeBSD__) +#define LINUX 0 +#define OSX 0 +#define WIN 0 +#define FREEBSD 1 +#elif defined (__sun) +#define SUN 1 +#define LINUX 1 +#define PORTAUDIO 1 +#define PA18API 1 +#define OSX 0 +#define WIN 0 +#elif defined (POSIX) +#undef POSIX +#define POSIX 1 +#else +#error unknown target +#endif + +#if defined(DACAUDIO) +#undef DACAUDIO +#define DACAUDIO 1 +#elif LINUX && !defined(PORTAUDIO) +#define ALSA 1 +#define PORTAUDIO 0 +#else +#define ALSA 0 +#define PORTAUDIO 1 +#endif + +#if !defined(LOOPBACK) +#if SUN +#define EVENTFD 0 +#define WINEVENT 0 +#define SELFPIPE 1 +#elif LINUX && !defined(SELFPIPE) +#define EVENTFD 1 +#define SELFPIPE 0 +#define WINEVENT 0 +#endif +#if (LINUX && !EVENTFD) || OSX || FREEBSD +#define EVENTFD 0 +#define SELFPIPE 1 +#define WINEVENT 0 +#endif +#if WIN +#define EVENTFD 0 +#define SELFPIPE 0 +#define WINEVENT 1 +#endif +#else +#define EVENTFD 0 +#define SELFPIPE 0 +#define WINEVENT 0 +#undef LOOPBACK +#define LOOPBACK 1 +#endif + +#if defined(RESAMPLE) || defined(RESAMPLE_MP) +#undef RESAMPLE +#define RESAMPLE 1 // resampling +#define PROCESS 1 // any sample processing (only resampling at present) +#else +#define RESAMPLE 0 +#define PROCESS 0 +#endif +#if defined(RESAMPLE_MP) +#undef RESAMPLE_MP +#define RESAMPLE_MP 1 +#else +#define RESAMPLE_MP 0 +#endif + +#if defined(FFMPEG) +#undef FFMPEG +#define FFMPEG 1 +#else +#define FFMPEG 0 +#endif + +#if (LINUX || OSX) && defined(VISEXPORT) +#undef VISEXPORT +#define VISEXPORT 1 // visulizer export support uses linux shared memory +#else +#define VISEXPORT 0 +#endif + +#if LINUX && defined(IR) +#undef IR +#define IR 1 +#else +#define IR 0 +#endif + +#if defined(DSD) +#undef DSD +#define DSD 1 +#define IF_DSD(x) { x } +#else +#undef DSD +#define DSD 0 +#define IF_DSD(x) +#endif + +#if defined(LINKALL) +#undef LINKALL +#define LINKALL 1 // link all libraries at build time - requires all to be available at run time +#else +#define LINKALL 0 +#endif + +#if defined (USE_SSL) +#undef USE_SSL +#define USE_SSL 1 +#else +#define USE_SSL 0 +#endif + + +#if !LINKALL + +// dynamically loaded libraries at run time + +#if LINUX +#define LIBFLAC "libFLAC.so.8" +#define LIBMAD "libmad.so.0" +#define LIBMPG "libmpg123.so.0" +#define LIBVORBIS "libvorbisfile.so.3" +#define LIBTREMOR "libvorbisidec.so.1" +#define LIBFAAD "libfaad.so.2" +#define LIBAVUTIL "libavutil.so.%d" +#define LIBAVCODEC "libavcodec.so.%d" +#define LIBAVFORMAT "libavformat.so.%d" +#define LIBSOXR "libsoxr.so.0" +#define LIBLIRC "liblirc_client.so.0" +#endif + +#if OSX +#define LIBFLAC "libFLAC.8.dylib" +#define LIBMAD "libmad.0.dylib" +#define LIBMPG "libmpg123.0.dylib" +#define LIBVORBIS "libvorbisfile.3.dylib" +#define LIBTREMOR "libvorbisidec.1.dylib" +#define LIBFAAD "libfaad.2.dylib" +#define LIBAVUTIL "libavutil.%d.dylib" +#define LIBAVCODEC "libavcodec.%d.dylib" +#define LIBAVFORMAT "libavformat.%d.dylib" +#define LIBSOXR "libsoxr.0.dylib" +#endif + +#if WIN +#define LIBFLAC "libFLAC.dll" +#define LIBMAD "libmad-0.dll" +#define LIBMPG "libmpg123-0.dll" +#define LIBVORBIS "libvorbisfile.dll" +#define LIBTREMOR "libvorbisidec.dll" +#define LIBFAAD "libfaad2.dll" +#define LIBAVUTIL "avutil-%d.dll" +#define LIBAVCODEC "avcodec-%d.dll" +#define LIBAVFORMAT "avformat-%d.dll" +#define LIBSOXR "libsoxr.dll" +#endif + +#if FREEBSD +#define LIBFLAC "libFLAC.so.11" +#define LIBMAD "libmad.so.2" +#define LIBMPG "libmpg123.so.0" +#define LIBVORBIS "libvorbisfile.so.6" +#define LIBTREMOR "libvorbisidec.so.1" +#define LIBFAAD "libfaad.so.2" +#define LIBAVUTIL "libavutil.so.%d" +#define LIBAVCODEC "libavcodec.so.%d" +#define LIBAVFORMAT "libavformat.so.%d" +#endif + +#endif // !LINKALL + +// config options +#define STREAMBUF_SIZE (2 * 1024 * 1024) +#define OUTPUTBUF_SIZE (44100 * 8 * 10) +#define OUTPUTBUF_SIZE_CROSSFADE (OUTPUTBUF_SIZE * 12 / 10) + +#define MAX_HEADER 4096 // do not reduce as icy-meta max is 4080 + +#if ALSA +#define ALSA_BUFFER_TIME 40 +#define ALSA_PERIOD_COUNT 4 +#define OUTPUT_RT_PRIORITY 45 +#endif + +#define SL_LITTLE_ENDIAN (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + +#if SUN || OSXPPC +#undef SL_LITTLE_ENDIAN +#endif + +#include +#include +#include +#include +#include +#include +#include + +#if LINUX || OSX || FREEBSD || POSIX +#include +#include +#include +#include +#include +#include +#include +#if !LINKALL +#include +#endif +#include +#include +#if SUN +#include +#endif /* SUN */ + +#define STREAM_THREAD_STACK_SIZE 8 * 1024 +#define DECODE_THREAD_STACK_SIZE 8 * 1024 +#define OUTPUT_THREAD_STACK_SIZE 8 * 1024 +#define IR_THREAD_STACK_SIZE 8 * 1024 +#if !OSX +#define thread_t pthread_t; +#endif +#define closesocket(s) close(s) +#define last_error() errno +#define ERROR_WOULDBLOCK EWOULDBLOCK + +#ifdef SUN +typedef uint8_t u8_t; +typedef uint16_t u16_t; +typedef uint32_t u32_t; +typedef uint64_t u64_t; +#elif POSIX +typedef unsigned long long u64_t; +#else +typedef u_int8_t u8_t; +typedef u_int16_t u16_t; +typedef u_int32_t u32_t; +typedef u_int64_t u64_t; +#endif /* SUN */ +typedef int16_t s16_t; +typedef int32_t s32_t; +typedef int64_t s64_t; + +#define mutex_type pthread_mutex_t +#define mutex_create(m) pthread_mutex_init(&m, NULL) +#if POSIX +#define mutex_create_p(m) mutex_create(m) +#else +#define mutex_create_p(m) pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(&m, &attr); pthread_mutexattr_destroy(&attr) +#endif +#define mutex_lock(m) pthread_mutex_lock(&m) +#define mutex_unlock(m) pthread_mutex_unlock(&m) +#define mutex_destroy(m) pthread_mutex_destroy(&m) +#define thread_type pthread_t + +#endif + +#if WIN + +#include +#include +#include + +#define STREAM_THREAD_STACK_SIZE (1024 * 64) +#define DECODE_THREAD_STACK_SIZE (1024 * 128) +#define OUTPUT_THREAD_STACK_SIZE (1024 * 64) + +typedef unsigned __int8 u8_t; +typedef unsigned __int16 u16_t; +typedef unsigned __int32 u32_t; +typedef unsigned __int64 u64_t; +typedef __int16 s16_t; +typedef __int32 s32_t; +typedef __int64 s64_t; + +typedef BOOL bool; +#define true TRUE +#define false FALSE + +#define inline __inline + +#define mutex_type HANDLE +#define mutex_create(m) m = CreateMutex(NULL, FALSE, NULL) +#define mutex_create_p mutex_create +#define mutex_lock(m) WaitForSingleObject(m, INFINITE) +#define mutex_unlock(m) ReleaseMutex(m) +#define mutex_destroy(m) CloseHandle(m) +#define thread_type HANDLE + +#define usleep(x) Sleep(x/1000) +#define sleep(x) Sleep(x*1000) +#define last_error() WSAGetLastError() +#define ERROR_WOULDBLOCK WSAEWOULDBLOCK +#define open _open +#define read _read +#define snprintf _snprintf + +#define in_addr_t u32_t +#define socklen_t int +#define ssize_t int + +#define RTLD_NOW 0 + +#endif + +#if !defined(MSG_NOSIGNAL) +#define MSG_NOSIGNAL 0 +#endif + +typedef u32_t frames_t; +typedef int sockfd; + +#if EVENTFD +#include +#define event_event int +#define event_handle struct pollfd +#define wake_create(e) e = eventfd(0, 0) +#define wake_signal(e) eventfd_write(e, 1) +#define wake_clear(e) eventfd_t val; eventfd_read(e, &val) +#define wake_close(e) close(e) +#endif + +#if SELFPIPE +#define event_handle struct pollfd +#define event_event struct wake +#define wake_create(e) pipe(e.fds); set_nonblock(e.fds[0]); set_nonblock(e.fds[1]) +#define wake_signal(e) write(e.fds[1], ".", 1) +#define wake_clear(e) char c[10]; read(e, &c, 10) +#define wake_close(e) close(e.fds[0]); close(e.fds[1]) +struct wake { + int fds[2]; +}; +#endif + +#if LOOPBACK +#define event_handle struct pollfd +#define event_event struct wake +#define wake_create(e) _wake_create(&e) +#define wake_signal(e) send(e.fds[1], ".", 1, 0) +#define wake_clear(e) char c; recv(e, &c, 1, 0) +#define wake_close(e) closesocket(e.mfds); closesocket(e.fds[0]); closesocket(e.fds[1]) +struct wake { + int mfds; + int fds[2]; +}; +void _wake_create(event_event*); +#endif + +#if WINEVENT +#define event_event HANDLE +#define event_handle HANDLE +#define wake_create(e) e = CreateEvent(NULL, FALSE, FALSE, NULL) +#define wake_signal(e) SetEvent(e) +#define wake_close(e) CloseHandle(e) +#endif + +// printf/scanf formats for u64_t +#if (LINUX && __WORDSIZE == 64) || (FREEBSD && __LP64__) +#define FMT_u64 "%lu" +#define FMT_x64 "%lx" +#elif __GLIBC_HAVE_LONG_LONG || defined __GNUC__ || WIN || SUN +#define FMT_u64 "%llu" +#define FMT_x64 "%llx" +#else +#error can not support u64_t +#endif + +#define MAX_SILENCE_FRAMES 2048 + +#define FIXED_ONE 0x10000 + +#define BYTES_PER_FRAME 8 + +#define min(a,b) (((a) < (b)) ? (a) : (b)) + +// logging +typedef enum { lERROR = 0, lWARN, lINFO, lDEBUG, lSDEBUG } log_level; + +const char *logtime(void); +void logprint(const char *fmt, ...); + +#define LOG_ERROR(fmt, ...) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_WARN(fmt, ...) if (loglevel >= lWARN) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_INFO(fmt, ...) if (loglevel >= lINFO) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_DEBUG(fmt, ...) if (loglevel >= lDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_SDEBUG(fmt, ...) if (loglevel >= lSDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) + +// utils.c (non logging) +typedef enum { EVENT_TIMEOUT = 0, EVENT_READ, EVENT_WAKE } event_type; +#if WIN && USE_SSL +char* strcasestr(const char *haystack, const char *needle); +#endif + +char *next_param(char *src, char c); +u32_t gettime_ms(void); +void get_mac(u8_t *mac); +void set_nonblock(sockfd s); +int connect_timeout(sockfd sock, const struct sockaddr *addr, socklen_t addrlen, int timeout); +void server_addr(char *server, in_addr_t *ip_ptr, unsigned *port_ptr); +void set_readwake_handles(event_handle handles[], sockfd s, event_event e); +event_type wait_readwake(event_handle handles[], int timeout); +void packN(u32_t *dest, u32_t val); +void packn(u16_t *dest, u16_t val); +u32_t unpackN(u32_t *src); +u16_t unpackn(u16_t *src); +#if OSX +void set_nosigpipe(sockfd s); +#else +#define set_nosigpipe(s) +#endif +#if SUN +void init_daemonize(void); +int daemon(int,int); +#endif +#if WIN +void winsock_init(void); +void winsock_close(void); +void *dlopen(const char *filename, int flag); +void *dlsym(void *handle, const char *symbol); +char *dlerror(void); +int poll(struct pollfd *fds, unsigned long numfds, int timeout); +#endif +#if LINUX || FREEBSD +void touch_memory(u8_t *buf, size_t size); +#endif + +// buffer.c +struct buffer { + u8_t *buf; + u8_t *readp; + u8_t *writep; + u8_t *wrap; + size_t size; + size_t base_size; + mutex_type mutex; +}; + +// _* called with mutex locked +unsigned _buf_used(struct buffer *buf); +unsigned _buf_space(struct buffer *buf); +unsigned _buf_cont_read(struct buffer *buf); +unsigned _buf_cont_write(struct buffer *buf); +void _buf_inc_readp(struct buffer *buf, unsigned by); +void _buf_inc_writep(struct buffer *buf, unsigned by); +void buf_flush(struct buffer *buf); +void buf_adjust(struct buffer *buf, size_t mod); +void _buf_resize(struct buffer *buf, size_t size); +void buf_init(struct buffer *buf, size_t size); +void buf_destroy(struct buffer *buf); + +// slimproto.c +void slimproto(log_level level, char *server, u8_t mac[6], const char *name, const char *namefile, const char *modelname, int maxSampleRate); +void slimproto_stop(void); +void wake_controller(void); + +// stream.c +typedef enum { STOPPED = 0, DISCONNECT, STREAMING_WAIT, + STREAMING_BUFFERING, STREAMING_FILE, STREAMING_HTTP, SEND_HEADERS, RECV_HEADERS } stream_state; +typedef enum { DISCONNECT_OK = 0, LOCAL_DISCONNECT = 1, REMOTE_DISCONNECT = 2, UNREACHABLE = 3, TIMEOUT = 4 } disconnect_code; + +struct streamstate { + stream_state state; + disconnect_code disconnect; + char *header; + size_t header_len; + bool sent_headers; + bool cont_wait; + u64_t bytes; + unsigned threshold; + u32_t meta_interval; + u32_t meta_next; + u32_t meta_left; + bool meta_send; +}; + +void stream_init(log_level level, unsigned stream_buf_size); +void stream_close(void); +void stream_file(const char *header, size_t header_len, unsigned threshold); +void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait); +bool stream_disconnect(void); + +// decode.c +typedef enum { DECODE_STOPPED = 0, DECODE_READY, DECODE_RUNNING, DECODE_COMPLETE, DECODE_ERROR } decode_state; + +struct decodestate { + decode_state state; + bool new_stream; + mutex_type mutex; +#if PROCESS + bool direct; + bool process; +#endif +}; + +#if PROCESS +struct processstate { + u8_t *inbuf, *outbuf; + unsigned max_in_frames, max_out_frames; + unsigned in_frames, out_frames; + unsigned in_sample_rate, out_sample_rate; + unsigned long total_in, total_out; +}; +#endif + +struct codec { + char id; + char *types; + unsigned min_read_bytes; + unsigned min_space; + void (*open)(u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness); + void (*close)(void); + decode_state (*decode)(void); +}; + +void decode_init(log_level level, const char *include_codecs, const char *exclude_codecs); +void decode_close(void); +void decode_flush(void); +unsigned decode_newstream(unsigned sample_rate, unsigned supported_rates[]); +void codec_open(u8_t format, u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness); + +#if PROCESS +// process.c +void process_samples(void); +void process_drain(void); +void process_flush(void); +unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned supported_rates[]); +void process_init(char *opt); +#endif + +#if RESAMPLE +// resample.c +void resample_samples(struct processstate *process); +bool resample_drain(struct processstate *process); +bool resample_newstream(struct processstate *process, unsigned raw_sample_rate, unsigned supported_rates[]); +void resample_flush(void); +bool resample_init(char *opt); +#endif + +// 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_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; +typedef enum { S32_LE, S24_LE, S24_3LE, S16_LE, U8, U16_LE, U16_BE, U32_LE, U32_BE } output_format; +#else +typedef enum { S32_LE, S24_LE, S24_3LE, S16_LE } output_format; +#endif + +typedef enum { FADE_INACTIVE = 0, FADE_DUE, FADE_ACTIVE } fade_state; +typedef enum { FADE_UP = 1, FADE_DOWN, FADE_CROSS } fade_dir; +typedef enum { FADE_NONE = 0, FADE_CROSSFADE, FADE_IN, FADE_OUT, FADE_INOUT } fade_mode; + +#define MAX_SUPPORTED_SAMPLERATES 18 +#define TEST_RATES = { 768000, 705600, 384000, 352800, 192000, 176400, 96000, 88200, 48000, 44100, 32000, 24000, 22500, 16000, 12000, 11025, 8000, 0 } + +struct outputstate { + output_state state; + output_format format; + const char *device; +#if ALSA + unsigned buffer; + unsigned period; +#endif + bool track_started; +#if PORTAUDIO + bool pa_reopen; + unsigned latency; + int pa_hostapi_option; +#endif + int (* write_cb)(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR, s32_t cross_gain_in, s32_t cross_gain_out, s32_t **cross_ptr); + unsigned start_frames; + unsigned frames_played; + unsigned frames_played_dmp;// frames played at the point delay is measured + unsigned current_sample_rate; + unsigned supported_rates[MAX_SUPPORTED_SAMPLERATES]; // ordered largest first so [0] is max_rate + unsigned default_sample_rate; + bool error_opening; + unsigned device_frames; + u32_t updated; + u32_t track_start_time; + u32_t current_replay_gain; + union { + u32_t pause_frames; + u32_t skip_frames; + u32_t start_at; + }; + unsigned next_sample_rate; // set in decode thread + u8_t *track_start; // set in decode thread + u32_t gainL; // set by slimproto + u32_t gainR; // set by slimproto + bool invert; // set by slimproto + u32_t next_replay_gain; // set by slimproto + unsigned threshold; // set by slimproto + fade_state fade; + u8_t *fade_start; + u8_t *fade_end; + fade_dir fade_dir; + fade_mode fade_mode; // set by slimproto + unsigned fade_secs; // set by slimproto + unsigned rate_delay; + bool delay_active; + u32_t stop_time; + u32_t idle_to; +#if DSD + dsd_format next_fmt; // set in decode thread + dsd_format outfmt; + dsd_format dsdfmt; // set in dsd_init - output for DSD: DOP, DSD_U8, ... + unsigned dsd_delay; // set in dsd_init - delay in ms switching to/from dop +#endif +}; + +void output_init_common(log_level level, const char *device, unsigned output_buf_size, unsigned rates[], unsigned idle); +void output_close_common(void); +void output_flush(void); +// _* called with mutex locked +frames_t _output_frames(frames_t avail); +void _checkfade(bool); + +// output_alsa.c +#if ALSA +void list_devices(void); +void list_mixers(const char *output_device); +void set_volume(unsigned left, unsigned right); +bool test_open(const char *device, unsigned rates[], bool userdef_rates); +void output_init_alsa(log_level level, const char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned rt_priority, unsigned idle, char *mixer_device, char *volume_mixer, bool mixer_unmute, bool mixer_linear); +void output_close_alsa(void); +#endif + +// output_pa.c +#if PORTAUDIO +void list_devices(void); +void set_volume(unsigned left, unsigned right); +bool test_open(const char *device, unsigned rates[], bool userdef_rates); +void output_init_pa(log_level level, const char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle); +void output_close_pa(void); +void _pa_open(void); +#endif + +// output_dac.c +#if DACAUDIO +void set_volume(unsigned left, unsigned right); +void output_init_dac(log_level level, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle); +void output_close_dac(void); +#endif + +// output_stdout.c +void output_init_stdout(log_level level, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay); +void output_close_stdout(void); + +// output_pack.c +void _scale_and_pack_frames(void *outputptr, s32_t *inputptr, frames_t cnt, s32_t gainL, s32_t gainR, output_format format); +void _apply_cross(struct buffer *outputbuf, frames_t out_frames, s32_t cross_gain_in, s32_t cross_gain_out, s32_t **cross_ptr); +void _apply_gain(struct buffer *outputbuf, frames_t count, s32_t gainL, s32_t gainR); +s32_t gain(s32_t gain, s32_t sample); +s32_t to_gain(float f); + +// output_vis.c +#if VISEXPORT +void _vis_export(struct buffer *outputbuf, struct outputstate *output, frames_t out_frames, bool silence); +void output_vis_init(log_level level, u8_t *mac); +void vis_stop(void); +#else +#define _vis_export(...) +#define vis_stop() +#endif + +// dop.c +#if DSD +bool is_stream_dop(u8_t *lptr, u8_t *rptr, int step, frames_t frames); +void update_dop(u32_t *ptr, frames_t frames, bool invert); +void dsd_silence_frames(u32_t *ptr, frames_t frames); +void dsd_invert(u32_t *ptr, frames_t frames); +void dsd_init(dsd_format format, unsigned delay); +#endif + +// codecs +#define MAX_CODECS 9 + +struct codec *register_flac(void); +struct codec *register_pcm(void); +struct codec *register_mad(void); +struct codec *register_mpg(void); +struct codec *register_vorbis(void); +struct codec *register_faad(void); +struct codec *register_dsd(void); +struct codec *register_ff(const char *codec); + +//gpio.c +#if GPIO +void relay( int state); +void relay_script(int state); +int gpio_pin; +bool gpio_active_low; +bool gpio_active; +char *power_script; +// my amp state +int ampstate; +#endif + +// ir.c +#if IR +struct irstate { + mutex_type mutex; + u32_t code; + u32_t ts; +}; + +void ir_init(log_level level, char *lircrc); +void ir_close(void); +#endif + +// sslsym.c +#if USE_SSL && !LINKALL +bool load_ssl_symbols(void); +void free_ssl_symbols(void); +bool ssl_loaded; +#endif + diff --git a/stream.c b/stream.c new file mode 100644 index 00000000..58ada147 --- /dev/null +++ b/stream.c @@ -0,0 +1,590 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// stream thread + +#define _GNU_SOURCE + +#include "squeezelite.h" + +#include + +#if USE_SSL +#include "openssl/ssl.h" +#include "openssl/err.h" +#endif + +#if SUN +#include +#endif +static log_level loglevel; + +static struct buffer buf; +struct buffer *streambuf = &buf; + +#define LOCK mutex_lock(streambuf->mutex) +#define UNLOCK mutex_unlock(streambuf->mutex) + +static sockfd fd; + +struct streamstate stream; + +#if USE_SSL +static SSL_CTX *SSLctx; +SSL *ssl; +#endif + +#if !USE_SSL +#define _recv(ssl, fc, buf, n, opt) recv(fd, buf, n, opt) +#define _send(ssl, fd, buf, n, opt) send(fd, buf, n, opt) +#define _poll(ssl, pollinfo, timeout) poll(pollinfo, 1, timeout) +#define _last_error() last_error() +#else +#define _last_error() ERROR_WOULDBLOCK + +static int _recv(SSL *ssl, int fd, void *buffer, size_t bytes, int options) { + int n; + if (!ssl) return recv(fd, buffer, bytes, options); + n = SSL_read(ssl, (u8_t*) buffer, bytes); + if (n <= 0 && SSL_get_error(ssl, n) == SSL_ERROR_ZERO_RETURN) return 0; + return n; +} + +static int _send(SSL *ssl, int fd, void *buffer, size_t bytes, int options) { + int n; + if (!ssl) return send(fd, buffer, bytes, options); + while (1) { + int err; + ERR_clear_error(); + if ((n = SSL_write(ssl, (u8_t*) buffer, bytes)) >= 0) return n; + err = SSL_get_error(ssl, n); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue; + LOG_INFO("SSL write error %d", err ); + return n; + } +} + +/* +can't mimic exactly poll as SSL is a real pain. Even if SSL_pending returns +0, there might be bytes to read but when select (poll) return > 0, there might +be no frame available. As well select (poll) < 0 does not mean that there is +no data pending +*/ +static int _poll(SSL *ssl, struct pollfd *pollinfo, int timeout) { + if (!ssl) return poll(pollinfo, 1, timeout); + if (pollinfo->events & POLLIN && SSL_pending(ssl)) { + if (pollinfo->events & POLLOUT) poll(pollinfo, 1, 0); + pollinfo->revents = POLLIN; + return 1; + } + return poll(pollinfo, 1, timeout); +} +#endif + + +static bool send_header(void) { + char *ptr = stream.header; + int len = stream.header_len; + + unsigned try = 0; + ssize_t n; + + while (len) { + n = _send(ssl, fd, ptr, len, MSG_NOSIGNAL); + if (n <= 0) { + if (n < 0 && _last_error() == ERROR_WOULDBLOCK && try < 10) { + LOG_SDEBUG("retrying (%d) writing to socket", ++try); + usleep(1000); + continue; + } + LOG_INFO("failed writing to socket: %s", strerror(last_error())); + stream.disconnect = LOCAL_DISCONNECT; + stream.state = DISCONNECT; + wake_controller(); + return false; + } + LOG_SDEBUG("wrote %d bytes to socket", n); + ptr += n; + len -= n; + } + LOG_SDEBUG("wrote header"); + return true; +} + +static bool running = true; + +static void _disconnect(stream_state state, disconnect_code disconnect) { + stream.state = state; + stream.disconnect = disconnect; +#if USE_SSL + if (ssl) { + SSL_shutdown(ssl); + SSL_free(ssl); + ssl = NULL; + } +#endif + closesocket(fd); + fd = -1; + wake_controller(); +} + +static void *stream_thread() { + + while (running) { + + struct pollfd pollinfo; + size_t space; + + LOCK; + + space = min(_buf_space(streambuf), _buf_cont_write(streambuf)); + + if (fd < 0 || !space || stream.state <= STREAMING_WAIT) { + UNLOCK; + usleep(100000); + continue; + } + + if (stream.state == STREAMING_FILE) { + + int n = read(fd, streambuf->writep, space); + if (n == 0) { + LOG_INFO("end of stream"); + _disconnect(DISCONNECT, DISCONNECT_OK); + } + if (n > 0) { + _buf_inc_writep(streambuf, n); + stream.bytes += n; + LOG_SDEBUG("streambuf read %d bytes", n); + } + if (n < 0) { + LOG_WARN("error reading: %s", strerror(last_error())); + _disconnect(DISCONNECT, REMOTE_DISCONNECT); + } + + UNLOCK; + continue; + + } else { + + pollinfo.fd = fd; + pollinfo.events = POLLIN; + if (stream.state == SEND_HEADERS) { + pollinfo.events |= POLLOUT; + } + } + + UNLOCK; + + if (_poll(ssl, &pollinfo, 100)) { + + LOCK; + + // check socket has not been closed while in poll + if (fd < 0) { + UNLOCK; + continue; + } + + if ((pollinfo.revents & POLLOUT) && stream.state == SEND_HEADERS) { + if (send_header()) stream.state = RECV_HEADERS; + stream.header_len = 0; + UNLOCK; + continue; + } + + if (pollinfo.revents & (POLLIN | POLLHUP)) { + + // get response headers + if (stream.state == RECV_HEADERS) { + + // read one byte at a time to catch end of header + char c; + static int endtok; + + int n = _recv(ssl, fd, &c, 1, 0); + if (n <= 0) { + if (n < 0 && _last_error() == ERROR_WOULDBLOCK) { + UNLOCK; + continue; + } + LOG_INFO("error reading headers: %s", n ? strerror(last_error()) : "closed"); + _disconnect(STOPPED, LOCAL_DISCONNECT); + UNLOCK; + continue; + } + + *(stream.header + stream.header_len) = c; + stream.header_len++; + + if (stream.header_len > MAX_HEADER - 1) { + LOG_ERROR("received headers too long: %u", stream.header_len); + _disconnect(DISCONNECT, LOCAL_DISCONNECT); + } + + if (stream.header_len > 1 && (c == '\r' || c == '\n')) { + endtok++; + if (endtok == 4) { + *(stream.header + stream.header_len) = '\0'; + LOG_INFO("headers: len: %d\n%s", stream.header_len, stream.header); + stream.state = stream.cont_wait ? STREAMING_WAIT : STREAMING_BUFFERING; + wake_controller(); + } + } else { + endtok = 0; + } + + UNLOCK; + continue; + } + + // receive icy meta data + if (stream.meta_interval && stream.meta_next == 0) { + + if (stream.meta_left == 0) { + // read meta length + u8_t c; + int n = _recv(ssl, fd, &c, 1, 0); + if (n <= 0) { + if (n < 0 && _last_error() == ERROR_WOULDBLOCK) { + UNLOCK; + continue; + } + LOG_INFO("error reading icy meta: %s", n ? strerror(last_error()) : "closed"); + _disconnect(STOPPED, LOCAL_DISCONNECT); + UNLOCK; + continue; + } + stream.meta_left = 16 * c; + stream.header_len = 0; // amount of received meta data + // MAX_HEADER must be more than meta max of 16 * 255 + } + + if (stream.meta_left) { + int n = _recv(ssl, fd, stream.header + stream.header_len, stream.meta_left, 0); + if (n <= 0) { + if (n < 0 && _last_error() == ERROR_WOULDBLOCK) { + UNLOCK; + continue; + } + LOG_INFO("error reading icy meta: %s", n ? strerror(last_error()) : "closed"); + _disconnect(STOPPED, LOCAL_DISCONNECT); + UNLOCK; + continue; + } + stream.meta_left -= n; + stream.header_len += n; + } + + if (stream.meta_left == 0) { + if (stream.header_len) { + *(stream.header + stream.header_len) = '\0'; + LOG_INFO("icy meta: len: %u\n%s", stream.header_len, stream.header); + stream.meta_send = true; + wake_controller(); + } + stream.meta_next = stream.meta_interval; + UNLOCK; + continue; + } + + // stream body into streambuf + } else { + int n; + + space = min(_buf_space(streambuf), _buf_cont_write(streambuf)); + if (stream.meta_interval) { + space = min(space, stream.meta_next); + } + + n = _recv(ssl, fd, streambuf->writep, space, 0); + if (n == 0) { + LOG_INFO("end of stream"); + _disconnect(DISCONNECT, DISCONNECT_OK); + } + if (n < 0 && _last_error() != ERROR_WOULDBLOCK) { + LOG_INFO("error reading: %s", strerror(last_error())); + _disconnect(DISCONNECT, REMOTE_DISCONNECT); + } + + if (n > 0) { + _buf_inc_writep(streambuf, n); + stream.bytes += n; + if (stream.meta_interval) { + stream.meta_next -= n; + } + } else { + UNLOCK; + continue; + } + + if (stream.state == STREAMING_BUFFERING && stream.bytes > stream.threshold) { + stream.state = STREAMING_HTTP; + wake_controller(); + } + + LOG_SDEBUG("streambuf read %d bytes", n); + } + } + + UNLOCK; + + } else { + + LOG_SDEBUG("poll timeout"); + } + } + +#if USE_SSL + if (SSLctx) { + SSL_CTX_free(SSLctx); + } +#endif + + return 0; +} + +static thread_type thread; + +void stream_init(log_level level, unsigned stream_buf_size) { + loglevel = level; + + LOG_INFO("init stream"); + LOG_DEBUG("streambuf size: %u", stream_buf_size); + + buf_init(streambuf, stream_buf_size); + if (streambuf->buf == NULL) { + LOG_ERROR("unable to malloc buffer"); + exit(0); + } + +#if USE_SSL +#if !LINKALL + if (ssl_loaded) { +#endif + SSL_library_init(); + SSLctx = SSL_CTX_new(SSLv23_client_method()); + if (SSLctx == NULL) { + LOG_ERROR("unable to allocate SSL context"); + exit(0); + } + SSL_CTX_set_options(SSLctx, SSL_OP_NO_SSLv2); +#if !LINKALL + } +#endif + ssl = NULL; +#endif + +#if SUN + signal(SIGPIPE, SIG_IGN); /* Force sockets to return -1 with EPIPE on pipe signal */ +#endif + stream.state = STOPPED; + stream.header = malloc(MAX_HEADER); + *stream.header = '\0'; + + fd = -1; + +#if LINUX || FREEBSD + touch_memory(streambuf->buf, streambuf->size); +#endif + +#if LINUX || OSX || FREEBSD || POSIX + pthread_attr_t attr; + pthread_attr_init(&attr); +#ifdef PTHREAD_STACK_MIN + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + STREAM_THREAD_STACK_SIZE); +#endif + pthread_create(&thread, &attr, stream_thread, NULL); + pthread_attr_destroy(&attr); +#endif +#if WIN + thread = CreateThread(NULL, STREAM_THREAD_STACK_SIZE, (LPTHREAD_START_ROUTINE)&stream_thread, NULL, 0, NULL); +#endif +} + +void stream_close(void) { + LOG_INFO("close stream"); + LOCK; + running = false; + UNLOCK; +#if LINUX || OSX || FREEBSD || POSIX + pthread_join(thread, NULL); +#endif + free(stream.header); + buf_destroy(streambuf); +} + +void stream_file(const char *header, size_t header_len, unsigned threshold) { + buf_flush(streambuf); + + LOCK; + + stream.header_len = header_len; + memcpy(stream.header, header, header_len); + *(stream.header+header_len) = '\0'; + + LOG_INFO("opening local file: %s", stream.header); + +#if WIN + fd = open(stream.header, O_RDONLY | O_BINARY); +#else + fd = open(stream.header, O_RDONLY); +#endif + + stream.state = STREAMING_FILE; + if (fd < 0) { + LOG_INFO("can't open file: %s", stream.header); + stream.state = DISCONNECT; + } + wake_controller(); + + stream.cont_wait = false; + stream.meta_interval = 0; + stream.meta_next = 0; + stream.meta_left = 0; + stream.meta_send = false; + stream.sent_headers = false; + stream.bytes = 0; + stream.threshold = threshold; + + UNLOCK; +} + +void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait) { + struct sockaddr_in addr; + + int sock = socket(AF_INET, SOCK_STREAM, 0); + + if (sock < 0) { + LOG_ERROR("failed to create socket"); + return; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = ip; + addr.sin_port = port; + + LOG_INFO("connecting to %s:%d", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); + + set_nonblock(sock); + set_nosigpipe(sock); + + if (connect_timeout(sock, (struct sockaddr *) &addr, sizeof(addr), 10) < 0) { + LOG_INFO("unable to connect to server"); + LOCK; + stream.state = DISCONNECT; + stream.disconnect = UNREACHABLE; + UNLOCK; + return; + } + +#if USE_SSL + if (ntohs(port) == 443) { + char *server = strcasestr(header, "Host:"); + + ssl = SSL_new(SSLctx); + SSL_set_fd(ssl, sock); + + // add SNI + if (server) { + char *p, *servername = malloc(1024); + + sscanf(server, "Host:%255[^:]s", servername); + for (p = servername; *p == ' '; p++); + SSL_set_tlsext_host_name(ssl, p); + free(servername); + } + + while (1) { + int status, err = 0; + + ERR_clear_error(); + status = SSL_connect(ssl); + + // successful negotiation + if (status == 1) break; + + // error or non-blocking requires more time + if (status < 0) { + err = SSL_get_error(ssl, status); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue; + } + + LOG_WARN("unable to open SSL socket %d (%d)", status, err); + closesocket(sock); + SSL_free(ssl); + ssl = NULL; + LOCK; + stream.state = DISCONNECT; + stream.disconnect = UNREACHABLE; + UNLOCK; + + return; + } + } else { + ssl = NULL; + } +#endif + + buf_flush(streambuf); + + LOCK; + + fd = sock; + stream.state = SEND_HEADERS; + stream.cont_wait = cont_wait; + stream.meta_interval = 0; + stream.meta_next = 0; + stream.meta_left = 0; + stream.meta_send = false; + stream.header_len = header_len; + memcpy(stream.header, header, header_len); + *(stream.header+header_len) = '\0'; + + LOG_INFO("header: %s", stream.header); + + stream.sent_headers = false; + stream.bytes = 0; + stream.threshold = threshold; + + UNLOCK; +} + +bool stream_disconnect(void) { + bool disc = false; + LOCK; +#if USE_SSL + if (ssl) { + SSL_shutdown(ssl); + SSL_free(ssl); + ssl = NULL; + } +#endif + if (fd != -1) { + closesocket(fd); + fd = -1; + disc = true; + } + stream.state = STOPPED; + UNLOCK; + return disc; +} diff --git a/utils.c b/utils.c new file mode 100644 index 00000000..8252a69e --- /dev/null +++ b/utils.c @@ -0,0 +1,563 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "squeezelite.h" + +#if LINUX || OSX || FREEBSD || POSIX +#include +#include +#include +#if FREEBSD +#include +#include +#include +#endif +#endif +#if SUN +#include +#include +#include +#include +#include +#include +#include +#include +#endif +#if WIN +#include +#if USE_SSL +#include +#include +#include +#endif +#endif +#if OSX +#include +#include +#include +#include +#endif + +#include + +// logging functions +const char *logtime(void) { + static char buf[100]; +#if WIN + SYSTEMTIME lt; + GetLocalTime(<); + sprintf(buf, "[%02d:%02d:%02d.%03d]", lt.wHour, lt.wMinute, lt.wSecond, lt.wMilliseconds); +#else + struct timeval tv; + gettimeofday(&tv, NULL); + strftime(buf, sizeof(buf), "[%T.", localtime(&tv.tv_sec)); + sprintf(buf+strlen(buf), "%06ld]", (long)tv.tv_usec); +#endif + return buf; +} + +void logprint(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + fflush(stderr); +} + +// cmdline parsing +char *next_param(char *src, char c) { + static char *str = NULL; + char *ptr, *ret; + if (src) str = src; + if (str && (ptr = strchr(str, c))) { + ret = str; + *ptr = '\0'; + str = ptr + 1; + } else { + ret = str; + str = NULL; + } + + return ret && ret[0] ? ret : NULL; +} + +// clock +u32_t gettime_ms(void) { +#if WIN + return GetTickCount(); +#else +#if LINUX || FREEBSD || POSIX + struct timespec ts; +#ifdef CLOCK_MONOTONIC + if (!clock_gettime(CLOCK_MONOTONIC, &ts)) { +#else + if (!clock_gettime(CLOCK_REALTIME, &ts)) { +#endif + return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + } +#endif + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec * 1000 + tv.tv_usec / 1000; +#endif +} + +// mac address +#if LINUX && !defined(SUN) +// search first 4 interfaces returned by IFCONF +void get_mac(u8_t mac[]) { + char *utmac; + struct ifconf ifc; + struct ifreq *ifr, *ifend; + struct ifreq ifreq; + struct ifreq ifs[4]; + + utmac = getenv("UTMAC"); + if (utmac) + { + if ( strlen(utmac) == 17 ) + { + if (sscanf(utmac,"%2hhx:%2hhx:%2hhx:%2hhx:%2hhx:%2hhx", + &mac[0],&mac[1],&mac[2],&mac[3],&mac[4],&mac[5]) == 6) + { + return; + } + } + + } + + mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0; + + int s = socket(AF_INET, SOCK_DGRAM, 0); + + ifc.ifc_len = sizeof(ifs); + ifc.ifc_req = ifs; + + if (ioctl(s, SIOCGIFCONF, &ifc) == 0) { + ifend = ifs + (ifc.ifc_len / sizeof(struct ifreq)); + + for (ifr = ifc.ifc_req; ifr < ifend; ifr++) { + if (ifr->ifr_addr.sa_family == AF_INET) { + + strncpy(ifreq.ifr_name, ifr->ifr_name, sizeof(ifreq.ifr_name)); + if (ioctl (s, SIOCGIFHWADDR, &ifreq) == 0) { + memcpy(mac, ifreq.ifr_hwaddr.sa_data, 6); + if (mac[0]+mac[1]+mac[2] != 0) { + break; + } + } + } + } + } + + close(s); +} +#endif + +#if SUN +void get_mac(u8_t mac[]) { + struct arpreq parpreq; + struct sockaddr_in *psa; + struct in_addr inaddr; + struct hostent *phost; + char hostname[MAXHOSTNAMELEN]; + char **paddrs; + char *utmac; + int sock; + int status=0; + + utmac = getenv("UTMAC"); + if (utmac) + { + if ( strlen(utmac) == 17 ) + { + if (sscanf(utmac,"%2hhx:%2hhx:%2hhx:%2hhx:%2hhx:%2hhx", + &mac[0],&mac[1],&mac[2],&mac[3],&mac[4],&mac[5]) == 6) + { + return; + } + } + + } + + mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0; + + gethostname(hostname, MAXHOSTNAMELEN); + + phost = gethostbyname(hostname); + + paddrs = phost->h_addr_list; + memcpy(&inaddr.s_addr, *paddrs, sizeof(inaddr.s_addr)); + + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + + if(sock == -1) + { + mac[5] = 1; + return; + } + + memset(&parpreq, 0, sizeof(struct arpreq)); + psa = (struct sockaddr_in *) &parpreq.arp_pa; + memset(psa, 0, sizeof(struct sockaddr_in)); + psa->sin_family = AF_INET; + memcpy(&psa->sin_addr, *paddrs, sizeof(struct in_addr)); + + status = ioctl(sock, SIOCGARP, &parpreq); + + if(status == -1) + { + mac[5] = 2; + return; + } + + mac[0] = (unsigned char) parpreq.arp_ha.sa_data[0]; + mac[1] = (unsigned char) parpreq.arp_ha.sa_data[1]; + mac[2] = (unsigned char) parpreq.arp_ha.sa_data[2]; + mac[3] = (unsigned char) parpreq.arp_ha.sa_data[3]; + mac[4] = (unsigned char) parpreq.arp_ha.sa_data[4]; + mac[5] = (unsigned char) parpreq.arp_ha.sa_data[5]; +} +#endif + +#if OSX || FREEBSD +void get_mac(u8_t mac[]) { + struct ifaddrs *addrs, *ptr; + const struct sockaddr_dl *dlAddr; + const unsigned char *base; + + mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0; + + if (getifaddrs(&addrs) == 0) { + ptr = addrs; + while (ptr) { + if (ptr->ifa_addr->sa_family == AF_LINK && ((const struct sockaddr_dl *) ptr->ifa_addr)->sdl_type == IFT_ETHER) { + dlAddr = (const struct sockaddr_dl *)ptr->ifa_addr; + base = (const unsigned char*) &dlAddr->sdl_data[dlAddr->sdl_nlen]; + memcpy(mac, base, min(dlAddr->sdl_alen, 6)); + break; + } + ptr = ptr->ifa_next; + } + freeifaddrs(addrs); + } +} +#endif + +#if WIN +#pragma comment(lib, "IPHLPAPI.lib") +void get_mac(u8_t mac[]) { + IP_ADAPTER_INFO AdapterInfo[16]; + DWORD dwBufLen = sizeof(AdapterInfo); + DWORD dwStatus = GetAdaptersInfo(AdapterInfo, &dwBufLen); + + mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0; + + if (GetAdaptersInfo(AdapterInfo, &dwBufLen) == ERROR_SUCCESS) { + memcpy(mac, AdapterInfo[0].Address, 6); + } +} +#endif + +void set_nonblock(sockfd s) { +#if WIN + u_long iMode = 1; + ioctlsocket(s, FIONBIO, &iMode); +#else + int flags = fcntl(s, F_GETFL,0); + fcntl(s, F_SETFL, flags | O_NONBLOCK); +#endif +} + +// connect for socket already set to non blocking with timeout in seconds +int connect_timeout(sockfd sock, const struct sockaddr *addr, socklen_t addrlen, int timeout) { + fd_set w, e; + struct timeval tval; + + if (connect(sock, addr, addrlen) < 0) { +#if !WIN + if (last_error() != EINPROGRESS) { +#else + if (last_error() != WSAEWOULDBLOCK) { +#endif + return -1; + } + } + + FD_ZERO(&w); + FD_SET(sock, &w); + e = w; + tval.tv_sec = timeout; + tval.tv_usec = 0; + + // only return 0 if w set and sock error is zero, otherwise return error code + if (select(sock + 1, NULL, &w, &e, timeout ? &tval : NULL) == 1 && FD_ISSET(sock, &w)) { + int error = 0; + socklen_t len = sizeof(error); + getsockopt(sock, SOL_SOCKET, SO_ERROR, (void *)&error, &len); + return error; + } + + return -1; +} + +void server_addr(char *server, in_addr_t *ip_ptr, unsigned *port_ptr) { + struct addrinfo *res = NULL; + struct addrinfo hints; + const char *port = NULL; + + if (strtok(server, ":")) { + port = strtok(NULL, ":"); + if (port) { + *port_ptr = atoi(port); + } + } + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_INET; + + getaddrinfo(server, NULL, &hints, &res); + + if (res && res->ai_addr) { + *ip_ptr = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr; + } + + if (res) { + freeaddrinfo(res); + } +} + +void set_readwake_handles(event_handle handles[], sockfd s, event_event e) { +#if WINEVENT + handles[0] = WSACreateEvent(); + handles[1] = e; + WSAEventSelect(s, handles[0], FD_READ | FD_CLOSE); +#elif SELFPIPE || LOOPBACK + handles[0].fd = s; + handles[1].fd = e.fds[0]; + handles[0].events = POLLIN; + handles[1].events = POLLIN; +#else + handles[0].fd = s; + handles[1].fd = e; + handles[0].events = POLLIN; + handles[1].events = POLLIN; +#endif +} + +event_type wait_readwake(event_handle handles[], int timeout) { +#if WINEVENT + int wait = WSAWaitForMultipleEvents(2, handles, FALSE, timeout, FALSE); + if (wait == WSA_WAIT_EVENT_0) { + WSAResetEvent(handles[0]); + return EVENT_READ; + } else if (wait == WSA_WAIT_EVENT_0 + 1) { + return EVENT_WAKE; + } else { + return EVENT_TIMEOUT; + } +#else + if (poll(handles, 2, timeout) > 0) { + if (handles[0].revents) { + return EVENT_READ; + } + if (handles[1].revents) { + wake_clear(handles[1].fd); + return EVENT_WAKE; + } + } + return EVENT_TIMEOUT; +#endif +} + +#if LOOPBACK +void _wake_create(event_event* e) { + struct sockaddr_in addr; + short port; + socklen_t len; + + e->mfds = e->fds[0] = e->fds[1] = -1; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + // create sending socket - will wait for connections + addr.sin_port = 0; + e->mfds = socket(AF_INET, SOCK_STREAM, 0); + bind(e->mfds, (struct sockaddr*) &addr, sizeof(addr)); + len = sizeof(struct sockaddr); + + // get assigned port & listen + getsockname(e->mfds, (struct sockaddr *) &addr, &len); + port = addr.sin_port; + listen(e->mfds, 1); + + // create receiving socket + addr.sin_port = 0; + e->fds[0] = socket(AF_INET, SOCK_STREAM, 0); + bind(e->fds[0], (struct sockaddr*) &addr, sizeof(addr)); + + // connect to sender (we listen so it can be blocking) + addr.sin_port = port; + connect(e->fds[0], (struct sockaddr*) &addr, sizeof(addr)); + + // this one will work or fail, but not block + len = sizeof(struct sockaddr); + e->fds[1] = accept(e->mfds, (struct sockaddr*) &addr, &len); +} +#endif + +// pack/unpack to network byte order +void packN(u32_t *dest, u32_t val) { + u8_t *ptr = (u8_t *)dest; + *(ptr) = (val >> 24) & 0xFF; *(ptr+1) = (val >> 16) & 0xFF; *(ptr+2) = (val >> 8) & 0xFF; *(ptr+3) = val & 0xFF; +} + +void packn(u16_t *dest, u16_t val) { + u8_t *ptr = (u8_t *)dest; + *(ptr) = (val >> 8) & 0xFF; *(ptr+1) = val & 0xFF; +} + +u32_t unpackN(u32_t *src) { + u8_t *ptr = (u8_t *)src; + return *(ptr) << 24 | *(ptr+1) << 16 | *(ptr+2) << 8 | *(ptr+3); +} + +u16_t unpackn(u16_t *src) { + u8_t *ptr = (u8_t *)src; + return *(ptr) << 8 | *(ptr+1); +} + +#if OSX +void set_nosigpipe(sockfd s) { + int set = 1; + setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int)); +} +#endif + +#if WIN +void winsock_init(void) { + WSADATA wsaData; + WORD wVersionRequested = MAKEWORD(2, 2); + int WSerr = WSAStartup(wVersionRequested, &wsaData); + if (WSerr != 0) { + LOG_ERROR("Bad winsock version"); + exit(1); + } +} + +void winsock_close(void) { + WSACleanup(); +} + +void *dlopen(const char *filename, int flag) { + SetLastError(0); + return LoadLibrary((LPCTSTR)filename); +} + +void *dlsym(void *handle, const char *symbol) { + SetLastError(0); + return (void *)GetProcAddress(handle, symbol); +} + +char *dlerror(void) { + static char ret[32]; + int last = GetLastError(); + if (last) { + sprintf(ret, "code: %i", last); + SetLastError(0); + return ret; + } + return NULL; +} + +int poll(struct pollfd *fds, unsigned long numfds, int timeout) { + fd_set r, w; + struct timeval tv; + int ret, i, max_fds = fds[0].fd; + + FD_ZERO(&r); + FD_ZERO(&w); + + for (i = 0; i < numfds; i++) { + if (fds[i].events & POLLIN) FD_SET(fds[i].fd, &r); + if (fds[i].events & POLLOUT) FD_SET(fds[i].fd, &w); + if (max_fds < fds[i].fd) max_fds = fds[i].fd; + } + + tv.tv_sec = timeout / 1000; + tv.tv_usec = 1000 * (timeout % 1000); + + ret = select(max_fds + 1, &r, &w, NULL, &tv); + + if (ret < 0) return ret; + + for (i = 0; i < numfds; i++) { + fds[i].revents = 0; + if (FD_ISSET(fds[i].fd, &r)) fds[i].revents |= POLLIN; + if (FD_ISSET(fds[i].fd, &w)) fds[i].revents |= POLLOUT; + } + + return ret; +} +#endif + +#if LINUX || FREEBSD +void touch_memory(u8_t *buf, size_t size) { + u8_t *ptr; + for (ptr = buf; ptr < buf + size; ptr += sysconf(_SC_PAGESIZE)) { + *ptr = 0; + } +} +#endif + +#if WIN && USE_SSL +char *strcasestr(const char *haystack, const char *needle) { + size_t length_needle; + size_t length_haystack; + size_t i; + + if (!haystack || !needle) + return NULL; + + length_needle = strlen(needle); + length_haystack = strlen(haystack) - length_needle + 1; + + for (i = 0; i < length_haystack; i++) + { + size_t j; + + for (j = 0; j < length_needle; j++) + { + unsigned char c1; + unsigned char c2; + + c1 = haystack[i+j]; + c2 = needle[j]; + if (toupper(c1) != toupper(c2)) + goto next; + } + return (char *) haystack + i; + next: + ; + } + + return NULL; +} +#endif