diff --git a/ChangeLog.txt b/ChangeLog.txt new file mode 100644 index 0000000..aba0924 --- /dev/null +++ b/ChangeLog.txt @@ -0,0 +1,57 @@ +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 (beta) +================== + +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) + diff --git a/Makefile b/Makefile index cedf8fb..65e224b 100644 --- a/Makefile +++ b/Makefile @@ -3,23 +3,60 @@ LDFLAGS ?= -lasound -lpthread -lm -lrt EXECUTABLE ?= squeezelite -SOURCES = main.c slimproto.c utils.c output.c buffer.c stream.c decode.c process.c resample.c flac.c pcm.c mad.c vorbis.c faad.c mpg.c ffmpeg.c -DEPS = squeezelite.h slimproto.h +# 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 -UNAME = $(shell uname -s) +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 -ifneq (,$(findstring -DLINKALL, $(CFLAGS))) - LDFLAGS += -lFLAC -lmad -lvorbisfile -lfaad -lmpg123 -ifneq (,$(findstring -DFFMPEG, $(CFLAGS))) - LDFLAGS += -lavcodec -lavformat -lavutil +SOURCES_DSD = dsd.c dop.c dsd2pcm/dsd2pcm.c +SOURCES_FF = ffmpeg.c +SOURCES_RESAMPLE = process.c resample.c +SOURCES_VIS = output_vis.c + +LINK_LINUX = -ldl + +LINKALL = -lFLAC -lmad -lvorbisfile -lfaad -lmpg123 +LINKALL_FF = -lavcodec -lavformat -lavutil +LINKALL_RESAMPLE = -lsoxr + +DEPS = squeezelite.h slimproto.h + +UNAME = $(shell uname -s) + +# add optional sources +ifneq (,$(findstring $(OPT_DSD), $(CFLAGS))) + SOURCES += $(SOURCES_DSD) endif -ifneq (,$(findstring -DRESAMPLE, $(CFLAGS))) - LDFLAGS += -lsoxr +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 + +# 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 else -# if not LINKALL and linux add -ldl +# if not LINKALL and linux add LINK_LINUX ifeq ($(UNAME), Linux) - LDFLAGS += -ldl + LDFLAGS += $(LINK_LINUX) endif endif @@ -33,7 +70,7 @@ $(OBJECTS): $(DEPS) .c.o: - $(CC) $(CFLAGS) $< -c -o $@ + $(CC) $(CFLAGS) $(CPPFLAGS) $< -c -o $@ clean: rm -f $(OBJECTS) $(EXECUTABLE) diff --git a/decode.c b/decode.c index 83e2d26..bb23c1e 100644 --- a/decode.c +++ b/decode.c @@ -129,8 +129,11 @@ LOG_INFO("init decode"); // register codecs - // alc,wma,wmap,wmal,aac,spt,ogg,ogf,flc,aif,pcm,mp3 + // dsf,dff,alc,wma,wmap,wmal,aac,spt,ogg,ogf,flc,aif,pcm,mp3 i = 0; +#if DSD + if (!opt || strstr(opt, "dsd")) codecs[i++] = register_dsd(); +#endif #if FFMPEG if (!opt || strstr(opt, "alac")) codecs[i++] = register_ff("alc"); if (!opt || strstr(opt, "wma")) codecs[i++] = register_ff("wma"); @@ -191,11 +194,11 @@ UNLOCK_D; } -unsigned decode_newstream(unsigned sample_rate, unsigned max_sample_rate) { +unsigned decode_newstream(unsigned sample_rate, unsigned supported_rates[]) { MAY_PROCESS( if (decode.process) { - return process_newstream(&decode.direct, sample_rate, max_sample_rate); + return process_newstream(&decode.direct, sample_rate, supported_rates); } ); diff --git a/dop.c b/dop.c new file mode 100644 index 0000000..fbc77f9 --- /dev/null +++ b/dop.c @@ -0,0 +1,90 @@ +/* + * 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 for frames in the output buffer +// performaned on all output including silence to maintain marker phase consitency +void update_dop_marker(u32_t *ptr, frames_t frames) { + static u32_t marker = 0x05; + 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) { + LOCK_O; + output.has_dop = enable; + UNLOCK_O; +} + +#endif // DSD diff --git a/dsd.c b/dsd.c new file mode 100644 index 0000000..b2f3c7b --- /dev/null +++ b/dsd.c @@ -0,0 +1,622 @@ +/* + * 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 . + * + */ + +// 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_WARN("stream too short"); + return DECODE_ERROR; + } + + 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) { + // /2 for dop should never result in 0 as header len is always even + LOG_WARN("frames got to zero"); + return DECODE_ERROR; + } + + 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"); + 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 index c0ed8ff..84d558e 100644 --- a/faad.c +++ b/faad.c @@ -375,7 +375,8 @@ LOCK_O; LOG_INFO("setting track_start"); - output.next_sample_rate = decode_newstream(samplerate, output.max_sample_rate); + 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; diff --git a/ffmpeg.c b/ffmpeg.c index 588b54c..87b88be 100644 --- a/ffmpeg.c +++ b/ffmpeg.c @@ -338,7 +338,8 @@ LOCK_O; LOG_INFO("setting track_start"); - output.next_sample_rate = decode_newstream(ff->codecC->sample_rate, output.max_sample_rate); + 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; diff --git a/flac.c b/flac.c index 84884cd..815b0e2 100644 --- a/flac.c +++ b/flac.c @@ -114,10 +114,25 @@ if (decode.new_stream) { LOCK_O; LOG_INFO("setting track_start"); - output.next_sample_rate = decode_newstream(frame->header.sample_rate, output.max_sample_rate); 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); - decode.new_stream = false; +#endif + UNLOCK_O; } diff --git a/mad.c b/mad.c index ba321fc..255bbd8 100644 --- a/mad.c +++ b/mad.c @@ -209,7 +209,8 @@ if (decode.new_stream) { LOCK_O; LOG_INFO("setting track_start"); - output.next_sample_rate = decode_newstream(m->synth.pcm.samplerate, output.max_sample_rate); + 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; diff --git a/main.c b/main.c index 0827dde..b0ac032 100644 --- a/main.c +++ b/main.c @@ -28,7 +28,7 @@ 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\"\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" @@ -40,6 +40,7 @@ " -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: " #if FFMPEG @@ -55,10 +56,10 @@ #if ALSA " -p \t\tSet real time priority of output thread (1-99)\n" #endif - " -r \t\tMax sample rate for output device, enables output device to be off when squeezelite is started\n" + " -r \t\tSpecify sample rates supported by device, enables output device to be off when squeezelite is started; rates = | - | ,,\n" #if RESAMPLE - " -u [params]\t\tUpsample, params = ::::::,\n" - " \t\t\t recipe = (v|h|m|l|q)(|L|I|M)(|s), (|X) = async - resample to max rate for device, otherwise resample to max sync rate\n" + " -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" @@ -66,6 +67,9 @@ " \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\t\t\tOutput device supports DSD over PCM (DoP)\n" +#endif #if VISEXPORT " -v \t\t\tVisulizer support\n" #endif @@ -74,15 +78,15 @@ #endif " -t \t\t\tLicense terms\n" "\n" - "Build options: " + "Build options:" #if LINUX - "LINUX" + " LINUX" #endif #if WIN - "WIN" + " WIN" #endif #if OSX - "OSX" + " OSX" #endif #if ALSA " ALSA" @@ -107,6 +111,12 @@ #endif #if VISEXPORT " VISEXPORT" +#endif +#if DSD + " DSD" +#endif +#if LINKALL + " LINKALL" #endif "\n\n", argv0); @@ -124,6 +134,10 @@ "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 ); } @@ -144,21 +158,17 @@ u8_t mac[6]; unsigned stream_buf_size = STREAMBUF_SIZE; unsigned output_buf_size = 0; // set later - unsigned max_rate = 0; - char *upsample = NULL; + unsigned rates[MAX_SUPPORTED_SAMPLERATES] = { 0 }; + char *resample = NULL; + char *output_params = NULL; #if LINUX bool daemonize = false; #endif #if ALSA - unsigned alsa_buffer = ALSA_BUFFER_TIME; - unsigned alsa_period = ALSA_PERIOD_COUNT; - char *alsa_sample_fmt = NULL; - bool alsa_mmap = true; unsigned rt_priority = OUTPUT_RT_PRIORITY; #endif -#if PORTAUDIO - unsigned pa_latency = 0; - int pa_osx_playnice = -1; +#if DSD + bool dop = false; #endif #if VISEXPORT bool visexport = false; @@ -181,7 +191,10 @@ optind += 2; } else if (strstr("ltz" #if RESAMPLE - "u" + "uR" +#endif +#if DSD + "D" #endif #if VISEXPORT "v" @@ -198,25 +211,8 @@ case 'o': output_device = optarg; break; - case 'a': - { -#if ALSA - char *t = next_param(optarg, ':'); - char *c = next_param(NULL, ':'); - char *s = next_param(NULL, ':'); - char *m = 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); -#endif -#if PORTAUDIO - char *l = next_param(optarg, ':'); - char *p = next_param(NULL, ':'); - if (l) pa_latency = (unsigned)atoi(l); - if (p) pa_osx_playnice = atoi(p); -#endif - } + case 'a': + output_params = optarg; break; case 'b': { @@ -263,7 +259,40 @@ } break; case 'r': - max_rate = atoi(optarg); + if (strstr(optarg,",")) { + // parse sample rates and sort them + char *r = next_param(optarg, ','); + 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 { + // optstr is - or , extract rates from test rates within this range + unsigned ref[] TEST_RATES; + char *maxstr = next_param(optarg, '-'); + char *minstr = next_param(NULL, '-'); + unsigned max = maxstr ? atoi(maxstr) : ref[0]; + unsigned min = minstr ? atoi(minstr) : 0; + int i, j; + 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]; + } + } + } break; case 's': server = optarg; @@ -289,11 +318,17 @@ break; #if RESAMPLE case 'u': + case 'R': if (optind < argc && argv[optind] && argv[optind][0] != '-') { - upsample = argv[optind++]; + resample = argv[optind++]; } else { - upsample = ""; - } + resample = ""; + } + break; +#endif +#if DSD + case 'D': + dop = true; break; #endif #if VISEXPORT @@ -323,11 +358,17 @@ signal(SIGHUP, sighandler); #endif - // set the output buffer size if not specified on the command line to take account of upsampling + // 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 (upsample) { - output_buf_size *= max_rate ? max_rate / 44100 : 8; + 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; } } @@ -351,25 +392,32 @@ stream_init(log_stream, stream_buf_size); -#if ALSA - output_init(log_output, output_device, output_buf_size, alsa_buffer, alsa_period, alsa_sample_fmt, alsa_mmap, - max_rate, rt_priority); + if (!strcmp(output_device, "-")) { + output_init_stdout(log_output, output_buf_size, output_params, rates); + } else { +#if ALSA + output_init_alsa(log_output, output_device, output_buf_size, output_params, rates, rt_priority); #endif #if PORTAUDIO - output_init(log_output, output_device, output_buf_size, pa_latency, pa_osx_playnice, max_rate); + output_init_pa(log_output, output_device, output_buf_size, output_params, rates); +#endif + } + +#if DSD + dop_init(dop); #endif #if VISEXPORT if (visexport) { - output_vis_init(mac); + output_vis_init(log_output, mac); } #endif decode_init(log_decode, codecs); #if RESAMPLE - if (upsample) { - process_init(upsample); + if (resample) { + process_init(resample); } #endif @@ -382,7 +430,17 @@ decode_close(); stream_close(); - output_close(); + + if (!strcmp(output_device, "-")) { + output_close_stdout(); + } else { +#if ALSA + output_close_alsa(); +#endif +#if PORTAUDIO + output_close_pa(); +#endif + } #if WIN winsock_close(); diff --git a/mpg.c b/mpg.c index 4f3f6e6..774b566 100644 --- a/mpg.c +++ b/mpg.c @@ -102,12 +102,6 @@ bytes = min(bytes, READ_SIZE); space = min(space, WRITE_SIZE); - if (stream.state <= DISCONNECT && bytes == 0) { - UNLOCK_O_direct; - UNLOCK_S; - return DECODE_COMPLETE; - } - if (m->use16bit) { space = (space / BYTES_PER_FRAME) * 4; } @@ -129,7 +123,8 @@ LOG_INFO("setting track_start"); LOCK_O_not_direct; - output.next_sample_rate = decode_newstream(rate, output.max_sample_rate); + 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; @@ -163,14 +158,16 @@ ); UNLOCK_O_direct; - UNLOCK_S; LOG_SDEBUG("write %u frames", size / BYTES_PER_FRAME); - if (ret == MPG123_DONE) { + 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"); diff --git a/output.c b/output.c index 60b09b4..cbf4aa7 100644 --- a/output.c +++ b/output.c @@ -18,1395 +18,234 @@ * */ -// Output using Alsa or Portaudio: -// - ALSA output is the preferred output for linux as it allows direct hardware access -// - PortAudio is the output output supported on other platforms and also builds on linux for test purposes +// Common output function #include "squeezelite.h" -#if ALSA -#include -#include -#include + +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 -#if PORTAUDIO -#include -#if OSX -#include -#endif -#endif -#if VISEXPORT && !ALSA -#include -#include -#endif - -#if ALSA - -#define MAX_SILENCE_FRAMES 1024 -#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; - u8_t *write_buf; -} alsa; - -static u8_t silencebuf[MAX_SILENCE_FRAMES * BYTES_PER_FRAME]; - -#endif // ALSA - -#if PORTAUDIO - -#define MAX_SILENCE_FRAMES 102400 // silencebuf not used in pa case so set large - -// ouput device -static struct { - unsigned rate; - PaStream *stream; -} pa; - -#endif // PORTAUDIO - -#if VISEXPORT - -#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; - -#endif // VISEXPORT - -static log_level loglevel; - -struct outputstate output; - -static struct buffer buf; - -struct buffer *outputbuf = &buf; - -static bool running = true; #define LOCK mutex_lock(outputbuf->mutex) #define UNLOCK mutex_unlock(outputbuf->mutex) -#define MAX_SCALESAMPLE 0x7fffffffffffLL -#define MIN_SCALESAMPLE -MAX_SCALESAMPLE - -static inline 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); -} - -static inline s32_t to_gain(float f) { - return (s32_t)(f * 65536.0F); -} - -#if ALSA - -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"); -} - -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(snd_pcm_t *pcmp) { - int err; - if ((err = snd_pcm_close(pcmp)) < 0) { - LOG_INFO("snd_pcm_close error: %s", snd_strerror(err)); - } -} - -static bool test_open(const char *device, u32_t *max_rate) { - 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; - } - - // get max rate - if ((err = snd_pcm_hw_params_get_rate_max(hw_params, max_rate, 0)) < 0) { - LOG_ERROR("unable to get max sample rate: %s", snd_strerror(err)); - return false; - } - - if (*max_rate > 384000) { - *max_rate = 384000; - } - - 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(snd_pcm_t **pcmp, 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(*pcmp); - - // 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; - } - - 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 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); - - // 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.period_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; -} - -#endif // ALSA - -#if PORTAUDIO - -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) { - 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)); - } -} - -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); - } - - for (i = 0; i < Pa_GetDeviceCount(); ++i) { - if (!strncmp(Pa_GetDeviceInfo(i)->name, 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); - -static bool test_open(const char *device, u32_t *max_rate) { - PaStreamParameters outputParameters; - PaError err; - u32_t rates[] = { 384000, 352800, 192000, 176400, 96000, 88200, 48000, 44100, 0 }; - int device_id, i; - - 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_OpenStream on some windows apis - for (i = 0; rates[i]; ++i) { - err = Pa_OpenStream(&pa.stream, NULL, &outputParameters, (double)rates[i], paFramesPerBufferUnspecified, paNoFlag, - pa_callback, NULL); - if (err == paNoError) { - Pa_CloseStream(pa.stream); - *max_rate = rates[i]; - break; - } - } - - if (!rates[i]) { - LOG_WARN("no available rate found"); - return false; - } - - if ((err = Pa_OpenStream(&pa.stream, NULL, &outputParameters, (double)*max_rate, paFramesPerBufferUnspecified, - paNoFlag, pa_callback, NULL)) != paNoError) { - LOG_WARN("error opening stream: %s", Pa_GetErrorText(err)); - return false; - } - - if ((err = Pa_CloseStream(pa.stream)) != paNoError) { - LOG_WARN("error closing stream: %s", Pa_GetErrorText(err)); - 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; +// 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; + + 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(); - 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; - } + } + + // 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 { - // 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; + 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 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)); - } - - if ((err = Pa_StartStream(pa.stream)) != paNoError) { - LOG_WARN("error starting stream: %s", Pa_GetErrorText(err)); - } - } - - if (err && !monitor_thread_running) { - // create a thread to check for output state change or device return -#if LINUX || OSX - 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 - } -} - -#endif // PORTAUDIO - - -#if ALSA - -// output thread for Alsa - -static void *output_thread(void *arg) { - snd_pcm_t *pcmp = NULL; - bool start = true; - bool output_off = false, 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); - if (!!alsa_open(&pcmp, output.device, output.current_sample_rate, output.buffer, output.period)) { - sleep(5); + if (output.track_start && !silence) { + if (output.track_start == outputbuf->readp) { + 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.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; continue; - } - start = true; - } - - 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); - pcmp = NULL; - probe_device = true; - continue; - } - - if (start && alsa.mmap) { - if ((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); - pcmp = NULL; - probe_device = true; - continue; - } - LOG_INFO("start error: %s", snd_strerror(err)); + } 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); } - } else { - start = false; - } - } - - 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); - pcmp = NULL; - probe_device = true; - continue; - } - LOG_WARN("recover failed: %s", snd_strerror(err)); - } - start = true; - continue; - } - - if (avail < alsa.period_size) { - 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; - } - avail = snd_pcm_avail_update(pcmp); - } - - // restrict avail in writei mode as write_buf is restricted to period_size - if (!alsa.mmap) { - 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; - alsa_close(pcmp); - pcmp = NULL; - output_off = true; - LOG_INFO("disabling output"); -#if VISEXPORT - if (vis_mmap) { - pthread_rwlock_wrlock(&vis_mmap->rwlock); - vis_mmap->running = false; - pthread_rwlock_unlock(&vis_mmap->rwlock); - } -#endif - continue; - } - -#endif // ALSA - -#if PORTAUDIO - static int pa_callback(const void *pa_input, void *pa_output, unsigned long pa_frames_wanted, - const PaStreamCallbackTimeInfo *time_info, PaStreamCallbackFlags statusFlags, void *userData) { - - u8_t *optr = (u8_t *)pa_output; - frames_t avail = pa_frames_wanted; -#endif - frames_t frames, size; - bool silence; - - s32_t cross_gain_in = 0, cross_gain_out = 0; s32_t *cross_ptr = NULL; - -#if PORTAUDIO - LOCK; -#endif - - frames = _buf_used(outputbuf) / BYTES_PER_FRAME; - silence = false; - - // start when threshold met, note: avail * 4 may need tuning - if (output.state == OUTPUT_BUFFER && frames > output.threshold * output.next_sample_rate / 100 && -#if ALSA - frames > alsa.buffer_size * 2 -#endif -#if PORTAUDIO - frames > avail * 4 -#endif - ) { - output.state = OUTPUT_RUNNING; - 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 slience 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 slience 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; - -#if ALSA - snd_pcm_sframes_t delay; - snd_pcm_delay(pcmp, &delay); - if (delay >= 0) { - output.device_frames = delay; - } else { - LOG_WARN("snd_pcm_delay returns: %d", delay); - if (delay == -EPIPE) { - // EPIPE indicates underrun - attempt to recover - UNLOCK; - continue; - } - } -#endif -#if PORTAUDIO - output.device_frames = (unsigned)((time_info->outputBufferDacTime - Pa_GetStreamTime(pa.stream)) * output.current_sample_rate); -#endif - output.updated = gettime_ms(); - - while (size > 0) { - frames_t out_frames; - - frames_t cont_frames = _buf_cont_read(outputbuf) / BYTES_PER_FRAME; - - 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.track_start && !silence) { - if (output.track_start == outputbuf->readp) { - 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.current_sample_rate = output.next_sample_rate; - if (!output.fade == FADE_ACTIVE || !output.fade_mode == FADE_CROSSFADE) { + } + 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; - } - output.track_start = NULL; - if (output.current_sample_rate != output.next_sample_rate) { - output.current_sample_rate = output.next_sample_rate; - break; - } - continue; - } 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 (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); + } else { + LOG_INFO("fade complete"); + output.fade = FADE_INACTIVE; } } - 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; + // 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.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); } - 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"); + if (output.next_replay_gain) { + cross_gain_in = gain(cross_gain_in, output.next_replay_gain); } - output.fade = FADE_INACTIVE; - output.current_replay_gain = output.next_replay_gain; + gainL = output.gainL; + gainR = output.gainR; + cross_ptr = (s32_t *)(output.fade_end + cur_f * BYTES_PER_FRAME); } else { - LOG_INFO("fade complete"); + LOG_INFO("unable to continue crossfade - too few samples"); 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.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; - 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; - -#if ALSA - if (alsa.mmap || alsa.format != NATIVE_FORMAT) { - - // in all alsa cases except NATIVE_FORMAT non mmap we take this path: - // - mmap: scale and pack to output format, write direct into mmap region - // - non mmap: scale and pack into alsa.write_buf, which is the used with writei to send to alsa - const snd_pcm_channel_area_t *areas; - snd_pcm_uframes_t offset; - snd_pcm_uframes_t alsa_frames = (snd_pcm_uframes_t)out_frames; - - if (alsa.mmap) { - - 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)); - break; - } - - out_frames = (frames_t)alsa_frames; - - // temp for debugging mmap issues - if ((areas[0].first + offset * areas[0].step) % 8 != 0) { - LOG_ERROR("Error: mmap offset not multiple of 8!"); - } - } - - // perform crossfade buffer copying here as we do not know the actual out_frames value until here - if (output.fade == FADE_ACTIVE && output.fade_dir == FADE_CROSS && 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++; - } - } - - void *outputptr = alsa.mmap ? (areas[0].addr + (areas[0].first + offset * areas[0].step) / 8) : alsa.write_buf; - s32_t *inputptr = (s32_t *) (silence ? silencebuf : outputbuf->readp); - frames_t cnt = out_frames; - - switch(alsa.format) { - case SND_PCM_FORMAT_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 SND_PCM_FORMAT_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 SND_PCM_FORMAT_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 SND_PCM_FORMAT_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 (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"); - break; - } - } else { - snd_pcm_sframes_t w = snd_pcm_writei(pcmp, alsa.write_buf, out_frames); - if (w < 0) { - if (w != -EAGAIN && ((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); - pcmp = NULL; - } - } - break; - } else { - if (w != out_frames) { - LOG_WARN("writei only wrote %u of %u", w, out_frames); - } - out_frames = w; - } - } - - } else { - -#endif // ALSA - -#if PORTAUDIO - if (1) { -#endif - if (!silence) { - - if (output.fade == FADE_ACTIVE && output.fade_dir == FADE_CROSS && 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 (gainL != FIXED_ONE || gainR!= FIXED_ONE) { - unsigned count = out_frames; - 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; - } - } - } -#if ALSA - // only used in S32_LE non mmap LE case, write the 32 samples straight with writei, no need for intermediate buffer - snd_pcm_sframes_t w = snd_pcm_writei(pcmp, silence ? silencebuf : outputbuf->readp, out_frames); - if (w < 0) { - if (w != -EAGAIN && ((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); - pcmp = NULL; - } - } - break; - } else { - if (w != out_frames) { - LOG_WARN("writei only wrote %u of %u", w, out_frames); - } - out_frames = w; - } -#endif -#if PORTAUDIO - if (!silence) { - memcpy(optr, outputbuf->readp, out_frames * BYTES_PER_FRAME); - } else { - memset(optr, 0, out_frames * BYTES_PER_FRAME); - } - - optr += out_frames * BYTES_PER_FRAME; -#endif - } - - size -= out_frames; - -#if VISEXPORT - // 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 - 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); - } - } -#endif + } + + 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) { + LOG_WARN("error in write cb"); + frames -= out_frames; + 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; + } + } - if (!silence) { - _buf_inc_readp(outputbuf, out_frames * BYTES_PER_FRAME); - output.frames_played += out_frames; - } - } - - LOG_SDEBUG("wrote %u frames", frames); - -#if ALSA - UNLOCK; - } - - return 0; + LOG_SDEBUG("wrote %u frames", frames); + + return frames; } -#endif - -#if PORTAUDIO - if (frames < pa_frames_wanted) { - LOG_SDEBUG("pad with silence"); - memset(optr, 0, (pa_frames_wanted - frames) * BYTES_PER_FRAME); - } - - if (output.state == OUTPUT_OFF) { - LOG_INFO("output off"); - UNLOCK; - return paComplete; - } else if (pa.rate != output.current_sample_rate) { - UNLOCK; - return paComplete; - } else { - UNLOCK; - return paContinue; - } - } -#endif void _checkfade(bool start) { frames_t bytes; @@ -1470,154 +309,83 @@ } } -#if ALSA -static pthread_t thread; -void output_init(log_level level, const char *device, unsigned output_buf_size, unsigned alsa_buffer, unsigned alsa_period, - const char *alsa_sample_fmt, bool mmap, unsigned max_rate, unsigned rt_priority) { -#endif -#if PORTAUDIO -void output_init(log_level level, const char *device, unsigned output_buf_size, unsigned latency, int osx_playnice, unsigned max_rate) { - PaError err; -#endif +void output_init_common(log_level level, const char *device, unsigned output_buf_size, unsigned rates[]) { + unsigned i; + loglevel = level; - - LOG_INFO("init output"); 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 buffer"); + LOG_ERROR("unable to malloc output buffer"); exit(0); } - LOCK; + 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); + ) output.state = OUTPUT_STOPPED; - output.current_sample_rate = 44100; output.device = device; output.fade = FADE_INACTIVE; - -#if ALSA - alsa.mmap = mmap; - alsa.write_buf = NULL; - alsa.format = 0; - output.buffer = alsa_buffer; - output.period = alsa_period; - - memset(silencebuf, 0, sizeof(silencebuf)); - - 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); -#endif - -#if PORTAUDIO - output.latency = latency; - output.osx_playnice = osx_playnice; - 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)); - UNLOCK; - exit(0); - } -#endif - - if (!max_rate) { - if (!test_open(output.device, &output.max_sample_rate)) { + output.error_opening = false; + + if (!rates[0]) { + if (!test_open(output.device, output.supported_rates)) { LOG_ERROR("unable to open output device"); - UNLOCK; exit(0); } } else { - output.max_sample_rate = max_rate; - } - - LOG_INFO("output: %s maxrate: %u", output.device, output.max_sample_rate); - -#if ALSA - -#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, max_rate ? "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); - } -#endif - -#if PORTAUDIO - _pa_open(); -#endif - - UNLOCK; + 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 = 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); + } } -#if VISEXPORT -void output_vis_init(u8_t *mac) { - sprintf(vis_shm_path, "/squeezelite-%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - LOCK; - 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 = output.current_sample_rate; - 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; - } - UNLOCK; +void output_close_common(void) { + buf_destroy(outputbuf); + free(silencebuf); + IF_DSD( + free(silencebuf_dop); + ) } -#endif void output_flush(void) { LOG_INFO("flush output buffer"); @@ -1626,53 +394,10 @@ 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.frames_played = 0; UNLOCK; } - -void output_close(void) { -#if PORTAUDIO - PaError err; -#endif - - LOG_INFO("close output"); - - LOCK; - - running = false; - -#if ALSA - UNLOCK; - pthread_join(thread, NULL); - if (alsa.write_buf) free(alsa.write_buf); -#endif - -#if PORTAUDIO - 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; -#endif - -#if VISEXPORT - if (vis_mmap) { - pthread_rwlock_destroy(&vis_mmap->rwlock); - munmap(vis_mmap, sizeof(struct vis_t)); - } - - if (vis_fd != -1) { - shm_unlink(vis_shm_path); - close(vis_fd); - } -#endif - - buf_destroy(outputbuf); -} diff --git a/output_alsa.c b/output_alsa.c new file mode 100644 index 0000000..65ab64f --- /dev/null +++ b/output_alsa.c @@ -0,0 +1,699 @@ +/* + * 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 . + * + */ + +// Output using Alsa + +#include "squeezelite.h" + +#if ALSA + +#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; +} 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"); +} + +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; + } + + 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_marker((u32_t *) inputptr, out_frames); + } + ) + + 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 = false, 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)); + } + } 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 in writei mode as write_buf is restricted to period_size + if (!alsa.mmap) { + 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 { + LOG_DEBUG("snd_pcm_delay returns: %d", err); + } + } else { + output.device_frames = delay; + output.updated = gettime_ms(); + } + + // process frames + _output_frames(avail); + + UNLOCK; + } + + 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 rt_priority) { + + 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 *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; + output.format = 0; + output.buffer = alsa_buffer; + output.period = alsa_period; + output.start_frames = 0; + output.write_cb = &_write_frames; + + 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); + +#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..df049af --- /dev/null +++ b/output_pa.c @@ -0,0 +1,426 @@ +/* + * 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 . + * + */ + +// 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) { + 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)); + } +} + +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)); + } + + if ((err = Pa_StartStream(pa.stream)) != paNoError) { + LOG_WARN("error starting stream: %s", Pa_GetErrorText(err)); + } + } + + if (err && !monitor_thread_running) { + vis_stop(); + + // create a thread to check for output state change or device return +#if LINUX || OSX + 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_marker((u32_t *) outputbuf->readp, out_frames); + } + ) + + memcpy(optr, outputbuf->readp, out_frames * BYTES_PER_FRAME); + + } else { + + u8_t *buf = silencebuf; + + IF_DSD( + if (output.dop) { + buf = silencebuf_dop; + update_dop_marker((u32_t *) buf, out_frames); + } + ) + + 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; + frames_t frames; + + optr = (u8_t *)pa_output; + + LOCK; + + output.device_frames = (unsigned)((time_info->outputBufferDacTime - Pa_GetStreamTime(pa.stream)) * output.current_sample_rate); + output.updated = gettime_ms(); + + frames = _output_frames(pa_frames_wanted); + + if (frames < pa_frames_wanted) { + LOG_SDEBUG("pad with silence"); + memset(optr, 0, (pa_frames_wanted - frames) * 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[]) { + 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; + 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); + + 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..744cc8d --- /dev/null +++ b/output_pack.c @@ -0,0 +1,270 @@ +/* + * 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 . + * + */ + +// 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..4be963b --- /dev/null +++ b/output_stdout.c @@ -0,0 +1,175 @@ +/* + * 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 . + * + */ + +// 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_marker((u32_t *)obuf, out_frames); + } + ) + + _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(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[]) { + 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; + + 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); + +#if LINUX || OSX + 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..7e0967a --- /dev/null +++ b/output_vis.c @@ -0,0 +1,139 @@ +/* + * 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 . + * + */ + +// Export audio samples for visualiser process (16 bit only best endevours) + +#include "squeezelite.h" + +#if VISEXPORT + +#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]); + + 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; + } +} + +#endif // VISEXPORT diff --git a/pcm.c b/pcm.c index 262415c..1ea070b 100644 --- a/pcm.c +++ b/pcm.c @@ -59,10 +59,13 @@ 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; -void _check_header(void) { +static void _check_header(void) { u8_t *ptr = streambuf->readp; unsigned bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); header_format format = UNKNOWN; @@ -100,6 +103,8 @@ if (format == WAVE && !memcmp(ptr, "data", 4)) { ptr += 8; _buf_inc_readp(streambuf, ptr - streambuf->readp); + audio_left = len; + limit = true; return; } @@ -108,6 +113,8 @@ // following 4 bytes is blocksize - ignored ptr += 8 + 8; _buf_inc_readp(streambuf, ptr + offset - streambuf->readp); + audio_left = len; + limit = true; return; } @@ -150,10 +157,11 @@ } static decode_state pcm_decode(void) { - size_t in, out; + unsigned bytes, in, out; frames_t frames, count; u32_t *optr; u8_t *iptr; + u8_t tmp[16]; LOCK_S; @@ -163,7 +171,7 @@ LOCK_O_direct; - in = min(_buf_used(streambuf), _buf_cont_read(streambuf)) / (channels * sample_size); + bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf)); IF_DIRECT( out = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME; @@ -172,7 +180,7 @@ out = process.max_in_frames; ); - if (stream.state <= DISCONNECT && in == 0) { + if ((stream.state <= DISCONNECT && bytes == 0) || (limit && audio_left == 0)) { UNLOCK_O_direct; UNLOCK_S; return DECODE_COMPLETE; @@ -181,27 +189,44 @@ if (decode.new_stream) { LOG_INFO("setting track_start"); LOCK_O_not_direct; - output.next_sample_rate = decode_newstream(sample_rate, output.max_sample_rate); + 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_DIRECT( - optr = (u32_t *)outputbuf->writep; - ); - IF_PROCESS( - optr = (u32_t *)process.inbuf; - ); - - iptr = (u8_t *)streambuf->readp; + if (limit && frames * bytes_per_frame > audio_left) { + LOG_INFO("reached end of audio"); + frames = audio_left / bytes_per_frame; + } count = frames * channels; @@ -309,7 +334,11 @@ LOG_SDEBUG("decoded %u frames", frames); - _buf_inc_readp(streambuf, frames * channels * sample_size); + _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); @@ -329,6 +358,7 @@ 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); diff --git a/process.c b/process.c index 84aee71..907f804 100644 --- a/process.c +++ b/process.c @@ -116,9 +116,9 @@ } // new stream - called with decode mutex set -unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned max_sample_rate) { +unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned supported_rates[]) { - bool active = NEWSTREAM_FUNC(&process, raw_sample_rate, max_sample_rate); + bool active = NEWSTREAM_FUNC(&process, raw_sample_rate, supported_rates); LOG_INFO("processing: %s", active ? "active" : "inactive"); diff --git a/resample.c b/resample.c index fa1c498..a62812c 100644 --- a/resample.c +++ b/resample.c @@ -40,6 +40,7 @@ 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); @@ -122,15 +123,36 @@ } } -bool resample_newstream(struct processstate *process, unsigned raw_sample_rate, unsigned max_sample_rate) { - unsigned outrate; - - if (r->max_rate) { - outrate = max_sample_rate; - } else { - outrate = raw_sample_rate; - while (outrate <= max_sample_rate) outrate <<= 1; - outrate >>= 1; +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; + } + } + } + + 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; @@ -235,6 +257,7 @@ r->resampler = NULL; r->old_clips = 0; r->max_rate = false; + r->exception = false; if (!load_soxr()) { LOG_WARN("resampling disabled"); @@ -274,6 +297,8 @@ 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) { diff --git a/slimproto.c b/slimproto.c index f044c34..d0c570c 100644 --- a/slimproto.c +++ b/slimproto.c @@ -759,7 +759,7 @@ if (!running) return; LOCK_O; - sprintf(fixed_cap, ",MaxSampleRate=%u", output.max_sample_rate); + sprintf(fixed_cap, ",MaxSampleRate=%u", output.supported_rates[0]); for (i = 0; i < MAX_CODECS; i++) { if (codecs[i] && codecs[i]->id && strlen(fixed_cap) < 128 - 10) { diff --git a/squeezelite.h b/squeezelite.h index 77a66a8..c697bf4 100644 --- a/squeezelite.h +++ b/squeezelite.h @@ -18,9 +18,9 @@ * */ -// make may define: PORTAUDIO, SELFPIPE or RESAMPLE to influence build - -#define VERSION "v1.3.1" +// make may define: PORTAUDIO, SELFPIPE, RESAMPLE, VISEXPORT, DSD, LINKALL to influence build + +#define VERSION "v1.4-beta1" // build detection #if defined(linux) @@ -86,6 +86,16 @@ #define VISEXPORT 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 @@ -97,6 +107,7 @@ #if !LINKALL // dynamically loaded libraries at run time + #if LINUX #define LIBFLAC "libFLAC.so.8" #define LIBMAD "libmad.so.0" @@ -237,6 +248,7 @@ #define ERROR_WOULDBLOCK WSAEWOULDBLOCK #define open _open #define read _read +#define snprintf _snprintf #define in_addr_t u32_t #define socklen_t int @@ -293,6 +305,8 @@ #else #error can not support u64_t #endif + +#define MAX_SILENCE_FRAMES 2048 #define FIXED_ONE 0x10000 @@ -435,7 +449,7 @@ void decode_init(log_level level, const char *opt); void decode_close(void); void decode_flush(void); -unsigned decode_newstream(unsigned sample_rate, unsigned max_sample_rate); +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 @@ -443,7 +457,7 @@ void process_samples(void); void process_drain(void); void process_flush(void); -unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned max_sample_rate); +unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned supported_rates[]); void process_init(char *opt); #endif @@ -451,21 +465,27 @@ // 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 max_sample_rate); +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.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; @@ -477,9 +497,13 @@ 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 current_sample_rate; - unsigned max_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 current_replay_gain; @@ -500,26 +524,68 @@ fade_dir fade_dir; fade_mode fade_mode; // set by slimproto unsigned fade_secs; // set by slimproto -}; - +#if DSD + bool next_dop; // set in decode thread + bool dop; + bool has_dop; // set in dop_init - output device supports dop +#endif +}; + +void output_init_common(log_level level, const char *device, unsigned output_buf_size, unsigned rates[]); +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); -#if ALSA -void output_init(log_level level, const char *device, unsigned output_buf_size, unsigned alsa_buffer, unsigned alsa_period, const char *alsa_sample_fmt, bool mmap, unsigned max_rate, unsigned rt_priority); -#endif +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 rt_priority); +void output_close_alsa(void); +#endif + +// output_pa.c #if PORTAUDIO -void output_init(log_level level, const char *device, unsigned output_buf_size, unsigned latency, int osx_playnice, unsigned max_rate); -#endif +void list_devices(void); +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[]); +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[]); +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 output_vis_init(u8_t *mac); -#endif -void output_flush(void); -void output_close(void); -// _* called with mutex locked -void _checkfade(bool); -void _pa_open(void); +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_marker(u32_t *ptr, frames_t frames); +void dop_silence_frames(u32_t *ptr, frames_t frames); +void dop_init(bool enable); +#endif // codecs -#define MAX_CODECS 8 +#define MAX_CODECS 9 struct codec *register_flac(void); struct codec *register_pcm(void); @@ -527,4 +593,5 @@ 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); diff --git a/vorbis.c b/vorbis.c index c7f661a..4dee7ec 100644 --- a/vorbis.c +++ b/vorbis.c @@ -147,7 +147,8 @@ LOG_INFO("setting track_start"); LOCK_O_not_direct; - output.next_sample_rate = decode_newstream(info->rate, output.max_sample_rate); + 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;