ControlSpec on server?

I wonder if someone has figured this out already,

I want a SynthDef to be able to modulate a value with some arbitrary spec, something like

~spec = \freq.asSpec;

SynthDef(\modulate, { |out, in, value, spec|
  var mod = In.ar(in);
  var sig = spec.map(spec.unmap(value) + mod);
  Out.ar(out, sig);
}).add;

But this won’t work because ControlSpec doesn’t have a server side analog and anyway different warp types have different math in their map/unmap methods.

Is the best way to make a different SynthDef for each Warp type, or am I missing something easier?

Maybe make a Buffer from a spec by filling an Array and then using .loadCollection

Oh cool idea! Definitely hadn’t thought of that.

Not sure it’s practical for this case since there may be a lot of them, and they may change rapidly (like frequently needing to free them all and then allocate many more)… I’ll try it out though and see how it performs :slight_smile:

edit: actually, while this works great for spec.map, (using BufRd to index into the buffer, mapping a 0-1 signal to the size of the buffer) I’m not sure how this would work for spec.unmap

Envelopes have the same warp functions and logic as ControlSpecs, and you can use IEnvGen to index into an envelope. As a bonus, you can send full envelopes as parameters, so I think you can do something like:

// Just min/max/numeric warp
Ndef(\test, {
    var env = Env([\min.kr, \max.kr], [1], warp: \warp.kr(-2));
    var input = MouseX.kr(0, 1);
    IEnvGen.kr(env, input).poll;
});

// Take entire envelope as a parameter
Ndef(\test, {
    var env = \env.kr(Env([0, 1], [1], 0).asArray);
    var input = MouseX.kr(0, 1);
    IEnvGen.kr(env, input).poll;
}).set(\env, Env([0.1, 100], [1], warp:\exp));

(My sc build is funky rn so I haven’t tested this :slight_smile: - but it should work without big changes)

Actually, would be nice to make a simple Map.ar psuedo-ugen that does this under the good, then you could just do:

Map.ar(spec, input);
1 Like

This is cool, but Envelopes don’t as far as I can tell have an equivalent to \amp or \db warping (FaderWarp and DbFaderWarp) – unless this is the same as the \squared curve?

Also, how would you use Envelopes to unmap?

I apologize, I didn’t see the map-unmap workflow in your code, this will obviously not work (but this IS a cool workflow).

In case of dB warp, iirc this has some specific logic that’s really meant more for UI controls (it fudges numbers so you can go down to -inf in a straightforward way). In this case, it would be better to send raw DB values to your synth, and then map and unmap with .dbamp / .ampdb.

My intuition would be that for a numeric warp spec, you would use:

value = input.curvelin(min, max, 0, 1, warp) + mod;
value = value.lincurve(0, 1, min, max, warp);

But I can’t recall if this is a perfect reversal of the warp calculations?

Yeah I’m mostly using the specs for midi/GUI controls, but at that point it becomes convenient to be able to use them as the modulation spec as well… So the modulation is equivalent to automatically turning the knob at audio rate.

Here is my latest attempt which is not very elegant but does work correctly as far as I have tested (though actually now realizing I haven’t fully implemented the “step” control):

(
var warpModFuncs = ();

[CosineWarp, SineWarp, LinearWarp, ExponentialWarp].do { |warp|
  warpModFuncs[warp] = { |val, modVal, minval, maxval, step|
    var spec = ControlSpec(minval, maxval, warp, step);
    var unmappedVal = spec.unmap(val);
    var moddedVal = unmappedVal + modVal;
    spec.map(moddedVal);
  };
};

warpModFuncs[DbFaderWarp] = { |val, modVal, minval, maxval, step|
  var valAmp = val.dbamp;
  var minvalAmp = minval.dbamp;
  var rangeAmp = maxval.dbamp - minvalAmp;
  var unmappedVal = Select.kr(rangeAmp > 0, [
    1 - sqrt(1 - ((valAmp - minvalAmp) / rangeAmp)),
    ((valAmp - minvalAmp) / rangeAmp).sqrt
  ]);
  var moddedVal = (unmappedVal + modVal).clip(0, 1);
  Select.ar(K2A.ar(rangeAmp > 0), [
    (1 - (1 - moddedVal).squared) * rangeAmp + minvalAmp,
    moddedVal.squared * rangeAmp + minvalAmp
  ]).ampdb;
};

warpModFuncs[FaderWarp] = { |val, modVal, minval, maxval, step|
  var range = maxval - minval;
  var unmappedVal = Select.kr(range > 0, [
    1 - sqrt(1 - ((val - minval) / range)),
    ((val - minval) / range).sqrt
  ]);
  var moddedVal = (unmappedVal + modVal).clip(0, 1);
  Select.ar(K2A.ar(range > 0), [
    (1 - (1 - moddedVal).squared) * range + minval,
    moddedVal.squared * range + minval
  ]);
};

warpModFuncs.keysValuesDo { |warp, func|
  var defName = ("modulate" ++ warp.asString).asSymbol;
  SynthDef(defName, { |out, in, amp, val, minval, maxval, step|
    var modVal = In.ar(in) * amp;
    Out.ar(out, func.(val, modVal, minval, maxval, step));
  }).add;
};

SynthDef(\modulateCurveWarp, { |out, in, amp, val, minval, maxval, step, curve|
  var range = maxval - minval;
  var grow = exp(curve);
      var a = range / (1.0 - grow);
      var b = minval + a;
  var modVal = In.ar(in) * amp;
  var unmappedVal = log((b - val) / a) / curve;
  var moddedVal = unmappedVal + modVal;
  Out.ar(out, b - (a * pow(grow, moddedVal)));
}).add;
)

Basically used like this:

~bus = Bus.audio(s)
~bus2 = Bus.audio(s)

~spec = \freq.asSpec;

x = { SinOsc.ar(\freq.kr(440)) }.play
y = { SinOsc.ar(1) }.play(x, ~bus, 0, \addBefore);
z = Synth(\modulateExponentialWarp, [out: ~bus2, in: ~bus, amp: 1, val: 440, minval: ~spec.minval, maxval: ~spec.maxval, step: ~spec.step], y, \addAfter);
x.set(\freq, ~bus2.asMap)
z.set(\amp, 0.1)
1 Like

Curvelin was broken some years ago, but it should be a correct inverse function now.

Maybe in the lincurve line above, set clip to \none to avoid a nonlinearity if mod pushes the normalized value outside the 0-1 range.

hjh