First time here? Check out the FAQ!
x

How do you get a running average for an EventVariable?

0 votes
692 views
I'm trying to get BPM detection working and I want the BPM value to update as each new reading comes in but I want it averaged over the last four readings. So I want a running average of the last four readings. What's the best way of doing that?

Do I need to create an array with a writeIndex, adding values into the array and then manually iterating over the array to calculate the average? If that is the way to do it how do you iterate over an array in CapyTalk?

I've tried using an AveragingLowPass Filter (as suggested in Kyma X explained) by converting my EventVariable into audio using the CapyTalkToSound Sound, but that's not working. I set the cuttoff frequency to values between 0.1 hz and 0.001hz but the average seems to be operating over a period of much less than a second, so it's effectively just tracking my EventVariable which updates around 1hz. Does the AveragingLowPassFilter have a minimum cutoff frequency?
asked Oct 8, 2017 in Capytalk & Smalltalk by alan-jackson (Virtuoso) (15,840 points)
edited Oct 8, 2017 by alan-jackson

2 Answers

0 votes
 
Best answer

Hey Alan,

Nice to see that someone is into EventVariables as well ;)

Here is my solution, you should adapt input, trigger and length to your needs.I'm using that snippet in a SoundToGlobalController:

| input trigger length valArray idx avg |

 

input := !Value.

trigger := input hasChangedInLast: 0.1 s.

length := 4.

 

valArray := EventVariable new size: length.

idx := EventVariable new initialValue: 0.

avg := EventVariable new initialValue: 0.

 

trigger evaluate: (

(avg <+ (avg - (valArray @< idx) + input)),

((valArray @< idx) <+ input),

(idx <+ ((idx + 1) mod: length))

),

avg / length

 

Hope it helps :)

answered Oct 9, 2017 by kymaguy (Virtuoso) (10,580 points)
selected Oct 17, 2017 by alan-jackson
Thanks! Oops. I meant EventValue (ie !HotValue).
oh, so I'm alone again... ;)
Thanks for the code! I cut-and-pasted that in and it worked perfectly. I didn't really understand it but thanks for explaining <+ and <@ at KISS. I'll read your code more carefully now and who knows...  maybe you won't be alone!
Maybe a cpp translation makes it easier for you:

if (trigger) {
avg = avg - valArray[i] + input;
valArray[i] = input;
i = (i+1)%length;
}
output = avg / length;

Basically I'm subtracting the last value from the sum and add the new one, then I overwrite the last value with the new one and increase the index. finally I divide the sum (here called avg, should have called it sum actually to avoid confusion) by the length :)
0 votes

I'm trying to get BPM detection working and I want the BPM value to update as each new reading comes in but I want it averaged over the last four readings. 

Assuming that you have an extracted trigger, you could try:

(<your detected trigger> durationBetweenTriggers: 4) s bpm removeUnits

Or to answer the general question of a moving average of length 4:

| length updateTrig beatDur |

length := 4.
updateTrig :=
<your detected trigger>.

beatDur := !LocalTime - (updateTrig shiftRegister: !LocalTime length: 2).

((beatDur - (updateTrig shiftRegister: beatDur length: length + 1)) accumulateIf: updateTrig initialValue: 0 reset: 0) / length

Or, if you just want a running average of length 4 of some value:

| length updateTrig |

length := 4.
updateTrig :=
<triggers at the rate you want to update the average>.

((!Value - (updateTrig shiftRegister: !Value length: length + 1)) accumulateIf: updateTrig initialValue: 0 reset: 0) / length

Or could you use the trigger that you are extracting directly to trigger other Sounds (rather than converting it to !BPM?

answered Oct 9, 2017 by ssc (Savant) (128,160 points)
edited Oct 20, 2017 by ssc
using shiftRegister: and accumulateIf: is more elegant of course ;)
I couldn't get this to work. It just kept returning the value 0.25. Or rather to be more precise it gave the value 0.125 once, I think, and then settled on 0.25.

I tried the expression:

    updateTrig shiftRegister: !Value length: 4

And that seemed to do what I expected. It returned !Value four "samples" later. When I added the "accumulateIf" part it stopped doing what I expected and just returned 0.25.

I'd be interested to know why it didn't work but it's not a big issue as I've got a working solution using Gustav's code above.
Thanks for letting us know that it did not work for you! We have found and fixed a small problem in accumulateIf:. The fixed version will be available in a couple of days.

We've revised the answer above to use accumulateIf:initialValue:reset: instead of accumulateIf: (otherwise the average would be biased by 0.5).
...