Hi everyone!
This topic may be very idiosyncratic to my live coding practice, but it may also be interesting for other folks. I am working on an external library to perform low-level feature extraction from node proxies. A situation I come frequently is when I extract some low-level features (e.g., pitch, amplitude, etc.) and map them to some parameters of another node. This process is conducted in two steps, first the unmapping in which the extracted parameters are scaled to the range 0-1 and secondly the mapping in which the scaled values are mapped to suitable ranges for other parameters.
During live coding, I end up messing with the parameters of the first node, and the ranges in which the features are responsive are lost. So in order to avoid having to readjust the ranges manually, I am implementing dynamic unmapping mechanisms which adapt the responsiveness to any changes. In principle, it tracks a range from the difference of running min and max values over a time window. Here is my first method for pitch extraction:
+ BusPlug {
dPitch {
arg timeWindow=5, maxLagTime=0.5;
var maxFreq=(Server.default.sampleRate/2).cpsmidi;
var in=this.ar;
var input=if(in.isArray,{in},{[in]}); //put the input in an array in case of a mono channel
var numChannels=input.size;
var sampNum=(timeWindow/ControlDur.ir).round; //calculate number of sumples
var pitch=A2K.kr(ZeroCrossing.ar(in)).cpsmidi; //track raw pitch, convert to midi
var ringBuf=numChannels.collect{LocalBuf(sampNum)}; // create buffers for each channel
var phasor=Phasor.kr(0,1,0,sampNum);
//circularly write raw pitch in correspinding buffers
var writeBuf=numChannels.collect{arg i; BufWr.kr(pitch[i], ringBuf[i], phasor)};
// extract min values from the ring buffers, substract infinitesimal value for preventing nan in stable signals
var min=numChannels.collect{arg i; BufMin.kr(ringBuf[i])[0]-1e-5};
// extract max values from the ring buffers, add infinitesimal value for preventing nan in stable signals
var max=numChannels.collect{arg i; BufMax.kr(ringBuf[i])[0]+1e-5};
// unamp pitch to the range 0-1 for each channel
var unmapedPitch=numChannels.collect{arg i; [min[i], max[i]].asSpec.unmap(pitch[i])};
// calculate the difference between max and min for each channel
var range=numChannels.collect{arg i; (max[i]-min[i])};
// calculate the ratio to the maximum possible pitch range
var ratio=range/maxFreq;
// filter the unmaped pitch, small range -> high lag time, big range -> low lag time
var laggedScaledPitch=unmapedPitch.lag(ratio.reciprocal.logist(1/4)*maxLagTime);
// fade in the unmaped pitch to avoid artifacts in initialization (buffers are filled with zeros and the calculation is unreliable before the buffers are full)
var fadedLaggedScaledPitch=laggedScaledPitch*Line.kr(dur: timeWindow);
^fadedLaggedScaledPitch;
}
}
You also need the logistic function for that to work:
+ Object {
logist {
arg k=1, l=1;
var denominator=(1+exp(k*this.neg));
var result=l/denominator;
^result
}
}
Its behavior is a bit peculiar, but it seems to do the job. This approach may not be suitable for all live coding situations, so I am going to implement static unmappings as well. What worries me is the high CPU usage, about 10% in my machine.
Are there any parts that can be implemented more efficiently? Any suggestions for the implementation or the approach in general?
Here is a simple example:
(
Ndef(\test_sinosc, {
SinOsc.ar(LFNoise0.kr(1/2!2).range(100,200));
} * -42.dbamp
).fadeTime_(1).play
)
(
Ndef(\test_mapping, {
var mpitch=Ndef(\test_sinosc).dPitch;
var freq=[50,2000,\exp].asSpec.map(mpitch);
var x=SinOsc.ar(freq,0, -32.dbamp);
x
}).fadeTime_(1).play
)
Thank you!