diff --git a/ChangeLog.txt b/ChangeLog.txt new file mode 100644 index 0000000..7b6571b --- /dev/null +++ b/ChangeLog.txt @@ -0,0 +1,137 @@ +Version 1.0 - 15/2/13 +===================== +- initial release + +Version 1.1 - 12/4/13 +===================== + +Minor changes +- add timeout on slimproto connection to detect dead server +- fix issue with clipping on windows by disabling portaudio dither +- silence alsa error messages on linux alsa builds unless debugging is enabled +- hide some additional error messages unless debuging is enabled so usb dacs produce less error messages when turned off and on + +Version 1.2 - 6/7/13 +==================== + +Features +- support of upsampling via libsoxr + +Minor changes +- command line option for setting the service address now requires "-s" before the server address +- fixes a bug where the channels could become swapped when using S16_LE ALSA output +- falls back to polling for a new server if one is not found for more than 30 seconds +- fixes play of wav/aiff local files when the LocalPlayer plugin is active + +Version 1.3 - 6/10/13 +===================== + +Features +- support for wma/alac decode via ffmpeg library (requires compilation with -DFFMPEG) +- support for export of audio data to jivelite to enable visulizations on linux (requires compilation with -DVISEXPORT) + +Minor changes +- support async as well as sync resampling rates +- support on/off of audio device with portaudio +- improved gapless support for aac/mad when skipping to mid track (based on patches from Wouter Ellenbroek) +- various bug fixes + +Version 1.3.1 - 25/11/13 +======================== + +Minor changes +- support of compile time linking for distro packaging, uses -DLINKALL option + +Version 1.4 28/12/13 +==================== + +Features +- native support of dsd playback to dop capable dac or via conversion to pcm and resampling +- support dop in flac playback to dop dacs +- support of output to stdout + +Minor changes +- support of resampling only when sample rate is not natively supported +- fix problem with libmpg123 playback not playing to end of track +- add ablity for player name change to be stored locally in a file (to emulate hardware where name is stored on player) + +Version 1.5 12/1/14 +=================== + +Minor changes +- add configurable delay for switch between pcm and dop +- allow visexport to work with jivelite running as any user +- bug fixes for dsf playback, for status progress on windows using wdm-ks output, and to avoid 100% cpu +- change some logging levels for slimproto to aid readability + +Version 1.6 23/3/14 +=================== + +Minor changes +- add support for direct file playback on windows +- add configurable delay for switch between pcm sample rates +- support build on freebsd +- fix gapless playback on portaudio builds +- fix gapless playback for mp3 localfile case with tags at start of file + +Version 1.6.1 22/4/14 +===================== + +Minor changes +- fix bug with PA version changing sample rate between tracks +- fix crash when skipping in ogg while resampling +- fix typo + +Version 1.6.2 26/5/14 +===================== + +Minor changes +- fix XRUN on track change when resampling on low power cpus +- log command line to logfile when debugging enabled +- option to exclude codecs (-e) +- support parallel execution of libsoxr + +Version 1.6.3 14/6/14 +===================== + +Minor changes +- reduce time to start track when playing local files +- disable use of OPENMP when RESAMPLE build option defined, add new option RESAMPLE_MP to enable it + +Version 1.6.4 7/7/14 +==================== + +Minor changes +- improve synchronisation feedback accuracy + +Version 1.6.5 21/11/14 +====================== + +Minor changes +- fix problem opening ALSA device if 44100 is not supported +- trap setting of hw player mac address + +Version 1.7 1/1/15 +================== + +Minor changes +- allow player modelname to be set at compile or run time +- workaround alsa drivers reporting very large number of available frames +- fix clicks on localfile playback of AIFF files +- add -P option to store process id in a file +- improve error messages for command line parsing + +Version 1.7.1 10/1/15 +===================== + +Minor changes +- fix crash which could occur when resampling + +Version 1.8 1/2/15 +================== + +Features +- support for closing output device when idle with -C option +- support for basic IR input using LIRC on Linux +- support for volume adjustment or unmuting of alsa mixer +- support for inverting output polarity via LMS setting (requires recent 7.9 server) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..94af310 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,49 @@ +Squeezelite - lightweight headless squeezebox emulator + +(c) Adrian Smith 2012-2015, triode1@btinternet.com + +Released under GPLv3 license: + +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 . + + +--------------------------------------------------------------------- + +If built with DSD support, this software also includes code subject to the following license: + +Copyright 2009, 2011 Sebastian Gesemann. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY SEBASTIAN GESEMANN ''AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEBASTIAN GESEMANN OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Sebastian Gesemann. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dca2abd --- /dev/null +++ b/Makefile @@ -0,0 +1,85 @@ +# Cross compile support - create a Makefile which defines these three variables and then includes this Makefile... +CFLAGS ?= -Wall -fPIC -O2 $(OPTS) +LDFLAGS ?= -lasound -lpthread -lm -lrt +EXECUTABLE ?= squeezelite + +# passing one or more of these in $(OPTS) enables optional feature inclusion +OPT_DSD = -DDSD +OPT_FF = -DFFMPEG +OPT_LINKALL = -DLINKALL +OPT_RESAMPLE= -DRESAMPLE +OPT_VIS = -DVISEXPORT +OPT_IR = -DIR + +SOURCES = \ + main.c slimproto.c buffer.c stream.c utils.c \ + output.c output_alsa.c output_pa.c output_stdout.c output_pack.c decode.c \ + flac.c pcm.c mad.c vorbis.c faad.c mpg.c + +SOURCES_DSD = dsd.c dop.c dsd2pcm/dsd2pcm.c +SOURCES_FF = ffmpeg.c +SOURCES_RESAMPLE = process.c resample.c +SOURCES_VIS = output_vis.c +SOURCES_IR = ir.c + +LINK_LINUX = -ldl + +LINKALL = -lFLAC -lmad -lvorbisfile -lfaad -lmpg123 +LINKALL_FF = -lavcodec -lavformat -lavutil +LINKALL_RESAMPLE = -lsoxr +LINKALL_IR = -llirc_client + +DEPS = squeezelite.h slimproto.h + +UNAME = $(shell uname -s) + +# add optional sources +ifneq (,$(findstring $(OPT_DSD), $(CFLAGS))) + SOURCES += $(SOURCES_DSD) +endif +ifneq (,$(findstring $(OPT_FF), $(CFLAGS))) + SOURCES += $(SOURCES_FF) +endif +ifneq (,$(findstring $(OPT_RESAMPLE), $(CFLAGS))) + SOURCES += $(SOURCES_RESAMPLE) +endif +ifneq (,$(findstring $(OPT_VIS), $(CFLAGS))) + SOURCES += $(SOURCES_VIS) +endif +ifneq (,$(findstring $(OPT_IR), $(CFLAGS))) + SOURCES += $(SOURCES_IR) +endif + +# add optional link options +ifneq (,$(findstring $(OPT_LINKALL), $(CFLAGS))) + LDFLAGS += $(LINKALL) +ifneq (,$(findstring $(OPT_FF), $(CFLAGS))) + LDFLAGS += $(LINKALL_FF) +endif +ifneq (,$(findstring $(OPT_RESAMPLE), $(CFLAGS))) + LDFLAGS += $(LINKALL_RESAMPLE) +endif +ifneq (,$(findstring $(OPT_IR), $(CFLAGS))) + LDFLAGS += $(LINKALL_IR) +endif +else +# if not LINKALL and linux add LINK_LINUX +ifeq ($(UNAME), Linux) + LDFLAGS += $(LINK_LINUX) +endif +endif + +OBJECTS = $(SOURCES:.c=.o) + +all: $(EXECUTABLE) + +$(EXECUTABLE): $(OBJECTS) + $(CC) $(OBJECTS) $(LDFLAGS) -o $@ + +$(OBJECTS): $(DEPS) + +.c.o: + $(CC) $(CFLAGS) $(CPPFLAGS) $< -c -o $@ + +clean: + rm -f $(OBJECTS) $(EXECUTABLE) diff --git a/Makefile.freebsd b/Makefile.freebsd new file mode 100644 index 0000000..a335ace --- /dev/null +++ b/Makefile.freebsd @@ -0,0 +1,4 @@ +CPPFLAGS = -I/usr/local/include -I/usr/local/include/portaudio2 +LDFLAGS = -L/usr/local/lib -L/usr/local/lib/portaudio2 -lportaudio -lpthread -lm + +include Makefile diff --git a/Makefile.osx b/Makefile.osx new file mode 100644 index 0000000..59fa4c8 --- /dev/null +++ b/Makefile.osx @@ -0,0 +1,7 @@ +# OSX build - adjust -I to point to header files for codecs and portaudio +CFLAGS = -arch x86_64 -arch i386 -Wall -fPIC -O2 -I./include $(OPTS) +LDFLAGS = -arch x86_64 -arch i386 -lpthread libportaudio.a -framework CoreAudio -framework AudioToolbox -framework AudioUnit -framework Carbon + +EXECUTABLE ?= squeezelite-osx + +include Makefile diff --git a/Makefile.pa b/Makefile.pa new file mode 100644 index 0000000..636be5a --- /dev/null +++ b/Makefile.pa @@ -0,0 +1,6 @@ +# Make with portaudio rather than direct alsa +OPTS += -DPORTAUDIO +LDFLAGS = -lportaudio -lpthread -ldl -lrt +EXECUTABLE = squeezelite-pa + +include Makefile diff --git a/Makefile.resample b/Makefile.resample new file mode 100644 index 0000000..0f29eca --- /dev/null +++ b/Makefile.resample @@ -0,0 +1,3 @@ +OPTS = -DRESAMPLE + +include Makefile diff --git a/buffer.c b/buffer.c new file mode 100644 index 0000000..fd83a76 --- /dev/null +++ b/buffer.c @@ -0,0 +1,114 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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/decode.c b/decode.c new file mode 100644 index 0000000..add5fcb --- /dev/null +++ b/decode.c @@ -0,0 +1,254 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 thread_type thread; + +void decode_init(log_level level, const char *include_codecs, const char *exclude_codecs) { + int i; + + loglevel = level; + + LOG_INFO("init decode, include codecs: %s exclude codecs: %s", include_codecs ? include_codecs : "", exclude_codecs); + + // 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 || strstr(include_codecs, "dsd"))) codecs[i++] = register_dsd(); +#endif +#if FFMPEG + if (!strstr(exclude_codecs, "alac") && (!include_codecs || strstr(include_codecs, "alac"))) codecs[i++] = register_ff("alc"); + if (!strstr(exclude_codecs, "wma") && (!include_codecs || strstr(include_codecs, "wma"))) codecs[i++] = register_ff("wma"); +#endif + if (!strstr(exclude_codecs, "aac") && (!include_codecs || strstr(include_codecs, "aac"))) codecs[i++] = register_faad(); + if (!strstr(exclude_codecs, "ogg") && (!include_codecs || strstr(include_codecs, "ogg"))) codecs[i++] = register_vorbis(); + if (!strstr(exclude_codecs, "flac") && (!include_codecs || strstr(include_codecs, "flac"))) codecs[i++] = register_flac(); + if (!strstr(exclude_codecs, "pcm") && (!include_codecs || strstr(include_codecs, "pcm"))) 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 || strstr(include_codecs, "mp3") || strstr(include_codecs, "mad"))) codecs[i] = register_mad(); + if (!(strstr(exclude_codecs, "mp3") || strstr(exclude_codecs, "mpg")) && !codecs[i] && + (!include_codecs || strstr(include_codecs, "mp3") || strstr(include_codecs, "mpg"))) codecs[i] = register_mpg(); + + mutex_create(decode.mutex); + +#if LINUX || OSX || FREEBSD + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + DECODE_THREAD_STACK_SIZE); + 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/dop.c b/dop.c new file mode 100644 index 0000000..b498202 --- /dev/null +++ b/dop.c @@ -0,0 +1,102 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012, 2013, triode1@btinternet.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 . + * + */ + +// DSP over PCM (DOP) specific functions + +#include "squeezelite.h" + +#if DSD + +extern struct buffer *outputbuf; +extern struct outputstate output; + +#define LOCK_O mutex_lock(outputbuf->mutex) +#define UNLOCK_O mutex_unlock(outputbuf->mutex) + +// check for 32 dop marker frames to see if this is dop in flac +// dop is always encoded in 24 bit samples with marker 0x0005xxxx or 0x00FAxxxx +bool is_flac_dop(u32_t *lptr, u32_t *rptr, frames_t frames) { + int matched = 0; + u32_t next = 0; + + while (frames--) { + if (((*lptr & 0x00FF0000) == 0x00050000 && (*rptr & 0x00FF0000) == 0x00050000) || + ((*lptr & 0x00FF0000) == 0x00FA0000 && (*rptr & 0x00FF0000) == 0x00FA0000)) { + if (*lptr >> 24 == next) { + matched++; + next = ( 0x05 + 0xFA ) - next; + } else { + next = *lptr >> 24; + matched = 1; + } + } else { + return false; + } + if (matched == 32) { + return true; + } + + ++lptr; ++rptr; + } + return false; +} + +// update the dop marker and potentially invert polarity for frames in the output buffer +// performaned on all output including silence to maintain marker phase consitency +void update_dop(u32_t *ptr, frames_t frames, bool invert) { + static u32_t marker = 0x05; + if (!invert) { + while (frames--) { + u32_t scaled_marker = marker << 24; + *ptr = (*ptr & 0x00FFFFFF) | scaled_marker; + ++ptr; + *ptr = (*ptr & 0x00FFFFFF) | scaled_marker; + ++ptr; + marker = ( 0x05 + 0xFA ) - marker; + } + } else { + while (frames--) { + u32_t scaled_marker = marker << 24; + *ptr = ((~(*ptr)) & 0x00FFFFFF) | scaled_marker; + ++ptr; + *ptr = ((~(*ptr)) & 0x00FFFFFF) | scaled_marker; + ++ptr; + marker = ( 0x05 + 0xFA ) - marker; + } + } +} + +// fill silence buffer with 10101100 which represents dop silence +// leave marker zero it will be updated at output, leave lsb zero +void dop_silence_frames(u32_t *ptr, frames_t frames) { + while (frames--) { + *ptr++ = 0x00ACAC00; + *ptr++ = 0x00ACAC00; + } +} + +void dop_init(bool enable, unsigned delay) { + LOCK_O; + output.has_dop = enable; + output.dop_delay = delay; + UNLOCK_O; +} + +#endif // DSD diff --git a/dsd.c b/dsd.c new file mode 100644 index 0000000..32d4795 --- /dev/null +++ b/dsd.c @@ -0,0 +1,628 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// dsd support + +#include "squeezelite.h" + +#if DSD + +// use dsd2pcm from Sebastian Gesemann for conversion to pcm: +#include "./dsd2pcm/dsd2pcm.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; + +#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 BLOCK 4096 // expected size of dsd block +#define BLOCK_FRAMES BLOCK * BYTES_PER_FRAME +#define WRAP_BUF_SIZE 16 + +typedef enum { UNKNOWN=0, DSF, DSDIFF } dsd_type; + +static bool dop = false; // local copy of output.has_dop to avoid holding output lock + +struct dsd { + dsd_type type; + u32_t consume; + u32_t sample_rate; + u32_t channels; + u64_t sample_bytes; + u32_t block_size; + bool lsb_first; + dsd2pcm_ctx *dsd2pcm_ctx[2]; + float *transfer[2]; +}; + +static struct dsd *d; + +static u64_t unpack64be(const u8_t *p) { + return + (u64_t)p[0] << 56 | (u64_t)p[1] << 48 | (u64_t)p[2] << 40 | (u64_t)p[3] << 32 | + (u64_t)p[4] << 24 | (u64_t)p[5] << 16 | (u64_t)p[6] << 8 | (u64_t)p[7]; +} + +static u64_t unpack64le(const u8_t *p) { + return + (u64_t)p[7] << 56 | (u64_t)p[6] << 48 | (u64_t)p[5] << 40 | (u64_t)p[4] << 32 | + (u64_t)p[3] << 24 | (u64_t)p[2] << 16 | (u64_t)p[1] << 8 | (u64_t)p[0]; +} + +static u32_t unpack32le(const u8_t *p) { + return + (u32_t)p[3] << 24 | (u32_t)p[2] << 16 | (u32_t)p[1] << 8 | (u32_t)p[0]; +} + +static int _read_header(void) { + unsigned bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + s32_t consume; + + if (!d->type && bytes >= 4) { + if (!memcmp(streambuf->readp, "FRM8", 4)) { + d->type = DSDIFF; + } else if (!memcmp(streambuf->readp, "DSD ", 4)) { + d->type = DSF; + } else { + LOG_WARN("bad type"); + return -1; + } + } + + while (bytes >= 16) { + char id[5]; + u64_t len = d->type == DSDIFF ? unpack64be(streambuf->readp + 4) : unpack64le(streambuf->readp + 4); + memcpy(id, streambuf->readp, 4); + id[4] = '\0'; + consume = 0; + + if (d->type == DSDIFF) { + if (!strcmp(id, "FRM8")) { + if (!memcmp(streambuf->readp + 12, "DSD ", 4)) { + consume = 16; // read into + } else { + LOG_WARN("bad dsdiff FRM8"); + return -1; + } + } + if (!strcmp(id, "PROP") && !memcmp(streambuf->readp + 12, "SND ", 4)) { + consume = 16; // read into + } + if (!strcmp(id, "FVER")) { + LOG_INFO("DSDIFF version: %u.%u.%u.%u", *(streambuf->readp + 12), *(streambuf->readp + 13), + *(streambuf->readp + 14), *(streambuf->readp + 15)); + } + if (!strcmp(id, "FS ")) { + d->sample_rate = unpackN((void *)(streambuf->readp + 12)); + LOG_INFO("sample rate: %u", d->sample_rate); + } + if (!strcmp(id, "CHNL")) { + d->channels = unpackn((void *)(streambuf->readp + 12)); + LOG_INFO("channels: %u", d->channels); + } + if (!strcmp(id, "DSD ")) { + LOG_INFO("found dsd len: " FMT_u64, len); + d->sample_bytes = len; + _buf_inc_readp(streambuf, 12); + bytes -= 12; + return 1; // got to the audio + } + } + + if (d->type == DSF) { + if (!strcmp(id, "fmt ")) { + if (bytes >= len && bytes >= 52) { + u32_t version = unpack32le((void *)(streambuf->readp + 12)); + u32_t format = unpack32le((void *)(streambuf->readp + 16)); + LOG_INFO("DSF version: %u format: %u", version, format); + if (format != 0) { + LOG_WARN("only support DSD raw format"); + return -1; + } + d->channels = unpack32le((void *)(streambuf->readp + 24)); + d->sample_rate = unpack32le((void *)(streambuf->readp + 28)); + d->lsb_first = (unpack32le((void *)(streambuf->readp + 32)) == 1); + d->sample_bytes = unpack64le((void *)(streambuf->readp + 36)) / 8; + d->block_size = unpack32le((void *)(streambuf->readp + 44)); + LOG_INFO("channels: %u", d->channels); + LOG_INFO("sample rate: %u", d->sample_rate); + LOG_INFO("lsb first: %u", d->lsb_first); + LOG_INFO("sample bytes: " FMT_u64, d->sample_bytes); + LOG_INFO("block size: %u", d->block_size); + } else { + consume = -1; // come back later + } + } + if (!strcmp(id, "data")) { + LOG_INFO("found dsd len: " FMT_u64, len); + _buf_inc_readp(streambuf, 12); + bytes -= 12; + return 1; // got to the audio + } + } + + // default to consuming whole chunk + if (!consume) { + consume = (s32_t)((d->type == DSDIFF) ? len + 12 : len); + } + + if (bytes >= consume) { + LOG_DEBUG("id: %s len: " FMT_u64 " consume: %d", id, len, consume); + _buf_inc_readp(streambuf, consume); + bytes -= consume; + } else if (consume > 0) { + LOG_DEBUG("id: %s len: " FMT_u64 " consume: %d - partial consume: %u", id, len, consume, bytes); + _buf_inc_readp(streambuf, bytes); + d->consume = consume - bytes; + break; + } else { + break; + } + } + + return 0; +} + +static decode_state _decode_dsf(void) { + + // samples in streambuf are interleaved on block basis + // we transfer whole blocks for all channels in one call and so itterate the while loop below to handle wraps + + unsigned bytes = _buf_used(streambuf); + unsigned block_left = d->block_size; + + unsigned bytes_per_frame = dop ? 2 : 1; + + if (bytes < d->block_size * d->channels) { + LOG_INFO("stream too short"); // this can occur when scanning the track + return DECODE_COMPLETE; + } + + IF_PROCESS( + process.in_frames = 0; + ); + + while (block_left) { + + frames_t frames, out, count; + unsigned bytes_read; + + u8_t *iptrl = (u8_t *)streambuf->readp; + u8_t *iptrr = (u8_t *)streambuf->readp + d->block_size; + u32_t *optr; + + if (iptrr >= streambuf->wrap) { + iptrr -= streambuf->size; + } + + bytes = min(block_left, min(streambuf->wrap - iptrl, streambuf->wrap - iptrr)); + + IF_DIRECT( + out = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME; + optr = (u32_t *)outputbuf->writep; + ); + IF_PROCESS( + out = process.max_in_frames - process.in_frames; + optr = (u32_t *)(process.inbuf + process.in_frames * BYTES_PER_FRAME); + ); + + frames = min(bytes, d->sample_bytes) / bytes_per_frame; + if (frames == 0) { + if (dop && d->sample_bytes == 1 && bytes >= 2) { + // 1 byte left add a byte of silence and play + *(iptrl + 1) = *(iptrr + 1) = 0x69; + frames = 1; + } else { + // should not get here due to wrapping m/2 for dop should never result in 0 as header len is always even + LOG_INFO("frames got to zero"); + return DECODE_COMPLETE; + } + } + + frames = min(frames, out); + frames = min(frames, BLOCK); + bytes_read = frames * bytes_per_frame; + + count = frames; + + if (dop) { + + if (d->channels == 1) { + if (d->lsb_first) { + while (count--) { + *(optr++) = dsd2pcm_bitreverse[*(iptrl)] << 16 | dsd2pcm_bitreverse[*(iptrl+1)] << 8; + *(optr++) = dsd2pcm_bitreverse[*(iptrl)] << 16 | dsd2pcm_bitreverse[*(iptrl+1)] << 8; + iptrl += 2; + } + } else { + while (count--) { + *(optr++) = *(iptrl) << 16 | *(iptrl+1) << 8; + *(optr++) = *(iptrl) << 16 | *(iptrl+1) << 8; + iptrl += 2; + } + } + } else { + if (d->lsb_first) { + while (count--) { + *(optr++) = dsd2pcm_bitreverse[*(iptrl)] << 16 | dsd2pcm_bitreverse[*(iptrl+1)] << 8; + *(optr++) = dsd2pcm_bitreverse[*(iptrr)] << 16 | dsd2pcm_bitreverse[*(iptrr+1)] << 8; + iptrl += 2; + iptrr += 2; + } + } else { + while (count--) { + *(optr++) = *(iptrl) << 16 | *(iptrl+1) << 8; + *(optr++) = *(iptrr) << 16 | *(iptrr+1) << 8; + iptrl += 2; + iptrr += 2; + } + } + } + + } else { + + if (d->channels == 1) { + float *iptrf = d->transfer[0]; + dsd2pcm_translate(d->dsd2pcm_ctx[0], frames, iptrl, 1, d->lsb_first, iptrf, 1); + while (count--) { + double scaled = *iptrf++ * 0x7fffffff; + if (scaled > 2147483647.0) scaled = 2147483647.0; + if (scaled < -2147483648.0) scaled = -2147483648.0; + *optr++ = (s32_t)scaled; + *optr++ = (s32_t)scaled; + } + } else { + float *iptrfl = d->transfer[0]; + float *iptrfr = d->transfer[1]; + dsd2pcm_translate(d->dsd2pcm_ctx[0], frames, iptrl, 1, d->lsb_first, iptrfl, 1); + dsd2pcm_translate(d->dsd2pcm_ctx[1], frames, iptrr, 1, d->lsb_first, iptrfr, 1); + while (count--) { + double scaledl = *iptrfl++ * 0x7fffffff; + double scaledr = *iptrfr++ * 0x7fffffff; + if (scaledl > 2147483647.0) scaledl = 2147483647.0; + if (scaledl < -2147483648.0) scaledl = -2147483648.0; + if (scaledr > 2147483647.0) scaledr = 2147483647.0; + if (scaledr < -2147483648.0) scaledr = -2147483648.0; + *optr++ = (s32_t)scaledl; + *optr++ = (s32_t)scaledr; + } + } + + } + + _buf_inc_readp(streambuf, bytes_read); + + block_left -= bytes_read; + + if (d->sample_bytes > bytes_read) { + d->sample_bytes -= bytes_read; + } else { + LOG_INFO("end of track samples"); + block_left = 0; + d->sample_bytes = 0; + } + + IF_DIRECT( + _buf_inc_writep(outputbuf, frames * BYTES_PER_FRAME); + ); + IF_PROCESS( + process.in_frames += frames; + ); + + LOG_SDEBUG("write %u frames", frames); + } + + // skip the other channel blocks + // the right channel has already been read and is guarenteed to be in streambuf so can be skipped immediately + if (d->channels > 1) { + _buf_inc_readp(streambuf, d->block_size); + } + if (d->channels > 2) { + d->consume = d->block_size * (d->channels - 2); + } + + return DECODE_RUNNING; +} + +static decode_state _decode_dsdiff(void) { + + // samples in streambuf are interleaved on byte per channel + // we process as little as necessary per call and only need to handle frames wrapping round streambuf + + unsigned bytes_per_frame, bytes_read; + frames_t out, frames, count; + u8_t *iptr; + u32_t *optr; + u8_t tmp[WRAP_BUF_SIZE]; + + unsigned 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 (dop) { + bytes_per_frame = d->channels * 2; + } else { + bytes_per_frame = d->channels; + out = min(out, BLOCK); + } + + frames = min(min(bytes, d->sample_bytes) / bytes_per_frame, out); + bytes_read = frames * bytes_per_frame; + + iptr = (u8_t *)streambuf->readp; + + IF_DIRECT( + optr = (u32_t *)outputbuf->writep; + ); + IF_PROCESS( + optr = (u32_t *)process.inbuf; + ); + + // handle wrap around end of streambuf and partial dop frame at end of stream + if (!frames && bytes < bytes_per_frame) { + memset(tmp, 0x69, WRAP_BUF_SIZE); // 0x69 = dsd silence + memcpy(tmp, streambuf->readp, bytes); + if (_buf_used(streambuf) > bytes_per_frame) { + memcpy(tmp + bytes, streambuf->buf, bytes_per_frame - bytes); + bytes_read = bytes_per_frame; + } else { + bytes_read = bytes; + } + iptr = tmp; + frames = 1; + } + + count = frames; + + if (dop) { + + if (d->channels == 1) { + while (count--) { + *(optr++) = *(iptr) << 16 | *(iptr+1) << 8; + *(optr++) = *(iptr) << 16 | *(iptr+1) << 8; + iptr += bytes_per_frame; + } + } else { + while (count--) { + *(optr++) = *(iptr ) << 16 | *(iptr + d->channels) << 8; + *(optr++) = *(iptr+1) << 16 | *(iptr + d->channels + 1) << 8; + iptr += bytes_per_frame; + } + } + + } else { + + if (d->channels == 1) { + float *iptrf = d->transfer[0]; + dsd2pcm_translate(d->dsd2pcm_ctx[0], frames, iptr, 1, 0, iptrf, 1); + while (count--) { + double scaled = *iptrf++ * 0x7fffffff; + if (scaled > 2147483647.0) scaled = 2147483647.0; + if (scaled < -2147483648.0) scaled = -2147483648.0; + *optr++ = (s32_t)scaled; + *optr++ = (s32_t)scaled; + } + } else { + float *iptrfl = d->transfer[0]; + float *iptrfr = d->transfer[1]; + dsd2pcm_translate(d->dsd2pcm_ctx[0], frames, iptr, d->channels, 0, iptrfl, 1); + dsd2pcm_translate(d->dsd2pcm_ctx[1], frames, iptr + 1, d->channels, 0, iptrfr, 1); + while (count--) { + double scaledl = *iptrfl++ * 0x7fffffff; + double scaledr = *iptrfr++ * 0x7fffffff; + if (scaledl > 2147483647.0) scaledl = 2147483647.0; + if (scaledl < -2147483648.0) scaledl = -2147483648.0; + if (scaledr > 2147483647.0) scaledr = 2147483647.0; + if (scaledr < -2147483648.0) scaledr = -2147483648.0; + *optr++ = (s32_t)scaledl; + *optr++ = (s32_t)scaledr; + } + } + + } + + _buf_inc_readp(streambuf, bytes_read); + + if (d->sample_bytes > bytes_read) { + d->sample_bytes -= bytes_read; + } else { + LOG_INFO("end of track samples"); + d->sample_bytes = 0; + } + + IF_DIRECT( + _buf_inc_writep(outputbuf, frames * BYTES_PER_FRAME); + ); + IF_PROCESS( + process.in_frames = frames; + ); + + LOG_SDEBUG("write %u frames", frames); + + return DECODE_RUNNING; +} + + +static decode_state dsd_decode(void) { + decode_state ret; + + LOCK_S; + + if ((stream.state <= DISCONNECT && !_buf_used(streambuf)) || (!decode.new_stream && d->sample_bytes == 0)) { + UNLOCK_S; + return DECODE_COMPLETE; + } + + if (d->consume) { + unsigned consume = min(d->consume, min(_buf_used(streambuf), _buf_cont_read(streambuf))); + LOG_DEBUG("consume: %u of %u", consume, d->consume); + _buf_inc_readp(streambuf, consume); + d->consume -= consume; + if (d->consume) { + UNLOCK_S; + return DECODE_RUNNING; + } + } + + if (decode.new_stream) { + int r = _read_header(); + if (r < 1) { + UNLOCK_S; + return DECODE_ERROR; + } + if (r == 0) { + UNLOCK_S; + return DECODE_RUNNING; + } + // otherwise got to start of audio + + LOCK_O; + + LOG_INFO("setting track_start"); + output.track_start = outputbuf->writep; + + dop = output.has_dop; + + if (dop && d->sample_rate / 16 > output.supported_rates[0]) { + LOG_INFO("DOP sample rate too high for device - converting to PCM"); + dop = false; + } + + if (dop) { + LOG_INFO("DOP output"); + output.next_dop = true; + output.next_sample_rate = d->sample_rate / 16; + output.fade = FADE_INACTIVE; + } else { + LOG_INFO("DSD to PCM output"); + output.next_dop = false; + output.next_sample_rate = decode_newstream(d->sample_rate / 8, output.supported_rates); + if (output.fade_mode) _checkfade(true); + } + + decode.new_stream = false; + + UNLOCK_O; + } + + LOCK_O_direct; + + switch (d->type) { + case DSF: + ret = _decode_dsf(); + break; + case DSDIFF: + ret = _decode_dsdiff(); + break; + default: + ret = DECODE_ERROR; + } + + UNLOCK_O_direct; + UNLOCK_S; + + return ret; +} + +static void dsd_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + d->type = UNKNOWN; + + if (!d->dsd2pcm_ctx[0]) { + d->dsd2pcm_ctx[0] = dsd2pcm_init(); + d->dsd2pcm_ctx[1] = dsd2pcm_init(); + } else { + dsd2pcm_reset(d->dsd2pcm_ctx[0]); + dsd2pcm_reset(d->dsd2pcm_ctx[1]); + } + if (!d->transfer[1]) { + d->transfer[0] = malloc(sizeof(float) * BLOCK); + d->transfer[1] = malloc(sizeof(float) * BLOCK); + } +} + +static void dsd_close(void) { + if (d->dsd2pcm_ctx[0]) { + dsd2pcm_destroy(d->dsd2pcm_ctx[0]); + dsd2pcm_destroy(d->dsd2pcm_ctx[1]); + d->dsd2pcm_ctx[0] = NULL; + d->dsd2pcm_ctx[1] = NULL; + } + if (d->transfer[0]) { + free(d->transfer[0]); + free(d->transfer[1]); + d->transfer[0] = NULL; + d->transfer[1] = NULL; + } +} + +struct codec *register_dsd(void) { + static struct codec ret = { + 'd', // id + "dsf,dff", // types + BLOCK * 2, // min read + BLOCK_FRAMES,// min space + dsd_open, // open + dsd_close, // close + dsd_decode, // decode + }; + + d = malloc(sizeof(struct dsd)); + if (!d) { + return NULL; + } + + memset(d, 0, sizeof(struct dsd)); + + dsd2pcm_precalc(); + + LOG_INFO("using dsd to decode dsf,dff"); + return &ret; +} + +#endif // DSD diff --git a/dsd2pcm/LICENSE.txt b/dsd2pcm/LICENSE.txt new file mode 100644 index 0000000..36d5532 --- /dev/null +++ b/dsd2pcm/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright 2009, 2011 Sebastian Gesemann. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY SEBASTIAN GESEMANN ''AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEBASTIAN GESEMANN OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Sebastian Gesemann. diff --git a/dsd2pcm/dsd2pcm.c b/dsd2pcm/dsd2pcm.c new file mode 100644 index 0000000..e690c88 --- /dev/null +++ b/dsd2pcm/dsd2pcm.c @@ -0,0 +1,224 @@ +/* + +Copyright 2009, 2011 Sebastian Gesemann. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY SEBASTIAN GESEMANN ''AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEBASTIAN GESEMANN OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Sebastian Gesemann. + +---- + +Additions (c) Adrian Smith, 2013 under same licence terms: +- expose bitreverse array as dsd2pcm_bitreverse +- expose precalc function as dsd2pcm_precalc to allow it to be initalised + + */ + +#include +#include + +#include "dsd2pcm.h" + +#define HTAPS 48 /* number of FIR constants */ +#define FIFOSIZE 16 /* must be a power of two */ +#define FIFOMASK (FIFOSIZE-1) /* bit mask for FIFO offsets */ +#define CTABLES ((HTAPS+7)/8) /* number of "8 MACs" lookup tables */ + +#if FIFOSIZE*8 < HTAPS*2 +#error "FIFOSIZE too small" +#endif + +/* + * Properties of this 96-tap lowpass filter when applied on a signal + * with sampling rate of 44100*64 Hz: + * + * () has a delay of 17 microseconds. + * + * () flat response up to 48 kHz + * + * () if you downsample afterwards by a factor of 8, the + * spectrum below 70 kHz is practically alias-free. + * + * () stopband rejection is about 160 dB + * + * The coefficient tables ("ctables") take only 6 Kibi Bytes and + * should fit into a modern processor's fast cache. + */ + +/* + * The 2nd half (48 coeffs) of a 96-tap symmetric lowpass filter + */ +static const double htaps[HTAPS] = { + 0.09950731974056658, + 0.09562845727714668, + 0.08819647126516944, + 0.07782552527068175, + 0.06534876523171299, + 0.05172629311427257, + 0.0379429484910187, + 0.02490921351762261, + 0.0133774746265897, + 0.003883043418804416, + -0.003284703416210726, + -0.008080250212687497, + -0.01067241812471033, + -0.01139427235000863, + -0.0106813877974587, + -0.009007905078766049, + -0.006828859761015335, + -0.004535184322001496, + -0.002425035959059578, + -0.0006922187080790708, + 0.0005700762133516592, + 0.001353838005269448, + 0.001713709169690937, + 0.001742046839472948, + 0.001545601648013235, + 0.001226696225277855, + 0.0008704322683580222, + 0.0005381636200535649, + 0.000266446345425276, + 7.002968738383528e-05, + -5.279407053811266e-05, + -0.0001140625650874684, + -0.0001304796361231895, + -0.0001189970287491285, + -9.396247155265073e-05, + -6.577634378272832e-05, + -4.07492895872535e-05, + -2.17407957554587e-05, + -9.163058931391722e-06, + -2.017460145032201e-06, + 1.249721855219005e-06, + 2.166655190537392e-06, + 1.930520892991082e-06, + 1.319400334374195e-06, + 7.410039764949091e-07, + 3.423230509967409e-07, + 1.244182214744588e-07, + 3.130441005359396e-08 +}; + +static float ctables[CTABLES][256]; +unsigned char dsd2pcm_bitreverse[256]; +static int precalculated = 0; + +void dsd2pcm_precalc(void) +{ + int t, e, m, k; + double acc; + if (precalculated) return; + for (t=0, e=0; t<256; ++t) { + dsd2pcm_bitreverse[t] = e; + for (m=128; m && !((e^=m)&m); m>>=1) + ; + } + for (t=0; t8) k=8; + for (e=0; e<256; ++e) { + acc = 0.0; + for (m=0; m> (7-m)) & 1)*2-1) * htaps[t*8+m]; + } + ctables[CTABLES-1-t][e] = (float)acc; + } + } + precalculated = 1; +} + +struct dsd2pcm_ctx_s +{ + unsigned char fifo[FIFOSIZE]; + unsigned fifopos; +}; + +extern dsd2pcm_ctx* dsd2pcm_init() +{ + dsd2pcm_ctx* ptr; + if (!precalculated) dsd2pcm_precalc(); + ptr = (dsd2pcm_ctx*) malloc(sizeof(dsd2pcm_ctx)); + if (ptr) dsd2pcm_reset(ptr); + return ptr; +} + +extern void dsd2pcm_destroy(dsd2pcm_ctx* ptr) +{ + free(ptr); +} + +extern dsd2pcm_ctx* dsd2pcm_clone(dsd2pcm_ctx* ptr) +{ + dsd2pcm_ctx* p2; + p2 = (dsd2pcm_ctx*) malloc(sizeof(dsd2pcm_ctx)); + if (p2) { + memcpy(p2,ptr,sizeof(dsd2pcm_ctx)); + } + return p2; +} + +extern void dsd2pcm_reset(dsd2pcm_ctx* ptr) +{ + int i; + for (i=0; ififo[i] = 0x69; /* my favorite silence pattern */ + ptr->fifopos = 0; + /* 0x69 = 01101001 + * This pattern "on repeat" makes a low energy 352.8 kHz tone + * and a high energy 1.0584 MHz tone which should be filtered + * out completely by any playback system --> silence + */ +} + +extern void dsd2pcm_translate( + dsd2pcm_ctx* ptr, + size_t samples, + const unsigned char *src, ptrdiff_t src_stride, + int lsbf, + float *dst, ptrdiff_t dst_stride) +{ + unsigned ffp; + unsigned i; + unsigned bite1, bite2; + unsigned char* p; + double acc; + ffp = ptr->fifopos; + lsbf = lsbf ? 1 : 0; + while (samples-- > 0) { + bite1 = *src & 0xFFu; + if (lsbf) bite1 = dsd2pcm_bitreverse[bite1]; + ptr->fifo[ffp] = bite1; src += src_stride; + p = ptr->fifo + ((ffp-CTABLES) & FIFOMASK); + *p = dsd2pcm_bitreverse[*p & 0xFF]; + acc = 0; + for (i=0; ififo[(ffp -i) & FIFOMASK] & 0xFF; + bite2 = ptr->fifo[(ffp-(CTABLES*2-1)+i) & FIFOMASK] & 0xFF; + acc += ctables[i][bite1] + ctables[i][bite2]; + } + *dst = (float)acc; dst += dst_stride; + ffp = (ffp + 1) & FIFOMASK; + } + ptr->fifopos = ffp; +} + diff --git a/dsd2pcm/dsd2pcm.h b/dsd2pcm/dsd2pcm.h new file mode 100644 index 0000000..2c475f8 --- /dev/null +++ b/dsd2pcm/dsd2pcm.h @@ -0,0 +1,108 @@ +/* + +Copyright 2009, 2011 Sebastian Gesemann. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY SEBASTIAN GESEMANN ''AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEBASTIAN GESEMANN OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Sebastian Gesemann. + +---- + +Marked additions (c) Adrian Smith, 2013 under same licence terms + + */ + +#ifndef DSD2PCM_H_INCLUDED +#define DSD2PCM_H_INCLUDED + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct dsd2pcm_ctx_s; + +typedef struct dsd2pcm_ctx_s dsd2pcm_ctx; + +/** + * initializes a "dsd2pcm engine" for one channel + * (precomputes tables and allocates memory) + * + * This is the only function that is not thread-safe in terms of the + * POSIX thread-safety definition because it modifies global state + * (lookup tables are computed during the first call) + */ +extern dsd2pcm_ctx* dsd2pcm_init(void); + +/** + * deinitializes a "dsd2pcm engine" + * (releases memory, don't forget!) + */ +extern void dsd2pcm_destroy(dsd2pcm_ctx *ctx); + +/** + * clones the context and returns a pointer to the + * newly allocated copy + */ +extern dsd2pcm_ctx* dsd2pcm_clone(dsd2pcm_ctx *ctx); + +/** + * resets the internal state for a fresh new stream + */ +extern void dsd2pcm_reset(dsd2pcm_ctx *ctx); + +/** + * "translates" a stream of octets to a stream of floats + * (8:1 decimation) + * @param ctx -- pointer to abstract context (buffers) + * @param samples -- number of octets/samples to "translate" + * @param src -- pointer to first octet (input) + * @param src_stride -- src pointer increment + * @param lsbitfirst -- bitorder, 0=msb first, 1=lsbfirst + * @param dst -- pointer to first float (output) + * @param dst_stride -- dst pointer increment + */ +extern void dsd2pcm_translate(dsd2pcm_ctx *ctx, + size_t samples, + const unsigned char *src, ptrdiff_t src_stride, + int lsbitfirst, + float *dst, ptrdiff_t dst_stride); + +/** + * Additions by Adrian Smith (c) 2013 for Squeezelite + */ +extern unsigned char dsd2pcm_bitreverse[]; + +extern void dsd2pcm_precalc(void); +/** + * End of addition + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* include guard DSD2PCM_H_INCLUDED */ + diff --git a/faad.c b/faad.c new file mode 100644 index 0000000..96d33ca --- /dev/null +++ b/faad.c @@ -0,0 +1,642 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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" + +#include + +#define WRAPBUF_LEN 2048 + +struct chunk_table { + u32_t sample, offset; +}; + +struct faad { + NeAACDecHandle hAac; + u8_t type; + // following used for mp4 only + u32_t consume; + u32_t pos; + u32_t sample; + u32_t nextchunk; + void *stsc; + u32_t skip; + u64_t samples; + u64_t sttssamples; + bool empty; + struct chunk_table *chunkinfo; + // faad symbols to be dynamically loaded +#if !LINKALL + NeAACDecConfigurationPtr (* NeAACDecGetCurrentConfiguration)(NeAACDecHandle); + unsigned char (* NeAACDecSetConfiguration)(NeAACDecHandle, NeAACDecConfigurationPtr); + NeAACDecHandle (* NeAACDecOpen)(void); + void (* NeAACDecClose)(NeAACDecHandle); + long (* NeAACDecInit)(NeAACDecHandle, unsigned char *, unsigned long, unsigned long *, unsigned char *); + char (* NeAACDecInit2)(NeAACDecHandle, unsigned char *pBuffer, unsigned long, unsigned long *, unsigned char *); + void *(* NeAACDecDecode)(NeAACDecHandle, NeAACDecFrameInfo *, unsigned char *, unsigned long); + char *(* NeAACDecGetErrorMessage)(unsigned char); +#endif +}; + +static struct faad *a; + +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; + +#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 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 IF_DIRECT(x) { x } +#define IF_PROCESS(x) +#endif + +#if LINKALL +#define NEAAC(h, fn, ...) (NeAACDec ## fn)(__VA_ARGS__) +#else +#define NEAAC(h, fn, ...) (h)->NeAACDec##fn(__VA_ARGS__) +#endif + +// minimal code for mp4 file parsing to extract audio config and find media data + +// adapted from faad2/common/mp4ff +u32_t mp4_desc_length(u8_t **buf) { + u8_t b; + u8_t num_bytes = 0; + u32_t length = 0; + + do { + b = **buf; + *buf += 1; + num_bytes++; + length = (length << 7) | (b & 0x7f); + } while ((b & 0x80) && num_bytes < 4); + + return length; +} + +// read mp4 header to extract config data +static int read_mp4_header(unsigned long *samplerate_p, unsigned char *channels_p) { + size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + char type[5]; + u32_t len; + + while (bytes >= 8) { + // count trak to find the first playable one + static unsigned trak, play; + u32_t consume; + + len = unpackN((u32_t *)streambuf->readp); + memcpy(type, streambuf->readp + 4, 4); + type[4] = '\0'; + + if (!strcmp(type, "moov")) { + trak = 0; + play = 0; + } + if (!strcmp(type, "trak")) { + trak++; + } + + // extract audio config from within esds and pass to DecInit2 + if (!strcmp(type, "esds") && bytes > len) { + unsigned config_len; + u8_t *ptr = streambuf->readp + 12; + if (*ptr++ == 0x03) { + mp4_desc_length(&ptr); + ptr += 4; + } else { + ptr += 3; + } + mp4_desc_length(&ptr); + ptr += 13; + if (*ptr++ != 0x05) { + LOG_WARN("error parsing esds"); + return -1; + } + config_len = mp4_desc_length(&ptr); + if (NEAAC(a, Init2, a->hAac, ptr, config_len, samplerate_p, channels_p) == 0) { + LOG_DEBUG("playable aac track: %u", trak); + play = trak; + } + } + + // extract the total number of samples from stts + if (!strcmp(type, "stts") && bytes > len) { + u32_t i; + u8_t *ptr = streambuf->readp + 12; + u32_t entries = unpackN((u32_t *)ptr); + ptr += 4; + for (i = 0; i < entries; ++i) { + u32_t count = unpackN((u32_t *)ptr); + u32_t size = unpackN((u32_t *)(ptr + 4)); + a->sttssamples += count * size; + ptr += 8; + } + LOG_DEBUG("total number of samples contained in stts: " FMT_u64, a->sttssamples); + } + + // stash sample to chunk info, assume it comes before stco + if (!strcmp(type, "stsc") && bytes > len && !a->chunkinfo) { + a->stsc = malloc(len - 12); + if (a->stsc == NULL) { + LOG_WARN("malloc fail"); + return -1; + } + memcpy(a->stsc, streambuf->readp + 12, len - 12); + } + + // build offsets table from stco and stored stsc + if (!strcmp(type, "stco") && bytes > len && play == trak) { + u32_t i; + // extract chunk offsets + u8_t *ptr = streambuf->readp + 12; + u32_t entries = unpackN((u32_t *)ptr); + ptr += 4; + a->chunkinfo = malloc(sizeof(struct chunk_table) * (entries + 1)); + if (a->chunkinfo == NULL) { + LOG_WARN("malloc fail"); + return -1; + } + for (i = 0; i < entries; ++i) { + a->chunkinfo[i].offset = unpackN((u32_t *)ptr); + a->chunkinfo[i].sample = 0; + ptr += 4; + } + a->chunkinfo[i].sample = 0; + a->chunkinfo[i].offset = 0; + // fill in first sample id for each chunk from stored stsc + if (a->stsc) { + u32_t stsc_entries = unpackN((u32_t *)a->stsc); + u32_t sample = 0; + u32_t last = 0, last_samples = 0; + u8_t *ptr = (u8_t *)a->stsc + 4; + while (stsc_entries--) { + u32_t first = unpackN((u32_t *)ptr); + u32_t samples = unpackN((u32_t *)(ptr + 4)); + if (last) { + for (i = last - 1; i < first - 1; ++i) { + a->chunkinfo[i].sample = sample; + sample += last_samples; + } + } + if (stsc_entries == 0) { + for (i = first - 1; i < entries; ++i) { + a->chunkinfo[i].sample = sample; + sample += samples; + } + } + last = first; + last_samples = samples; + ptr += 12; + } + free(a->stsc); + a->stsc = NULL; + } + } + + // found media data, advance to start of first chunk and return + if (!strcmp(type, "mdat")) { + _buf_inc_readp(streambuf, 8); + a->pos += 8; + bytes -= 8; + if (play) { + LOG_DEBUG("type: mdat len: %u pos: %u", len, a->pos); + if (a->chunkinfo && a->chunkinfo[0].offset > a->pos) { + u32_t skip = a->chunkinfo[0].offset - a->pos; + LOG_DEBUG("skipping: %u", skip); + if (skip <= bytes) { + _buf_inc_readp(streambuf, skip); + a->pos += skip; + } else { + a->consume = skip; + } + } + a->sample = a->nextchunk = 1; + return 1; + } else { + LOG_DEBUG("type: mdat len: %u, no playable track found", len); + return -1; + } + } + + // parse key-value atoms within ilst ---- entries to get encoder padding within iTunSMPB entry for gapless + if (!strcmp(type, "----") && bytes > len) { + u8_t *ptr = streambuf->readp + 8; + u32_t remain = len - 8, size; + if (!memcmp(ptr + 4, "mean", 4) && (size = unpackN((u32_t *)ptr)) < remain) { + ptr += size; remain -= size; + } + if (!memcmp(ptr + 4, "name", 4) && (size = unpackN((u32_t *)ptr)) < remain && !memcmp(ptr + 12, "iTunSMPB", 8)) { + ptr += size; remain -= size; + } + if (!memcmp(ptr + 4, "data", 4) && remain > 16 + 48) { + // data is stored as hex strings: 0 start end samples + u32_t b, c; u64_t d; + if (sscanf((const char *)(ptr + 16), "%x %x %x " FMT_x64, &b, &b, &c, &d) == 4) { + LOG_DEBUG("iTunSMPB start: %u end: %u samples: " FMT_u64, b, c, d); + if (a->sttssamples && a->sttssamples < b + c + d) { + LOG_DEBUG("reducing samples as stts count is less"); + d = a->sttssamples - (b + c); + } + a->skip = b; + a->samples = d; + } + } + } + + // default to consuming entire box + consume = len; + + // read into these boxes so reduce consume + if (!strcmp(type, "moov") || !strcmp(type, "trak") || !strcmp(type, "mdia") || !strcmp(type, "minf") || !strcmp(type, "stbl") || + !strcmp(type, "udta") || !strcmp(type, "ilst")) { + consume = 8; + } + // special cases which mix mix data in the enclosing box which we want to read into + if (!strcmp(type, "stsd")) consume = 16; + if (!strcmp(type, "mp4a")) consume = 36; + if (!strcmp(type, "meta")) consume = 12; + + // consume rest of box if it has been parsed (all in the buffer) or is not one we want to parse + if (bytes >= consume) { + LOG_DEBUG("type: %s len: %u consume: %u", type, len, consume); + _buf_inc_readp(streambuf, consume); + a->pos += consume; + bytes -= consume; + } else if ( !(!strcmp(type, "esds") || !strcmp(type, "stts") || !strcmp(type, "stsc") || + !strcmp(type, "stco") || !strcmp(type, "----")) ) { + LOG_DEBUG("type: %s len: %u consume: %u - partial consume: %u", type, len, consume, bytes); + _buf_inc_readp(streambuf, bytes); + a->pos += bytes; + a->consume = consume - bytes; + break; + } else { + break; + } + } + + return 0; +} + +static decode_state faad_decode(void) { + size_t bytes_total; + size_t bytes_wrap; + NeAACDecFrameInfo info; + s32_t *iptr; + bool endstream; + frames_t frames; + + LOCK_S; + bytes_total = _buf_used(streambuf); + bytes_wrap = min(bytes_total, _buf_cont_read(streambuf)); + + if (stream.state <= DISCONNECT && !bytes_total) { + UNLOCK_S; + return DECODE_COMPLETE; + } + + if (a->consume) { + u32_t consume = min(a->consume, bytes_wrap); + LOG_DEBUG("consume: %u of %u", consume, a->consume); + _buf_inc_readp(streambuf, consume); + a->pos += consume; + a->consume -= consume; + UNLOCK_S; + return DECODE_RUNNING; + } + + if (decode.new_stream) { + int found = 0; + static unsigned char channels; + static unsigned long samplerate; + + if (a->type == '2') { + + // adts stream - seek for header + while (bytes_wrap >= 2 && (*(streambuf->readp) != 0xFF || (*(streambuf->readp + 1) & 0xF6) != 0xF0)) { + _buf_inc_readp(streambuf, 1); + bytes_total--; + bytes_wrap--; + } + + if (bytes_wrap >= 2) { + long n = NEAAC(a, Init, a->hAac, streambuf->readp, bytes_wrap, &samplerate, &channels); + if (n < 0) { + found = -1; + } else { + _buf_inc_readp(streambuf, n); + found = 1; + } + } + + } else { + + // mp4 - read header + found = read_mp4_header(&samplerate, &channels); + } + + if (found == 1) { + + LOG_INFO("samplerate: %u channels: %u", samplerate, channels); + bytes_total = _buf_used(streambuf); + bytes_wrap = min(bytes_total, _buf_cont_read(streambuf)); + + LOCK_O; + LOG_INFO("setting track_start"); + output.next_sample_rate = decode_newstream(samplerate, output.supported_rates); + IF_DSD( output.next_dop = false; ) + output.track_start = outputbuf->writep; + if (output.fade_mode) _checkfade(true); + decode.new_stream = false; + UNLOCK_O; + + } else if (found == -1) { + + LOG_WARN("error reading stream header"); + UNLOCK_S; + return DECODE_ERROR; + + } else { + + // not finished header parsing come back next time + UNLOCK_S; + return DECODE_RUNNING; + } + } + + if (bytes_wrap < WRAPBUF_LEN && bytes_total > WRAPBUF_LEN) { + + // make a local copy of frames which may have wrapped round the end of streambuf + u8_t buf[WRAPBUF_LEN]; + memcpy(buf, streambuf->readp, bytes_wrap); + memcpy(buf + bytes_wrap, streambuf->buf, WRAPBUF_LEN - bytes_wrap); + + iptr = NEAAC(a, Decode, a->hAac, &info, buf, WRAPBUF_LEN); + + } else { + + iptr = NEAAC(a, Decode, a->hAac, &info, streambuf->readp, bytes_wrap); + } + + if (info.error) { + LOG_WARN("error: %u %s", info.error, NEAAC(a, GetErrorMessage, info.error)); + } + + endstream = false; + + // mp4 end of chunk - skip to next offset + if (a->chunkinfo && a->chunkinfo[a->nextchunk].offset && a->sample++ == a->chunkinfo[a->nextchunk].sample) { + + if (a->chunkinfo[a->nextchunk].offset > a->pos) { + u32_t skip = a->chunkinfo[a->nextchunk].offset - a->pos; + if (skip != info.bytesconsumed) { + LOG_DEBUG("skipping to next chunk pos: %u consumed: %u != skip: %u", a->pos, info.bytesconsumed, skip); + } + if (bytes_total >= skip) { + _buf_inc_readp(streambuf, skip); + a->pos += skip; + } else { + a->consume = skip; + } + a->nextchunk++; + } else { + LOG_ERROR("error: need to skip backwards!"); + endstream = true; + } + + // adts and mp4 when not at end of chunk + } else if (info.bytesconsumed != 0) { + + _buf_inc_readp(streambuf, info.bytesconsumed); + a->pos += info.bytesconsumed; + + // error which doesn't advance streambuf - end + } else { + endstream = true; + } + + UNLOCK_S; + + if (endstream) { + LOG_WARN("unable to decode further"); + return DECODE_ERROR; + } + + if (!info.samples) { + a->empty = true; + return DECODE_RUNNING; + } + + frames = info.samples / info.channels; + + if (a->skip) { + u32_t skip; + if (a->empty) { + a->empty = false; + a->skip -= frames; + LOG_DEBUG("gapless: first frame empty, skipped %u frames at start", frames); + } + skip = min(frames, a->skip); + LOG_DEBUG("gapless: skipping %u frames at start", skip); + frames -= skip; + a->skip -= skip; + iptr += skip * info.channels; + } + + if (a->samples) { + if (a->samples < frames) { + LOG_DEBUG("gapless: trimming %u frames from end", frames - a->samples); + frames = (frames_t)a->samples; + } + a->samples -= frames; + } + + LOG_SDEBUG("write %u frames", frames); + + LOCK_O_direct; + + while (frames > 0) { + frames_t f; + frames_t count; + s32_t *optr; + + IF_DIRECT( + f = _buf_cont_write(outputbuf) / BYTES_PER_FRAME; + optr = (s32_t *)outputbuf->writep; + ); + IF_PROCESS( + f = process.max_in_frames; + optr = (s32_t *)process.inbuf; + ); + + f = min(f, frames); + count = f; + + if (info.channels == 2) { + while (count--) { + *optr++ = *iptr++ << 8; + *optr++ = *iptr++ << 8; + } + } else if (info.channels == 1) { + while (count--) { + *optr++ = *iptr << 8; + *optr++ = *iptr++ << 8; + } + } else { + LOG_WARN("unsupported number of channels"); + } + + frames -= f; + + IF_DIRECT( + _buf_inc_writep(outputbuf, f * BYTES_PER_FRAME); + ); + IF_PROCESS( + process.in_frames = f; + if (frames) LOG_ERROR("unhandled case"); + ); + } + + UNLOCK_O_direct; + + return DECODE_RUNNING; +} + +static void faad_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + NeAACDecConfigurationPtr conf; + + LOG_INFO("opening %s stream", size == '2' ? "adts" : "mp4"); + + a->type = size; + a->pos = a->consume = a->sample = a->nextchunk = 0; + + if (a->chunkinfo) { + free(a->chunkinfo); + } + if (a->stsc) { + free(a->stsc); + } + a->chunkinfo = NULL; + a->stsc = NULL; + a->skip = 0; + a->samples = 0; + a->sttssamples = 0; + a->empty = false; + + if (a->hAac) { + NEAAC(a, Close, a->hAac); + } + a->hAac = NEAAC(a, Open); + + conf = NEAAC(a, GetCurrentConfiguration, a->hAac); + + conf->outputFormat = FAAD_FMT_24BIT; + conf->downMatrix = 1; + + if (!NEAAC(a, SetConfiguration, a->hAac, conf)) { + LOG_WARN("error setting config"); + }; +} + +static void faad_close(void) { + NEAAC(a, Close, a->hAac); + a->hAac = NULL; + if (a->chunkinfo) { + free(a->chunkinfo); + a->chunkinfo = NULL; + } + if (a->stsc) { + free(a->stsc); + a->stsc = NULL; + } +} + +static bool load_faad() { +#if !LINKALL + void *handle = dlopen(LIBFAAD, RTLD_NOW); + char *err; + + if (!handle) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + a->NeAACDecGetCurrentConfiguration = dlsym(handle, "NeAACDecGetCurrentConfiguration"); + a->NeAACDecSetConfiguration = dlsym(handle, "NeAACDecSetConfiguration"); + a->NeAACDecOpen = dlsym(handle, "NeAACDecOpen"); + a->NeAACDecClose = dlsym(handle, "NeAACDecClose"); + a->NeAACDecInit = dlsym(handle, "NeAACDecInit"); + a->NeAACDecInit2 = dlsym(handle, "NeAACDecInit2"); + a->NeAACDecDecode = dlsym(handle, "NeAACDecDecode"); + a->NeAACDecGetErrorMessage = dlsym(handle, "NeAACDecGetErrorMessage"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBFAAD""); +#endif + + return true; +} + +struct codec *register_faad(void) { + static struct codec ret = { + 'a', // id + "aac", // types + WRAPBUF_LEN, // min read + 20480, // min space + faad_open, // open + faad_close, // close + faad_decode, // decode + }; + + a = malloc(sizeof(struct faad)); + if (!a) { + return NULL; + } + + a->hAac = NULL; + a->chunkinfo = NULL; + a->stsc = NULL; + + if (!load_faad()) { + return NULL; + } + + LOG_INFO("using faad to decode aac"); + return &ret; +} diff --git a/ffmpeg.c b/ffmpeg.c new file mode 100644 index 0000000..279f31d --- /dev/null +++ b/ffmpeg.c @@ -0,0 +1,738 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 FFMPEG + +#include +#include + +#define READ_SIZE 4096 * 4 // this is large enough to ensure ffmpeg always gets new data when decode is called +#define WRITE_SIZE 256 * 1024 // FIXME - make smaller, but still to absorb max wma output + +// FIXME - do we need to align these params as per ffmpeg on i386? +#define attribute_align_arg + +struct ff_s { + // state for ffmpeg decoder + bool wma; + u8_t wma_mmsh; + u8_t wma_playstream; + u8_t wma_metadatastream; + u8_t *readbuf; + bool end_of_stream; + AVInputFormat *input_format; + AVFormatContext *formatC; + AVCodecContext *codecC; + AVFrame *frame; + AVPacket *avpkt; + unsigned mmsh_bytes_left; + unsigned mmsh_bytes_pad; + unsigned mmsh_packet_len; +#if !LINKALL + // ffmpeg symbols to be dynamically loaded from libavcodec + unsigned (* avcodec_version)(void); + AVCodec * (* avcodec_find_decoder)(int); + int attribute_align_arg (* avcodec_open2)(AVCodecContext *, const AVCodec *, AVDictionary **); + AVFrame * (* avcodec_alloc_frame)(void); + void (* avcodec_free_frame)(AVFrame **); + int attribute_align_arg (* avcodec_decode_audio4)(AVCodecContext *, AVFrame *, int *, const AVPacket *); + // ffmpeg symbols to be dynamically loaded from libavformat + unsigned (* avformat_version)(void); + AVFormatContext * (* avformat_alloc_context)(void); + void (* avformat_free_context)(AVFormatContext *); + int (* avformat_open_input)(AVFormatContext **, const char *, AVInputFormat *, AVDictionary **); + int (* avformat_find_stream_info)(AVFormatContext *, AVDictionary **); + AVIOContext * (* avio_alloc_context)(unsigned char *, int, int, void *, + int (*read_packet)(void *, uint8_t *, int), int (*write_packet)(void *, uint8_t *, int), int64_t (*seek)(void *, int64_t, int)); + void (* av_init_packet)(AVPacket *); + void (* av_free_packet)(AVPacket *); + int (* av_read_frame)(AVFormatContext *, AVPacket *); + AVInputFormat * (* av_find_input_format)(const char *); + void (* av_register_all)(void); + // ffmpeg symbols to be dynamically loaded from libavutil + unsigned (* avutil_version)(void); + void (* av_log_set_callback)(void (*)(void*, int, const char*, va_list)); + void (* av_log_set_level)(int); + int (* av_strerror)(int, char *, size_t); + void * (* av_malloc)(size_t); + void (* av_freep)(void *); +#endif +}; + +static struct ff_s *ff; + +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; + +#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 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 IF_DIRECT(x) { x } +#define IF_PROCESS(x) +#endif + +#if LINKALL +#define AV(h, fn, ...) (av_ ## fn)(__VA_ARGS__) +#define AVIO(h, fn, ...) (avio_ ## fn)(__VA_ARGS__) +#define AVCODEC(h, fn, ...) (avcodec_ ## fn)(__VA_ARGS__) +#define AVFORMAT(h, fn, ...) (avformat_ ## fn)(__VA_ARGS__) +#else +#define AV(h, fn, ...) (h)->av_##fn(__VA_ARGS__) +#define AVIO(h, fn, ...) (h)->avio_##fn(__VA_ARGS__) +#define AVCODEC(h, fn, ...) (h)->avcodec_##fn(__VA_ARGS__) +#define AVFORMAT(h, fn, ...) (h)->avformat_##fn(__VA_ARGS__) +#endif + + +// our own version of useful error function not included in earlier ffmpeg versions +static char *av__err2str(errnum) { + static char buf[64]; + AV(ff, strerror, errnum, buf, 64); + return buf; +} + +// parser to extract asf data packet length from asf header +const u8_t header_guid[16] = { 0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C }; +const u8_t file_props_guid[16] = { 0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11, 0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65 }; + +static int _parse_packlen(void) { + int bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + u8_t *ptr = streambuf->readp; + int remain = 1; + + while (bytes >= 24 && remain > 0) { + u32_t len = *(ptr+16) | *(ptr+17) << 8 | *(ptr+18) << 16 | *(ptr+19) << 24; // assume msb 32 bits are 0 + if (!memcmp(ptr, header_guid, 16) && bytes >= 30) { + ptr += 30; + bytes -= 30; + remain = len - 30; + continue; + } + if (!memcmp(ptr, file_props_guid, 16) && len == 104) { + u32_t packlen = *(ptr+92) | *(ptr+93) << 8 | *(ptr+94) << 16 | *(ptr+95) << 24; + LOG_INFO("asf packet len: %u", packlen); + return packlen; + } + ptr += len; + bytes -= len; + remain -= len; + } + + LOG_WARN("could not parse packet length"); + return 0; +} + +static int _read_data(void *opaque, u8_t *buffer, int buf_size) { + size_t bytes; + + LOCK_S; + + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + ff->end_of_stream = (stream.state <= DISCONNECT && bytes == 0); + bytes = min(bytes, buf_size); + + // for chunked wma extract asf header and data frames from framing structure + // pad asf data frames to size of packet extracted from asf header + if (ff->wma_mmsh) { + unsigned chunk_type = 0, chunk_len = 0; + + if (ff->mmsh_bytes_left) { + // bytes remaining from previous frame + if (bytes >= ff->mmsh_bytes_left) { + bytes = ff->mmsh_bytes_left; + ff->mmsh_bytes_left = 0; + } else { + ff->mmsh_bytes_left -= bytes; + } + } else if (ff->mmsh_bytes_pad) { + // add padding for previous frame + bytes = min(ff->mmsh_bytes_pad, buf_size); + memset(buffer, 0, bytes); + ff->mmsh_bytes_pad -= bytes; + UNLOCK_S; + return bytes; + } else if (bytes >= 12) { + // new chunk header + chunk_type = (*(streambuf->readp) & 0x7f) | *(streambuf->readp + 1) << 8; + chunk_len = *(streambuf->readp + 2) | *(streambuf->readp + 3) << 8; + _buf_inc_readp(streambuf, 12); + bytes -= 12; + } else if (_buf_used(streambuf) >= 12) { + // new chunk header split over end of streambuf, read in two + u8_t header[12]; + memcpy(header, streambuf->readp, bytes); + _buf_inc_readp(streambuf, bytes); + memcpy(header + bytes, streambuf->readp, 12 - bytes); + _buf_inc_readp(streambuf, 12 - bytes); + chunk_type = (header[0] & 0x7f) | header[1] << 8; + chunk_len = header[2] | header[3] << 8; + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + bytes = min(bytes, buf_size); + } else { + // should not get here... + LOG_ERROR("chunk parser stalled bytes: %u %u", bytes, _buf_used(streambuf)); + UNLOCK_S; + return 0; + } + + if (chunk_type && chunk_len) { + if (chunk_type == 0x4824) { + // asf header - parse packet length + ff->mmsh_packet_len = _parse_packlen(); + ff->mmsh_bytes_pad = 0; + } else if (chunk_type == 0x4424 && ff->mmsh_packet_len) { + // asf data packet - add padding + ff->mmsh_bytes_pad = ff->mmsh_packet_len - chunk_len + 8; + } else { + LOG_INFO("unknown chunk: %04x", chunk_type); + // other packet - no padding + ff->mmsh_bytes_pad = 0; + } + + if (chunk_len - 8 <= bytes) { + bytes = chunk_len - 8; + ff->mmsh_bytes_left = 0; + } else { + ff->mmsh_bytes_left = chunk_len - 8 - bytes; + } + } + + } + + memcpy(buffer, streambuf->readp, bytes); + + _buf_inc_readp(streambuf, bytes); + + if (ff->mmsh_bytes_pad && bytes + ff->mmsh_bytes_pad < buf_size) { + memset(buffer + bytes, 0, ff->mmsh_bytes_pad); + bytes += ff->mmsh_bytes_pad; + ff->mmsh_bytes_pad = 0; + } + + UNLOCK_S; + + return bytes; +} + +static decode_state ff_decode(void) { + int r, len, got_frame; + AVPacket pkt_c; + s32_t *optr = NULL; + + if (decode.new_stream) { + + AVIOContext *avio; + AVStream *av_stream; + AVCodec *codec; + int o; + int audio_stream = -1; + + ff->mmsh_bytes_left = ff->mmsh_bytes_pad = ff->mmsh_packet_len = 0; + + if (!ff->readbuf) { + ff->readbuf = AV(ff, malloc, READ_SIZE + FF_INPUT_BUFFER_PADDING_SIZE); + } + + avio = AVIO(ff, alloc_context, ff->readbuf, READ_SIZE, 0, NULL, _read_data, NULL, NULL); + avio->seekable = 0; + + ff->formatC = AVFORMAT(ff, alloc_context); + if (ff->formatC == NULL) { + LOG_ERROR("null context"); + return DECODE_ERROR; + } + + ff->formatC->pb = avio; + ff->formatC->flags |= AVFMT_FLAG_CUSTOM_IO | AVFMT_FLAG_NOPARSE; + + o = AVFORMAT(ff, open_input, &ff->formatC, "", ff->input_format, NULL); + if (o < 0) { + LOG_WARN("avformat_open_input: %d %s", o, av__err2str(o)); + return DECODE_ERROR; + } + + LOG_INFO("format: name:%s lname:%s", ff->formatC->iformat->name, ff->formatC->iformat->long_name); + + o = AVFORMAT(ff, find_stream_info, ff->formatC, NULL); + if (o < 0) { + LOG_WARN("avformat_find_stream_info: %d %s", o, av__err2str(o)); + return DECODE_ERROR; + } + + if (ff->wma && ff->wma_playstream < ff->formatC->nb_streams) { + if (ff->formatC->streams[ff->wma_playstream]->codec->codec_type == AVMEDIA_TYPE_AUDIO) { + LOG_INFO("using wma stream sent from server: %i", ff->wma_playstream); + audio_stream = ff->wma_playstream; + } + } + + if (audio_stream == -1) { + int i; + for (i = 0; i < ff->formatC->nb_streams; ++i) { + if (ff->formatC->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) { + audio_stream = i; + LOG_INFO("found stream: %i", i); + break; + } + } + } + + if (audio_stream == -1) { + LOG_WARN("no audio stream found"); + return DECODE_ERROR; + } + + av_stream = ff->formatC->streams[audio_stream]; + + ff->codecC = av_stream->codec; + + codec = AVCODEC(ff, find_decoder, ff->codecC->codec_id); + + AVCODEC(ff, open2, ff->codecC, codec, NULL); + + ff->frame = AVCODEC(ff, alloc_frame); + + ff->avpkt = AV(ff, malloc, sizeof(AVPacket)); + if (ff->avpkt == NULL) { + LOG_ERROR("can't allocate avpkt"); + return DECODE_ERROR; + } + + AV(ff, init_packet, ff->avpkt); + ff->avpkt->data = NULL; + ff->avpkt->size = 0; + + LOCK_O; + LOG_INFO("setting track_start"); + output.next_sample_rate = decode_newstream(ff->codecC->sample_rate, output.supported_rates); + IF_DSD( output.next_dop = false; ) + output.track_start = outputbuf->writep; + if (output.fade_mode) _checkfade(true); + decode.new_stream = false; + UNLOCK_O; + } + + got_frame = 0; + + if ((r = AV(ff, read_frame, ff->formatC, ff->avpkt)) < 0) { + if (r == AVERROR_EOF) { + if (ff->end_of_stream) { + LOG_INFO("decode complete"); + return DECODE_COMPLETE; + } else { + LOG_INFO("codec end of file"); + } + } else { + LOG_ERROR("av_read_frame error: %i %s", r, av__err2str(r)); + } + return DECODE_RUNNING; + } + + // clone packet as we are adjusting it + pkt_c = *ff->avpkt; + + IF_PROCESS( + optr = (s32_t *)process.inbuf; + process.in_frames = 0; + ); + + while (pkt_c.size > 0 || got_frame) { + + len = AVCODEC(ff, decode_audio4, ff->codecC, ff->frame, &got_frame, &pkt_c); + if (len < 0) { + LOG_ERROR("avcodec_decode_audio4 error: %i %s", len, av__err2str(len)); + return DECODE_RUNNING; + } + + pkt_c.data += len; + pkt_c.size -= len; + + if (got_frame) { + + s16_t *iptr16 = (s16_t *)ff->frame->data[0]; + s32_t *iptr32 = (s32_t *)ff->frame->data[0]; + s16_t *iptr16l = (s16_t *)ff->frame->data[0]; + s16_t *iptr16r = (s16_t *)ff->frame->data[1]; + s32_t *iptr32l = (s32_t *)ff->frame->data[0]; + s32_t *iptr32r = (s32_t *)ff->frame->data[1]; + float *iptrfl = (float *)ff->frame->data[0]; + float *iptrfr = (float *)ff->frame->data[1]; + + frames_t frames = ff->frame->nb_samples; + + LOG_SDEBUG("got audio channels: %u samples: %u format: %u", ff->codecC->channels, ff->frame->nb_samples, + ff->codecC->sample_fmt); + + LOCK_O_direct; + + while (frames > 0) { + frames_t count; + frames_t f; + + IF_DIRECT( + optr = (s32_t *)outputbuf->writep; + f = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME; + f = min(f, frames); + ); + + IF_PROCESS( + if (process.in_frames + frames > process.max_in_frames) { + LOG_WARN("exceeded process buffer size - dropping frames"); + break; + } + f = frames; + ); + + count = f; + + if (ff->codecC->channels == 2) { + if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16) { + while (count--) { + *optr++ = *iptr16++ << 16; + *optr++ = *iptr16++ << 16; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32) { + while (count--) { + *optr++ = *iptr32++; + *optr++ = *iptr32++; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16P) { + while (count--) { + *optr++ = *iptr16l++ << 16; + *optr++ = *iptr16r++ << 16; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32P) { + while (count--) { + *optr++ = *iptr32l++; + *optr++ = *iptr32r++; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_FLTP) { + while (count--) { + double scaledl = *iptrfl++ * 0x7fffffff; + double scaledr = *iptrfr++ * 0x7fffffff; + if (scaledl > 2147483647.0) scaledl = 2147483647.0; + if (scaledl < -2147483648.0) scaledl = -2147483648.0; + if (scaledr > 2147483647.0) scaledr = 2147483647.0; + if (scaledr < -2147483648.0) scaledr = -2147483648.0; + *optr++ = (s32_t)scaledl; + *optr++ = (s32_t)scaledr; + } + } else { + LOG_WARN("unsupported sample format: %u", ff->codecC->sample_fmt); + } + } else if (ff->codecC->channels == 1) { + if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16) { + while (count--) { + *optr++ = *iptr16 << 16; + *optr++ = *iptr16++ << 16; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32) { + while (count--) { + *optr++ = *iptr32; + *optr++ = *iptr32++; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16P) { + while (count--) { + *optr++ = *iptr16l << 16; + *optr++ = *iptr16l++ << 16; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32P) { + while (count--) { + *optr++ = *iptr32l; + *optr++ = *iptr32l++; + } + } else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_FLTP) { + while (count--) { + double scaled = *iptrfl++ * 0x7fffffff; + if (scaled > 2147483647.0) scaled = 2147483647.0; + if (scaled < -2147483648.0) scaled = -2147483648.0; + *optr++ = (s32_t)scaled; + *optr++ = (s32_t)scaled; + } + } else { + LOG_WARN("unsupported sample format: %u", ff->codecC->sample_fmt); + } + } else { + LOG_WARN("unsupported number of channels"); + } + + frames -= f; + + IF_DIRECT( + _buf_inc_writep(outputbuf, f * BYTES_PER_FRAME); + ); + + IF_PROCESS( + process.in_frames += f; + ); + } + + UNLOCK_O_direct; + } + } + + AV(ff, free_packet, ff->avpkt); + + return DECODE_RUNNING; +} + +static void _free_ff_data(void) { + if (ff->formatC) { + if (ff->formatC->pb) AV(ff, freep, &ff->formatC->pb); + AVFORMAT(ff, free_context, ff->formatC); + ff->formatC = NULL; + } + + if (ff->frame) { + // ffmpeg version dependant free function +#if !LINKALL + ff->avcodec_free_frame ? AVCODEC(ff, free_frame, &ff->frame) : AV(ff, freep, &ff->frame); +#elif LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(54,28,0) + AVCODEC(ff, free_frame, &ff->frame); +#else + AV(ff, freep, &ff->frame); +#endif + ff->frame = NULL; + } + + if (ff->avpkt) { + AV(ff, free_packet, ff->avpkt); + AV(ff, freep, &ff->avpkt); + ff->avpkt = NULL; + } +} + +static void ff_open_wma(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + _free_ff_data(); + + ff->input_format = AV(ff, find_input_format, "asf"); + if (ff->input_format == NULL) { + LOG_ERROR("asf format not supported by ffmpeg library"); + } + + ff->wma = true; + ff->wma_mmsh = size - '0'; + ff->wma_playstream = rate - 1; + ff->wma_metadatastream = chan != '?' ? chan : 0; + + LOG_INFO("open wma chunking: %u playstream: %u metadatastream: %u", ff->wma_mmsh, ff->wma_playstream, ff->wma_metadatastream); +} + +static void ff_open_alac(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + _free_ff_data(); + + ff->input_format = AV(ff, find_input_format, "mp4"); + if (ff->input_format == NULL) { + LOG_ERROR("mp4 format not supported by ffmpeg library"); + } + + ff->wma = false; + ff->wma_mmsh = 0; + + LOG_INFO("open alac"); +} + +static void ff_close(void) { + _free_ff_data(); + + if (ff->readbuf) { + AV(ff, freep, &ff->readbuf); + ff->readbuf = NULL; + } +} + +static bool load_ff() { +#if !LINKALL + void *handle_codec = NULL, *handle_format = NULL, *handle_util = NULL; + char name[30]; + char *err; + + // we try to load the ffmpeg library version which matches the header file we are compiled with as structs differ between versions + + sprintf(name, LIBAVCODEC, LIBAVCODEC_VERSION_MAJOR); + handle_codec = dlopen(name, RTLD_NOW); + if (!handle_codec) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + sprintf(name, LIBAVFORMAT, LIBAVFORMAT_VERSION_MAJOR); + handle_format = dlopen(name, RTLD_NOW); + if (!handle_format) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + sprintf(name, LIBAVUTIL, LIBAVUTIL_VERSION_MAJOR); + handle_util = dlopen(name, RTLD_NOW); + if (!handle_util) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + ff->avcodec_version = dlsym(handle_codec, "avcodec_version"); + ff->avcodec_find_decoder = dlsym(handle_codec, "avcodec_find_decoder"); + ff->avcodec_open2 = dlsym(handle_codec, "avcodec_open2"); + ff->avcodec_alloc_frame = dlsym(handle_codec, "avcodec_alloc_frame"); + ff->avcodec_free_frame = dlsym(handle_codec, "avcodec_free_frame"); + ff->avcodec_decode_audio4 = dlsym(handle_codec, "avcodec_decode_audio4"); + ff->av_init_packet = dlsym(handle_codec, "av_init_packet"); + ff->av_free_packet = dlsym(handle_codec, "av_free_packet"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBAVCODEC" (%u.%u.%u)", LIBAVCODEC_VERSION_MAJOR, ff->avcodec_version() >> 16, (ff->avcodec_version() >> 8) & 0xff, ff->avcodec_version() & 0xff); + + ff->avformat_version = dlsym(handle_format, "avformat_version"); + ff->avformat_alloc_context = dlsym(handle_format, "avformat_alloc_context"); + ff->avformat_free_context = dlsym(handle_format, "avformat_free_context"); + ff->avformat_open_input = dlsym(handle_format, "avformat_open_input"); + ff->avformat_find_stream_info = dlsym(handle_format, "avformat_find_stream_info"); + ff->avio_alloc_context = dlsym(handle_format, "avio_alloc_context"); + ff->av_read_frame = dlsym(handle_format, "av_read_frame"); + ff->av_find_input_format= dlsym(handle_format, "av_find_input_format"); + ff->av_register_all = dlsym(handle_format, "av_register_all"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBAVFORMAT" (%u.%u.%u)", LIBAVFORMAT_VERSION_MAJOR, ff->avformat_version() >> 16, (ff->avformat_version() >> 8) & 0xff, ff->avformat_version() & 0xff); + + ff->avutil_version = dlsym(handle_util, "avutil_version"); + ff->av_log_set_callback = dlsym(handle_util, "av_log_set_callback"); + ff->av_log_set_level = dlsym(handle_util, "av_log_set_level"); + ff->av_strerror = dlsym(handle_util, "av_strerror"); + ff->av_malloc = dlsym(handle_util, "av_malloc"); + ff->av_freep = dlsym(handle_util, "av_freep"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBAVUTIL" (%u.%u.%u)", LIBAVUTIL_VERSION_MAJOR, ff->avutil_version() >> 16, (ff->avutil_version() >> 8) & 0xff, ff->avutil_version() & 0xff); + +#endif + + return true; +} + +static int ff_log_level = 0; + +void av_err_callback(void *avcl, int level, const char *fmt, va_list vl) { + if (level > ff_log_level) return; + fprintf(stderr, "%s ffmpeg: ", logtime()); + vfprintf(stderr, fmt, vl); + fflush(stderr); +} + +static bool registered = false; + +struct codec *register_ff(const char *codec) { + if (!registered) { + + ff = malloc(sizeof(struct ff_s)); + if (!ff) { + return NULL; + } + + memset(ff, 0, sizeof(struct ff_s)); + + if (!load_ff()) { + return NULL; + } + + switch (loglevel) { + case lERROR: + ff_log_level = AV_LOG_ERROR; break; + case lWARN: + ff_log_level = AV_LOG_WARNING; break; + case lINFO: + ff_log_level = AV_LOG_INFO; break; + case lDEBUG: + ff_log_level = AV_LOG_VERBOSE; break; + default: break; + } + + AV(ff, log_set_callback, av_err_callback); + + AV(ff, register_all); + + registered = true; + } + + if (!strcmp(codec, "wma")) { + + static struct codec ret = { + 'w', // id + "wma,wmap,wmal", // types + READ_SIZE, // min read + WRITE_SIZE, // min space + ff_open_wma, // open + ff_close, // close + ff_decode, // decode + }; + + LOG_INFO("using ffmpeg to decode wma,wmap,wmal"); + return &ret; + } + + if (!strcmp(codec, "alc")) { + + static struct codec ret = { + 'l', // id + "alc", // types + READ_SIZE, // min read + WRITE_SIZE, // min space + ff_open_alac,// open + ff_close, // close + ff_decode, // decode + }; + + LOG_INFO("using ffmpeg to decode alc"); + return &ret; + } + + return NULL; +} + +#endif diff --git a/flac.c b/flac.c new file mode 100644 index 0000000..a5c7b3b --- /dev/null +++ b/flac.c @@ -0,0 +1,288 @@ +/* + * Squeezelite - lightweight headless squeezeplay emulator for linux + * + * (c) Adrian Smith 2012, triode1@btinternet.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" + +#include + +struct flac { + FLAC__StreamDecoder *decoder; +#if !LINKALL + // FLAC symbols to be dynamically loaded + const char **FLAC__StreamDecoderErrorStatusString; + const char **FLAC__StreamDecoderStateString; + FLAC__StreamDecoder * (* FLAC__stream_decoder_new)(void); + FLAC__bool (* FLAC__stream_decoder_reset)(FLAC__StreamDecoder *decoder); + void (* FLAC__stream_decoder_delete)(FLAC__StreamDecoder *decoder); + FLAC__StreamDecoderInitStatus (* FLAC__stream_decoder_init_stream)( + FLAC__StreamDecoder *decoder, + FLAC__StreamDecoderReadCallback read_callback, + FLAC__StreamDecoderSeekCallback seek_callback, + FLAC__StreamDecoderTellCallback tell_callback, + FLAC__StreamDecoderLengthCallback length_callback, + FLAC__StreamDecoderEofCallback eof_callback, + FLAC__StreamDecoderWriteCallback write_callback, + FLAC__StreamDecoderMetadataCallback metadata_callback, + FLAC__StreamDecoderErrorCallback error_callback, + void *client_data + ); + FLAC__bool (* FLAC__stream_decoder_process_single)(FLAC__StreamDecoder *decoder); + FLAC__StreamDecoderState (* FLAC__stream_decoder_get_state)(const FLAC__StreamDecoder *decoder); +#endif +}; + +static struct flac *f; + +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; + +#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 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 IF_DIRECT(x) { x } +#define IF_PROCESS(x) +#endif + +#if LINKALL +#define FLAC(h, fn, ...) (FLAC__ ## fn)(__VA_ARGS__) +#define FLAC_A(h, a) (FLAC__ ## a) +#else +#define FLAC(h, fn, ...) (h)->FLAC__##fn(__VA_ARGS__) +#define FLAC_A(h, a) (h)->FLAC__ ## a +#endif + +static FLAC__StreamDecoderReadStatus read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *want, void *client_data) { + size_t bytes; + bool end; + + LOCK_S; + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + bytes = min(bytes, *want); + end = (stream.state <= DISCONNECT && bytes == 0); + + memcpy(buffer, streambuf->readp, bytes); + _buf_inc_readp(streambuf, bytes); + UNLOCK_S; + + *want = bytes; + + return end ? FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM : FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; +} + +static FLAC__StreamDecoderWriteStatus write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, + const FLAC__int32 *const buffer[], void *client_data) { + + size_t frames = frame->header.blocksize; + unsigned bits_per_sample = frame->header.bits_per_sample; + unsigned channels = frame->header.channels; + + FLAC__int32 *lptr = (FLAC__int32 *)buffer[0]; + FLAC__int32 *rptr = (FLAC__int32 *)buffer[channels > 1 ? 1 : 0]; + + if (decode.new_stream) { + LOCK_O; + LOG_INFO("setting track_start"); + output.track_start = outputbuf->writep; + decode.new_stream = false; + +#if DSD + if (output.has_dop && bits_per_sample == 24 && is_flac_dop((u32_t *)lptr, (u32_t *)rptr, frames)) { + LOG_INFO("file contains DOP"); + output.next_dop = true; + output.next_sample_rate = frame->header.sample_rate; + output.fade = FADE_INACTIVE; + } else { + output.next_sample_rate = decode_newstream(frame->header.sample_rate, output.supported_rates); + output.next_dop = false; + if (output.fade_mode) _checkfade(true); + } +#else + output.next_sample_rate = decode_newstream(frame->header.sample_rate, output.supported_rates); + if (output.fade_mode) _checkfade(true); +#endif + + UNLOCK_O; + } + + LOCK_O_direct; + + while (frames > 0) { + frames_t f; + frames_t count; + s32_t *optr; + + IF_DIRECT( + optr = (s32_t *)outputbuf->writep; + f = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME; + ); + IF_PROCESS( + optr = (s32_t *)process.inbuf; + f = process.max_in_frames; + ); + + f = min(f, frames); + + count = f; + + if (bits_per_sample == 8) { + while (count--) { + *optr++ = *lptr++ << 24; + *optr++ = *rptr++ << 24; + } + } else if (bits_per_sample == 16) { + while (count--) { + *optr++ = *lptr++ << 16; + *optr++ = *rptr++ << 16; + } + } else if (bits_per_sample == 24) { + while (count--) { + *optr++ = *lptr++ << 8; + *optr++ = *rptr++ << 8; + } + } else if (bits_per_sample == 32) { + while (count--) { + *optr++ = *lptr++; + *optr++ = *rptr++; + } + } else { + LOG_ERROR("unsupported bits per sample: %u", bits_per_sample); + } + + frames -= f; + + IF_DIRECT( + _buf_inc_writep(outputbuf, f * BYTES_PER_FRAME); + ); + IF_PROCESS( + process.in_frames = f; + if (frames) LOG_ERROR("unhandled case"); + ); + } + + UNLOCK_O_direct; + + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; +} + +static void error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) { + LOG_INFO("flac error: %s", FLAC_A(f, StreamDecoderErrorStatusString)[status]); +} + +static void flac_open(u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness) { + if (f->decoder) { + FLAC(f, stream_decoder_reset, f->decoder); + } else { + f->decoder = FLAC(f, stream_decoder_new); + } + FLAC(f, stream_decoder_init_stream, f->decoder, &read_cb, NULL, NULL, NULL, NULL, &write_cb, NULL, &error_cb, NULL); +} + +static void flac_close(void) { + FLAC(f, stream_decoder_delete, f->decoder); + f->decoder = NULL; +} + +static decode_state flac_decode(void) { + bool ok = FLAC(f, stream_decoder_process_single, f->decoder); + FLAC__StreamDecoderState state = FLAC(f, stream_decoder_get_state, f->decoder); + + if (!ok && state != FLAC__STREAM_DECODER_END_OF_STREAM) { + LOG_INFO("flac error: %s", FLAC_A(f, StreamDecoderStateString)[state]); + }; + + if (state == FLAC__STREAM_DECODER_END_OF_STREAM) { + return DECODE_COMPLETE; + } else if (state > FLAC__STREAM_DECODER_END_OF_STREAM) { + return DECODE_ERROR; + } else { + return DECODE_RUNNING; + } +} + +static bool load_flac() { +#if !LINKALL + void *handle = dlopen(LIBFLAC, RTLD_NOW); + char *err; + + if (!handle) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + f->FLAC__StreamDecoderErrorStatusString = dlsym(handle, "FLAC__StreamDecoderErrorStatusString"); + f->FLAC__StreamDecoderStateString = dlsym(handle, "FLAC__StreamDecoderStateString"); + f->FLAC__stream_decoder_new = dlsym(handle, "FLAC__stream_decoder_new"); + f->FLAC__stream_decoder_reset = dlsym(handle, "FLAC__stream_decoder_reset"); + f->FLAC__stream_decoder_delete = dlsym(handle, "FLAC__stream_decoder_delete"); + f->FLAC__stream_decoder_init_stream = dlsym(handle, "FLAC__stream_decoder_init_stream"); + f->FLAC__stream_decoder_process_single = dlsym(handle, "FLAC__stream_decoder_process_single"); + f->FLAC__stream_decoder_get_state = dlsym(handle, "FLAC__stream_decoder_get_state"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBFLAC); +#endif + + return true; +} + +struct codec *register_flac(void) { + static struct codec ret = { + 'f', // id + "flc", // types + 8192, // min read + 102400, // min space + flac_open, // open + flac_close, // close + flac_decode, // decode + }; + + f = malloc(sizeof(struct flac)); + if (!f) { + return NULL; + } + + f->decoder = NULL; + + if (!load_flac()) { + return NULL; + } + + LOG_INFO("using flac to decode flc"); + return &ret; +} diff --git a/ir.c b/ir.c new file mode 100644 index 0000000..2355ee4 --- /dev/null +++ b/ir.c @@ -0,0 +1,248 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// ir thread - linux only + +#include "squeezelite.h" + +#if IR + +#include + +#define LIRC_CLIENT_ID "squeezelite" + +static log_level loglevel; + +struct irstate ir; + +static struct lirc_config *config = NULL; +static sockfd fd = -1; + +static thread_type thread; + +#define LOCK_I mutex_lock(ir.mutex) +#define UNLOCK_I mutex_unlock(ir.mutex) + +#if !LINKALL +struct lirc { + // LIRC symbols to be dynamically loaded + int (* lirc_init)(char *prog, int verbose); + int (* lirc_deinit)(void); + int (* lirc_readconfig)(char *file, struct lirc_config **config, int (check) (char *s)); + void (* lirc_freeconfig)(struct lirc_config *config); + int (* lirc_nextcode)(char **code); + int (* lirc_code2char)(struct lirc_config *config, char *code, char **string); +}; + +static struct lirc *i; +#endif + +#if LINKALL +#define LIRC(h, fn, ...) (lirc_ ## fn)(__VA_ARGS__) +#else +#define LIRC(h, fn, ...) (h)->lirc_##fn(__VA_ARGS__) +#endif + +// cmds based on entires in Slim_Device_Remote.ir +// these may appear as config entries in .lircrc files +static struct { + char *cmd; + u32_t code; +} cmdmap[] = { + { "voldown", 0x768900ff }, + { "volup", 0x7689807f }, + { "rew", 0x7689c03f }, + { "fwd", 0x7689a05f }, + { "pause", 0x768920df }, + { "play", 0x768910ef }, + { "power", 0x768940bf }, + { "muting", 0x7689c43b }, + { "power_on", 0x76898f70 }, + { "power_off",0x76898778 }, + { NULL, 0 }, +}; + +// selected lirc namespace button names as defaults - some support repeat +static struct { + char *lirc; + u32_t code; + bool repeat; +} keymap[] = { + { "KEY_VOLUMEDOWN", 0x768900ff, true }, + { "KEY_VOLUMEUP", 0x7689807f, true }, + { "KEY_PREVIOUS", 0x7689c03f, false }, + { "KEY_REWIND", 0x7689c03f, false }, + { "KEY_NEXT", 0x7689a05f, false }, + { "KEY_FORWARD", 0x7689a05f, false }, + { "KEY_PAUSE", 0x768920df, true }, + { "KEY_PLAY", 0x768910ef, false }, + { "KEY_POWER", 0x768940bf, false }, + { "KEY_MUTE", 0x7689c43b, false }, + { NULL, 0 , false }, +}; + +static u32_t ir_cmd_map(const char *c) { + int i; + for (i = 0; cmdmap[i].cmd; i++) { + if (!strcmp(c, cmdmap[i].cmd)) { + return cmdmap[i].code; + } + } + return 0; +} + +static u32_t ir_key_map(const char *c, const char *r) { + int i; + for (i = 0; keymap[i].lirc; i++) { + if (!strcmp(c, keymap[i].lirc)) { + if (keymap[i].repeat || !strcmp(r, "00")) { + return keymap[i].code; + } + LOG_DEBUG("repeat suppressed"); + break; + } + } + return 0; +} + +static void *ir_thread() { + char *code; + + while (fd > 0 && LIRC(i, nextcode, &code) == 0) { + + u32_t now = gettime_ms(); + u32_t ir_code = 0; + + if (code == NULL) continue; + + if (config) { + // allow lirc_client to decode then lookup cmd in our table + // we can only send one IR event to slimproto so break after first one + char *c; + while (LIRC(i, code2char, config, code, &c) == 0 && c != NULL) { + ir_code = ir_cmd_map(c); + if (ir_code) { + LOG_DEBUG("ir cmd: %s -> %x", c, ir_code); + } + } + } + + if (!ir_code) { + // try to match on lirc button name if it is from the standard namespace + // this allows use of non slim remotes without a specific entry in .lircrc + char *b, *r; + strtok(code, " \n"); // discard + r = strtok(NULL, " \n"); // repeat count + b = strtok(NULL, " \n"); // key name + if (r && b) { + ir_code = ir_key_map(b, r); + LOG_DEBUG("ir lirc: %s [%s] -> %x", b, r, ir_code); + } + } + + if (ir_code) { + LOCK_I; + if (ir.code) { + LOG_DEBUG("code dropped"); + } + ir.code = ir_code; + ir.ts = now; + UNLOCK_I; + wake_controller(); + } + + free(code); + } + + return 0; +} + +#if !LINKALL +static bool load_lirc() { + void *handle = dlopen(LIBLIRC, RTLD_NOW); + char *err; + + if (!handle) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + i->lirc_init = dlsym(handle, "lirc_init"); + i->lirc_deinit = dlsym(handle, "lirc_deinit"); + i->lirc_readconfig = dlsym(handle, "lirc_readconfig"); + i->lirc_freeconfig = dlsym(handle, "lirc_freeconfig"); + i->lirc_nextcode = dlsym(handle, "lirc_nextcode"); + i->lirc_code2char = dlsym(handle, "lirc_code2char"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBLIRC); + return true; +} +#endif + +void ir_init(log_level level, char *lircrc) { + loglevel = level; + +#if !LINKALL + i = malloc(sizeof(struct lirc)); + if (!i || !load_lirc()) { + return; + } +#endif + + fd = LIRC(i, init, LIRC_CLIENT_ID, 0); + + if (fd > 0) { + if (LIRC(i, readconfig,lircrc, &config, NULL) != 0) { + LOG_WARN("error reading config: %s", lircrc); + } + + mutex_create(ir.mutex); + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + IR_THREAD_STACK_SIZE); + pthread_create(&thread, &attr, ir_thread, NULL); + pthread_attr_destroy(&attr); + + } else { + LOG_WARN("failed to connect to lircd - ir processing disabled"); + } +} + +void ir_close(void) { + if (fd > 0) { + fd = -1; + if (config) { + LIRC(i, freeconfig, config); + } + LIRC(i, deinit); + + pthread_cancel(thread); + pthread_join(thread, NULL); + mutex_destroy(ir.mutex); + } +} + +#endif //#if IR diff --git a/mad.c b/mad.c new file mode 100644 index 0000000..30e2498 --- /dev/null +++ b/mad.c @@ -0,0 +1,414 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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" + +#include + +#define MAD_DELAY 529 + +#define READBUF_SIZE 2048 // local buffer used by decoder: FIXME merge with any other decoders needing one? + +struct mad { + u8_t *readbuf; + unsigned readbuf_len; + struct mad_stream stream; + struct mad_frame frame; + struct mad_synth synth; + enum mad_error last_error; + // for lame gapless processing + int checktags; + u32_t consume; + u32_t skip; + u64_t samples; + u32_t padding; +#if !LINKALL + // mad symbols to be dynamically loaded + void (* mad_stream_init)(struct mad_stream *); + void (* mad_frame_init)(struct mad_frame *); + void (* mad_synth_init)(struct mad_synth *); + void (* mad_frame_finish)(struct mad_frame *); + void (* mad_stream_finish)(struct mad_stream *); + void (* mad_stream_buffer)(struct mad_stream *, unsigned char const *, unsigned long); + int (* mad_frame_decode)(struct mad_frame *, struct mad_stream *); + void (* mad_synth_frame)(struct mad_synth *, struct mad_frame const *); + char const *(* mad_stream_errorstr)(struct mad_stream const *); +#endif +}; + +static struct mad *m; + +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; + +#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 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 IF_DIRECT(x) { x } +#define IF_PROCESS(x) +#endif + +#if LINKALL +#define MAD(h, fn, ...) (mad_ ## fn)(__VA_ARGS__) +#else +#define MAD(h, fn, ...) (h)->mad_##fn(__VA_ARGS__) +#endif + +// based on libmad minimad.c scale +static inline u32_t scale(mad_fixed_t sample) { + sample += (1L << (MAD_F_FRACBITS - 24)); + + if (sample >= MAD_F_ONE) + sample = MAD_F_ONE - 1; + else if (sample < -MAD_F_ONE) + sample = -MAD_F_ONE; + + return (s32_t)(sample >> (MAD_F_FRACBITS + 1 - 24)) << 8; +} + +// check for id3.2 tag at start of file - http://id3.org/id3v2.4.0-structure, return length +static unsigned _check_id3_tag(size_t bytes) { + u8_t *ptr = streambuf->readp; + u32_t size = 0; + + if (bytes > 10 && *ptr == 'I' && *(ptr+1) == 'D' && *(ptr+2) == '3') { + // size is encoded as syncsafe integer, add 10 if footer present + if (*(ptr+6) < 0x80 && *(ptr+7) < 0x80 && *(ptr+8) < 0x80 && *(ptr+9) < 0x80) { + size = 10 + (*(ptr+6) << 21) + (*(ptr+7) << 14) + (*(ptr+8) << 7) + *(ptr+9) + ((*(ptr+5) & 0x10) ? 10 : 0); + LOG_DEBUG("id3.2 tag len: %u", size); + } + } + + return size; +} + +// check for lame gapless params, don't advance streambuf +static void _check_lame_header(size_t bytes) { + u8_t *ptr = streambuf->readp; + + if (*ptr == 0xff && (*(ptr+1) & 0xf0) == 0xf0 && bytes > 180) { + + u32_t frame_count = 0, enc_delay = 0, enc_padding = 0; + u8_t flags; + + // 2 channels + if (!memcmp(ptr + 36, "Xing", 4) || !memcmp(ptr + 36, "Info", 4)) { + ptr += 36 + 7; + // mono + } else if (!memcmp(ptr + 21, "Xing", 4) || !memcmp(ptr + 21, "Info", 4)) { + ptr += 21 + 7; + } + + flags = *ptr; + + if (flags & 0x01) { + frame_count = unpackN((u32_t *)(ptr + 1)); + ptr += 4; + } + if (flags & 0x02) ptr += 4; + if (flags & 0x04) ptr += 100; + if (flags & 0x08) ptr += 4; + + if (!!memcmp(ptr+1, "LAME", 4)) { + return; + } + + ptr += 22; + + enc_delay = (*ptr << 4 | *(ptr + 1) >> 4) + MAD_DELAY; + enc_padding = (*(ptr + 1) & 0xF) << 8 | *(ptr + 2); + enc_padding = enc_padding > MAD_DELAY ? enc_padding - MAD_DELAY : 0; + + // add one frame to initial skip for this (empty) frame + m->skip = enc_delay + 1152; + m->samples = frame_count * 1152 - enc_delay - enc_padding; + m->padding = enc_padding; + + LOG_INFO("gapless: skip: %u samples: " FMT_u64 " delay: %u padding: %u", m->skip, m->samples, enc_delay, enc_padding); + } +} + +static decode_state mad_decode(void) { + size_t bytes; + bool eos = false; + + LOCK_S; + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + + if (m->checktags) { + if (m->checktags == 1) { + m->consume = _check_id3_tag(bytes); + m->checktags = 2; + } + if (m->consume) { + u32_t consume = min(m->consume, bytes); + LOG_DEBUG("consume: %u of %u", consume, m->consume); + _buf_inc_readp(streambuf, consume); + m->consume -= consume; + UNLOCK_S; + return DECODE_RUNNING; + } + if (m->checktags == 2) { + if (!stream.meta_interval) { + _check_lame_header(bytes); + } + m->checktags = 0; + } + } + + if (m->stream.next_frame && m->readbuf_len) { + m->readbuf_len -= m->stream.next_frame - m->readbuf; + memmove(m->readbuf, m->stream.next_frame, m->readbuf_len); + } + + bytes = min(bytes, READBUF_SIZE - m->readbuf_len); + memcpy(m->readbuf + m->readbuf_len, streambuf->readp, bytes); + m->readbuf_len += bytes; + _buf_inc_readp(streambuf, bytes); + + if (stream.state <= DISCONNECT && _buf_used(streambuf) == 0) { + eos = true; + LOG_DEBUG("end of stream"); + memset(m->readbuf + m->readbuf_len, 0, MAD_BUFFER_GUARD); + m->readbuf_len += MAD_BUFFER_GUARD; + } + + UNLOCK_S; + + MAD(m, stream_buffer, &m->stream, m->readbuf, m->readbuf_len); + + while (true) { + size_t frames; + s32_t *iptrl; + s32_t *iptrr; + unsigned max_frames; + + if (MAD(m, frame_decode, &m->frame, &m->stream) == -1) { + decode_state ret; + if (!eos && m->stream.error == MAD_ERROR_BUFLEN) { + ret = DECODE_RUNNING; + } else if (eos && (m->stream.error == MAD_ERROR_BUFLEN || m->stream.error == MAD_ERROR_LOSTSYNC)) { + ret = DECODE_COMPLETE; + } else if (!MAD_RECOVERABLE(m->stream.error)) { + LOG_INFO("mad_frame_decode error: %s - stopping decoder", MAD(m, stream_errorstr, &m->stream)); + ret = DECODE_COMPLETE; + } else { + if (m->stream.error != m->last_error) { + // suppress repeat error messages + LOG_DEBUG("mad_frame_decode error: %s", MAD(m, stream_errorstr, &m->stream)); + } + ret = DECODE_RUNNING; + } + m->last_error = m->stream.error; + return ret; + }; + + MAD(m, synth_frame, &m->synth, &m->frame); + + if (decode.new_stream) { + LOCK_O; + LOG_INFO("setting track_start"); + output.next_sample_rate = decode_newstream(m->synth.pcm.samplerate, output.supported_rates); + IF_DSD( output.next_dop = false; ) + output.track_start = outputbuf->writep; + if (output.fade_mode) _checkfade(true); + decode.new_stream = false; + UNLOCK_O; + } + + LOCK_O_direct; + + IF_DIRECT( + max_frames = _buf_space(outputbuf) / BYTES_PER_FRAME; + ); + IF_PROCESS( + max_frames = process.max_in_frames - process.in_frames; + ); + + if (m->synth.pcm.length > max_frames) { + LOG_WARN("too many samples - dropping samples"); + m->synth.pcm.length = max_frames; + } + + frames = m->synth.pcm.length; + iptrl = m->synth.pcm.samples[0]; + iptrr = m->synth.pcm.samples[ m->synth.pcm.channels - 1 ]; + + if (m->skip) { + u32_t skip = min(m->skip, frames); + LOG_DEBUG("gapless: skipping %u frames at start", skip); + frames -= skip; + m->skip -= skip; + iptrl += skip; + iptrr += skip; + } + + if (m->samples) { + if (m->samples < frames) { + LOG_DEBUG("gapless: trimming %u frames from end", frames - m->samples); + frames = (size_t)m->samples; + } + m->samples -= frames; + if (m->samples > 0 && eos && !(m->stream.next_frame[0] == 0xff && (m->stream.next_frame[1] & 0xf0) == 0xf0)) { + // this is the last frame to be decoded, but more samples expected so we must have skipped, remove padding + // note this only works if the padding is less than one frame of 1152 bytes otherswise some gap will remain + LOG_DEBUG("gapless: early end - trimming padding from end"); + if (frames >= m->padding) { + frames -= m->padding; + } else { + frames = 0; + } + m->samples = 0; + } + } + + LOG_SDEBUG("write %u frames", frames); + + while (frames > 0) { + size_t f, count; + s32_t *optr; + + IF_DIRECT( + f = min(frames, _buf_cont_write(outputbuf) / BYTES_PER_FRAME); + optr = (s32_t *)outputbuf->writep; + ); + IF_PROCESS( + f = min(frames, process.max_in_frames - process.in_frames); + optr = (s32_t *)((u8_t *)process.inbuf + process.in_frames * BYTES_PER_FRAME); + ); + + count = f; + + while (count--) { + *optr++ = scale(*iptrl++); + *optr++ = scale(*iptrr++); + } + + frames -= f; + + IF_DIRECT( + _buf_inc_writep(outputbuf, f * BYTES_PER_FRAME); + ); + IF_PROCESS( + process.in_frames += f; + ); + } + + UNLOCK_O_direct; + } + + return eos ? DECODE_COMPLETE : DECODE_RUNNING; +} + +static void mad_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + if (!m->readbuf) { + m->readbuf = malloc(READBUF_SIZE + MAD_BUFFER_GUARD); + } + m->checktags = 1; + m->consume = 0; + m->skip = MAD_DELAY; + m->samples = 0; + m->readbuf_len = 0; + m->last_error = MAD_ERROR_NONE; + MAD(m, stream_init, &m->stream); + MAD(m, frame_init, &m->frame); + MAD(m, synth_init, &m->synth); +} + +static void mad_close(void) { + mad_synth_finish(&m->synth); // macro only in current version + MAD(m, frame_finish, &m->frame); + MAD(m, stream_finish, &m->stream); + free(m->readbuf); + m->readbuf = NULL; +} + +static bool load_mad() { +#if !LINKALL + void *handle = dlopen(LIBMAD, RTLD_NOW); + char *err; + + if (!handle) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + m->mad_stream_init = dlsym(handle, "mad_stream_init"); + m->mad_frame_init = dlsym(handle, "mad_frame_init"); + m->mad_synth_init = dlsym(handle, "mad_synth_init"); + m->mad_frame_finish = dlsym(handle, "mad_frame_finish"); + m->mad_stream_finish = dlsym(handle, "mad_stream_finish"); + m->mad_stream_buffer = dlsym(handle, "mad_stream_buffer"); + m->mad_frame_decode = dlsym(handle, "mad_frame_decode"); + m->mad_synth_frame = dlsym(handle, "mad_synth_frame"); + m->mad_stream_errorstr = dlsym(handle, "mad_stream_errorstr"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBMAD); +#endif + + return true; +} + +struct codec *register_mad(void) { + static struct codec ret = { + 'm', // id + "mp3", // types + READBUF_SIZE, // min read + 206800, // min space + mad_open, // open + mad_close, // close + mad_decode, // decode + }; + + m = malloc(sizeof(struct mad)); + if (!m) { + return NULL; + } + + m->readbuf = NULL; + m->readbuf_len = 0; + + if (!load_mad()) { + return NULL; + } + + LOG_INFO("using mad to decode mp3"); + return &ret; +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..5f3d48c --- /dev/null +++ b/main.c @@ -0,0 +1,634 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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" + +#include + +#define TITLE "Squeezelite " VERSION ", Copyright 2012-2015 Adrian Smith." + +#define CODECS_BASE "flac,pcm,mp3,ogg,aac" +#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_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 OSX + " -a :\t\tSpecify Portaudio params to open output device, l = target latency in ms, r = allow OSX to resample (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" + " -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 + " -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" +#if ALSA + " -p \t\tSet real time priority of output thread (1-99)\n" +#endif +#if LINUX || FREEBSD + " -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 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 + " -D [delay]\t\tOutput device supports DSD over PCM (DoP), delay = optional delay switching between PCM and DoP in ms\n" +#endif +#if VISEXPORT + " -v \t\t\tVisualiser support\n" +#endif +# if ALSA + " -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" +#endif +#if LINUX || FREEBSD + " -z \t\t\tDaemonize\n" +#endif + " -t \t\t\tLicense terms\n" + " -? \t\t\tDisplay this help text\n" + "\n" + "Build options:" +#if LINUX + " LINUX" +#endif +#if WIN + " WIN" +#endif +#if OSX + " OSX" +#endif +#if FREEBSD + " FREEBSD" +#endif +#if ALSA + " ALSA" +#endif +#if PORTAUDIO + " PORTAUDIO" +#endif +#if EVENTFD + " EVENTFD" +#endif +#if SELFPIPE + " SELFPIPE" +#endif +#if WINEVENT + " WINEVENT" +#endif +#if RESAMPLE_MP + " RESAMPLE_MP" +#else +#if RESAMPLE + " RESAMPLE" +#endif +#endif +#if FFMPEG + " FFMPEG" +#endif +#if VISEXPORT + " VISEXPORT" +#endif +#if IR + " IR" +#endif +#if DSD + " DSD" +#endif +#if LINKALL + " LINKALL" +#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\n" +#if DSD + "Contains dsd2pcm library Copyright 2009, 2011 Sebastian Gesemann which\n" + "is subject to its own license.\n\n" +#endif + ); +} + +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; + 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 + bool daemonize = false; + char *pidfile = NULL; + FILE *pidfp = NULL; +#endif +#if ALSA + unsigned rt_priority = OUTPUT_RT_PRIORITY; + char *output_mixer = NULL; + bool output_mixer_unmute = false; +#endif +#if DSD + bool dop = false; + unsigned dop_delay = 0; +#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 + + 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 + "UV" +#endif + , opt) && optind < argc - 1) { + optarg = argv[optind + 1]; + optind += 2; + } else if (strstr("ltz?" +#if ALSA + "L" +#endif +#if RESAMPLE + "uR" +#endif +#if DSD + "D" +#endif +#if VISEXPORT + "v" +#endif +#if IR + "i" +#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; + 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); + } + } + break; + case 's': + server = optarg; + break; + case 'n': + name = optarg; + break; + case 'N': + namefile = optarg; + 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 + case 'P': + pidfile = optarg; + break; +#endif + case 'l': + list_devices(); + exit(0); + break; +#if ALSA + case 'L': + list_mixers(output_device); + exit(0); + break; +#endif +#if RESAMPLE + case 'u': + case 'R': + if (optind < argc && argv[optind] && argv[optind][0] != '-') { + resample = argv[optind++]; + } else { + resample = ""; + } + break; +#endif +#if DSD + case 'D': + dop = true; + if (optind < argc && argv[optind] && argv[optind][0] != '-') { + dop_delay = atoi(argv[optind++]); + } + break; +#endif +#if VISEXPORT + case 'v': + visexport = true; + break; +#endif +#if ALSA + 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 LINUX || FREEBSD + case 'z': + daemonize = true; + 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 + + // 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 + 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", getpid()); + fclose(pidfp); + } +#endif + +#if WIN + winsock_init(); +#endif + + stream_init(log_stream, stream_buf_size); + + 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, output_mixer, + output_mixer_unmute); +#endif +#if PORTAUDIO + output_init_pa(log_output, output_device, output_buf_size, output_params, rates, rate_delay, idle); +#endif + } + +#if DSD + dop_init(dop, dop_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); + + decode_close(); + stream_close(); + + if (!strcmp(output_device, "-")) { + output_close_stdout(); + } else { +#if ALSA + output_close_alsa(); +#endif +#if PORTAUDIO + output_close_pa(); +#endif + } + +#if IR + ir_close(); +#endif + +#if WIN + winsock_close(); +#endif + +#if LINUX || FREEBSD + if (pidfile) { + unlink(pidfile); + free(pidfile); + } +#endif + + exit(0); +} diff --git a/mpg.c b/mpg.c new file mode 100644 index 0000000..f3074f2 --- /dev/null +++ b/mpg.c @@ -0,0 +1,276 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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" + +#include + +#define READ_SIZE 512 +#define WRITE_SIZE 32 * 1024 + +struct mpg { + mpg123_handle *h; + bool use16bit; +#if !LINKALL + // mpg symbols to be dynamically loaded + int (* mpg123_init)(void); + int (* mpg123_feature)(const enum mpg123_feature_set); + void (* mpg123_rates)(const long **, size_t *); + int (* mpg123_format_none)(mpg123_handle *); + int (* mpg123_format)(mpg123_handle *, long, int, int); + mpg123_handle *(* mpg123_new)(const char*, int *); + void (* mpg123_delete)(mpg123_handle *); + int (* mpg123_open_feed)(mpg123_handle *); + int (* mpg123_decode)(mpg123_handle *, const unsigned char *, size_t, unsigned char *, size_t, size_t *); + int (* mpg123_getformat)(mpg123_handle *, long *, int *, int *); + const char* (* mpg123_plain_strerror)(int); +#endif +}; + +static struct mpg *m; + +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; + +#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 + +#if LINKALL +#define MPG123(h, fn, ...) (mpg123_ ## fn)(__VA_ARGS__) +#else +#define MPG123(h, fn, ...) (h)->mpg123_##fn(__VA_ARGS__) +#endif + +static decode_state mpg_decode(void) { + size_t bytes, space, size; + int ret; + u8_t *write_buf; + + LOCK_S; + LOCK_O_direct; + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + + IF_DIRECT( + space = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)); + write_buf = outputbuf->writep; + ); + IF_PROCESS( + space = process.max_in_frames; + write_buf = process.inbuf; + ); + + bytes = min(bytes, READ_SIZE); + space = min(space, WRITE_SIZE); + + if (m->use16bit) { + space = (space / BYTES_PER_FRAME) * 4; + } + + // only get the new stream information on first call so we can reset decode.direct appropriately + if (decode.new_stream) { + space = 0; + } + + ret = MPG123(m, decode, m->h, streambuf->readp, bytes, write_buf, space, &size); + + if (ret == MPG123_NEW_FORMAT) { + + if (decode.new_stream) { + long rate; + int channels, enc; + + MPG123(m, getformat, m->h, &rate, &channels, &enc); + + LOG_INFO("setting track_start"); + LOCK_O_not_direct; + output.next_sample_rate = decode_newstream(rate, output.supported_rates); + IF_DSD( output.next_dop = false; ) + output.track_start = outputbuf->writep; + if (output.fade_mode) _checkfade(true); + decode.new_stream = false; + UNLOCK_O_not_direct; + + } else { + LOG_WARN("format change mid stream - not supported"); + } + } + + // expand 16bit output to 32bit samples + if (m->use16bit) { + s16_t *iptr; + s32_t *optr; + size_t count = size / 2; + size = count * 4; + iptr = (s16_t *)write_buf + count; + optr = (s32_t *)write_buf + count; + while (count--) { + *--optr = *--iptr << 16; + } + } + + _buf_inc_readp(streambuf, bytes); + + IF_DIRECT( + _buf_inc_writep(outputbuf, size); + ); + IF_PROCESS( + process.in_frames = size / BYTES_PER_FRAME; + ); + + UNLOCK_O_direct; + + LOG_SDEBUG("write %u frames", size / BYTES_PER_FRAME); + + if (ret == MPG123_DONE || (bytes == 0 && size == 0 && stream.state <= DISCONNECT)) { + UNLOCK_S; + LOG_INFO("stream complete"); + return DECODE_COMPLETE; + } + + UNLOCK_S; + + if (ret == MPG123_ERR) { + LOG_WARN("Error"); + return DECODE_COMPLETE; + } + + // OK and NEED_MORE keep running + return DECODE_RUNNING; +} + +static void mpg_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + int err; + const long *list; + size_t count, i; + + if (m->h) { + MPG123(m, delete, m->h); + } + + m->h = MPG123(m, new, NULL, &err); + + if (m->h == NULL) { + LOG_WARN("new error: %s", MPG123(m, plain_strerror, err)); + } + + // restrict output to 32bit or 16bit signed 2 channel based on library capability + MPG123(m, rates, &list, &count); + MPG123(m, format_none, m->h); + for (i = 0; i < count; i++) { + MPG123(m, format, m->h, list[i], 2, m->use16bit ? MPG123_ENC_SIGNED_16 : MPG123_ENC_SIGNED_32); + } + + err = MPG123(m, open_feed, m->h); + + if (err) { + LOG_WARN("open feed error: %s", MPG123(m, plain_strerror, err)); + } +} + +static void mpg_close(void) { + MPG123(m, delete, m->h); + m->h = NULL; +} + +static bool load_mpg() { +#if !LINKALL + void *handle = dlopen(LIBMPG, RTLD_NOW); + char *err; + + if (!handle) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + m->mpg123_init = dlsym(handle, "mpg123_init"); + m->mpg123_feature = dlsym(handle, "mpg123_feature"); + m->mpg123_rates = dlsym(handle, "mpg123_rates"); + m->mpg123_format_none = dlsym(handle, "mpg123_format_none"); + m->mpg123_format = dlsym(handle, "mpg123_format"); + m->mpg123_new = dlsym(handle, "mpg123_new"); + m->mpg123_delete = dlsym(handle, "mpg123_delete"); + m->mpg123_open_feed = dlsym(handle, "mpg123_open_feed"); + m->mpg123_decode = dlsym(handle, "mpg123_decode"); + m->mpg123_getformat = dlsym(handle, "mpg123_getformat"); + m->mpg123_plain_strerror = dlsym(handle, "mpg123_plain_strerror"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBMPG); +#endif + + return true; +} + +struct codec *register_mpg(void) { + static struct codec ret = { + 'm', // id + "mp3", // types + READ_SIZE, // min read + WRITE_SIZE, // min space + mpg_open, // open + mpg_close, // close + mpg_decode, // decode + }; + + m = malloc(sizeof(struct mpg)); + if (!m) { + return NULL; + } + + m->h = NULL; + + if (!load_mpg()) { + return NULL; + } + + MPG123(m, init); + + m->use16bit = MPG123(m, feature, MPG123_FEATURE_OUTPUT_32BIT); + + LOG_INFO("using mpg to decode mp3"); + return &ret; +} diff --git a/output.c b/output.c new file mode 100644 index 0000000..6176c8b --- /dev/null +++ b/output.c @@ -0,0 +1,436 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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_dop; +#endif + +#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.dop != output.next_dop) { + delay = output.dop_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.dop = output.next_dop; + ) + if (!output.fade == FADE_ACTIVE || !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.dop) { + 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) { + bytes /= 2; + } + + 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_dop = malloc(MAX_SILENCE_FRAMES * BYTES_PER_FRAME); + if (!silencebuf_dop) { + LOG_ERROR("unable to malloc silence dop buffer"); + exit(0); + } + dop_silence_frames((u32_t *)silencebuf_dop, 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; + + if (!rates[0]) { + if (!test_open(output.device, output.supported_rates)) { + LOG_ERROR("unable to open output device"); + exit(0); + } + } else { + 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_dop); + ) +} + +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_alsa.c b/output_alsa.c new file mode 100644 index 0000000..aa01560 --- /dev/null +++ b/output_alsa.c @@ -0,0 +1,904 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// Output using Alsa + +#include "squeezelite.h" + +#if ALSA + +#include +#include +#include +#include + +#define MAX_DEVICE_LEN 128 + +static snd_pcm_format_t fmts[] = { SND_PCM_FORMAT_S32_LE, SND_PCM_FORMAT_S24_LE, SND_PCM_FORMAT_S24_3LE, SND_PCM_FORMAT_S16_LE, + SND_PCM_FORMAT_UNKNOWN }; + +#if SL_LITTLE_ENDIAN +#define NATIVE_FORMAT SND_PCM_FORMAT_S32_LE +#else +#define NATIVE_FORMAT SND_PCM_FORMAT_S32_BE +#endif + +// ouput device +static struct { + char device[MAX_DEVICE_LEN + 1]; + snd_pcm_format_t format; + snd_pcm_uframes_t buffer_size; + snd_pcm_uframes_t period_size; + unsigned rate; + bool mmap; + bool reopen; + u8_t *write_buf; + const char *volume_mixer_name; + int volume_mixer_index; +} alsa; + +static snd_pcm_t *pcmp = NULL; + +extern u8_t *silencebuf; +#if DSD +extern u8_t *silencebuf_dop; +#endif + +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) + +void list_devices(void) { + void **hints, **n; + if (snd_device_name_hint(-1, "pcm", &hints) >= 0) { + n = hints; + printf("Output devices:\n"); + while (*n) { + char *name = snd_device_name_get_hint(*n, "NAME"); + char *desc = snd_device_name_get_hint(*n, "DESC"); + if (name) printf(" %-30s", name); + if (desc) { + char *s1 = strtok(desc, "\n"); + char *s2 = strtok(NULL, "\n"); + if (s1) printf(" - %s", s1); + if (s2) printf(" - %s", s2); + } + printf("\n"); + if (name) free(name); + if (desc) free(desc); + n++; + } + snd_device_name_free_hint(hints); + } + printf("\n"); +} + +void list_mixers(const char *output_device) { + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + snd_mixer_elem_t *elem; + snd_mixer_selem_id_alloca(&sid); + + LOG_INFO("listing mixers for: %s", output_device); + + if ((err = snd_mixer_open(&handle, 0)) < 0) { + LOG_ERROR("open error: %s", snd_strerror(err)); + return; + } + if ((err = snd_mixer_attach(handle, output_device)) < 0) { + LOG_ERROR("attach error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return; + } + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + LOG_ERROR("register error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return; + } + if ((err = snd_mixer_load(handle)) < 0) { + LOG_ERROR("load error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return; + } + + printf("Volume controls for %s\n", output_device); + for (elem = snd_mixer_first_elem(handle); elem; elem = snd_mixer_elem_next(elem)) { + if (snd_mixer_selem_has_playback_volume(elem)) { + snd_mixer_selem_get_id(elem, sid); + printf(" %s", snd_mixer_selem_id_get_name(sid)); + if (snd_mixer_selem_id_get_index(sid)) { + printf(",%d", snd_mixer_selem_id_get_index(sid)); + } + printf("\n"); + } + } + printf("\n"); + + snd_mixer_close(handle); +} + +#define MINVOL_DB 72 // LMS volume map for SqueezePlay sends values in range ~ -72..0 dB + +static void set_mixer(const char *device, const char *mixer, int mixer_index, bool setmax, float ldB, float rdB) { + int err; + long nleft, nright; + long min, max; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + snd_mixer_elem_t* elem; + + if ((err = snd_mixer_open(&handle, 0)) < 0) { + LOG_ERROR("open error: %s", snd_strerror(err)); + return; + } + if ((err = snd_mixer_attach(handle, device)) < 0) { + LOG_ERROR("attach error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return; + } + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + LOG_ERROR("register error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return; + } + if ((err = snd_mixer_load(handle)) < 0) { + LOG_ERROR("load error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return; + } + + snd_mixer_selem_id_alloca(&sid); + + snd_mixer_selem_id_set_index(sid, mixer_index); + snd_mixer_selem_id_set_name(sid, mixer); + + if ((elem = snd_mixer_find_selem(handle, sid)) == NULL) { + LOG_ERROR("error find selem %s", mixer); + snd_mixer_close(handle); + return; + } + + if (snd_mixer_selem_has_playback_switch(elem)) { + snd_mixer_selem_set_playback_switch_all(elem, 1); // unmute + } + + err = snd_mixer_selem_get_playback_dB_range(elem, &min, &max); + + if (err < 0 || max - min < 1000) { + // unable to get db range or range is less than 10dB - ignore and set using raw values + if ((err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max)) < 0) { + LOG_ERROR("unable to get volume raw range"); + } else { + long lraw, rraw; + if (setmax) { + lraw = rraw = max; + } else { + lraw = ((ldB > -MINVOL_DB ? MINVOL_DB + floor(ldB) : 0) / MINVOL_DB * (max-min)) + min; + rraw = ((rdB > -MINVOL_DB ? MINVOL_DB + floor(rdB) : 0) / MINVOL_DB * (max-min)) + min; + } + LOG_DEBUG("setting vol raw [%ld..%ld]", min, max); + if ((err = snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, lraw)) < 0) { + LOG_ERROR("error setting left volume: %s", snd_strerror(err)); + } + if ((err = snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, rraw)) < 0) { + LOG_ERROR("error setting right volume: %s", snd_strerror(err)); + } + } + } else { + // set db directly + LOG_DEBUG("setting vol dB [%ld..%ld]", min, max); + if (setmax) { + // set to 0dB if available as this should be max volume for music recored at max pcm values + if (max >= 0 && min <= 0) { + ldB = rdB = 0; + } else { + ldB = rdB = max; + } + } + if ((err = snd_mixer_selem_set_playback_dB(elem, SND_MIXER_SCHN_FRONT_LEFT, 100 * ldB, 1)) < 0) { + LOG_ERROR("error setting left volume: %s", snd_strerror(err)); + } + if ((err = snd_mixer_selem_set_playback_dB(elem, SND_MIXER_SCHN_FRONT_RIGHT, 100 * rdB, 1)) < 0) { + LOG_ERROR("error setting right volume: %s", snd_strerror(err)); + } + } + + if ((err = snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &nleft)) < 0) { + LOG_ERROR("error getting left vol: %s", snd_strerror(err)); + } + if ((err = snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, &nright)) < 0) { + LOG_ERROR("error getting right vol: %s", snd_strerror(err)); + } + + LOG_DEBUG("%s left: %3.1fdB -> %ld right: %3.1fdB -> %ld", mixer, ldB, nleft, rdB, nright); + + snd_mixer_close(handle); +} + +void set_volume(unsigned left, unsigned right) { + float ldB, rdB; + + if (!alsa.volume_mixer_name) { + LOG_DEBUG("setting internal gain left: %u right: %u", left, right); + LOCK; + output.gainL = left; + output.gainR = right; + UNLOCK; + return; + } else { + LOCK; + output.gainL = FIXED_ONE; + output.gainR = FIXED_ONE; + UNLOCK; + } + + // convert 16.16 fixed point to dB + ldB = 20 * log10( left / 65536.0F ); + rdB = 20 * log10( right / 65536.0F ); + + set_mixer(output.device, alsa.volume_mixer_name, alsa.volume_mixer_index, false, ldB, rdB); +} + +static void *alsa_error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...) { + va_list args; + if ((loglevel >= lINFO && err == 0) || loglevel >= lDEBUG) { + fprintf(stderr, "%s ALSA %s:%d ", logtime(), function, line); + va_start(args, fmt); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + fflush(stderr); + } + return NULL; +} + +static void alsa_close(void) { + int err; + if ((err = snd_pcm_close(pcmp)) < 0) { + LOG_INFO("snd_pcm_close error: %s", snd_strerror(err)); + } +} + +bool test_open(const char *device, unsigned rates[]) { + int err; + snd_pcm_t *pcm; + snd_pcm_hw_params_t *hw_params; + hw_params = (snd_pcm_hw_params_t *) alloca(snd_pcm_hw_params_sizeof()); + memset(hw_params, 0, snd_pcm_hw_params_sizeof()); + + // open device + if ((err = snd_pcm_open(&pcm, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + LOG_ERROR("playback open error: %s", snd_strerror(err)); + return false; + } + + // get max params + if ((err = snd_pcm_hw_params_any(pcm, hw_params)) < 0) { + LOG_ERROR("hwparam init error: %s", snd_strerror(err)); + return false; + } + + // find supported sample rates to enable client side resampling of non supported rates + unsigned i, ind; + unsigned ref[] TEST_RATES; + + for (i = 0, ind = 0; ref[i]; ++i) { + if (snd_pcm_hw_params_test_rate(pcm, hw_params, ref[i], 0) == 0) { + rates[ind++] = ref[i]; + } + } + + if ((err = snd_pcm_close(pcm)) < 0) { + LOG_ERROR("snd_pcm_close error: %s", snd_strerror(err)); + return false; + } + + return true; +} + +static bool pcm_probe(const char *device) { + int err; + snd_pcm_t *pcm; + + if ((err = snd_pcm_open(&pcm, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + return false; + } + + if ((err = snd_pcm_close(pcm)) < 0) { + LOG_ERROR("snd_pcm_close error: %s", snd_strerror(err)); + } + + return true; +} + +static int alsa_open(const char *device, unsigned sample_rate, unsigned alsa_buffer, unsigned alsa_period) { + int err; + snd_pcm_hw_params_t *hw_params; + snd_pcm_hw_params_alloca(&hw_params); + + // close if already open + if (pcmp) alsa_close(); + + // reset params + alsa.rate = 0; + alsa.period_size = 0; + strcpy(alsa.device, device); + + if (strlen(device) > MAX_DEVICE_LEN - 4 - 1) { + LOG_ERROR("device name too long: %s", device); + return -1; + } + + LOG_INFO("opening device at: %u", sample_rate); + + bool retry; + do { + // open device + if ((err = snd_pcm_open(&pcmp, alsa.device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + LOG_ERROR("playback open error: %s", snd_strerror(err)); + return err; + } + + // init params + memset(hw_params, 0, snd_pcm_hw_params_sizeof()); + if ((err = snd_pcm_hw_params_any(pcmp, hw_params)) < 0) { + LOG_ERROR("hwparam init error: %s", snd_strerror(err)); + return err; + } + + // open hw: devices without resampling, if sample rate fails try plughw: with resampling + bool hw = !strncmp(alsa.device, "hw:", 3); + retry = false; + + if ((err = snd_pcm_hw_params_set_rate_resample(pcmp, hw_params, !hw)) < 0) { + LOG_ERROR("resampling setup failed: %s", snd_strerror(err)); + return err; + } + + if ((err = snd_pcm_hw_params_set_rate(pcmp, hw_params, sample_rate, 0)) < 0) { + if (hw) { + strcpy(alsa.device + 4, device); + memcpy(alsa.device, "plug", 4); + LOG_INFO("reopening device %s in plug mode as %s for resampling", device, alsa.device); + snd_pcm_close(pcmp); + retry = true; + } + } + + } while (retry); + + // set access + if (!alsa.mmap || snd_pcm_hw_params_set_access(pcmp, hw_params, SND_PCM_ACCESS_MMAP_INTERLEAVED) < 0) { + if ((err = snd_pcm_hw_params_set_access(pcmp, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + LOG_ERROR("access type not available: %s", snd_strerror(err)); + return err; + } + alsa.mmap = false; + } + + // set the sample format + snd_pcm_format_t *fmt = alsa.format ? &alsa.format : (snd_pcm_format_t *)fmts; + do { + if (snd_pcm_hw_params_set_format(pcmp, hw_params, *fmt) >= 0) { + LOG_INFO("opened device %s using format: %s sample rate: %u mmap: %u", alsa.device, snd_pcm_format_name(*fmt), sample_rate, alsa.mmap); + alsa.format = *fmt; + break; + } + if (alsa.format) { + LOG_ERROR("unable to open audio device requested format: %s", snd_pcm_format_name(alsa.format)); + return -1; + } + ++fmt; + if (*fmt == SND_PCM_FORMAT_UNKNOWN) { + LOG_ERROR("unable to open audio device with any supported format"); + return -1; + } + } while (*fmt != SND_PCM_FORMAT_UNKNOWN); + + // set the output format to be used by _scale_and_pack + switch(alsa.format) { + case SND_PCM_FORMAT_S32_LE: + output.format = S32_LE; break; + case SND_PCM_FORMAT_S24_LE: + output.format = S24_LE; break; + case SND_PCM_FORMAT_S24_3LE: + output.format = S24_3LE; break; + case SND_PCM_FORMAT_S16_LE: + output.format = S16_LE; break; + default: + break; + } + + // set channels + if ((err = snd_pcm_hw_params_set_channels (pcmp, hw_params, 2)) < 0) { + LOG_ERROR("channel count not available: %s", snd_strerror(err)); + return err; + } + + // set period size - value of < 50 treated as period count, otherwise size in bytes + if (alsa_period < 50) { + unsigned count = alsa_period; + if ((err = snd_pcm_hw_params_set_periods_near(pcmp, hw_params, &count, 0)) < 0) { + LOG_ERROR("unable to set period count %s", snd_strerror(err)); + return err; + } + } else { + snd_pcm_uframes_t size = alsa_period; + int dir = 0; + if ((err = snd_pcm_hw_params_set_period_size_near(pcmp, hw_params, &size, &dir)) < 0) { + LOG_ERROR("unable to set period size %s", snd_strerror(err)); + return err; + } + } + + // set buffer size - value of < 500 treated as buffer time in ms, otherwise size in bytes + if (alsa_buffer < 500) { + unsigned time = alsa_buffer * 1000; + int dir = 0; + if ((err = snd_pcm_hw_params_set_buffer_time_near(pcmp, hw_params, &time, &dir)) < 0) { + LOG_ERROR("unable to set buffer time %s", snd_strerror(err)); + return err; + } + } else { + snd_pcm_uframes_t size = alsa_buffer; + if ((err = snd_pcm_hw_params_set_buffer_size_near(pcmp, hw_params, &size)) < 0) { + LOG_ERROR("unable to set buffer size %s", snd_strerror(err)); + return err; + } + } + + // get period_size + if ((err = snd_pcm_hw_params_get_period_size(hw_params, &alsa.period_size, 0)) < 0) { + LOG_ERROR("unable to get period size: %s", snd_strerror(err)); + return err; + } + + // get buffer_size + if ((err = snd_pcm_hw_params_get_buffer_size(hw_params, &alsa.buffer_size)) < 0) { + LOG_ERROR("unable to get buffer size: %s", snd_strerror(err)); + return err; + } + + LOG_INFO("buffer: %u period: %u -> buffer size: %u period size: %u", alsa_buffer, alsa_period, alsa.buffer_size, alsa.period_size); + + // ensure we have two buffer sizes of samples before starting output + output.start_frames = alsa.buffer_size * 2; + + // create an intermediate buffer for non mmap case for all but NATIVE_FORMAT + // this is used to pack samples into the output format before calling writei + if (!alsa.mmap && !alsa.write_buf && alsa.format != NATIVE_FORMAT) { + alsa.write_buf = malloc(alsa.buffer_size * BYTES_PER_FRAME); + if (!alsa.write_buf) { + LOG_ERROR("unable to malloc write_buf"); + return -1; + } + } + + // set params + if ((err = snd_pcm_hw_params(pcmp, hw_params)) < 0) { + LOG_ERROR("unable to set hw params: %s", snd_strerror(err)); + return err; + } + + // dump info + if (loglevel == lSDEBUG) { + static snd_output_t *debug_output; + snd_output_stdio_attach(&debug_output, stderr, 0); + snd_pcm_dump(pcmp, debug_output); + } + + // this indicates we have opened the device ok + alsa.rate = sample_rate; + + return 0; +} + +static int _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) { + + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t offset; + void *outputptr; + s32_t *inputptr; + int err; + + if (alsa.mmap) { + snd_pcm_uframes_t alsa_frames = (snd_pcm_uframes_t)out_frames; + + snd_pcm_avail_update(pcmp); + + if ((err = snd_pcm_mmap_begin(pcmp, &areas, &offset, &alsa_frames)) < 0) { + LOG_WARN("error from mmap_begin: %s", snd_strerror(err)); + return -1; + } + + out_frames = (frames_t)alsa_frames; + } + + if (!silence) { + // applying cross fade is delayed until this point as mmap_begin can change out_frames + 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); + } + } + + inputptr = (s32_t *) (silence ? silencebuf : outputbuf->readp); + + IF_DSD( + if (output.dop) { + if (silence) { + inputptr = (s32_t *) silencebuf_dop; + } + update_dop((u32_t *) inputptr, out_frames, output.invert && !silence); + } + ) + + if (alsa.mmap || alsa.format != NATIVE_FORMAT) { + + outputptr = alsa.mmap ? (areas[0].addr + (areas[0].first + offset * areas[0].step) / 8) : alsa.write_buf; + + _scale_and_pack_frames(outputptr, inputptr, out_frames, gainL, gainR, output.format); + + } else { + + outputptr = (void *)inputptr; + + if (!silence) { + + if (gainL != FIXED_ONE || gainR!= FIXED_ONE) { + _apply_gain(outputbuf, out_frames, gainL, gainR); + } + } + } + + if (alsa.mmap) { + + snd_pcm_sframes_t w = snd_pcm_mmap_commit(pcmp, offset, out_frames); + if (w < 0 || w != out_frames) { + LOG_WARN("mmap_commit error"); + return -1; + } + + } else { + + snd_pcm_sframes_t w = snd_pcm_writei(pcmp, outputptr, out_frames); + if (w < 0) { + //if (w != -EAGAIN && ((err = snd_pcm_recover(pcmp, w, 1)) < 0)) { + if (((err = snd_pcm_recover(pcmp, w, 1)) < 0)) { + static unsigned recover_count = 0; + LOG_WARN("recover failed: %s [%u]", snd_strerror(err), ++recover_count); + if (recover_count >= 10) { + recover_count = 0; + alsa_close(); + pcmp = NULL; + } + } + return -1; + } else { + if (w != out_frames) { + LOG_WARN("writei only wrote %u of %u", w, out_frames); + } + out_frames = w; + } + } + + return (int)out_frames; +} + +static void *output_thread(void *arg) { + bool start = true; + bool output_off = (output.state == OUTPUT_OFF); + bool probe_device = (arg != NULL); + int err; + + while (running) { + + // disabled output - player is off + while (output_off) { + usleep(100000); + LOCK; + output_off = (output.state == OUTPUT_OFF); + UNLOCK; + if (!running) return 0; + } + + // wait until device returns - to allow usb audio devices to be turned off + if (probe_device) { + while (!pcm_probe(output.device)) { + LOG_DEBUG("waiting for device %s to return", output.device); + sleep(5); + } + probe_device = false; + } + + if (!pcmp || alsa.rate != output.current_sample_rate) { + LOG_INFO("open output device: %s", output.device); + LOCK; + + // FIXME - some alsa hardware requires opening twice for a new sample rate to work + // this is a workaround which should be removed + if (alsa.reopen) { + alsa_open(output.device, output.current_sample_rate, output.buffer, output.period); + } + + if (!!alsa_open(output.device, output.current_sample_rate, output.buffer, output.period)) { + output.error_opening = true; + UNLOCK; + sleep(5); + continue; + } + output.error_opening = false; + start = true; + UNLOCK; + } + + snd_pcm_state_t state = snd_pcm_state(pcmp); + + if (state == SND_PCM_STATE_XRUN) { + LOG_INFO("XRUN"); + if ((err = snd_pcm_recover(pcmp, -EPIPE, 1)) < 0) { + LOG_INFO("XRUN recover failed: %s", snd_strerror(err)); + } + start = true; + continue; + } else if (state == SND_PCM_STATE_SUSPENDED) { + if ((err = snd_pcm_recover(pcmp, -ESTRPIPE, 1)) < 0) { + LOG_INFO("SUSPEND recover failed: %s", snd_strerror(err)); + } + } else if (state == SND_PCM_STATE_DISCONNECTED) { + LOG_INFO("Device %s no longer available", output.device); + alsa_close(); + pcmp = NULL; + probe_device = true; + continue; + } + + snd_pcm_sframes_t avail = snd_pcm_avail_update(pcmp); + + if (avail < 0) { + if ((err = snd_pcm_recover(pcmp, avail, 1)) < 0) { + if (err == -ENODEV) { + LOG_INFO("Device %s no longer available", output.device); + alsa_close(); + pcmp = NULL; + probe_device = true; + continue; + } + LOG_WARN("recover failed: %s", snd_strerror(err)); + } + start = true; + continue; + } + + if (avail < alsa.period_size) { + if (start) { + if (alsa.mmap && ((err = snd_pcm_start(pcmp)) < 0)) { + if ((err = snd_pcm_recover(pcmp, err, 1)) < 0) { + if (err == -ENODEV) { + LOG_INFO("Device %s no longer available", output.device); + alsa_close(); + pcmp = NULL; + probe_device = true; + continue; + } + LOG_INFO("start error: %s", snd_strerror(err)); + usleep(10000); + } + } else { + start = false; + } + } else { + if ((err = snd_pcm_wait(pcmp, 1000)) < 0) { + if ((err = snd_pcm_recover(pcmp, err, 1)) < 0) { + LOG_INFO("pcm wait error: %s", snd_strerror(err)); + } + start = true; + } + } + continue; + } + + // restrict avail to within sensible limits as alsa drivers can return erroneous large values + // in writei mode restrict to period_size due to size of write_buf + if (alsa.mmap) { + avail = min(avail, alsa.buffer_size); + } else { + avail = min(avail, alsa.period_size); + } + + // avoid spinning in cases where wait returns but no bytes available (seen with pulse audio) + if (avail == 0) { + LOG_SDEBUG("avail 0 - sleeping"); + usleep(10000); + continue; + } + + LOCK; + + // turn off if requested + if (output.state == OUTPUT_OFF) { + UNLOCK; + LOG_INFO("disabling output"); + alsa_close(); + pcmp = NULL; + output_off = true; + vis_stop(); + continue; + } + + // measure output delay + snd_pcm_sframes_t delay; + if ((err = snd_pcm_delay(pcmp, &delay)) < 0) { + if (err == -EPIPE) { + // EPIPE indicates underrun - attempt to recover + UNLOCK; + continue; + } else if (err == -EIO) { + // EIO can occur with non existant pulse server + UNLOCK; + LOG_SDEBUG("snd_pcm_delay returns: EIO - sleeping"); + usleep(100000); + continue; + } else { + LOG_DEBUG("snd_pcm_delay returns: %d", err); + } + } else { + output.device_frames = delay; + output.updated = gettime_ms(); + output.frames_played_dmp = output.frames_played; + } + + // process frames + frames_t wrote = _output_frames(avail); + + UNLOCK; + + // some output devices such as alsa null refuse any data, avoid spinning + if (!wrote) { + LOG_SDEBUG("wrote 0 - sleeping"); + usleep(10000); + } + } + + return 0; +} + +static pthread_t thread; + +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 *volume_mixer, bool mixer_unmute) { + + unsigned alsa_buffer = ALSA_BUFFER_TIME; + unsigned alsa_period = ALSA_PERIOD_COUNT; + char *alsa_sample_fmt = NULL; + bool alsa_mmap = true; + bool alsa_reopen = false; + + char *volume_mixer_name = next_param(volume_mixer, ','); + char *volume_mixer_index = next_param(NULL, ','); + + char *t = next_param(params, ':'); + char *c = next_param(NULL, ':'); + char *s = next_param(NULL, ':'); + char *m = next_param(NULL, ':'); + char *r = next_param(NULL, ':'); + + if (t) alsa_buffer = atoi(t); + if (c) alsa_period = atoi(c); + if (s) alsa_sample_fmt = s; + if (m) alsa_mmap = atoi(m); + if (r) alsa_reopen = atoi(r); + + loglevel = level; + + LOG_INFO("init output"); + + memset(&output, 0, sizeof(output)); + + alsa.mmap = alsa_mmap; + alsa.write_buf = NULL; + alsa.format = 0; + alsa.reopen = alsa_reopen; + + if (!mixer_unmute) { + alsa.volume_mixer_name = volume_mixer_name; + alsa.volume_mixer_index = volume_mixer_index ? atoi(volume_mixer_index) : 0; + } + + output.format = 0; + output.buffer = alsa_buffer; + output.period = alsa_period; + output.start_frames = 0; + output.write_cb = &_write_frames; + output.rate_delay = rate_delay; + + if (alsa_sample_fmt) { + if (!strcmp(alsa_sample_fmt, "32")) alsa.format = SND_PCM_FORMAT_S32_LE; + if (!strcmp(alsa_sample_fmt, "24")) alsa.format = SND_PCM_FORMAT_S24_LE; + if (!strcmp(alsa_sample_fmt, "24_3")) alsa.format = SND_PCM_FORMAT_S24_3LE; + if (!strcmp(alsa_sample_fmt, "16")) alsa.format = SND_PCM_FORMAT_S16_LE; + } + + LOG_INFO("requested alsa_buffer: %u alsa_period: %u format: %s mmap: %u", output.buffer, output.period, + alsa_sample_fmt ? alsa_sample_fmt : "any", alsa.mmap); + + snd_lib_error_set_handler((snd_lib_error_handler_t)alsa_error_handler); + + output_init_common(level, device, output_buf_size, rates, idle); + + if (mixer_unmute && volume_mixer_name) { + set_mixer(output.device, volume_mixer_name, volume_mixer_index ? atoi(volume_mixer_index) : 0, true, 0, 0); + } + +#if LINUX + // RT linux - aim to avoid pagefaults by locking memory: + // https://rt.wiki.kernel.org/index.php/Threaded_RT-application_with_memory_locking_and_stack_handling_example + if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) { + LOG_INFO("unable to lock memory: %s", strerror(errno)); + } else { + LOG_INFO("memory locked"); + } + + mallopt(M_TRIM_THRESHOLD, -1); + mallopt(M_MMAP_MAX, 0); + + touch_memory(silencebuf, MAX_SILENCE_FRAMES * BYTES_PER_FRAME); + touch_memory(outputbuf->buf, outputbuf->size); +#endif + + // start output thread + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + OUTPUT_THREAD_STACK_SIZE); + pthread_create(&thread, &attr, output_thread, rates[0] ? "probe" : NULL); + pthread_attr_destroy(&attr); + + // try to set this thread to real-time scheduler class, only works as root or if user has permission + struct sched_param param; + param.sched_priority = rt_priority; + if (pthread_setschedparam(thread, SCHED_FIFO, ¶m) != 0) { + LOG_DEBUG("unable to set output sched fifo: %s", strerror(errno)); + } else { + LOG_DEBUG("set output sched fifo rt: %u", param.sched_priority); + } +} + +void output_close_alsa(void) { + LOG_INFO("close output"); + + LOCK; + running = false; + UNLOCK; + + pthread_join(thread, NULL); + + if (alsa.write_buf) free(alsa.write_buf); + + output_close_common(); +} + +#endif // ALSA + diff --git a/output_pa.c b/output_pa.c new file mode 100644 index 0000000..025904b --- /dev/null +++ b/output_pa.c @@ -0,0 +1,455 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// Portaudio output + +#include "squeezelite.h" + +#if PORTAUDIO + +#include +#if OSX +#include +#endif + +// ouput device +static struct { + unsigned rate; + PaStream *stream; +} pa; + +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) + +extern u8_t *silencebuf; +#if DSD +extern u8_t *silencebuf_dop; +#endif + +void list_devices(void) { + PaError err; + int i; + + if ((err = Pa_Initialize()) != paNoError) { + LOG_WARN("error initialising port audio: %s", Pa_GetErrorText(err)); + return; + } + + printf("Output devices:\n"); + for (i = 0; i < Pa_GetDeviceCount(); ++i) { + if (Pa_GetDeviceInfo(i)->maxOutputChannels) { + printf(" %i - %s [%s]\n", i, Pa_GetDeviceInfo(i)->name, Pa_GetHostApiInfo(Pa_GetDeviceInfo(i)->hostApi)->name); + } + } + printf("\n"); + + if ((err = Pa_Terminate()) != paNoError) { + LOG_WARN("error closing port audio: %s", Pa_GetErrorText(err)); + } +} + +void set_volume(unsigned left, unsigned right) { + LOG_DEBUG("setting internal gain left: %u right: %u", left, right); + LOCK; + output.gainL = left; + output.gainR = right; + UNLOCK; +} + +static int pa_device_id(const char *device) { + int len = strlen(device); + int i; + + if (!strncmp(device, "default", 7)) { + return Pa_GetDefaultOutputDevice(); + } + if (len >= 1 && len <= 2 && device[0] >= '0' && device[0] <= '9') { + return atoi(device); + } + +#define DEVICE_ID_MAXLEN 256 + for (i = 0; i < Pa_GetDeviceCount(); ++i) { + char tmp[DEVICE_ID_MAXLEN]; + snprintf(tmp, DEVICE_ID_MAXLEN, "%s [%s]", Pa_GetDeviceInfo(i)->name, Pa_GetHostApiInfo(Pa_GetDeviceInfo(i)->hostApi)->name); + if (!strncmp(tmp, device, len)) { + return i; + } + } + + return -1; +} + +static int pa_callback(const void *pa_input, void *pa_output, unsigned long pa_frames_wanted, + const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData); + +bool test_open(const char *device, unsigned rates[]) { + PaStreamParameters outputParameters; + PaError err; + unsigned ref[] TEST_RATES; + int device_id, i, ind; + + if ((device_id = pa_device_id(device)) == -1) { + LOG_INFO("device %s not found", device); + return false; + } + + outputParameters.device = device_id; + outputParameters.channelCount = 2; + outputParameters.sampleFormat = paInt32; + outputParameters.suggestedLatency = + output.latency ? (double)output.latency/(double)1000 : Pa_GetDeviceInfo(outputParameters.device)->defaultHighOutputLatency; + outputParameters.hostApiSpecificStreamInfo = NULL; + + // check supported sample rates + // Note use Pa_OpenStream as it appears more reliable than Pa_IsFormatSupported on some windows apis + for (i = 0, ind = 0; ref[i]; ++i) { + err = Pa_OpenStream(&pa.stream, NULL, &outputParameters, (double)ref[i], paFramesPerBufferUnspecified, paNoFlag, + pa_callback, NULL); + if (err == paNoError) { + Pa_CloseStream(pa.stream); + rates[ind++] = ref[i]; + } + } + + if (!rates[0]) { + LOG_WARN("no available rate found"); + return false; + } + + pa.stream = NULL; + return true; +} + +static void pa_stream_finished(void *userdata) { + if (running) { + LOG_INFO("stream finished"); + LOCK; + output.pa_reopen = true; + wake_controller(); + UNLOCK; + } +} + +static thread_type monitor_thread; +bool monitor_thread_running = false; + +static void *pa_monitor() { + bool output_off; + + LOCK; + + if (monitor_thread_running) { + LOG_DEBUG("monitor thread already running"); + UNLOCK; + return 0; + } + + LOG_DEBUG("start monitor thread"); + + monitor_thread_running = true; + output_off = (output.state == OUTPUT_OFF); + + while (monitor_thread_running) { + if (output_off) { + if (output.state != OUTPUT_OFF) { + LOG_INFO("output on"); + break; + } + } else { + // this is a hack to partially support hot plugging of devices + // we rely on terminating and reinitalising PA to get an updated list of devices and use name for output.device + LOG_INFO("probing device %s", output.device); + Pa_Terminate(); + Pa_Initialize(); + pa.stream = NULL; + if (pa_device_id(output.device) != -1) { + LOG_INFO("device reopen"); + break; + } + } + + UNLOCK; + sleep(output_off ? 1 : 5); + LOCK; + } + + LOG_DEBUG("end monitor thread"); + + monitor_thread_running = false; + pa.stream = NULL; + + _pa_open(); + + UNLOCK; + + return 0; +} + +void _pa_open(void) { + PaStreamParameters outputParameters; + PaError err = paNoError; + int device_id; + + if (pa.stream) { + if ((err = Pa_CloseStream(pa.stream)) != paNoError) { + LOG_WARN("error closing stream: %s", Pa_GetErrorText(err)); + } + } + + if (output.state == OUTPUT_OFF) { + // we get called when transitioning to OUTPUT_OFF to create the probe thread + // set err to avoid opening device and logging messages + err = 1; + + } else if ((device_id = pa_device_id(output.device)) == -1) { + LOG_INFO("device %s not found", output.device); + err = 1; + + } else { + + outputParameters.device = device_id; + outputParameters.channelCount = 2; + outputParameters.sampleFormat = paInt32; + outputParameters.suggestedLatency = + output.latency ? (double)output.latency/(double)1000 : Pa_GetDeviceInfo(outputParameters.device)->defaultHighOutputLatency; + outputParameters.hostApiSpecificStreamInfo = NULL; + +#if OSX + // enable pro mode which aims to avoid resampling if possible + // see http://code.google.com/p/squeezelite/issues/detail?id=11 & http://code.google.com/p/squeezelite/issues/detail?id=37 + // command line controls osx_playnice which is -1 if not specified, 0 or 1 - choose playnice if -1 or 1 + PaMacCoreStreamInfo macInfo; + unsigned long streamInfoFlags; + if (output.osx_playnice) { + LOG_INFO("opening device in PlayNice mode"); + streamInfoFlags = paMacCorePlayNice; + } else { + LOG_INFO("opening device in Pro mode"); + streamInfoFlags = paMacCorePro; + } + PaMacCore_SetupStreamInfo(&macInfo, streamInfoFlags); + outputParameters.hostApiSpecificStreamInfo = &macInfo; +#endif + } + + if (!err && + (err = Pa_OpenStream(&pa.stream, NULL, &outputParameters, (double)output.current_sample_rate, paFramesPerBufferUnspecified, + paPrimeOutputBuffersUsingStreamCallback | paDitherOff, pa_callback, NULL)) != paNoError) { + LOG_WARN("error opening device %i - %s : %s", outputParameters.device, Pa_GetDeviceInfo(outputParameters.device)->name, + Pa_GetErrorText(err)); + } + + if (!err) { + LOG_INFO("opened device %i - %s at %u latency %u ms", outputParameters.device, Pa_GetDeviceInfo(outputParameters.device)->name, + (unsigned int)Pa_GetStreamInfo(pa.stream)->sampleRate, (unsigned int)(Pa_GetStreamInfo(pa.stream)->outputLatency * 1000)); + + pa.rate = output.current_sample_rate; + + if ((err = Pa_SetStreamFinishedCallback(pa.stream, pa_stream_finished)) != paNoError) { + LOG_WARN("error setting finish callback: %s", Pa_GetErrorText(err)); + } + + UNLOCK; // StartStream can call pa_callback in a sychronised thread on freebsd, remove lock while it is called + + if ((err = Pa_StartStream(pa.stream)) != paNoError) { + LOG_WARN("error starting stream: %s", Pa_GetErrorText(err)); + } + + LOCK; + } + + if (err && !monitor_thread_running) { + vis_stop(); + + // create a thread to check for output state change or device return +#if LINUX || OSX || FREEBSD + pthread_create(&monitor_thread, NULL, pa_monitor, NULL); +#endif +#if WIN + monitor_thread = CreateThread(NULL, OUTPUT_THREAD_STACK_SIZE, (LPTHREAD_START_ROUTINE)&pa_monitor, NULL, 0, NULL); +#endif + } + + output.error_opening = !!err; +} + +static u8_t *optr; + +static int _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) { + + 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); + } + + if (gainL != FIXED_ONE || gainR!= FIXED_ONE) { + _apply_gain(outputbuf, out_frames, gainL, gainR); + } + + IF_DSD( + if (output.dop) { + update_dop((u32_t *) outputbuf->readp, out_frames, output.invert); + } + ) + + memcpy(optr, outputbuf->readp, out_frames * BYTES_PER_FRAME); + + } else { + + u8_t *buf = silencebuf; + + IF_DSD( + if (output.dop) { + buf = silencebuf_dop; + update_dop((u32_t *) buf, out_frames, false); // don't invert silence + } + ) + + memcpy(optr, buf, out_frames * BYTES_PER_FRAME); + } + + optr += out_frames * BYTES_PER_FRAME; + + return (int)out_frames; +} + +static int pa_callback(const void *pa_input, void *pa_output, unsigned long pa_frames_wanted, + const PaStreamCallbackTimeInfo *time_info, PaStreamCallbackFlags statusFlags, void *userData) { + int ret; + double stream_time; + frames_t frames; + + optr = (u8_t *)pa_output; + + LOCK; + + stream_time = Pa_GetStreamTime(pa.stream); + + if (time_info->outputBufferDacTime > stream_time) { + // workaround for wdm-ks which can return outputBufferDacTime with a different epoch + output.device_frames = (unsigned)((time_info->outputBufferDacTime - stream_time) * output.current_sample_rate); + } else { + output.device_frames = 0; + } + + output.updated = gettime_ms(); + output.frames_played_dmp = output.frames_played; + + do { + frames = _output_frames(pa_frames_wanted); + pa_frames_wanted -= frames; + } while (pa_frames_wanted > 0 && frames != 0); + + if (pa_frames_wanted > 0) { + LOG_DEBUG("pad with silence"); + memset(optr, 0, pa_frames_wanted * BYTES_PER_FRAME); + } + + if (output.state == OUTPUT_OFF) { + LOG_INFO("output off"); + ret = paComplete; + } else if (pa.rate != output.current_sample_rate) { + ret = paComplete; + } else { + ret = paContinue; + } + + UNLOCK; + + return ret; +} + +void output_init_pa(log_level level, const char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, + unsigned idle) { + PaError err; + unsigned latency = 0; + int osx_playnice = -1; + + char *l = next_param(params, ':'); + char *p = next_param(NULL, ':'); + + if (l) latency = (unsigned)atoi(l); + if (p) osx_playnice = atoi(p); + + loglevel = level; + + LOG_INFO("init output"); + + memset(&output, 0, sizeof(output)); + + output.latency = latency; + output.osx_playnice = osx_playnice; + output.format = 0; + output.start_frames = 0; + output.write_cb = &_write_frames; + output.rate_delay = rate_delay; + pa.stream = NULL; + + LOG_INFO("requested latency: %u", output.latency); + + if ((err = Pa_Initialize()) != paNoError) { + LOG_WARN("error initialising port audio: %s", Pa_GetErrorText(err)); + exit(0); + } + + output_init_common(level, device, output_buf_size, rates, idle); + + LOCK; + + _pa_open(); + + UNLOCK; +} + +void output_close_pa(void) { + PaError err; + + LOG_INFO("close output"); + + LOCK; + + running = false; + monitor_thread_running = false; + + if (pa.stream) { + if ((err = Pa_AbortStream(pa.stream)) != paNoError) { + LOG_WARN("error closing stream: %s", Pa_GetErrorText(err)); + } + } + + if ((err = Pa_Terminate()) != paNoError) { + LOG_WARN("error closing port audio: %s", Pa_GetErrorText(err)); + } + + UNLOCK; + + output_close_common(); +} + +#endif // PORTAUDIO diff --git a/output_pack.c b/output_pack.c new file mode 100644 index 0000000..16e7ab7 --- /dev/null +++ b/output_pack.c @@ -0,0 +1,270 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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) { + 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/output_stdout.c b/output_stdout.c new file mode 100644 index 0000000..37544ab --- /dev/null +++ b/output_stdout.c @@ -0,0 +1,177 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// Stdout output + +#include "squeezelite.h" + +#define FRAME_BLOCK MAX_SILENCE_FRAMES + +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) + +extern u8_t *silencebuf; +#if DSD +extern u8_t *silencebuf_dop; +#endif + +// 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 int _stdout_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; + } + + IF_DSD( + if (output.dop) { + if (silence) { + obuf = silencebuf_dop; + } + update_dop((u32_t *)obuf, out_frames, output.invert && !silence); + } + ) + + _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; +} + +static thread_type thread; + +void output_init_stdout(log_level level, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay) { + loglevel = level; + + LOG_INFO("init output stdout"); + + 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 = &_stdout_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 + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + OUTPUT_THREAD_STACK_SIZE); + 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_stdout(void) { + LOG_INFO("close output"); + + LOCK; + running = false; + UNLOCK; + + free(buf); + + output_close_common(); +} diff --git a/output_vis.c b/output_vis.c new file mode 100644 index 0000000..087836e --- /dev/null +++ b/output_vis.c @@ -0,0 +1,144 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// Export audio samples for visualiser process (16 bit only best endevours) + +#include "squeezelite.h" + +#if VISEXPORT + +#include +#include +#include + +#define VIS_BUF_SIZE 16384 +#define VIS_LOCK_NS 1000000 // ns to wait for vis wrlock + +static struct vis_t { + pthread_rwlock_t rwlock; + u32_t buf_size; + u32_t buf_index; + bool running; + u32_t rate; + time_t updated; + s16_t buffer[VIS_BUF_SIZE]; +} *vis_mmap = NULL; + +static char vis_shm_path[40]; +static int vis_fd = -1; + +static log_level loglevel; + +// attempt to write audio to vis_mmap but do not wait more than VIS_LOCK_NS to get wrlock +// this can result in missing audio export to the mmap region, but this is preferable dropping audio +void _vis_export(struct buffer *outputbuf, struct outputstate *output, frames_t out_frames, bool silence) { + if (vis_mmap) { + int err; + + err = pthread_rwlock_trywrlock(&vis_mmap->rwlock); + if (err) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += VIS_LOCK_NS; + if (ts.tv_nsec > 1000000000) { + ts.tv_sec += 1; + ts.tv_nsec -= 1000000000; + } + err = pthread_rwlock_timedwrlock(&vis_mmap->rwlock, &ts); + } + + if (err) { + LOG_DEBUG("failed to get wrlock - skipping visulizer export"); + + } else { + + if (silence) { + vis_mmap->running = false; + } else { + frames_t vis_cnt = out_frames; + s32_t *ptr = (s32_t *) outputbuf->readp; + unsigned i = vis_mmap->buf_index; + + if (!output->current_replay_gain) { + while (vis_cnt--) { + vis_mmap->buffer[i++] = *(ptr++) >> 16; + vis_mmap->buffer[i++] = *(ptr++) >> 16; + if (i == VIS_BUF_SIZE) i = 0; + } + } else { + while (vis_cnt--) { + vis_mmap->buffer[i++] = gain(*(ptr++), output->current_replay_gain) >> 16; + vis_mmap->buffer[i++] = gain(*(ptr++), output->current_replay_gain) >> 16; + if (i == VIS_BUF_SIZE) i = 0; + } + } + + vis_mmap->updated = time(NULL); + vis_mmap->running = true; + vis_mmap->buf_index = i; + vis_mmap->rate = output->current_sample_rate; + } + + pthread_rwlock_unlock(&vis_mmap->rwlock); + } + } +} + +void vis_stop(void) { + if (vis_mmap) { + pthread_rwlock_wrlock(&vis_mmap->rwlock); + vis_mmap->running = false; + pthread_rwlock_unlock(&vis_mmap->rwlock); + } +} + +void output_vis_init(log_level level, u8_t *mac) { + loglevel = level; + + sprintf(vis_shm_path, "/squeezelite-%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + mode_t old_mask = umask(000); // allow any user to read our shm when created + + vis_fd = shm_open(vis_shm_path, O_CREAT | O_RDWR, 0666); + if (vis_fd != -1) { + if (ftruncate(vis_fd, sizeof(struct vis_t)) == 0) { + vis_mmap = (struct vis_t *)mmap(NULL, sizeof(struct vis_t), PROT_READ | PROT_WRITE, MAP_SHARED, vis_fd, 0); + } + } + + if (vis_mmap > 0) { + pthread_rwlockattr_t attr; + pthread_rwlockattr_init(&attr); + pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); + pthread_rwlock_init(&vis_mmap->rwlock, &attr); + vis_mmap->buf_size = VIS_BUF_SIZE; + vis_mmap->running = false; + vis_mmap->rate = 44100; + pthread_rwlockattr_destroy(&attr); + LOG_INFO("opened visulizer shared memory as %s", vis_shm_path); + } else { + LOG_WARN("unable to open visualizer shared memory"); + vis_mmap = NULL; + } + + umask(old_mask); +} + +#endif // VISEXPORT diff --git a/pcm.c b/pcm.c new file mode 100644 index 0000000..c867ef5 --- /dev/null +++ b/pcm.c @@ -0,0 +1,386 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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; + +#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 +}; + +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; + LOG_INFO("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); + audio_left = len - 8 - offset; + LOG_INFO("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) { + _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.next_sample_rate = decode_newstream(sample_rate, output.supported_rates); + output.track_start = outputbuf->writep; + IF_DSD( output.next_dop = false; ) + if (output.fade_mode) _checkfade(true); + decode.new_stream = false; + 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) { + 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; +} diff --git a/process.c b/process.c new file mode 100644 index 0000000..6a107c6 --- /dev/null +++ b/process.c @@ -0,0 +1,193 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// sample processing - only included when building with PROCESS set + +#include "squeezelite.h" + +#if PROCESS + +extern log_level loglevel; + +extern struct buffer *outputbuf; +extern struct decodestate decode; +struct processstate process; +extern struct codec *codec; + +#define LOCK_D mutex_lock(decode.mutex); +#define UNLOCK_D mutex_unlock(decode.mutex); +#define LOCK_O mutex_lock(outputbuf->mutex) +#define UNLOCK_O mutex_unlock(outputbuf->mutex) + +// macros to map to processing functions - currently only resample.c +// this can be made more generic when multiple processing mechanisms get added +#if RESAMPLE +#define SAMPLES_FUNC resample_samples +#define DRAIN_FUNC resample_drain +#define NEWSTREAM_FUNC resample_newstream +#define FLUSH_FUNC resample_flush +#define INIT_FUNC resample_init +#endif + + +// transfer all processed frames to the output buf +static void _write_samples(void) { + size_t frames = process.out_frames; + u32_t *iptr = (u32_t *)process.outbuf; + unsigned cnt = 10; + + LOCK_O; + + while (frames > 0) { + + frames_t f = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME; + u32_t *optr = (u32_t *)outputbuf->writep; + + if (f > 0) { + + f = min(f, frames); + + memcpy(optr, iptr, f * BYTES_PER_FRAME); + + frames -= f; + + _buf_inc_writep(outputbuf, f * BYTES_PER_FRAME); + iptr += f * BYTES_PER_FRAME / sizeof(*iptr); + + } else if (cnt--) { + + // there should normally be space in the output buffer, but may need to wait during drain phase + UNLOCK_O; + usleep(10000); + LOCK_O; + + } else { + + // bail out if no space found after 100ms to avoid locking + LOG_ERROR("unable to get space in output buffer"); + UNLOCK_O; + return; + } + } + + UNLOCK_O; +} + +// process samples - called with decode mutex set +void process_samples(void) { + + SAMPLES_FUNC(&process); + + _write_samples(); + + process.in_frames = 0; +} + +// drain at end of track - called with decode mutex set +void process_drain(void) { + bool done; + + do { + + done = DRAIN_FUNC(&process); + + _write_samples(); + + } while (!done); + + LOG_DEBUG("processing track complete - frames in: %lu out: %lu", process.total_in, process.total_out); +} + +// new stream - called with decode mutex set +unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned supported_rates[]) { + + bool active = NEWSTREAM_FUNC(&process, raw_sample_rate, supported_rates); + + LOG_INFO("processing: %s", active ? "active" : "inactive"); + + *direct = !active; + + if (active) { + + unsigned max_in_frames, max_out_frames; + + process.in_frames = process.out_frames = 0; + process.total_in = process.total_out = 0; + + max_in_frames = codec->min_space / BYTES_PER_FRAME ; + + // increase size of output buffer by 10% as output rate is not an exact multiple of input rate + if (process.out_sample_rate % process.in_sample_rate == 0) { + max_out_frames = max_in_frames * (process.out_sample_rate / process.in_sample_rate); + } else { + max_out_frames = (int)(1.1 * (float)max_in_frames * (float)process.out_sample_rate / (float)process.in_sample_rate); + } + + if (process.max_in_frames != max_in_frames) { + LOG_DEBUG("creating process buf in frames: %u", max_in_frames); + if (process.inbuf) free(process.inbuf); + process.inbuf = malloc(max_in_frames * BYTES_PER_FRAME); + process.max_in_frames = max_in_frames; + } + + if (process.max_out_frames != max_out_frames) { + LOG_DEBUG("creating process buf out frames: %u", max_out_frames); + if (process.outbuf) free(process.outbuf); + process.outbuf = malloc(max_out_frames * BYTES_PER_FRAME); + process.max_out_frames = max_out_frames; + } + + if (!process.inbuf || !process.outbuf) { + LOG_ERROR("malloc fail creating process buffers"); + *direct = true; + return raw_sample_rate; + } + + return process.out_sample_rate; + } + + return raw_sample_rate; +} + +// process flush - called with decode mutex set +void process_flush(void) { + + LOG_INFO("process flush"); + + FLUSH_FUNC(); + + process.in_frames = 0; +} + +// init - called with no mutex +void process_init(char *opt) { + + bool enabled = INIT_FUNC(opt); + + memset(&process, 0, sizeof(process)); + + if (enabled) { + LOCK_D; + decode.process = true; + UNLOCK_D; + } +} + +#endif // #if PROCESS diff --git a/resample.c b/resample.c new file mode 100644 index 0000000..6157638 --- /dev/null +++ b/resample.c @@ -0,0 +1,366 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// upsampling using libsoxr - only included if RESAMPLE set + +#include "squeezelite.h" + +#if RESAMPLE + +#include +#include + +extern log_level loglevel; + +struct soxr { + soxr_t resampler; + size_t old_clips; + unsigned long q_recipe; + unsigned long q_flags; + double q_precision; /* Conversion precision (in bits). 20 */ + double q_phase_response; /* 0=minimum, ... 50=linear, ... 100=maximum 50 */ + double q_passband_end; /* 0dB pt. bandwidth to preserve; nyquist=1 0.913 */ + double q_stopband_begin; /* Aliasing/imaging control; > passband_end 1 */ + double scale; + bool max_rate; + bool exception; +#if !LINKALL + // soxr symbols to be dynamically loaded + soxr_io_spec_t (* soxr_io_spec)(soxr_datatype_t itype, soxr_datatype_t otype); + soxr_quality_spec_t (* soxr_quality_spec)(unsigned long recipe, unsigned long flags); + soxr_t (* soxr_create)(double, double, unsigned, soxr_error_t *, + soxr_io_spec_t const *, soxr_quality_spec_t const *, soxr_runtime_spec_t const *); + void (* soxr_delete)(soxr_t); + soxr_error_t (* soxr_process)(soxr_t, soxr_in_t, size_t, size_t *, soxr_out_t, size_t olen, size_t *); + size_t *(* soxr_num_clips)(soxr_t); +#if RESAMPLE_MP + soxr_runtime_spec_t (* soxr_runtime_spec)(unsigned num_threads); +#endif + // soxr_strerror is a macro so not included here +#endif +}; + +static struct soxr *r; + +#if LINKALL +#define SOXR(h, fn, ...) (soxr_ ## fn)(__VA_ARGS__) +#else +#define SOXR(h, fn, ...) (h)->soxr_##fn(__VA_ARGS__) +#endif + + +void resample_samples(struct processstate *process) { + size_t idone, odone; + size_t clip_cnt; + + soxr_error_t error = + SOXR(r, process, r->resampler, process->inbuf, process->in_frames, &idone, process->outbuf, process->max_out_frames, &odone); + if (error) { + LOG_INFO("soxr_process error: %s", soxr_strerror(error)); + return; + } + + if (idone != process->in_frames) { + // should not get here if buffers are big enough... + LOG_ERROR("should not get here - partial sox process: %u of %u processed %u of %u out", + (unsigned)idone, process->in_frames, (unsigned)odone, process->max_out_frames); + } + + process->out_frames = odone; + process->total_in += idone; + process->total_out += odone; + + clip_cnt = *(SOXR(r, num_clips, r->resampler)); + if (clip_cnt - r->old_clips) { + LOG_SDEBUG("resampling clips: %u", (unsigned)(clip_cnt - r->old_clips)); + r->old_clips = clip_cnt; + } +} + +bool resample_drain(struct processstate *process) { + size_t odone; + size_t clip_cnt; + + soxr_error_t error = SOXR(r, process, r->resampler, NULL, 0, NULL, process->outbuf, process->max_out_frames, &odone); + if (error) { + LOG_INFO("soxr_process error: %s", soxr_strerror(error)); + return true; + } + + process->out_frames = odone; + process->total_out += odone; + + clip_cnt = *(SOXR(r, num_clips, r->resampler)); + if (clip_cnt - r->old_clips) { + LOG_DEBUG("resampling clips: %u", (unsigned)(clip_cnt - r->old_clips)); + r->old_clips = clip_cnt; + } + + if (odone == 0) { + + LOG_INFO("resample track complete - total track clips: %u", r->old_clips); + + SOXR(r, delete, r->resampler); + r->resampler = NULL; + + return true; + + } else { + + return false; + } +} + +bool resample_newstream(struct processstate *process, unsigned raw_sample_rate, unsigned supported_rates[]) { + unsigned outrate = 0; + int i; + + if (r->exception) { + // find direct match - avoid resampling + for (i = 0; supported_rates[i]; i++) { + if (raw_sample_rate == supported_rates[i]) { + outrate = raw_sample_rate; + break; + } + } + // else find next highest sync sample rate + while (!outrate && i >= 0) { + if (supported_rates[i] > raw_sample_rate && supported_rates[i] % raw_sample_rate == 0) { + outrate = supported_rates[i]; + break; + } + i--; + } + } + + if (!outrate) { + if (r->max_rate) { + // resample to max rate for device + outrate = supported_rates[0]; + } else { + // resample to max sync sample rate + for (i = 0; supported_rates[i]; i++) { + if (supported_rates[i] % raw_sample_rate == 0 || raw_sample_rate % supported_rates[i] == 0) { + outrate = supported_rates[i]; + break; + } + } + } + if (!outrate) { + outrate = supported_rates[0]; + } + } + + process->in_sample_rate = raw_sample_rate; + process->out_sample_rate = outrate; + + if (r->resampler) { + SOXR(r, delete, r->resampler); + r->resampler = NULL; + } + + if (raw_sample_rate != outrate) { + + soxr_io_spec_t io_spec; + soxr_quality_spec_t q_spec; + soxr_error_t error; +#if RESAMPLE_MP + soxr_runtime_spec_t r_spec; +#endif + + LOG_INFO("resampling from %u -> %u", raw_sample_rate, outrate); + + io_spec = SOXR(r, io_spec, SOXR_INT32_I, SOXR_INT32_I); + io_spec.scale = r->scale; + + q_spec = SOXR(r, quality_spec, r->q_recipe, r->q_flags); + if (r->q_precision > 0) { + q_spec.precision = r->q_precision; + } + if (r->q_passband_end > 0) { + q_spec.passband_end = r->q_passband_end; + } + if (r->q_stopband_begin > 0) { + q_spec.stopband_begin = r->q_stopband_begin; + } + if (r->q_phase_response > -1) { + q_spec.phase_response = r->q_phase_response; + } + +#if RESAMPLE_MP + r_spec = SOXR(r, runtime_spec, 0); // make use of libsoxr OpenMP support allowing parallel execution if multiple cores +#endif + + LOG_DEBUG("resampling with soxr_quality_spec_t[precision: %03.1f, passband_end: %03.6f, stopband_begin: %03.6f, " + "phase_response: %03.1f, flags: 0x%02x], soxr_io_spec_t[scale: %03.2f]", q_spec.precision, + q_spec.passband_end, q_spec.stopband_begin, q_spec.phase_response, q_spec.flags, io_spec.scale); + +#if RESAMPLE_MP + r->resampler = SOXR(r, create, raw_sample_rate, outrate, 2, &error, &io_spec, &q_spec, &r_spec); +#else + r->resampler = SOXR(r, create, raw_sample_rate, outrate, 2, &error, &io_spec, &q_spec, NULL); +#endif + + if (error) { + LOG_INFO("soxr_create error: %s", soxr_strerror(error)); + return false; + } + + r->old_clips = 0; + return true; + + } else { + + LOG_INFO("disable resampling - rates match"); + return false; + } +} + +void resample_flush(void) { + if (r->resampler) { + SOXR(r, delete, r->resampler); + r->resampler = NULL; + } +} + +static bool load_soxr(void) { +#if !LINKALL + void *handle = dlopen(LIBSOXR, RTLD_NOW); + char *err; + + if (!handle) { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + + r->soxr_io_spec = dlsym(handle, "soxr_io_spec"); + r->soxr_quality_spec = dlsym(handle, "soxr_quality_spec"); + r->soxr_create = dlsym(handle, "soxr_create"); + r->soxr_delete = dlsym(handle, "soxr_delete"); + r->soxr_process = dlsym(handle, "soxr_process"); + r->soxr_num_clips = dlsym(handle, "soxr_num_clips"); +#if RESAMPLE_MP + r->soxr_runtime_spec = dlsym(handle, "soxr_runtime_spec"); +#endif + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded "LIBSOXR); +#endif + + return true; +} + +bool resample_init(char *opt) { + char *recipe = NULL, *flags = NULL; + char *atten = NULL; + char *precision = NULL, *passband_end = NULL, *stopband_begin = NULL, *phase_response = NULL; + + r = malloc(sizeof(struct soxr)); + if (!r) { + LOG_WARN("resampling disabled"); + return false; + } + + r->resampler = NULL; + r->old_clips = 0; + r->max_rate = false; + r->exception = false; + + if (!load_soxr()) { + LOG_WARN("resampling disabled"); + return false; + } + + if (opt) { + recipe = next_param(opt, ':'); + flags = next_param(NULL, ':'); + atten = next_param(NULL, ':'); + precision = next_param(NULL, ':'); + passband_end = next_param(NULL, ':'); + stopband_begin = next_param(NULL, ':'); + phase_response = next_param(NULL, ':'); + } + + // default to HQ (20 bit) if not user specified + r->q_recipe = SOXR_HQ; + r->q_flags = 0; + // default to 1db of attenuation if not user specified + r->scale = pow(10, -1.0 / 20); + // override recipe derived values with user specified values + r->q_precision = 0; + r->q_passband_end = 0; + r->q_stopband_begin = 0; + r->q_phase_response = -1; + + if (recipe && recipe[0] != '\0') { + if (strchr(recipe, 'v')) r->q_recipe = SOXR_VHQ; + if (strchr(recipe, 'h')) r->q_recipe = SOXR_HQ; + if (strchr(recipe, 'm')) r->q_recipe = SOXR_MQ; + if (strchr(recipe, 'l')) r->q_recipe = SOXR_LQ; + if (strchr(recipe, 'q')) r->q_recipe = SOXR_QQ; + if (strchr(recipe, 'L')) r->q_recipe |= SOXR_LINEAR_PHASE; + if (strchr(recipe, 'I')) r->q_recipe |= SOXR_INTERMEDIATE_PHASE; + if (strchr(recipe, 'M')) r->q_recipe |= SOXR_MINIMUM_PHASE; + if (strchr(recipe, 's')) r->q_recipe |= SOXR_STEEP_FILTER; + // X = async resampling to max_rate + if (strchr(recipe, 'X')) r->max_rate = true; + // E = exception, only resample if native rate is not supported + if (strchr(recipe, 'E')) r->exception = true; + } + + if (flags) { + r->q_flags = strtoul(flags, 0, 16); + } + + if (atten) { + double scale = pow(10, -atof(atten) / 20); + if (scale > 0 && scale <= 1.0) { + r->scale = scale; + } + } + + if (precision) { + r->q_precision = atof(precision); + } + + if (passband_end) { + r->q_passband_end = atof(passband_end) / 100; + } + + if (stopband_begin) { + r->q_stopband_begin = atof(stopband_begin) / 100; + } + + if (phase_response) { + r->q_phase_response = atof(phase_response); + } + + LOG_INFO("resampling %s recipe: 0x%02x, flags: 0x%02x, scale: %03.2f, precision: %03.1f, passband_end: %03.5f, stopband_begin: %03.5f, phase_response: %03.1f", + r->max_rate ? "async" : "sync", + r->q_recipe, r->q_flags, r->scale, r->q_precision, r->q_passband_end, r->q_stopband_begin, r->q_phase_response); + + return true; +} + +#endif // #if RESAMPLE diff --git a/slimproto.c b/slimproto.c new file mode 100644 index 0000000..81d2f30 --- /dev/null +++ b/slimproto.c @@ -0,0 +1,921 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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" +#include "slimproto.h" + +static log_level loglevel; + +#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]) { + const char *base_cap = "Model=squeezelite,AccuratePlayPoints=1,HasDigitalOut=1,HasPolarityInversion=1,Firmware=" VERSION; + struct HELO_packet pkt; + + 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); + if (now > status.updated) ms_played += (now - status.updated); + 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; + 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; + + LOG_INFO("switch server"); + + new_server = serv->server_ip; + + 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) && !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; + } +#if PORTAUDIO + if (output.pa_reopen) { + _pa_open(); + output.pa_reopen = false; + } +#endif + if (_start_output && (output.state == OUTPUT_STOPPED || 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(void) { + struct sockaddr_in d; + struct sockaddr_in s; + char *buf; + struct pollfd pollinfo; + + 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)); + } + + } 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) { + 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; + int i; + + wake_create(wake_e); + + loglevel = level; + running = true; + + if (server) { + server_addr(server, &slimproto_ip, &slimproto_port); + } + + if (!slimproto_ip) { + slimproto_ip = discover_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, + 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) { + 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) { + + 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(); + } + + } 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); + } + + 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 0000000..319ee8d --- /dev/null +++ b/slimproto.h @@ -0,0 +1,176 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 + +#pragma pack(push, 1) + +// 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; +}; + +#pragma pack(pop) diff --git a/squeezelite.h b/squeezelite.h new file mode 100644 index 0000000..455fdfd --- /dev/null +++ b/squeezelite.h @@ -0,0 +1,664 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 . + * + */ + +// make may define: PORTAUDIO, SELFPIPE, RESAMPLE, RESAMPLE_MP, VISEXPORT, IR, DSD, LINKALL to influence build + +#define VERSION "v1.8" + +#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 +#else +#error unknown target +#endif + +#if LINUX && !defined(PORTAUDIO) +#define ALSA 1 +#define PORTAUDIO 0 +#else +#define ALSA 0 +#define PORTAUDIO 1 +#endif + +#if 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 + +#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 && 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 !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__) + +#include +#include +#include +#include +#include +#include +#include + +#if LINUX || OSX || FREEBSD +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STREAM_THREAD_STACK_SIZE 64 * 1024 +#define DECODE_THREAD_STACK_SIZE 128 * 1024 +#define OUTPUT_THREAD_STACK_SIZE 64 * 1024 +#define IR_THREAD_STACK_SIZE 64 * 1024 +#define thread_t pthread_t; +#define closesocket(s) close(s) +#define last_error() errno +#define ERROR_WOULDBLOCK EWOULDBLOCK + +typedef u_int8_t u8_t; +typedef u_int16_t u16_t; +typedef u_int32_t u32_t; +typedef u_int64_t u64_t; +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) +#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) +#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 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 +#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; + +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 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); +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; + +typedef enum { S32_LE, S24_LE, S24_3LE, S16_LE } output_format; + +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 16 +#define TEST_RATES = { 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 osx_playnice; +#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 + bool next_dop; // set in decode thread + bool dop; + bool has_dop; // set in dop_init - output device supports dop + unsigned dop_delay; // set in dop_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[]); +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 *volume_mixer, bool mixer_unmute); +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[]); +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_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_flac_dop(u32_t *lptr, u32_t *rptr, frames_t frames); +void update_dop(u32_t *ptr, frames_t frames, bool invert); +void dop_silence_frames(u32_t *ptr, frames_t frames); +void dop_init(bool enable, 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); + +// 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 diff --git a/stream.c b/stream.c new file mode 100644 index 0000000..3fdb28b --- /dev/null +++ b/stream.c @@ -0,0 +1,433 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 + +#include "squeezelite.h" + +#include + +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; + +static void send_header(void) { + char *ptr = stream.header; + int len = stream.header_len; + + unsigned try = 0; + ssize_t n; + + while (len) { + n = send(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; + } + LOG_SDEBUG("wrote %d bytes to socket", n); + ptr += n; + len -= n; + } + LOG_SDEBUG("wrote header"); +} + +static bool running = true; + +static void _disconnect(stream_state state, disconnect_code disconnect) { + stream.state = state; + stream.disconnect = disconnect; + 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(&pollinfo, 1, 100)) { + + LOCK; + + // check socket has not been closed while in poll + if (fd < 0) { + UNLOCK; + continue; + } + + if ((pollinfo.revents & POLLOUT) && stream.state == SEND_HEADERS) { + send_header(); + stream.header_len = 0; + stream.state = RECV_HEADERS; + 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(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(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(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(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; + } + } + + 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"); + } + } + + 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); + } + + 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 + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + STREAM_THREAD_STACK_SIZE); + 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 + 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; + } + + 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 (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 0000000..ba67d77 --- /dev/null +++ b/utils.c @@ -0,0 +1,388 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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 +#include +#include +#include +#if FREEBSD +#include +#include +#include +#endif +#endif +#if WIN +#include +#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 + struct timespec ts; + if (!clock_gettime(CLOCK_MONOTONIC, &ts)) { + 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 +// search first 4 interfaces returned by IFCONF +void get_mac(u8_t mac[]) { + struct ifconf ifc; + struct ifreq *ifr, *ifend; + struct ifreq ifreq; + struct ifreq ifs[4]; + + 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 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 + 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 +} + +// 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; +} + +// this only implements numfds == 1 +int poll(struct pollfd *fds, unsigned long numfds, int timeout) { + fd_set r, w; + struct timeval tv; + int ret; + + FD_ZERO(&r); + FD_ZERO(&w); + + if (fds[0].events & POLLIN) FD_SET(fds[0].fd, &r); + if (fds[0].events & POLLOUT) FD_SET(fds[0].fd, &w); + + tv.tv_sec = timeout / 1000; + tv.tv_usec = 1000 * (timeout % 1000); + + ret = select(fds[0].fd + 1, &r, &w, NULL, &tv); + + if (ret < 0) return ret; + + fds[0].revents = 0; + if (FD_ISSET(fds[0].fd, &r)) fds[0].revents |= POLLIN; + if (FD_ISSET(fds[0].fd, &w)) fds[0].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 diff --git a/vorbis.c b/vorbis.c new file mode 100644 index 0000000..0809bee --- /dev/null +++ b/vorbis.c @@ -0,0 +1,331 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.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" + +// automatically select between floating point (preferred) and fixed point libraries: +// NOTE: works with Tremor version here: http://svn.xiph.org/trunk/Tremor, not vorbisidec.1.0.2 currently in ubuntu + +// we take common definations from even though we can use tremor at run time +// tremor's OggVorbis_File struct is normally smaller so this is ok, but padding added to malloc in case it is bigger +#define OV_EXCLUDE_STATIC_CALLBACKS + +#include + +struct vorbis { + OggVorbis_File *vf; + bool opened; +#if !LINKALL + // vorbis symbols to be dynamically loaded - from either vorbisfile or vorbisidec (tremor) version of library + vorbis_info *(* ov_info)(OggVorbis_File *vf, int link); + int (* ov_clear)(OggVorbis_File *vf); + long (* ov_read)(OggVorbis_File *vf, char *buffer, int length, int bigendianp, int word, int sgned, int *bitstream); + long (* ov_read_tremor)(OggVorbis_File *vf, char *buffer, int length, int *bitstream); + int (* ov_open_callbacks)(void *datasource, OggVorbis_File *vf, const char *initial, long ibytes, ov_callbacks callbacks); +#endif +}; + +static struct vorbis *v; + +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; + +#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 + +#if LINKALL +#define OV(h, fn, ...) (ov_ ## fn)(__VA_ARGS__) +#define TREMOR(h) 0 +extern int ov_read_tremor(); // needed to enable compilation, not linked +#else +#define OV(h, fn, ...) (h)->ov_##fn(__VA_ARGS__) +#define TREMOR(h) (h)->ov_read_tremor +#endif + +// called with mutex locked within vorbis_decode to avoid locking O before S +static size_t _read_cb(void *ptr, size_t size, size_t nmemb, void *datasource) { + size_t bytes; + + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); + bytes = min(bytes, size * nmemb); + + memcpy(ptr, streambuf->readp, bytes); + _buf_inc_readp(streambuf, bytes); + + return bytes / size; +} + +// these are needed for older versions of tremor, later versions and libvorbis allow NULL to be used +static int _seek_cb(void *datasource, ogg_int64_t offset, int whence) { return -1; } +static int _close_cb(void *datasource) { return 0; } +static long _tell_cb(void *datasource) { return 0; } + +static decode_state vorbis_decode(void) { + static int channels; + bool end; + frames_t frames; + int bytes, s, n; + u8_t *write_buf; + + LOCK_S; + LOCK_O_direct; + end = (stream.state <= DISCONNECT); + + IF_DIRECT( + frames = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME; + ); + IF_PROCESS( + frames = process.max_in_frames; + ); + + if (!frames && end) { + UNLOCK_O_direct; + UNLOCK_S; + return DECODE_COMPLETE; + } + + if (decode.new_stream) { + ov_callbacks cbs; + int err; + struct vorbis_info *info; + + cbs.read_func = _read_cb; + + if (TREMOR(v)) { + cbs.seek_func = _seek_cb; cbs.close_func = _close_cb; cbs.tell_func = _tell_cb; + } else { + cbs.seek_func = NULL; cbs.close_func = NULL; cbs.tell_func = NULL; + } + + if ((err = OV(v, open_callbacks, streambuf, v->vf, NULL, 0, cbs)) < 0) { + LOG_WARN("open_callbacks error: %d", err); + UNLOCK_O_direct; + UNLOCK_S; + return DECODE_COMPLETE; + } + v->opened = true; + + info = OV(v, info, v->vf, -1); + + LOG_INFO("setting track_start"); + LOCK_O_not_direct; + output.next_sample_rate = decode_newstream(info->rate, output.supported_rates); + IF_DSD( output.next_dop = false; ) + output.track_start = outputbuf->writep; + if (output.fade_mode) _checkfade(true); + decode.new_stream = false; + UNLOCK_O_not_direct; + + IF_PROCESS( + frames = process.max_in_frames; + ); + + channels = info->channels; + + if (channels > 2) { + LOG_WARN("too many channels: %d", channels); + UNLOCK_O_direct; + UNLOCK_S; + return DECODE_ERROR; + } + } + + bytes = frames * 2 * channels; // samples returned are 16 bits + + IF_DIRECT( + write_buf = outputbuf->writep; + ); + IF_PROCESS( + write_buf = process.inbuf; + ); + + // write the decoded frames into outputbuf even though they are 16 bits per sample, then unpack them + if (!TREMOR(v)) { +#if SL_LITTLE_ENDIAN + n = OV(v, read, v->vf, (char *)write_buf, bytes, 0, 2, 1, &s); +#else + n = OV(v, read, v->vf, (char *)write_buf, bytes, 1, 2, 1, &s); +#endif + } else { + n = OV(v, read_tremor, v->vf, (char *)write_buf, bytes, &s); + } + + if (n > 0) { + + frames_t count; + s16_t *iptr; + s32_t *optr; + + frames = n / 2 / channels; + count = frames * channels; + + // work backward to unpack samples to 4 bytes per sample + iptr = (s16_t *)write_buf + count; + optr = (s32_t *)write_buf + frames * 2; + + if (channels == 2) { + while (count--) { + *--optr = *--iptr << 16; + } + } else if (channels == 1) { + while (count--) { + *--optr = *--iptr << 16; + *--optr = *iptr << 16; + } + } + + IF_DIRECT( + _buf_inc_writep(outputbuf, frames * BYTES_PER_FRAME); + ); + IF_PROCESS( + process.in_frames = frames; + ); + + LOG_SDEBUG("wrote %u frames", frames); + + } else if (n == 0) { + + LOG_INFO("end of stream"); + UNLOCK_O_direct; + UNLOCK_S; + return DECODE_COMPLETE; + + } else if (n == OV_HOLE) { + + // recoverable hole in stream, seen when skipping + LOG_DEBUG("hole in stream"); + + } else { + + LOG_INFO("ov_read error: %d", n); + UNLOCK_O_direct; + UNLOCK_S; + return DECODE_COMPLETE; + } + + UNLOCK_O_direct; + UNLOCK_S; + + return DECODE_RUNNING; +} + +static void vorbis_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) { + if (!v->vf) { + v->vf = malloc(sizeof(OggVorbis_File) + 128); // add some padding as struct size may be larger + memset(v->vf, 0, sizeof(OggVorbis_File) + 128); + } else { + if (v->opened) { + OV(v, clear, v->vf); + v->opened = false; + } + } +} + +static void vorbis_close(void) { + if (v->opened) { + OV(v, clear, v->vf); + v->opened = false; + } + free(v->vf); + v->vf = NULL; +} + +static bool load_vorbis() { +#if !LINKALL + void *handle = dlopen(LIBVORBIS, RTLD_NOW); + char *err; + bool tremor = false; + + if (!handle) { + handle = dlopen(LIBTREMOR, RTLD_NOW); + if (handle) { + tremor = true; + } else { + LOG_INFO("dlerror: %s", dlerror()); + return false; + } + } + + v->ov_read = tremor ? NULL : dlsym(handle, "ov_read"); + v->ov_read_tremor = tremor ? dlsym(handle, "ov_read") : NULL; + v->ov_info = dlsym(handle, "ov_info"); + v->ov_clear = dlsym(handle, "ov_clear"); + v->ov_open_callbacks = dlsym(handle, "ov_open_callbacks"); + + if ((err = dlerror()) != NULL) { + LOG_INFO("dlerror: %s", err); + return false; + } + + LOG_INFO("loaded %s", tremor ? LIBTREMOR : LIBVORBIS); +#endif + + return true; +} + +struct codec *register_vorbis(void) { + static struct codec ret = { + 'o', // id + "ogg", // types + 2048, // min read + 20480, // min space + vorbis_open, // open + vorbis_close, // close + vorbis_decode,// decode + }; + + v = malloc(sizeof(struct vorbis)); + if (!v) { + return NULL; + } + + v->vf = NULL; + v->opened = false; + + if (!load_vorbis()) { + return NULL; + } + + LOG_INFO("using vorbis to decode ogg"); + return &ret; +}