beast icon indicating copy to clipboard operation
beast copied to clipboard

Feature Request: support portamento and logscale parameters

Open swesterfeld opened this issue 7 years ago • 9 comments

It might be nice to support portamento via mono synths. Previously this was this bugzilla bug:

https://bugzilla.gnome.org/show_bug.cgi?id=353137

I've improved the portamento implementation, speed can now be set via time (ms). The module should work fine, however I don't like the linear time property, as small times can't be edited well. Here is a suggestion of how the mapping slider -> time could work:

0 0 1 1.92 2 15.7 3 53.9 4 124 5 244 6 422 7 672 8 1000

I attached the implementation as zip as github doesn't allow me to attach them individually (unsupported file type).

bseportamento.zip

swesterfeld avatar May 12 '17 19:05 swesterfeld

On 12.05.2017 21:03, swesterfeld wrote:

Here is a suggestion of how the mapping slider -> time could work:

0 0 1 1.92 2 15.7 3 53.9 4 124 5 244 6 422 7 672 8 1000

Here's what Octave gives me for a polynomial approximation of third degree (degree 2 doesn't fit well) to those points:

x = [0, 1, 2, 3, 4, 5, 6, 7, 8] y = [0, 1.92, 15.7, 53.9, 124, 244, 422, 672, 1000] z = 0:0.001:8 p = polyfit (x,y,3) plot (x, y, 'o-r', z, p(1)*z.^3+p(2)*z.^2+p(3)*z+p(4)); format long p p = 1.9500336700336658 0.0536435786436373 -0.1588287638289820
0.1167676767678972

-- Yours sincerely, Tim Janik


https://testbit.eu/timj/ Free software author.

tim-janik avatar May 12 '17 22:05 tim-janik

Ok, thats nice, but I don't like the approach of hardcoding some magic (from octave) coefficients in the code, because you can't tweak them to the plugins needs. We have estimated four parameters here, so for each such parameter set, we need four input points to get a function that goes through these parameters.

So what I'd like to have (API wise) would be something like this:

  • min
  • max
  • center
  • slope

where min max and center directly corresponds to the points for these positions. Slope is the parameter which determines some curviness, and can be set indiviudally to choose one particular function from the set of possible ones.

Ideally this would be well-behaved in the sense that whatever the input is, the resulting function is monotonically increasing. So it might be better not to use polynomials (which don't have this property), but fit some exponential function to the specs (which is always monotonically increasing).

Note that for the portamento plugin, we don't need a perfect fit to the data, but just something which goes through min|max|center, is monotonically increasing, and looks similar to the data.

swesterfeld avatar May 13 '17 11:05 swesterfeld

Just as a note, your approximation is not monotonically increasing: using gnuplot: f(x)=(1.9500336700336658 * x * x * x + 0.0536435786436373 * x * x + -0.1588287638289820 * x + 0.1167676767678972) plot [0:8] "time-ms-data" using 1:(log($2)), log(f(x))

which goes down and then up between 0 and 1

swesterfeld avatar May 13 '17 11:05 swesterfeld

I think I found a function family that does the job. In gnuplot:

xparam(x,slope) = (exp (x * slope) - x * slope - 1) / (exp (slope) - slope - 1)
f(x)=(1.9500336700336658 * x * x * x + 0.0536435786436373 * x * x + -0.1588287638289820 * x + 0.1167676767678972)
plot [0:1] xparam(x,3)*1000, f(x*8)

The slope parameter (here slope=3) can be adjusted. Note that none of the xparam functions fit the data exactly. But I think they mimic the general behaviour quite well, so a slider using these functions should be user friendly.

Higher slope means more editing space for low values. All xparam functions are monotonically increasing, and therefore also invertible. However I found no direct solution to compute the inverse function, so I implemented a bisection search. Here is my code as C functions:

double
sm_xparam (double x, double slope)
{
  return (exp (x * slope) - x * slope - 1) / (exp (slope) - slope - 1);
}

double
sm_xparam_inv (double x, double slope)
{
  double xlow = 0.0;
  double xhigh = 1.0;

  for (int i = 0; i < 20; i++)
    {
      const double xnew = (xlow + xhigh) / 2;
      const double vnew = sm_xparam (xnew, slope);
      if (vnew < x)
        {
          xlow = xnew;
        }
      else
        {
          xhigh = xnew;
        }
    }
  return (xlow + xhigh) / 2;
}

swesterfeld avatar May 14 '17 09:05 swesterfeld

I think the slope approach is a bit over the top... You're right about the monotonically increasing requirement though. Looking at the function again, a much better approximation in practice is probably: f(x) = 1.953125 * x^3 The first factor is simply an artefact of your boundaries, 1000./(888)=1.953125000 UPDATE: Btw, if you really wanted to adjust the slope, within [0:1] you could simply use: f(x) = x^slope | where slope >=1

tim-janik avatar May 18 '17 16:05 tim-janik

Yes, thats what I was looking for. Using f(x)=x^slope is not only always monotonically increasing and adjustable, it is also trivial to invert (x^(1.0/slope)).

I can think of only one reason to use x^3 rather than the generic x^slope: if we had all properties automatable and some value ramp for portamento glide, then we would possibly want to compute the mapping from input modulation interval [0,1] to portamento glide at every sample (for this particular module it doesn't matter, but there may be time parameters that can be tweaked at runtime like compressor-attack-ms or adsr-attack-ms or delay-ms). Modulation should in general use a non-linear mapping from control value to dsp parameter, I think the obvious choice would be to use the same mapping like for the ui slider.

Anyway, this is just speculation about the far future. At this point in time, I'd rather have a param spec with slope that is "slightly inefficient", works for the ui, and is flexible, like x^slope.

Question is: who should implement it? I have grepped for log scale through the source, and it seems this affects code in SFI, BSE, BEAST, GXK. And it looks like this is lots of code, too. I had hoped for something simpler ...

I can try to read my way through all this, and come up with an implementation. Or, since you already know the code well, you could probably do it more elegantly and with less effort. Basically let me know if you want me to provide the code for the param spec or do it yourself.

swesterfeld avatar May 25 '17 10:05 swesterfeld

Stefan, can you please work this into a pull request?

tim-janik avatar Aug 30 '17 01:08 tim-janik

Yes, thats what I was looking for. Using f(x)=x^slope is not only always monotonically increasing and adjustable, it is also trivial to invert (x^(1.0/slope)).

Indeed, hadn't thought of it in terms of inversion, but I needed that for implementing logscale knobs for the new Processor parameters (cannow be found in ebeast/b/pro-input.vue), It turns out slope is determined by specifying a logscale center like so:

// Determine exponent, so that:
//   begin + pow (0.0, exponent) * (end - begin) == begin  ← exponent is irrelevant here
//   begin + pow (0.5, exponent) * (end - begin) == center
//   begin + pow (1.0, exponent) * (end - begin) == end    ← exponent is irrelevant here
// I.e. desired: log_0.5 ((center - begin) / (end - begin))

Example in gnuplot:

begin=32.7; end=8372; center=523; e=log((end-begin) / (center-begin)) / log(2)
print e; set logscale y; plot [0:1] begin + x**e * (end-begin), center

I can think of only one reason to use x^3 rather than the generic x^slope: if we had all properties automatable and some value ramp for portamento glide, then we would possibly want to compute the mapping from input modulation interval [0,1] to portamento glide at every sample (for this particular module it doesn't matter, but there may be time parameters that can be tweaked at runtime like compressor-attack-ms or adsr-attack-ms or delay-ms). Modulation should in general use a non-linear mapping from control value to dsp parameter, I think the obvious choice would be to use the same mapping like for the ui slider.

I have just added logscale mappings to the Attack/Decay Env in Blepsynth in milliseconds: min=0, max=8000, logcenter=1000 With the above formulas, that results in e=3: 0 + pow (x, 3) * 8, i.e. slope=3; f(x)=8*x^slope; But for the cutoff frequency, a completely different center was needed, so the slope there is indeed different, more like 4.75 - 5.

tim-janik avatar Jul 11 '20 10:07 tim-janik

I have just added logscale mappings

The mappings you added are obviously useful, especially for milliseconds (x^3) mappings, but these are not logscale. A true logscale mapping plots as line if you set logscale y in gnuplot. A true logscale mapping is fully determined by two points, a center is not necessary. Here is how that would compare to the function you implemented:

begin=32.7; end=8372; center=523; e=log((end-begin) / (center-begin)) / log(2)
print e; set logscale y; plot [0:1] begin + x**e * (end-begin), center, exp (log (begin) + x * (log (end) - log (begin)))

You can also see that this is not true logscale in BlepSynth frequency knob. It has more pixels for 20..40 Hz than it has for 12000..24000 Hz, even though what we wanted is that each octave takes the same number of pixels.

swesterfeld avatar Jul 13 '20 11:07 swesterfeld