From: Richard Kettlewell Date: Fri, 20 Nov 2009 18:58:18 +0000 (+0000) Subject: Use libsamplerate in disorder-normalize, if available. If it's not X-Git-Tag: 5.0.3~4^2~56 X-Git-Url: https://git.distorted.org.uk/~mdw/disorder/commitdiff_plain/2d0a6606ee8899dfe372bbdc760fd15c14f3fceb?hp=087a9b2eaf10974c1b3025bf7d439c2371625e70 Use libsamplerate in disorder-normalize, if available. If it's not installed then back off to the old way (which is to run an external copy of sox). --- diff --git a/.bzrignore b/.bzrignore index 3b13be6..21c00de 100644 --- a/.bzrignore +++ b/.bzrignore @@ -203,3 +203,5 @@ doc/disorder-choose.8.html config.aux/compile server/endian clients/rtpmon +libtests/t-resample +clients/resample diff --git a/CHANGES.html b/CHANGES.html index 785f699..851fa34 100644 --- a/CHANGES.html +++ b/CHANGES.html @@ -79,7 +79,11 @@ span.command {

The command backend now (optionally) sends silence instead of suspending writes when a pause occurs or no track is playing.

- + +

If libsamplerate is + available at build time then that will be used instead of invoking + SoX.

+

Disobedience

diff --git a/README b/README index 64b0320..57435a8 100644 --- a/README +++ b/README @@ -34,6 +34,7 @@ Build dependencies: libao 0.8.6 libasound 1.0.13 libFLAC 1.1.2 + libsamplerate 0.1.4 currently optional GNU C 4.1.2 } GNU Make 3.81 } Non-GNU versions will NOT work GNU Sed 4.1.5 } diff --git a/README.developers b/README.developers index ed09c21..84b8129 100644 --- a/README.developers +++ b/README.developers @@ -12,7 +12,7 @@ Dependencies: apt-get install gcc libc-dev automake autoconf libtool libgtk2.0-dev \ libgc-dev libgcrypt-dev libpcre3-dev libvorbis-dev \ libao-dev libmad0-dev libasound2-dev libdb4.3-dev \ - libflac-dev vorbis-tools wget + libflac-dev vorbis-tools wget libsamplerate0-dev On lenny use libdb4.5-deb. libdb4.6 does not work (and configure will refuse to use it). diff --git a/clients/Makefile.am b/clients/Makefile.am index 1817f15..91afc1c 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -17,7 +17,7 @@ # bin_PROGRAMS=disorder disorderfm disorder-playrtp -noinst_PROGRAMS=filename-bytes rtpmon +noinst_PROGRAMS=filename-bytes rtpmon resample noinst_SCRIPTS=dump2wav AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib @@ -44,6 +44,9 @@ rtpmon_LDADD=$(LIBOBJS) ../lib/libdisorder.a filename_bytes_SOURCES=filename-bytes.c +resample_SOURCES=resample.c +resample_LDADD=$(LIBOBJS) ../lib/libdisorder.a $(LIBSAMPLERATE) + install-exec-hook: $(LIBTOOL) --mode=finish $(DESTDIR)$(libdir) diff --git a/clients/resample.c b/clients/resample.c new file mode 100644 index 0000000..bc41013 --- /dev/null +++ b/clients/resample.c @@ -0,0 +1,170 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2009 Richard Kettlewell + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "common.h" + +#include +#include +#include +#include + +#include "resample.h" +#include "mem.h" +#include "syscalls.h" +#include "log.h" + +static int input_bits = 16; +static int input_channels = 2; +static int input_rate = 44100; +static int input_signed = 1; +static int input_endian = ENDIAN_NATIVE; +static int output_bits = 16; +static int output_channels = 2; +static int output_rate = 44100; +static int output_signed = 1; +static int output_endian = ENDIAN_NATIVE; + +static const struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "input-bits", required_argument, 0, 'b' }, + { "input-channels", required_argument, 0, 'c' }, + { "input-rate", required_argument, 0, 'r' }, + { "input-signed", no_argument, 0, 's' }, + { "input-unsigned", no_argument, 0, 'u' }, + { "input-endian", required_argument, 0, 'e' }, + { "output-bits", required_argument, 0, 'B' }, + { "output-channels", required_argument, 0, 'C' }, + { "output-rate", required_argument, 0, 'R' }, + { "output-signed", no_argument, 0, 'S' }, + { "output-unsigned", no_argument, 0, 'U' }, + { "output-endian", required_argument, 0, 'E' }, + { 0, 0, 0, 0 } +}; + +/* display usage message and terminate */ +static void help(void) { + xprintf("Usage:\n" + " resample [OPTIONS] < INPUT > OUTPUT\n" + "Options:\n" + " --help, -h Display usage message\n" + "Input format:\n" + " --input-bits, -b N Bits/sample (16)\n" + " --input-channels, -c N Samples/frame (2)\n" + " --input-rate, -r N Frames/second (44100)\n" + " --input-signed, -s Signed samples (yes)\n" + " --input-unsigned, -u Unsigned samples\n" + " --input-endian, -e big|little Sample endianness (native)\n" + "Output format:\n" + " --output-bits, -B N Bits/sample (16)\n" + " --output-channels, -C N Samples/frame (2)\n" + " --output-rate, -R N Frames/second (44100)\n" + " --output-signed, -S Signed samples (yes)\n" + " --output-unsigned, -U Unsigned samples\n" + " --output-endian, -E big|little Sample endianness (native)\n" + "Defaults are in brackets.\n" + "\n" + "Feeds raw sample data through resample_convert().\n"); + xfclose(stdout); + exit(0); +} + +static void converted(uint8_t *bytes, + size_t nbytes, + void attribute((unused)) *cd) { + while(nbytes > 0) { + ssize_t n = write(1, bytes, nbytes); + if(n < 0) + disorder_fatal(errno, "writing to stdout"); + bytes += n; + nbytes -= n; + } +} + +int main(int argc, char **argv) { + int n; + + mem_init(); + if(!setlocale(LC_CTYPE, "")) + disorder_fatal(errno, "error calling setlocale"); + while((n = getopt_long(argc, argv, "+hb:c:r:sue:B:C:R:SUE:", + options, 0)) >= 0) { + switch(n) { + case 'h': help(); + case 'b': input_bits = atoi(optarg); break; + case 'c': input_channels = atoi(optarg); break; + case 'r': input_rate = atoi(optarg); break; + case 's': input_signed = 1; break; + case 'u': input_signed = 1; break; + case 'e': + switch(optarg[0]) { + case 'b': case 'B': input_endian = ENDIAN_BIG; break; + case 'l': case 'L': input_endian = ENDIAN_LITTLE; break; + case 'n': case 'N': input_endian = ENDIAN_NATIVE; break; + default: disorder_fatal(0, "unknown endianness '%s'", optarg); + } + break; + case 'B': output_bits = atoi(optarg); break; + case 'C': output_channels = atoi(optarg); break; + case 'R': output_rate = atoi(optarg); break; + case 'S': output_signed = 1; break; + case 'U': output_signed = 1; break; + case 'E': + switch(optarg[0]) { + case 'b': case 'B': output_endian = ENDIAN_BIG; break; + case 'l': case 'L': output_endian = ENDIAN_LITTLE; break; + case 'n': case 'N': output_endian = ENDIAN_NATIVE; break; + default: disorder_fatal(0, "unknown endianness '%s'", optarg); + } + break; + default: disorder_fatal(0, "invalid option"); + } + } + struct resampler rs[1]; + resample_init(rs, input_bits, input_channels, input_rate, input_signed, + input_endian, output_bits, output_channels, output_rate, + output_signed, output_endian); +#define BUFFER_SIZE (1024 * 1024) + uint8_t *buffer = xmalloc_noptr(BUFFER_SIZE); + size_t used = 0; + int eof = 0; + + while(used || !eof) { + if(!eof) { + ssize_t r = read(0, buffer + used, BUFFER_SIZE - used); + if(r < 0) + disorder_fatal(errno, "reading from stdin"); + if(r == 0) + eof = 1; + used += r; + } + size_t consumed = resample_convert(rs, buffer, used, eof, converted, 0); + memmove(buffer, buffer + consumed, used - consumed); + used -= consumed; + } + if(close(1) < 0) + disorder_fatal(errno, "closing stdout"); + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/configure.ac b/configure.ac index 80cc04f..f527fff 100644 --- a/configure.ac +++ b/configure.ac @@ -438,6 +438,8 @@ if test $want_alsa = yes; then AC_CHECK_LIB([asound], [snd_pcm_open], [AC_SUBST(LIBASOUND,[-lasound])]) fi +AC_CHECK_LIB([samplerate],[src_new], + [AC_SUBST([LIBSAMPLERATE],[-lsamplerate])]) if test $want_server = yes; then RJK_CHECK_LIB(db, db_create, [#include ], [AC_SUBST(LIBDB,[-ldb])], @@ -523,6 +525,7 @@ AC_CHECK_HEADERS([dlfcn.h gcrypt.h \ syslog.h unistd.h],[:],[ missing_headers="$missing_headers $ac_header" ]) +AC_CHECK_HEADERS([samplerate.h]) if test ! -z "$missing_headers"; then AC_MSG_ERROR([missing headers:$missing_headers]) diff --git a/lib/Makefile.am b/lib/Makefile.am index 1ab3773..3c1e2f1 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -32,6 +32,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ basen.c basen.h \ base64.c base64.h \ bits.c bits.h \ + byte-order.h \ cache.c cache.h \ cgi.c cgi.h \ client.c client.h \ @@ -64,6 +65,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ queue.c queue.h \ random.c random.h \ regsub.c regsub.h \ + resample.c resample.h \ rights.c queue-rights.c rights.h \ rtp.h \ selection.c selection.h \ diff --git a/lib/byte-order.h b/lib/byte-order.h new file mode 100644 index 0000000..e80e51f --- /dev/null +++ b/lib/byte-order.h @@ -0,0 +1,44 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2009 Richard Kettlewell + * + * 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 . + */ +/** @file lib/byte-order.h + * @brief Byte order macros + */ + +#ifndef BYTE_ORDER_H +#define BYTE_ORDER_H + +#include + +#define ENDIAN_BIG 1 +#define ENDIAN_LITTLE 2 +#ifdef WORDS_BIGENDIAN +# define ENDIAN_NATIVE ENDIAN_BIG +#else +# define ENDIAN_NATIVE ENDIAN_LITTLE +#endif + +#endif /* BYTE_ORDER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/resample.c b/lib/resample.c new file mode 100644 index 0000000..c4c814e --- /dev/null +++ b/lib/resample.c @@ -0,0 +1,315 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2009 Richard Kettlewell + * + * 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 . + */ + +/** @file lib/resample.c + * @brief Audio resampling + * + * General purpose audio format conversion. Rate conversion only works if the + * SRC samplerate library is available, but the bitness/channel/endianness + * conversion works regardless. + */ + +#include "common.h" +#include "resample.h" +#include "log.h" +#include "mem.h" + +/** @brief Number of intermediate-format samples */ +#define SAMPLES 1024 + +/** @brief Multiplier for signed formats to allow easy switching */ +#define SIGNED 4 + +/** @brief Initialize a resampler + * @param rs Resampler + * @param input_bits Bits/sample in input + * @param input_channels Number of input channels + * @param input_signed Whether input samples are signed or unsigned + * @param input_rate Frames/second in input + * @param output_bits Bits/sample in output + * @param output_channels Number of output channels + * @param output_rate Frames/second in output + * @param output_signed Whether output samples are signed or unsigned + * + * For formats with more than two channels it's assume that the first + * two channels are left and right. No particular meaning is attached + * to additional channels other than to assume channel N in an input + * means the same as channel N in an output, for N>1. + */ +void resample_init(struct resampler *rs, + int input_bits, int input_channels, + int input_rate, int input_signed, + int input_endian, + int output_bits, int output_channels, + int output_rate, int output_signed, + int output_endian) { + memset(rs, 0, sizeof *rs); + assert(input_bits == 8 || input_bits == 16); + assert(output_bits == 8 || output_bits == 16); + assert(input_endian == ENDIAN_BIG || input_endian == ENDIAN_LITTLE); + assert(output_endian == ENDIAN_BIG || output_endian == ENDIAN_LITTLE); + assert(ENDIAN_BIG >= 0 && ENDIAN_BIG < SIGNED); + assert(ENDIAN_LITTLE >= 0 && ENDIAN_LITTLE < SIGNED); + rs->input_bits = input_bits; + rs->input_channels = input_channels; + rs->input_rate = input_rate; + rs->input_signed = SIGNED * !!input_signed; + rs->input_endian = input_endian; + rs->output_bits = output_bits; + rs->output_channels = output_channels; + rs->output_rate = output_rate; + rs->output_signed = SIGNED * !!output_signed; + rs->output_endian = output_endian; + rs->input_bytes_per_sample = (rs->input_bits + 7) / 8; + rs->input_bytes_per_frame = rs->input_channels * rs->input_bytes_per_sample; + if(rs->input_rate != rs->output_rate) { +#if HAVE_SAMPLERATE_H + int error_; + rs->state = src_new(SRC_SINC_BEST_QUALITY, rs->output_channels, &error_); + if(!rs->state) + disorder_fatal(0, "calling src_new: %s", src_strerror(error_)); +#else + disorder_fatal(0, "need to resample audio data but libsamplerate not available"); +#endif + } +} + +/** @brief Destroy a resampler + * @param rs Resampler + */ +void resample_close(struct resampler *rs) { +#if HAVE_SAMPLERATE_H + if(rs->state) + src_delete(rs->state); +#else + rs = 0; /* quieten compiler */ +#endif +} + +/** @brief Get one sample value and normalize it to [-1,1] + * @param rs Resampler state + * @param bytes Pointer to input data + * @param where Where to store result + * @return Number of bytes consumed + */ +static size_t resample_get_sample(const struct resampler *rs, + const uint8_t *bytes, + float *where) { + switch(rs->input_bits + rs->input_signed + rs->input_endian) { + case 8+ENDIAN_BIG: + case 8+ENDIAN_LITTLE: + *where = (bytes[0] - 128)/ 128.0; + return 1; + case 8+SIGNED+ENDIAN_BIG: + case 8+SIGNED+ENDIAN_LITTLE: + *where = (int8_t)bytes[0] / 128.0; + return 1; + case 16+ENDIAN_BIG: + *where = (bytes[0] * 256 + bytes[1] - 32768)/ 32768.0; + return 2; + break; + case 16+ENDIAN_LITTLE: + *where = (bytes[1] * 256 + bytes[0] - 32768)/ 32768.0; + return 2; + break; + case 16+SIGNED+ENDIAN_BIG: + *where = (int16_t)(bytes[0] * 256 + bytes[1])/ 32768.0; + return 2; + break; + case 16+SIGNED+ENDIAN_LITTLE: + *where = (int16_t)(bytes[1] * 256 + bytes[0])/ 32768.0; + return 2; + break; + default: + assert(!"unsupported sample format"); + } +} + +static inline int clip(int n, int min, int max) { + if(n >= min) { + if(n <= max) + return n; + else + return max; + } else + return min; +} + +/** @brief Store one sample value + * @param rs Resampler state + * @param sample Sample value + * @param bytes Where to store it + * @return Number of bytes stored + * + * The value is clipped naively if it will not fit. + */ +static size_t resample_put_sample(const struct resampler *rs, + float sample, + uint8_t *bytes) { + unsigned value; + switch(rs->output_bits + rs->output_signed + rs->output_endian) { + case 8+ENDIAN_BIG: + case 8+ENDIAN_LITTLE: + *bytes = clip(sample * 128.0 + 128, 0, 255); + return 1; + case 8+SIGNED+ENDIAN_BIG: + case 8+SIGNED+ENDIAN_LITTLE: + *bytes = clip((int)(sample * 128.0), -128, 127); + return 1; + case 16+ENDIAN_BIG: /* unsigned */ + value = clip(sample * 32768.0 + 32768, 0, 65535); + *bytes++ = value >> 8; + *bytes++ = value; + return 2; + case 16+ENDIAN_LITTLE: + value = clip(sample * 32768.0 + 32768, 0, 65535); + *bytes++ = value; + *bytes++ = value >> 8; + return 2; + case 16+SIGNED+ENDIAN_BIG: + value = clip(sample * 32768.0, -32768, 32767); + *bytes++ = value >> 8; + *bytes++ = value; + return 2; + case 16+SIGNED+ENDIAN_LITTLE: + value = clip(sample * 32768.0, -32768, 32767); + *bytes++ = value; + *bytes++ = value >> 8; + return 2; + default: + assert(!"unsupported sample format"); + } +} + +/** @brief Convert input samples to floats + * @param rs Resampler state + * @param bytes Input bytes + * @param nbytes Number of input bytes + * @param floats Where to store converted data + * + * @p floats must be big enough. As well as converting to floats this + * also converts to the output's channel format. + * + * Excess input channels are just discarded. If there are insufficient input + * channels the last one is duplicated as often as necessary to make up the + * numbers. This is a rather naff heuristic and may be improved in a future + * version, but mostly in DisOrder the output is pretty much always stereo and + * the input either mono or stereo, so the result isn't actually going to be + * too bad. + */ +static void resample_prepare_input(const struct resampler *rs, + const uint8_t *bytes, + size_t nframes, + float *floats) { + while(nframes > 0) { + int n; + + for(n = 0; n < rs->input_channels && n < rs->output_channels; ++n) { + bytes += resample_get_sample(rs, bytes, floats); + ++floats; + } + if(n < rs->input_channels) { + /* More input channels; discard them */ + bytes += (rs->input_channels - n) * rs->input_bytes_per_sample; + } else if(n < rs->output_channels) { + /* More output channels; duplicate the last input channel */ + for(; n < rs->output_channels; ++n) { + *floats = floats[-1]; + ++floats; + } + } + --nframes; + } +} + +/** @brief Convert between sample formats + * @param rs Resampler state + * @param bytes Bytes to convert + * @param nbytes Number of bytes to convert + * @param eof Set an end of input stream + * @param converted Called with converted data (possibly more than once) + * @param cd Passed to @p cd + * @return Number of bytes consumed + */ +size_t resample_convert(const struct resampler *rs, + const uint8_t *bytes, + size_t nbytes, + int eof, + void (*converted)(uint8_t *bytes, + size_t nbytes, + void *cd), + void *cd) { + size_t nframesin = nbytes / (rs->input_bytes_per_frame); + size_t nsamplesout; + float *input = xcalloc(nframesin * rs->output_channels, sizeof (float)); + float *output = 0; + + resample_prepare_input(rs, bytes, nframesin, input); +#if HAVE_SAMPLERATE_H + if(rs->state) { + /* A sample-rate conversion must be performed */ + SRC_DATA data; + /* Compute how many frames are expected to come out. */ + size_t maxframesout = nframesin * rs->output_rate / rs->input_rate + 1; + output = xcalloc(maxframesout * rs->output_channels, sizeof(float)); + data.data_in = input; + data.data_out = output; + data.input_frames = nframesin; + data.output_frames = maxframesout; + data.end_of_input = eof; + data.src_ratio = (double)rs->output_rate / rs->input_rate; + int error_ = src_process(rs->state, &data); + if(error_) + disorder_fatal(0, "calling src_process: %s", src_strerror(error_)); + nframesin = data.input_frames_used; + nsamplesout = data.output_frames_gen * rs->output_channels; + } +#endif + if(!output) { + /* No sample-rate conversion required */ + output = input; + nsamplesout = nframesin * rs->output_channels; + } + const float *op = output; + while(nsamplesout > 0) { + uint8_t buffer[4096]; + size_t bufused = 0; + + while(bufused < sizeof buffer && nsamplesout > 0) { + bufused += resample_put_sample(rs, *op++, buffer + bufused); + --nsamplesout; + } + converted(buffer, bufused, cd); + } + if(output != input) + xfree(output); + xfree(input); + eof = 0; /* quieten compiler */ + /* Report how many input bytes were actually consumed */ + //fprintf(stderr, "converted %zu frames\n", nframesin); + return nframesin * rs->input_bytes_per_frame; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/resample.h b/lib/resample.h new file mode 100644 index 0000000..e99aaeb --- /dev/null +++ b/lib/resample.h @@ -0,0 +1,68 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2009 Richard Kettlewell + * + * 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 . + */ + +/** @file lib/resample.h + * @brief Audio resampling + */ + +#ifndef RESAMPLE_H +#define RESAMPLE_H + +#if HAVE_SAMPLERATE_H +#include +#endif + +#include "byte-order.h" + +struct resampler { + int input_bits, input_channels, input_rate, input_signed, input_endian; + int output_bits, output_channels, output_rate, output_signed, output_endian; + int input_bytes_per_sample; + int input_bytes_per_frame; +#if HAVE_SAMPLERATE_H + SRC_STATE *state; +#endif +}; + +void resample_init(struct resampler *rs, + int input_bits, int input_channels, + int input_rate, int input_signed, + int input_endian, + int output_bits, int output_channels, + int output_rate, int output_signed, + int output_endian); +size_t resample_convert(const struct resampler *rs, + const uint8_t *bytes, + size_t nbytes, + int eof, + void (*converted)(uint8_t *bytes, + size_t nbytes, + void *cd), + void *cd); +void resample_close(struct resampler *rs); + +#endif /* RESAMPLE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/speaker-protocol.h b/lib/speaker-protocol.h index bd80e7d..3b21f0a 100644 --- a/lib/speaker-protocol.h +++ b/lib/speaker-protocol.h @@ -25,6 +25,8 @@ #ifndef SPEAKER_PROTOCOL_H #define SPEAKER_PROTOCOL_H +#include "byte-order.h" + /** @brief A message from the main server to the speaker, or vica versa */ struct speaker_message { /** @brief Message type @@ -123,13 +125,6 @@ struct stream_header { /** @brief Endianness */ uint8_t endian; -#define ENDIAN_BIG 1 -#define ENDIAN_LITTLE 2 -#ifdef WORDS_BIGENDIAN -# define ENDIAN_NATIVE ENDIAN_BIG -#else -# define ENDIAN_NATIVE ENDIAN_LITTLE -#endif } attribute((packed)); static inline int formats_equal(const struct stream_header *a, diff --git a/libtests/Makefile.am b/libtests/Makefile.am index 2f81f20..7be6c51 100644 --- a/libtests/Makefile.am +++ b/libtests/Makefile.am @@ -1,6 +1,6 @@ # # This file is part of DisOrder. -# Copyright (C) 2008 Richard Kettlewell +# Copyright (C) 2009 Richard Kettlewell # # 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 @@ -20,7 +20,7 @@ TESTS=t-addr t-arcfour t-basen t-bits t-cache t-casefold t-charset \ t-cookies t-dateparse t-event t-filepart t-hash t-heap t-hex \ t-kvp t-mime t-printf t-regsub t-selection t-signame t-sink \ t-split t-syscalls t-trackname t-unicode t-url t-utf8 t-vector \ - t-words t-wstat t-macros t-cgi t-eventdist + t-words t-wstat t-macros t-cgi t-eventdist t-resample noinst_PROGRAMS=$(TESTS) @@ -61,6 +61,8 @@ t_vector_SOURCES=t-vector.c test.c test.h t_words_SOURCES=t-words.c test.c test.h t_wstat_SOURCES=t-wstat.c test.c test.h t_eventdist_SOURCES=t-eventdist.c test.c test.h +t_resample_SOURCES=t-resample.c test.c test.h +t_resample_LDFLAGS=$(LIBSAMPLERATE) check-report: before-check check make-coverage-reports before-check: diff --git a/libtests/t-resample.c b/libtests/t-resample.c new file mode 100644 index 0000000..2eab79a --- /dev/null +++ b/libtests/t-resample.c @@ -0,0 +1,176 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2009 Richard Kettlewell + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "test.h" +#include "resample.h" +#include "vector.h" + +/* Accumulate converted bytes in a dynamic string */ +static void converted(uint8_t *bytes, + size_t nbytes, + void *cd) { + struct dynstr *d = cd; + dynstr_append_bytes(d, (void *)bytes, nbytes); +} + +/* Converter wrapper */ +static uint8_t *convert(const struct resampler *rs, + const uint8_t *input, size_t input_bytes, + size_t *output_bytes) { + struct dynstr d[1]; + + dynstr_init(d); + while(input_bytes > 0) { + size_t chunk = input_bytes > 1024 ? 1024 : input_bytes; + size_t consumed = resample_convert(rs, + input, input_bytes, + input_bytes == chunk, + converted, + d); + input += consumed; + input_bytes -= consumed; + } + *output_bytes = d->nvec; + return (uint8_t *)d->vec; +} + +static const struct { + const char *description; + int input_bits; + int input_channels; + int input_rate; + int input_signed; + int input_endian; + const char *input; + size_t input_bytes; + int output_bits; + int output_channels; + int output_rate; + int output_signed; + int output_endian; + const char *output; + size_t output_bytes; +} conversions[] = { + /* Conversions that don't change the sample rate */ + { + "empty input", + 8, 1, 8000, 0, ENDIAN_LITTLE, "", 0, + 8, 1, 8000, 0, ENDIAN_LITTLE, "", 0 + }, + { + "sign flip 8-bit unsigned->signed", + 8, 1, 8000, 0, ENDIAN_LITTLE, "\x00\x7F\x80\xFF", 4, + 8, 1, 8000, 1, ENDIAN_LITTLE, "\x80\xFF\x00\x7F", 4 + }, + { + "sign flip 8-bit signed->unsigned", + 8, 1, 8000, 1, ENDIAN_BIG, "\x80\xFF\x00\x7F", 4, + 8, 1, 8000, 0, ENDIAN_BIG, "\x00\x7F\x80\xFF", 4 + }, + { + "mono to stereo", + 8, 1, 8000, 0, ENDIAN_LITTLE, "\x00\x7F\x80\xFF", 4, + 8, 2, 8000, 0, ENDIAN_LITTLE, "\x00\x00\x7F\x7F\x80\x80\xFF\xFF", 8 + }, + { + "stereo to mono", + 8, 2, 8000, 0, ENDIAN_LITTLE, "\x00\x01\x7F\x02\x80\x03\xFF\x04", 8, + 8, 1, 8000, 0, ENDIAN_LITTLE, "\x00\x7F\x80\xFF", 4 + }, + { + "endian flip little->big", + 16, 1, 8000, 0, ENDIAN_LITTLE, "\x00\x01\x00\xFF\x01\x00\x01\xFF", 8, + 16, 1, 8000, 0, ENDIAN_BIG, "\x01\x00\xFF\x00\x00\x01\xFF\x01", 8, + }, + { + "endian flip big->little", + 16, 1, 8000, 0, ENDIAN_BIG, "\x01\x00\xFF\x00\x00\x01\xFF\x01", 8, + 16, 1, 8000, 0, ENDIAN_LITTLE, "\x00\x01\x00\xFF\x01\x00\x01\xFF", 8, + }, + { + "8-bit to 16-bit", + 8, 1, 8000, 0, ENDIAN_BIG, "\x00\x7F\x80\xFF", 4, + 16, 1, 8000, 0, ENDIAN_BIG, "\x00\x00\x7F\x00\x80\x00\xFF\x00", 8 + }, + { + "16-bit to 8-bit", + 16, 1, 8000, 0, ENDIAN_BIG, "\x00\x00\x7F\xFF\x80\x00\xFF\xFF", 8, + 8, 1, 8000, 0, ENDIAN_BIG, "\x00\x7F\x80\xFF", 4 + }, +#if HAVE_SAMPLERATE_H + /* Conversions that do change the sample rate */ + +#endif +}; +#define NCONVERSIONS (sizeof conversions / sizeof *conversions) + +static void test_resample(void) { + for(size_t n = 0; n < NCONVERSIONS; ++n) { + struct resampler rs[1]; + + resample_init(rs, + conversions[n].input_bits, + conversions[n].input_channels, + conversions[n].input_rate, + conversions[n].input_signed, + conversions[n].input_endian, + conversions[n].output_bits, + conversions[n].output_channels, + conversions[n].output_rate, + conversions[n].output_signed, + conversions[n].output_endian); + size_t output_bytes; + const uint8_t *output = convert(rs, + (const uint8_t *)conversions[n].input, + conversions[n].input_bytes, + &output_bytes); + if(output_bytes != conversions[n].output_bytes + || memcmp(output, conversions[n].output, output_bytes)) { + fprintf(stderr, "index %zu description %s mismatch\n", + n, conversions[n].description); + size_t k = 0; + while(k < conversions[n].output_bytes || k < output_bytes) { + size_t j = 0; + fprintf(stderr, "%8zu E:", k); + for(j = 0; j < 16; ++j) { + if(j + k < conversions[n].output_bytes) + fprintf(stderr, " %02x", conversions[n].output[j + k]); + } + fprintf(stderr, "\n G:"); + for(j = 0; j < 16; ++j) { + if(j + k < output_bytes) + fprintf(stderr, " %02x", output[j + k]); + } + fprintf(stderr, "\n"); + k += 16; + } + ++errors; + } + ++tests; + } +} + +TEST(resample); + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/server/Makefile.am b/server/Makefile.am index f67db97..376b53f 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -51,7 +51,7 @@ disorder_decode_DEPENDENCIES=../lib/libdisorder.a disorder_normalize_SOURCES=normalize.c disorder-server.h disorder_normalize_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ - $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) + $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(LIBSAMPLERATE) disorder_normalize_DEPENDENCIES=../lib/libdisorder.a disorder_rescan_SOURCES=rescan.c plugin.c api.c api-server.c exports.c \ diff --git a/server/normalize.c b/server/normalize.c index 52df3d7..bcfc3a8 100644 --- a/server/normalize.c +++ b/server/normalize.c @@ -1,6 +1,6 @@ /* * This file is part of DisOrder - * Copyright (C) 2007, 2008 Richard Kettlewell + * Copyright (C) 2007-2009 Richard Kettlewell * * 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 @@ -18,12 +18,15 @@ /** @file server/normalize.c * @brief Convert "raw" format output to the configured format * - * Currently we invoke sox even for trivial conversions such as byte-swapping. - * Ideally we would do all conversion including resampling in this one process - * and eliminate the dependency on sox. + * If libsamplerate is available then resample_convert() is used to do all + * conversions. If not then we invoke sox (even for trivial conversions such + * as byte-swapping). The sox support might be removed in a future version. */ #include "disorder-server.h" +#include "resample.h" + +static char buffer[1024 * 1024]; static const struct option options[] = { { "help", no_argument, 0, 'h' }, @@ -59,7 +62,6 @@ static void help(void) { * @param n Number of bytes to copy */ static void copy(int infd, int outfd, size_t n) { - char buffer[4096]; ssize_t written; while(n > 0) { @@ -84,6 +86,7 @@ static void copy(int infd, int outfd, size_t n) { } } +#if !HAVE_SAMPLERATE_H static void soxargs(const char ***pp, char **qq, const struct stream_header *header) { *(*pp)++ = "-t.raw"; @@ -120,11 +123,30 @@ static void soxargs(const char ***pp, char **qq, disorder_fatal(0, "unknown sox_generation %ld", config->sox_generation); } } +#else +static void converted(uint8_t *bytes, + size_t nbytes, + void attribute((unused)) *cd) { + /*syslog(LOG_INFO, "out: %02x %02x %02x %02x", + bytes[0], + bytes[1], + bytes[2], + bytes[3]);*/ + while(nbytes > 0) { + ssize_t n = write(1, bytes, nbytes); + if(n < 0) + disorder_fatal(errno, "writing to stdout"); + bytes += n; + nbytes -= n; + } +} +#endif int main(int argc, char attribute((unused)) **argv) { struct stream_header header, latest_format; - int n, p[2], outfd = -1, logsyslog = !isatty(2); + int n, outfd = -1, logsyslog = !isatty(2), rs_in_use = 0; pid_t pid = -1; + struct resampler rs[1]; set_progname(argv); if(!setlocale(LC_CTYPE, "")) @@ -149,6 +171,7 @@ int main(int argc, char attribute((unused)) **argv) { } memset(&latest_format, 0, sizeof latest_format); for(;;) { + /* Read one header */ n = 0; while((size_t)n < sizeof header) { int r = read(0, (char *)&header + n, sizeof header - n); @@ -174,11 +197,86 @@ int main(int argc, char attribute((unused)) **argv) { if(header.bits % 8 || !header.bits || header.bits > 64) disorder_fatal(0, "unsupported sample size %d bits", header.bits); if(header.endian != ENDIAN_BIG && header.endian != ENDIAN_LITTLE) - disorder_fatal(0, "unsupported byte order %x", header.bits); + disorder_fatal(0, "unsupported byte order %d", header.endian); /* Skip empty chunks regardless of their alleged format */ if(header.nbytes == 0) continue; /* If the format has changed we stop/start the converter */ +#if HAVE_SAMPLERATE_H + /* We have libsamplerate */ + if(formats_equal(&header, &config->sample_format)) + /* If the format is already correct then we just write out the data */ + copy(0, 1, header.nbytes); + else { + /* If we have a resampler active already check it is suitable and destroy + * it if not */ + if(!formats_equal(&header, &latest_format) && rs_in_use) { + resample_close(rs); + rs_in_use = 0; + } + /*syslog(LOG_INFO, "%d/%d/%d/%d/%d -> %d/%d/%d/%d/%d", + header.bits, + header.channels, + header.rate, + 1, + header.endian, + config->sample_format.bits, + config->sample_format.channels, + config->sample_format.rate, + 1, + config->sample_format.endian);*/ + if(!rs_in_use) { + /* Create a suitable resampler. */ + resample_init(rs, + header.bits, + header.channels, + header.rate, + 1, /* signed */ + header.endian, + config->sample_format.bits, + config->sample_format.channels, + config->sample_format.rate, + 1, /* signed */ + config->sample_format.endian); + latest_format = header; + rs_in_use = 1; + /* TODO speaker protocol does not record signedness of samples. It's + * assumed that they are always signed. This should be fixed in the + * future (and the sample format syntax extended in a compatible + * way). */ + } + /* Feed data through the resampler */ + size_t used = 0, left = header.nbytes; + while(used || left) { + if(left) { + size_t limit = (sizeof buffer) - used; + if(limit > left) + limit = left; + ssize_t r = read(0, buffer + used, limit); + if(r < 0) + disorder_fatal(errno, "reading from stdin"); + if(r == 0) + disorder_fatal(0, "unexpected EOF"); + left -= r; + used += r; + //syslog(LOG_INFO, "read %zd bytes", r); + } + /*syslog(LOG_INFO, " in: %02x %02x %02x %02x", + (uint8_t)buffer[0], + (uint8_t)buffer[1], + (uint8_t)buffer[2], + (uint8_t)buffer[3]);*/ + const size_t consumed = resample_convert(rs, + (uint8_t *)buffer, used, + !left, + converted, 0); + //syslog(LOG_INFO, "used=%zu consumed=%zu", used, consumed); + memmove(buffer, buffer + consumed, used - consumed); + used -= consumed; + } + } +#else + /* We do not have libsamplerate. We will use sox instead. */ if(!formats_equal(&header, &latest_format)) { if(pid != -1) { /* There's a running converter, stop it */ @@ -202,6 +300,7 @@ int main(int argc, char attribute((unused)) **argv) { *pp++ = "-"; /* stdout */ *pp = 0; /* This pipe will be sox's stdin */ + int p[2]; xpipe(p); if(!(pid = xfork())) { exitfn = _exit; @@ -221,6 +320,7 @@ int main(int argc, char attribute((unused)) **argv) { } /* Convert or copy this chunk */ copy(0, outfd, header.nbytes); +#endif } if(outfd != -1) xclose(outfd); @@ -231,6 +331,8 @@ int main(int argc, char attribute((unused)) **argv) { if(n) disorder_fatal(0, "sox failed: %#x", n); } + if(rs_in_use) + resample_close(rs); return 0; }