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
::\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