ControlSpec with midiratio

I am trying to create a ControlSpec which converts numbers to midiratios but I am not sure how to achieve this. I would like to create a spec which allows the following functionality but with the conversion to midiratio happening a a result of the spec, rather than manually converting the value. Any ideas?

(
~spec =  [0, 12, \lin, 1];
k = Knob().action_{|b| 
	~val = ~spec.asSpec.map(b.());
	{ k.value = ~spec.asSpec.unmap(~val); ~val.midiratio.debug(\midiratio) }.defer;
}.fixedSize_(200@200).moveTo(600, 200).front
)

I think you’d have to create and register a new subclass of Warp (“register” meaning, there’s a collection of symbols pointing to Warp classes, which is how we can write e.g. [1, 10, \exp].asSpec).

hjh

Yes, I was looking into Warp and asWarp, but there is very little information in the help docs about the inner workings of Warp. One can do something like [0, 12, 1.midiratio.asWarp].asSpec which doesn’t produce the desired result but maybe there is a way the warp can be defined to deliver same results as midiratio? I could not find out how midiratio is implemented and what the transfer function is - if I was better at math I could probably calculate myself.

Start with the linear warp as a template.

I’m not at the computer now, but I believe it should inherit from Warp and override map and unmap.

To implement your behavior, then, I think it could be like this (though untested):

MIDIRatioWarp : LinearWarp {
    map { |value|
        ^super.map(value).midiratio
    }
    unmap { |value|
        ^super.unmap(value.ratiomidi)
    }
}

Then you would need to add an entry into Warp.warps – I’m going to guess it would be like Warp.warps.put(\mratio, MIDIRatioWarp).

I’m pretty sure most of the Warp subclasses follow this basic pattern – the class file serves as a model for future extensions. (Probably the missing documentation is because Warps aren’t user-facing classes, and there’s an assumption that people developing new classes will read the class source and emulate the design patterns found there.)

hjh

Great, that works for a continuous knob. For a stepped knob I does not work to do [0, 12, \mratio, 1/13] to give you 0.midiratio, 1.midiratio…12.midiratio because steps are not of an equal size. In a related issue I often find myself wanting a stepped knob with uneven steps, like a subdivision knob giving you even and triplet divisions of a tempo. Is it possible to modify or expand the step value of ControlSpec (or Warp) so you would be able to supply an array of values like e.g
[ 1/8, 2, \lin, [ 1/8, 1/6, 1/4, 1/3, 1/2, 1, 1.5, 2]].asSpec
giving you a knob with 8 steps or in this case
[0, 12, \mratio, (0…12).midiratio].asSpec
giving you 13 steps corresponding to integer midiratios?
I suspect this is a more sticky problem and I should also say that I am new to writing classes.

I think it isn’t quite right to expect ControlSpec to be able to model every type of calculation that everyone would ever want to do.

“Steps” in ControlSpec are always linear, while a pitch ratio is exponential (aka “log-linear”). And the issue is a little more complicated here because it isn’t the Warp that handles step rounding – it’s actually hardcoded into ControlSpec’s constrain, map and unmap methods. So doing it with a new Warp is not possible.

I think the best solutions for this case are:

  1. Either… subclass ControlSpec and override the round logic. (Note, untested, but the point is to round the MIDI note value – so presumably step = 1. This is a different semantic from ControlSpec.)
    MIDIRatioSpec : ControlSpec {
    	constrain { arg value;
    		^value.asFloat.clip(clipLo, clipHi).ratiomidi.round(step).midiratio
    	}
    	map { arg value;
    		// maps a value from [0..1] to spec range
    		^warp.map(value.clip(0.0, 1.0)).round(step).midiratio;
    	}
    	unmap { arg value;
    		// maps a value from spec range to [0..1]
    		^warp.unmap(value.ratiomidi.round(step).clip(clipLo, clipHi));
    	}
    }
    
  2. Or, create a wrapper class that uses a ControlSpec for its native behavior – mapping the normal range onto a range of MIDI notes – and then converting to the ratio.
    MIDIRatioSpec {
    	var <>spec;
    	*new { |lowNote = 36, hiNote = 84, step = 1, default = 60, warp = \lin|
    		^super.newCopyArgs(
    			ControlSpec(lowNote, hiNote, warp, step, default)
    		)
    	}
    	map { |value|
    		^spec.map(value).midiratio
    	}
    	unmap { |value|
    		^spec.unmap(value.ratiomidi)
    	}
    }
    

The second is probably better in the long run.

hjh

1 Like

Yes, the 2nd examples works like a charm (probably 1st example too, but took your advice and implemented the 2nd solution).