How can I determine if a control signal has stabilized to within a certain margin regardless of the value it stabilizes at? Is there a Ugen that tells me how much a signal has settled?
Iâm not aware of a single, black-box UGen that can anticipate what you mean by âstabilizes.â But I think you could build this using a high-pass filter.
âStabilizesâ sounds to me like a relative absence of movement. In a completely stationary control value, all of the energy is at 0 Hz. If itâs moving slowly, the energy will be biased toward low frequencies. If itâs moving a lot, thereâs more likely to be higher-frequency energy.
So one way to measure moving/not-moving is to filter out the stationary/very-slow energy (high-pass); then, if the RMS level or Amplitude of this drops below a threshold, that would indicate stability.
(High-pass removes DC offset. DC offset = âthe value it stabilizes atâ so âregardless of thisâ implies starting with a high-pass filter.)
hjh
And, a demo:
(
a = { |freq = 600, lfoFreq = 2|
var trig = Impulse.kr(0);
var rectPulse = Trig1.kr(trig, 0.01);
// BPF *is* weighted-spring resonance *wink*
// chose this function because the vibration
// naturally decays toward the center
var spring = BPF.kr(rectPulse, lfoFreq, 0.1) * 50;
var modulatedFreq = (4 ** spring) * freq;
var moving = HPF.kr(modulatedFreq, lfoFreq);
var amp = Amplitude.kr(moving, 0.01, 0.1);
var eg = EnvGen.kr(
Env.asr(0.01, 1, 0.2),
// I found the amp threshold empirically:
// run the synth, and see the number range
// associated with a stable pitch
// Some freq vs lfoFreq combinations
// seem to stabilize more quickly
// I don't have time to work out that math today
gate: amp > 1,
doneAction: 2
);
amp.poll(5); // for hand-tuning amp threshold
(SinOsc.ar(modulatedFreq) * 0.1).dup
}.play(args: [
freq: exprand(300, 1600),
lfoFreq: rrand(1.5, 10)
]);
)
hjh
I am studying your example, not sure I understand fully yet, but it seems to me that this approach works because we a dealing with a bipolar signal. My use case, which I should have explained in more detail to begin with, is measuring the rate of pitch bends from a guitar, my audio-to-midi software is only sending pb messages in the range 8192-16383 which I map to a normalized value from the Mididef and send to a synthdef. From here I measure the rate of peaks and determine an avg. rate, so the rate is a unipolar signal which could (or could not) stabilize in a range of roughly 1 - 6 hz. 1 hz being a very slow vibrato and 6 hz being close to the fastest vibrato I can produce on guitar. Would the technique you have shown also work for such a signal, letâs say the signal stabilizes around 3 hz? You could define stable as max value - min value < threshold over a given window size. In this case you couldnât use an amplitude converging to 0 and you wouldnât really know the baseline value either. Maybe comparing amplitude to loudness of signal then?
No, it should work work. It may take some hand-tuning but I think the theory is sound.
EDIT: Btw, in my example, the modulation is bipolar but the signal being analyzed is always positive and nonzero. The analysis doesnât measure proximity to zero â the measurement is related to the amount and speed of movement. It should pick up any shape of movement.
Maybe try it? Youâd have to adjust the parameters by hand but give it a go first.
hjh
Ok will definitely test it when I am back home with my guitar and my setup.
Another demo, this one with sliding pitches.
(
SynthDef(\ping, { |out, freq = 440, decay = 1, amp = 0.1|
var eg = EnvGen.kr(Env.perc(0.01, decay), doneAction: 2);
var mod = SinOsc.ar(freq * 11);
var car = SinOsc.ar(
0,
Phasor.ar(0, freq * SampleDur.ir, 0, 1)
* 2pi + (mod * eg.linexp(0, 1, 1, 3))
);
Out.ar(out, (car * (eg * amp)).dup);
}).add;
)
(
a = { |stableFreqCtr = 600, stableFreqRange = 4, lagTime = 0.2, amp = 0.05|
var mod = Lag.kr(
Duty.kr(
Dwhite(0, 1).linexp(0, 1, 0.4, 0.8),
0,
Dwhite(-1, 1)
),
lagTime
);
var modulatedFreq = (stableFreqRange ** mod) * stableFreqCtr;
var moving = HPF.kr(modulatedFreq, lagTime.reciprocal);
var measuredAmp = Amplitude.kr(moving, 0.01, 0.1);
var trueIfStable = measuredAmp < 25; // hand-adjusted
measuredAmp.poll(5); // for hand-tuning amp threshold
SendReply.kr(trueIfStable, '/stableFreq', modulatedFreq);
(SinOsc.ar(modulatedFreq) * amp).dup
}.play;
OSCdef(\stableFreq, { |msg|
msg.postln;
Synth(\ping, [freq: msg[3]]);
}, '/stableFreq', s.addr);
)
The analysis part is:
var moving = HPF.kr(modulatedFreq, lagTime.reciprocal);
var measuredAmp = Amplitude.kr(moving, 0.01, 0.1);
var trueIfStable = measuredAmp < 25; // hand-adjusted
The HPF and Amplitude are the same as in my first demo â so the principle is sound. I changed the Boolean because the first demo is âkeep the gate open when itâs unstable,â while the second demo is âsend OSC when itâs (more) stable.â
(HPF removes stable energy, no matter how large the magnitude. The only energy thatâs left after HPF is unstable energy. A constant value â completely stable â is DC, 0 Hz. Take HPF.ar(x, 4) as an example: -12 dB/oct response. You should get -3 dB at 4 Hz, -15 at 2 Hz, -27 at 1 Hz, -39 at 0.5 Hz, -51 at 0.25 Hz, etc⌠But this never reaches 0. Therefore the attenuation at 0 Hz is -inf dB! So the HPF completely removes the offset away from 0, and measures only how the signal is moving.)
hjh
Great, these examples are very helpful. I am trying to analyze two aspects of the incoming signal, its frequency and how stable it is (which you already helped me with). How do you best determine the frequency of a (slow moving) control signal? I got decent results so far by latching the peak values of the signal (lag signal - sign of Slope of signal - sign < 0) and calculating the time between peaks but somehow I feel there must be a better/easier solution. I also tried Pitch.kr(K2A.ar(incomingSig)) with proper min and max freqs which sometimes works but is rather slow at picking up new frequencies. Any ideas?
Here is a different approach which works pretty well for my specific use case (pitchbend). Instead of using the method you showed (which I will definitely make use of for some other stuff) I am calculating the variance of the delta-times.
(
{
var t = TDuty.kr(Dseq([0.099, 0.101, 0.11, 0.098, 0.2, 0.048, 0.05, 0.051, 0.2, 0.15, 0.14, 0.16, 0.155], inf), gapFirst: 1);
var sig = Env.perc(0.02, 0.02, 1, [-2, 2]).kr(0, t);
var timer = Sweep.kr(0);
var trig = Slope.kr(sig.lag(0.1)).sign * -1 * SetResetFF.kr(sig > 0.08, sig < 0.03);
var times = 4.collect{|i| if (i == 0) { times = Latch.kr(timer, trig) } { times = LastValue.kr(times) }};
var difs = times.reverse.differentiate[1..];
var mean = difs.mean;
var deviation = difs.collect{|n| (n - mean).squared };
var variance = (deviation.sum / 3).sqrt;
[sig, Trig.kr(trig, 0.05), variance, (variance/mean) < 0.1]
}.plot(1.5, separately: true);
)
As a more general point, âstabilisingâ could also mean settling on a frequency and amplitude, or perhaps even any repeating pattern - not too sure how to measure that reliably, perhaps fft for the first, no clue about the second.