Thank you for your donation!


Custom soxr settings
#1
I've added the attached patch to allow custom soxr resampling.
It works great to upsample with custom param and you can also attenuate the signal.

The code is compatible with PiCorePlayer so you can enter strings like this:
http://archimago.blogspot.com/2018/11/mu....html#more

It fall back to the custom if the settings is not in the default supported list:

mpd.conf:

resampler {
plugin "soxr"
quality "v::4:28:99.5:100:73"
threads "0"
}

There is currently no GUI support so it will not show in the GUI.


Code:
diff --git a/src/pcm/SoxrResampler.cxx b/src/pcm/SoxrResampler.cxx
index 411e79b..a830d28 100644
--- a/src/pcm/SoxrResampler.cxx
+++ b/src/pcm/SoxrResampler.cxx
@@ -28,6 +28,22 @@

 #include <assert.h>
 #include <string.h>
+#include <math.h>
+
+struct soxr {
+       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;
+};
+
+static struct soxr soxr_custom_settings;
+char soxr_custom_string[200];

 static constexpr Domain soxr_domain("soxr");

@@ -40,6 +56,7 @@ static constexpr unsigned long SOXR_INVALID_RECIPE = -1;

 static soxr_quality_spec_t soxr_quality;
 static soxr_runtime_spec_t soxr_runtime;
+static soxr_io_spec_t soxr_iospec;

 static constexpr struct {
        unsigned long recipe;
@@ -53,6 +70,107 @@ static constexpr struct {
        { SOXR_INVALID_RECIPE, nullptr }
 };

+static char *next_param(char *src, char c) {
+        static char *str = NULL;
+        char *ptr, *ret;
+        if (src) str = src;
+        if (str && (ptr = strchr(str, c))) {
+                ret = str;
+                *ptr = '\0';
+                str = ptr + 1;
+        } else {
+                ret = str;
+                str = NULL;
+        }
+
+        return ret && ret[0] ? ret : NULL;
+}
+
+static bool resample_init(char *opt, struct soxr *r) {
+       char *recipe = NULL, *flags = NULL;
+       char *atten = NULL;
+       char *precision = NULL, *passband_end = NULL, *stopband_begin = NULL, *phase_response = NULL;
+
+       if (!r) {
+               return false;
+       }
+
+       r->max_rate = false;
+       r->exception = false;
+
+       if (opt) {
+               recipe = next_param(opt, ':');
+               flags = next_param(NULL, ':');
+               atten = next_param(NULL, ':');
+               precision = next_param(NULL, ':');
+               passband_end = next_param(NULL, ':');
+               stopband_begin = next_param(NULL, ':');
+               phase_response = next_param(NULL, ':');
+       }
+
+       // default to HQ (20 bit) if not user specified
+       r->q_recipe = SOXR_HQ;
+       r->q_flags = 0;
+       // default to 1db of attenuation if not user specified
+       r->scale = pow(10, -1.0 / 20);
+       // override recipe derived values with user specified values
+       r->q_precision = 0;
+       r->q_passband_end = 0;
+       r->q_stopband_begin = 0;
+       r->q_phase_response = -1;
+
+       if (recipe && recipe[0] != '\0') {
+               if (strchr(recipe, 'v')) r->q_recipe = SOXR_VHQ;
+               if (strchr(recipe, 'h')) r->q_recipe = SOXR_HQ;
+               if (strchr(recipe, 'm')) r->q_recipe = SOXR_MQ;
+               if (strchr(recipe, 'l')) r->q_recipe = SOXR_LQ;
+               if (strchr(recipe, 'q')) r->q_recipe = SOXR_QQ;
+               if (strchr(recipe, 'L')) r->q_recipe |= SOXR_LINEAR_PHASE;
+               if (strchr(recipe, 'I')) r->q_recipe |= SOXR_INTERMEDIATE_PHASE;
+               if (strchr(recipe, 'M')) r->q_recipe |= SOXR_MINIMUM_PHASE;
+               if (strchr(recipe, 's')) r->q_recipe |= SOXR_STEEP_FILTER;
+               // X = async resampling to max_rate
+               if (strchr(recipe, 'X')) r->max_rate = true;
+               // E = exception, only resample if native rate is not supported
+               if (strchr(recipe, 'E')) r->exception = true;
+       }
+
+       if (flags) {
+               r->q_flags = strtoul(flags, 0, 16);
+       }
+
+       if (atten) {
+               double scale = pow(10, -atof(atten) / 20);
+               if (scale > 0 && scale <= 1.0) {
+                       r->scale = scale;
+               }
+       }
+
+       if (precision) {
+               r->q_precision = atof(precision);
+       }
+
+       if (passband_end) {
+               r->q_passband_end = atof(passband_end) / 100;
+       }
+
+       if (stopband_begin) {
+               r->q_stopband_begin = atof(stopband_begin) / 100;
+       }
+
+       if (phase_response) {
+               r->q_phase_response = atof(phase_response);
+       }
+
+       snprintf(soxr_custom_string, sizeof(soxr_custom_string),
+               "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",
+               (unsigned int)r->q_recipe, (unsigned int)r->q_flags, r->scale, r->q_precision, r->q_passband_end, r->q_stopband_begin, r->q_phase_response);
+
+       return true;
+}
+
+
 gcc_const
 static const char *
 soxr_quality_name(unsigned long recipe) noexcept
@@ -84,15 +202,35 @@ pcm_resample_soxr_global_init(const ConfigBlock &block)
 {
        const char *quality_string = block.GetBlockValue("quality");
        unsigned long recipe = soxr_parse_quality(quality_string);
-       if (recipe == SOXR_INVALID_RECIPE) {
-               assert(quality_string != nullptr);

-               throw FormatRuntimeError("unknown quality setting '%s' in line %d",
-                                        quality_string, block.line);
+       if (recipe == SOXR_INVALID_RECIPE && resample_init((char*)quality_string, &soxr_custom_settings)) {
+                soxr_quality = soxr_quality_spec(soxr_custom_settings.q_recipe, soxr_custom_settings.q_flags);
+                if (soxr_custom_settings.q_precision > 0) {
+                        soxr_quality.precision = soxr_custom_settings.q_precision;
+                }
+                if (soxr_custom_settings.q_passband_end > 0) {
+                        soxr_quality.passband_end = soxr_custom_settings.q_passband_end;
+                }
+                if (soxr_custom_settings.q_stopband_begin > 0) {
+                        soxr_quality.stopband_begin = soxr_custom_settings.q_stopband_begin;
+                }
+                if (soxr_custom_settings.q_phase_response > -1) {
+                        soxr_quality.phase_response = soxr_custom_settings.q_phase_response;
+                }
+               soxr_iospec = soxr_io_spec(SOXR_FLOAT32_I, SOXR_FLOAT32_I);
+               soxr_iospec.scale = soxr_custom_settings.scale;
+       } else {
+               if (recipe == SOXR_INVALID_RECIPE) {
+                       assert(quality_string != nullptr);
+
+                       throw FormatRuntimeError("unknown quality setting '%s' in line %d",
+                                                quality_string, block.line);
+               }
+
+               soxr_quality = soxr_quality_spec(recipe, 0);
+               soxr_iospec.scale = 0;
        }

-       soxr_quality = soxr_quality_spec(recipe, 0);
-
        FormatDebug(soxr_domain,
                    "soxr converter '%s'",
                    soxr_quality_name(recipe));
@@ -110,12 +248,12 @@ SoxrPcmResampler::Open(AudioFormat &af, unsigned new_sample_rate)
        soxr_error_t e;
        soxr = soxr_create(af.sample_rate, new_sample_rate,
                           af.channels, &e,
-                          nullptr, &soxr_quality, &soxr_runtime);
+                          soxr_iospec.scale > 0 ? &soxr_iospec : nullptr, &soxr_quality, &soxr_runtime);
        if (soxr == nullptr)
                throw FormatRuntimeError("soxr initialization has failed: %s",
                                         e);

-       FormatDebug(soxr_domain, "soxr engine '%s'", soxr_engine(soxr));
+       FormatDebug(soxr_domain, "soxr engine '%s' soxr_quality '%s'", soxr_engine(soxr), soxr_custom_string);

        channels = af.channels;


I hope it is useful for someone else Smile
Reply
#2
Nice patch.

Whats the recipe for patching and building SoX (libsoxr) so this feature can be implemented in moOde and maintained going forward?

-Tim
Reply
#3
My guess is that I'll never take advantage of this (old ears) but FYI:

The Debian repo appears to use version 0.1.2 of the libsoxr code. The official SoX Resampler library on Sourceforge is up to 0.1.3. The bump may or may not matter for moOde, based on what I read in the release notes, but good to know.

Regards,
Kent
Reply
#4
@Tim Curtis I've just patched your mpd-0.21.16 source code and used your recipe to build and replace in moode  nothing more is needed.
Reply
#5
This is cool!
Reply
#6
(01-18-2020, 03:59 PM)zeb5274 Wrote: @Tim Curtis I've just patched your mpd-0.21.16 source code and used your recipe to build and replace in moode  nothing more is needed.

Hi,

Got it. Can u submit as a feature req to MPD maintainer?
https://github.com/MusicPlayerDaemon/MPD/issues
Reply
#7
(01-18-2020, 02:06 PM)Tim Curtis Wrote: Nice patch.

Whats the recipe for patching and building SoX (libsoxr) so this feature can be implemented in moOde and maintained going forward?

-Tim

(01-18-2020, 03:59 PM)zeb5274 Wrote: @Tim Curtis I've just patched your mpd-0.21.16 source code and used your recipe to build and replace in moode  nothing more is needed.

(01-18-2020, 07:58 PM)Tim Curtis Wrote:
(01-18-2020, 03:59 PM)zeb5274 Wrote: @Tim Curtis I've just patched your mpd-0.21.16 source code and used your recipe to build and replace in moode  nothing more is needed.

Hi,

Got it. Can u submit as a feature req to MPD maintainer?
https://github.com/MusicPlayerDaemon/MPD/issues

OK, I will cleanup and try to submit to MPD.
Reply
#8
New version that I will try to submit (cleanup code and config change):

resampler {
plugin "soxr"
quality "advanced"
advanced_settings "v::3.05:28:99.7:100:45"
threads "0"
}


Code:
diff --git a/src/pcm/SoxrResampler.cxx b/src/pcm/SoxrResampler.cxx
index 411e79b..dcb66b9 100644
--- a/src/pcm/SoxrResampler.cxx
+++ b/src/pcm/SoxrResampler.cxx
@@ -22,12 +22,30 @@
#include "config/Block.hxx"
#include "util/RuntimeError.hxx"
#include "util/Domain.hxx"
+#include "util/Alloc.hxx"
+
#include "Log.hxx"

#include <soxr.h>

#include <assert.h>
#include <string.h>
+#include <math.h>
+
+struct soxr {
+ 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;
+};
+
+static struct soxr soxr_advanced_settings;
+char soxr_advanced_string[200];

static constexpr Domain soxr_domain("soxr");

@@ -40,6 +58,7 @@ static constexpr unsigned long SOXR_INVALID_RECIPE = -1;

static soxr_quality_spec_t soxr_quality;
static soxr_runtime_spec_t soxr_runtime;
+static soxr_io_spec_t soxr_iospec;

static constexpr struct {
unsigned long recipe;
@@ -50,6 +69,7 @@ static constexpr struct {
{ SOXR_MQ, "medium" },
{ SOXR_LQ, "low" },
{ SOXR_QQ, "quick" },
+ { SOXR_32_BITQ, "advanced" },
{ SOXR_INVALID_RECIPE, nullptr }
};

@@ -65,6 +85,104 @@ soxr_quality_name(unsigned long recipe) noexcept
}
}

+static char *
+quality_advanced_nextparam(char *src, char c)
+{
+        static char *str = NULL;
+        char *ptr, *ret;
+
+        if (src)
+ str = src;
+        if (str && (ptr = strchr(str, c))) {
+                ret = str;
+                *ptr = '\0';
+                str = ptr + 1;
+        } else {
+                ret = str;
+                str = NULL;
+        }
+
+        return ret && ret[0] ? ret : NULL;
+}
+
+static bool
+soxr_parse_quality_advanced(const char *copt, struct soxr *r)  noexcept
+{
+ char *opt, *recipe = NULL, *flags = NULL;
+ char *atten = NULL;
+ char *precision = NULL, *passband_end = NULL, *stopband_begin = NULL, *phase_response = NULL;
+
+ opt = xstrdup(copt);
+
+ r->max_rate = false;
+ r->exception = false;
+
+ if (opt) {
+ recipe = quality_advanced_nextparam(opt, ':');
+ flags = quality_advanced_nextparam(NULL, ':');
+ atten = quality_advanced_nextparam(NULL, ':');
+ precision = quality_advanced_nextparam(NULL, ':');
+ passband_end = quality_advanced_nextparam(NULL, ':');
+ stopband_begin = quality_advanced_nextparam(NULL, ':');
+ phase_response = quality_advanced_nextparam(NULL, ':');
+ }
+
+ /* default to HQ (20 bit) if not user specified */
+ r->q_recipe = SOXR_HQ;
+ r->q_flags = 0;
+ /* default to 1db of attenuation if not user specified */
+ r->scale = pow(10, -1.0 / 20);
+ /* override recipe derived values with user specified values */
+ r->q_precision = 0;
+ r->q_passband_end = 0;
+ r->q_stopband_begin = 0;
+ r->q_phase_response = -1;
+
+ if (recipe && recipe[0] != '\0') {
+ if (strchr(recipe, 'v')) r->q_recipe = SOXR_VHQ;
+ if (strchr(recipe, 'h')) r->q_recipe = SOXR_HQ;
+ if (strchr(recipe, 'm')) r->q_recipe = SOXR_MQ;
+ if (strchr(recipe, 'l')) r->q_recipe = SOXR_LQ;
+ if (strchr(recipe, 'q')) r->q_recipe = SOXR_QQ;
+ if (strchr(recipe, 'L')) r->q_recipe |= SOXR_LINEAR_PHASE;
+ if (strchr(recipe, 'I')) r->q_recipe |= SOXR_INTERMEDIATE_PHASE;
+ if (strchr(recipe, 'M')) r->q_recipe |= SOXR_MINIMUM_PHASE;
+ if (strchr(recipe, 's')) r->q_recipe |= SOXR_STEEP_FILTER;
+ /* IGNORED: X = async resampling to max_rate */
+ if (strchr(recipe, 'X')) r->max_rate = true;
+ /* IGNORED: E = exception, only resample if native rate is not */
+ 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);
+
+ snprintf(soxr_advanced_string, sizeof(soxr_advanced_string),
+ "%s => recipe: 0x%02lx, flags: 0x%02lx, scale: %03.2f, precision: %03.1f, passband_end: %03.5f, stopband_begin: %03.5f, phase_response: %03.1f",
+ copt, r->q_recipe, r->q_flags, r->scale, r->q_precision, r->q_passband_end, r->q_stopband_begin, r->q_phase_response);
+
+ free(opt);
+ return true;
+}
+
gcc_pure
static unsigned long
soxr_parse_quality(const char *quality) noexcept
@@ -83,6 +201,7 @@ void
pcm_resample_soxr_global_init(const ConfigBlock &block)
{
const char *quality_string = block.GetBlockValue("quality");
+ const char *quality_advanced_string = block.GetBlockValue("advanced_settings");
unsigned long recipe = soxr_parse_quality(quality_string);
if (recipe == SOXR_INVALID_RECIPE) {
assert(quality_string != nullptr);
@@ -91,9 +210,27 @@ pcm_resample_soxr_global_init(const ConfigBlock &block)
quality_string, block.line);
}

- soxr_quality = soxr_quality_spec(recipe, 0);
-
- FormatDebug(soxr_domain,
+ /* default iospec */
+ soxr_iospec = soxr_io_spec(SOXR_FLOAT32_I, SOXR_FLOAT32_I);
+ soxr_iospec.scale = 1;
+
+ /* parse advanced recipe */
+ if (recipe == SOXR_32_BITQ && quality_advanced_string != nullptr &&
+    soxr_parse_quality_advanced(quality_advanced_string, &soxr_advanced_settings)) {
+                soxr_quality = soxr_quality_spec(soxr_advanced_settings.q_recipe, soxr_advanced_settings.q_flags);
+                if (soxr_advanced_settings.q_precision > 0)
+                        soxr_quality.precision = soxr_advanced_settings.q_precision;
+                if (soxr_advanced_settings.q_passband_end > 0)
+                        soxr_quality.passband_end = soxr_advanced_settings.q_passband_end;
+                if (soxr_advanced_settings.q_stopband_begin > 0)
+                        soxr_quality.stopband_begin = soxr_advanced_settings.q_stopband_begin;
+                if (soxr_advanced_settings.q_phase_response > -1)
+                        soxr_quality.phase_response = soxr_advanced_settings.q_phase_response;
+ soxr_iospec.scale = soxr_advanced_settings.scale;
+ } else
+ soxr_quality = soxr_quality_spec(recipe, 0);
+
+ FormatInfo(soxr_domain,
   "soxr converter '%s'",
   soxr_quality_name(recipe));

@@ -110,12 +247,13 @@ SoxrPcmResampler::Open(AudioFormat &af, unsigned new_sample_rate)
soxr_error_t e;
soxr = soxr_create(af.sample_rate, new_sample_rate,
  af.channels, &e,
-   nullptr, &soxr_quality, &soxr_runtime);
+   soxr_iospec.scale != 1 ? &soxr_iospec : nullptr, &soxr_quality, &soxr_runtime);
if (soxr == nullptr)
throw FormatRuntimeError("soxr initialization has failed: %s",
e);

- FormatDebug(soxr_domain, "soxr engine '%s'", soxr_engine(soxr));
+ FormatDebug(soxr_domain, "soxr engine '%s' advanced settings '%s'",
+ soxr_engine(soxr), soxr_advanced_string);

channels = af.channels;
Reply
#9
FYI https://github.com/MusicPlayerDaemon/MPD/pull/714
I will tell you if it is accepted.
Reply
#10
Great, thanks :-)
Reply


Forum Jump: