Determining if a control signal has stabilized

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

1 Like

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.