add resample

consumes A LOT of CPU. Need alos to change the command line to set the
sample rate (-r) to a fixed value (BT) but accept igher (-Z) from LMS
and enable resampling (-R)
-R -Z 96000 -r \"44100-44100\"  or similar
This commit is contained in:
philippe44
2019-06-22 15:43:04 -07:00
parent a55bac4306
commit 701c30a99b
8 changed files with 927 additions and 6 deletions

View File

@@ -99,7 +99,6 @@ menu "Squeezelite-ESP32"
help
Include FAAD library for aac decoding.
config INCLUDE_MAD
depends on SPIRAM_SUPPORT
bool "MAD"
default 1
help

View File

@@ -329,14 +329,14 @@ static decode_state alac_decode(void) {
bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
LOG_INFO("setting track_start");
LOCK_O_not_direct;
LOCK_O;
output.next_sample_rate = decode_newstream(l->sample_rate, output.supported_rates);
output.track_start = outputbuf->writep;
if (output.fade_mode) _checkfade(true);
decode.new_stream = false;
UNLOCK_O_not_direct;
UNLOCK_O;
} else if (found == -1) {
LOG_WARN("[%p]: error reading stream header");
UNLOCK_S;

View File

@@ -2,12 +2,13 @@
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
CFLAGS += -O3 -DPOSIX -DLINKALL -DLOOPBACK -DNO_FAAD -DEMBEDDED -DTREMOR_ONLY -DBYTES_PER_FRAME=4 \
CFLAGS += -O3 -DPOSIX -DLINKALL -DLOOPBACK -DNO_FAAD -DRESAMPLE -DEMBEDDED -DTREMOR_ONLY -DBYTES_PER_FRAME=4 \
-I$(COMPONENT_PATH)/../components/codecs/inc \
-I$(COMPONENT_PATH)/../components/codecs/inc/mad \
-I$(COMPONENT_PATH)/../components/codecs/inc/alac \
-I$(COMPONENT_PATH)/../components/codecs/inc/helix-aac \
-I$(COMPONENT_PATH)/../components/codecs/inc/vorbis \
-I$(COMPONENT_PATH)/../components/codecs/inc/vorbis \
-I$(COMPONENT_PATH)/../components/codecs/inc/soxr \
-I$(COMPONENT_PATH)/../components/platform_esp32
LDFLAGS += -s

194
main/process.c Normal file
View File

@@ -0,0 +1,194 @@
/*
* Squeezelite - lightweight headless squeezebox emulator
*
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
* Ralph Irving 2015-2017, ralph_irving@hotmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// sample processing - only included when building with PROCESS set
#include "squeezelite.h"
#if PROCESS
extern log_level loglevel;
extern struct buffer *outputbuf;
extern struct decodestate decode;
struct processstate process;
extern struct codec *codec;
#define LOCK_D mutex_lock(decode.mutex);
#define UNLOCK_D mutex_unlock(decode.mutex);
#define LOCK_O mutex_lock(outputbuf->mutex)
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
// macros to map to processing functions - currently only resample.c
// this can be made more generic when multiple processing mechanisms get added
#if RESAMPLE
#define SAMPLES_FUNC resample_samples
#define DRAIN_FUNC resample_drain
#define NEWSTREAM_FUNC resample_newstream
#define FLUSH_FUNC resample_flush
#define INIT_FUNC resample_init
#endif
// transfer all processed frames to the output buf
static void _write_samples(void) {
frames_t frames = process.out_frames;
ISAMPLE_T *iptr = (ISAMPLE_T *) process.outbuf;
unsigned cnt = 10;
LOCK_O;
while (frames > 0) {
frames_t f = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
ISAMPLE_T *optr = (ISAMPLE_T*) outputbuf->writep;
if (f > 0) {
f = min(f, frames);
memcpy(optr, iptr, f * BYTES_PER_FRAME);
frames -= f;
_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
iptr += f * BYTES_PER_FRAME / sizeof(*iptr);
} else if (cnt--) {
// there should normally be space in the output buffer, but may need to wait during drain phase
UNLOCK_O;
usleep(10000);
LOCK_O;
} else {
// bail out if no space found after 100ms to avoid locking
LOG_ERROR("unable to get space in output buffer");
UNLOCK_O;
return;
}
}
UNLOCK_O;
}
// process samples - called with decode mutex set
void process_samples(void) {
SAMPLES_FUNC(&process);
_write_samples();
process.in_frames = 0;
}
// drain at end of track - called with decode mutex set
void process_drain(void) {
bool done;
do {
done = DRAIN_FUNC(&process);
_write_samples();
} while (!done);
LOG_DEBUG("processing track complete - frames in: %lu out: %lu", process.total_in, process.total_out);
}
// new stream - called with decode mutex set
unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned supported_rates[]) {
bool active = NEWSTREAM_FUNC(&process, raw_sample_rate, supported_rates);
LOG_INFO("processing: %s", active ? "active" : "inactive");
*direct = !active;
if (active) {
unsigned max_in_frames, max_out_frames;
process.in_frames = process.out_frames = 0;
process.total_in = process.total_out = 0;
max_in_frames = codec->min_space / BYTES_PER_FRAME ;
// increase size of output buffer by 10% as output rate is not an exact multiple of input rate
if (process.out_sample_rate % process.in_sample_rate == 0) {
max_out_frames = max_in_frames * (process.out_sample_rate / process.in_sample_rate);
} else {
max_out_frames = (int)(1.1 * (float)max_in_frames * (float)process.out_sample_rate / (float)process.in_sample_rate);
}
if (process.max_in_frames != max_in_frames) {
LOG_DEBUG("creating process buf in frames: %u", max_in_frames);
if (process.inbuf) free(process.inbuf);
process.inbuf = malloc(max_in_frames * BYTES_PER_FRAME);
process.max_in_frames = max_in_frames;
}
if (process.max_out_frames != max_out_frames) {
LOG_DEBUG("creating process buf out frames: %u", max_out_frames);
if (process.outbuf) free(process.outbuf);
process.outbuf = malloc(max_out_frames * BYTES_PER_FRAME);
process.max_out_frames = max_out_frames;
}
if (!process.inbuf || !process.outbuf) {
LOG_ERROR("malloc fail creating process buffers");
*direct = true;
return raw_sample_rate;
}
return process.out_sample_rate;
}
return raw_sample_rate;
}
// process flush - called with decode mutex set
void process_flush(void) {
LOG_INFO("process flush");
FLUSH_FUNC();
process.in_frames = 0;
}
// init - called with no mutex
void process_init(char *opt) {
bool enabled = INIT_FUNC(opt);
memset(&process, 0, sizeof(process));
if (enabled) {
LOCK_D;
decode.process = true;
UNLOCK_D;
}
}
#endif // #if PROCESS

377
main/resample.c Normal file
View File

@@ -0,0 +1,377 @@
/*
* Squeezelite - lightweight headless squeezebox emulator
*
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
* Ralph Irving 2015-2017, ralph_irving@hotmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// upsampling using libsoxr - only included if RESAMPLE set
#include "squeezelite.h"
#if RESAMPLE
#include <math.h>
#include <soxr.h>
extern log_level loglevel;
struct soxr {
soxr_t resampler;
size_t old_clips;
unsigned long q_recipe;
unsigned long q_flags;
double q_precision; /* Conversion precision (in bits). 20 */
double q_phase_response; /* 0=minimum, ... 50=linear, ... 100=maximum 50 */
double q_passband_end; /* 0dB pt. bandwidth to preserve; nyquist=1 0.913 */
double q_stopband_begin; /* Aliasing/imaging control; > passband_end 1 */
double scale;
bool max_rate;
bool exception;
#if !LINKALL
// soxr symbols to be dynamically loaded
soxr_io_spec_t (* soxr_io_spec)(soxr_datatype_t itype, soxr_datatype_t otype);
soxr_quality_spec_t (* soxr_quality_spec)(unsigned long recipe, unsigned long flags);
soxr_t (* soxr_create)(double, double, unsigned, soxr_error_t *,
soxr_io_spec_t const *, soxr_quality_spec_t const *, soxr_runtime_spec_t const *);
void (* soxr_delete)(soxr_t);
soxr_error_t (* soxr_process)(soxr_t, soxr_in_t, size_t, size_t *, soxr_out_t, size_t olen, size_t *);
size_t *(* soxr_num_clips)(soxr_t);
#if RESAMPLE_MP
soxr_runtime_spec_t (* soxr_runtime_spec)(unsigned num_threads);
#endif
// soxr_strerror is a macro so not included here
#endif
};
static struct soxr *r;
#if LINKALL
#define SOXR(h, fn, ...) (soxr_ ## fn)(__VA_ARGS__)
#else
#define SOXR(h, fn, ...) (h)->soxr_##fn(__VA_ARGS__)
#endif
void resample_samples(struct processstate *process) {
size_t idone, odone;
size_t clip_cnt;
soxr_error_t error =
SOXR(r, process, r->resampler, process->inbuf, process->in_frames, &idone, process->outbuf, process->max_out_frames, &odone);
if (error) {
LOG_INFO("soxr_process error: %s", soxr_strerror(error));
return;
}
if (idone != process->in_frames) {
// should not get here if buffers are big enough...
LOG_ERROR("should not get here - partial sox process: %u of %u processed %u of %u out",
(unsigned)idone, process->in_frames, (unsigned)odone, process->max_out_frames);
}
process->out_frames = odone;
process->total_in += idone;
process->total_out += odone;
clip_cnt = *(SOXR(r, num_clips, r->resampler));
if (clip_cnt - r->old_clips) {
LOG_SDEBUG("resampling clips: %u", (unsigned)(clip_cnt - r->old_clips));
r->old_clips = clip_cnt;
}
}
bool resample_drain(struct processstate *process) {
size_t odone;
size_t clip_cnt;
soxr_error_t error = SOXR(r, process, r->resampler, NULL, 0, NULL, process->outbuf, process->max_out_frames, &odone);
if (error) {
LOG_INFO("soxr_process error: %s", soxr_strerror(error));
return true;
}
process->out_frames = odone;
process->total_out += odone;
clip_cnt = *(SOXR(r, num_clips, r->resampler));
if (clip_cnt - r->old_clips) {
LOG_DEBUG("resampling clips: %u", (unsigned)(clip_cnt - r->old_clips));
r->old_clips = clip_cnt;
}
if (odone == 0) {
LOG_INFO("resample track complete - total track clips: %u", r->old_clips);
SOXR(r, delete, r->resampler);
r->resampler = NULL;
return true;
} else {
return false;
}
}
bool resample_newstream(struct processstate *process, unsigned raw_sample_rate, unsigned supported_rates[]) {
unsigned outrate = 0;
int i;
if (r->exception) {
// find direct match - avoid resampling
for (i = 0; supported_rates[i]; i++) {
if (raw_sample_rate == supported_rates[i]) {
outrate = raw_sample_rate;
break;
}
}
// else find next highest sync sample rate
while (!outrate && i >= 0) {
if (supported_rates[i] > raw_sample_rate && supported_rates[i] % raw_sample_rate == 0) {
outrate = supported_rates[i];
break;
}
i--;
}
}
if (!outrate) {
if (r->max_rate) {
// resample to max rate for device
outrate = supported_rates[0];
} else {
// resample to max sync sample rate
for (i = 0; supported_rates[i]; i++) {
if (supported_rates[i] % raw_sample_rate == 0 || raw_sample_rate % supported_rates[i] == 0) {
outrate = supported_rates[i];
break;
}
}
}
if (!outrate) {
outrate = supported_rates[0];
}
}
process->in_sample_rate = raw_sample_rate;
process->out_sample_rate = outrate;
if (r->resampler) {
SOXR(r, delete, r->resampler);
r->resampler = NULL;
}
if (raw_sample_rate != outrate) {
soxr_io_spec_t io_spec;
soxr_quality_spec_t q_spec;
soxr_error_t error;
#if RESAMPLE_MP
soxr_runtime_spec_t r_spec;
#endif
LOG_INFO("resampling from %u -> %u", raw_sample_rate, outrate);
#if BYTES_PER_FRAME == 4
io_spec = SOXR(r, io_spec, SOXR_INT16_I, SOXR_INT16_I);
#else
io_spec = SOXR(r, io_spec, SOXR_INT32_I, SOXR_INT32_I);
#endif
io_spec.scale = r->scale;
q_spec = SOXR(r, quality_spec, r->q_recipe, r->q_flags);
if (r->q_precision > 0) {
q_spec.precision = r->q_precision;
}
if (r->q_passband_end > 0) {
q_spec.passband_end = r->q_passband_end;
}
if (r->q_stopband_begin > 0) {
q_spec.stopband_begin = r->q_stopband_begin;
}
if (r->q_phase_response > -1) {
q_spec.phase_response = r->q_phase_response;
}
#if RESAMPLE_MP
r_spec = SOXR(r, runtime_spec, 0); // make use of libsoxr OpenMP support allowing parallel execution if multiple cores
#endif
LOG_DEBUG("resampling with soxr_quality_spec_t[precision: %03.1f, passband_end: %03.6f, stopband_begin: %03.6f, "
"phase_response: %03.1f, flags: 0x%02x], soxr_io_spec_t[scale: %03.2f]", q_spec.precision,
q_spec.passband_end, q_spec.stopband_begin, q_spec.phase_response, q_spec.flags, io_spec.scale);
#if RESAMPLE_MP
r->resampler = SOXR(r, create, raw_sample_rate, outrate, 2, &error, &io_spec, &q_spec, &r_spec);
#else
r->resampler = SOXR(r, create, raw_sample_rate, outrate, 2, &error, &io_spec, &q_spec, NULL);
#endif
if (error) {
LOG_INFO("soxr_create error: %s", soxr_strerror(error));
return false;
}
r->old_clips = 0;
return true;
} else {
LOG_INFO("disable resampling - rates match");
return false;
}
}
void resample_flush(void) {
if (r->resampler) {
SOXR(r, delete, r->resampler);
r->resampler = NULL;
}
}
static bool load_soxr(void) {
#if !LINKALL
void *handle = dlopen(LIBSOXR, RTLD_NOW);
char *err;
if (!handle) {
LOG_INFO("dlerror: %s", dlerror());
return false;
}
r->soxr_io_spec = dlsym(handle, "soxr_io_spec");
r->soxr_quality_spec = dlsym(handle, "soxr_quality_spec");
r->soxr_create = dlsym(handle, "soxr_create");
r->soxr_delete = dlsym(handle, "soxr_delete");
r->soxr_process = dlsym(handle, "soxr_process");
r->soxr_num_clips = dlsym(handle, "soxr_num_clips");
#if RESAMPLE_MP
r->soxr_runtime_spec = dlsym(handle, "soxr_runtime_spec");
#endif
if ((err = dlerror()) != NULL) {
LOG_INFO("dlerror: %s", err);
return false;
}
LOG_INFO("loaded "LIBSOXR);
#endif
return true;
}
bool resample_init(char *opt) {
char *recipe = NULL, *flags = NULL;
char *atten = NULL;
char *precision = NULL, *passband_end = NULL, *stopband_begin = NULL, *phase_response = NULL;
r = malloc(sizeof(struct soxr));
if (!r) {
LOG_WARN("resampling disabled");
return false;
}
r->resampler = NULL;
r->old_clips = 0;
r->max_rate = false;
r->exception = false;
if (!load_soxr()) {
LOG_WARN("resampling disabled");
return false;
}
if (opt) {
recipe = next_param(opt, ':');
flags = next_param(NULL, ':');
atten = next_param(NULL, ':');
precision = next_param(NULL, ':');
passband_end = next_param(NULL, ':');
stopband_begin = next_param(NULL, ':');
phase_response = next_param(NULL, ':');
}
#if BYTES_PER_FRAME == 4
// default to LQ (16 bit) if not user specified
r->q_recipe = SOXR_LQ;
#else
// default to HQ (20 bit) if not user specified
r->q_recipe = SOXR_HQ;
#endif
r->q_flags = 0;
// default to 1db of attenuation if not user specified
r->scale = pow(10, -1.0 / 20);
// override recipe derived values with user specified values
r->q_precision = 0;
r->q_passband_end = 0;
r->q_stopband_begin = 0;
r->q_phase_response = -1;
if (recipe && recipe[0] != '\0') {
if (strchr(recipe, 'v')) r->q_recipe = SOXR_VHQ;
if (strchr(recipe, 'h')) r->q_recipe = SOXR_HQ;
if (strchr(recipe, 'm')) r->q_recipe = SOXR_MQ;
if (strchr(recipe, 'l')) r->q_recipe = SOXR_LQ;
if (strchr(recipe, 'q')) r->q_recipe = SOXR_QQ;
if (strchr(recipe, 'L')) r->q_recipe |= SOXR_LINEAR_PHASE;
if (strchr(recipe, 'I')) r->q_recipe |= SOXR_INTERMEDIATE_PHASE;
if (strchr(recipe, 'M')) r->q_recipe |= SOXR_MINIMUM_PHASE;
if (strchr(recipe, 's')) r->q_recipe |= SOXR_STEEP_FILTER;
// X = async resampling to max_rate
if (strchr(recipe, 'X')) r->max_rate = true;
// E = exception, only resample if native rate is not supported
if (strchr(recipe, 'E')) r->exception = true;
}
if (flags) {
r->q_flags = strtoul(flags, 0, 16);
}
if (atten) {
double scale = pow(10, -atof(atten) / 20);
if (scale > 0 && scale <= 1.0) {
r->scale = scale;
}
}
if (precision) {
r->q_precision = atof(precision);
}
if (passband_end) {
r->q_passband_end = atof(passband_end) / 100;
}
if (stopband_begin) {
r->q_stopband_begin = atof(stopband_begin) / 100;
}
if (phase_response) {
r->q_phase_response = atof(phase_response);
}
LOG_INFO("resampling %s recipe: 0x%02x, flags: 0x%02x, scale: %03.2f, precision: %03.1f, passband_end: %03.5f, stopband_begin: %03.5f, phase_response: %03.1f",
r->max_rate ? "async" : "sync",
r->q_recipe, r->q_flags, r->scale, r->q_precision, r->q_passband_end, r->q_stopband_begin, r->q_phase_response);
return true;
}
#endif // #if RESAMPLE