Accumulators and modulos in loops help

melt's icon

Hi all,

I am slowly trying to work my way through learning some GenExpr in order to build a looper. So far it's been going well, however, I've hit a wall in trying to achieve something I thought would be rather simple.

What I want to do is a "lazy" erase of a buffer, where the erase action start point (in the buffer) is variable, and that the erase loops around the end of the buffer, finishing at the start point (thus clearing the entire thing).

I've been trying to adapt this algorithm from Graham W.:

Data loopdata(768000);
History erase;
if (erase < dim(loopdata)) {
    end = erase + 32;
    while (erase < end) {
        poke(loopdata, 0, erase);
        erase += 1;
    }
}
out1 = erase < dim(loopdata);

But I'm having a hard time creating the conditions to make this work when I attempt to wrap the values around the end of the buffer with a modulo operator.

For example, I can't depend on "erase < end" because at the wrap point, end will be less than erase. Attempting this with different conditions, such as "erase != end-1" thinking it would achieve the same thing as "<" given we're always accumulating in the positive direction, and we always set "end" to a higher value than erase. However, I continue to have issues with the while loop never ending.

I am having better luck using a for loop for this, and defining the size of the erase chunk with the for loop condition. This means I don't need to worry about wrapping "end" and only wrapping "erase"

for (i=0; i<32; i+=1)

I also need to modify the if statement so that it doesn't exit at the end of the buffer. I solved this by using a count within the loop to count the total number of samples in the buffer, regardless of start and end.

So I wind up with something like this:

History count(0);
History acc(0);
History erase_flag(0);

// odub_head is the record head running in a loop, bufop_overdub 
// monitors a button press external to the patch. I'm capturing 
// the location of the record head at the moment I press overdub.

erase_start = sah(odub_head, change(bufop_overdub));
buffer_length = dim(luper_b);

if (change(bufop_overdub) == 1) {
	erase_flag = 1;
	count = 0;
	acc = erase_start;
	}

if (erase_flag == 1 && count < buffer_length) {
	for (i=0; i<32; i+=1) {
		poke(luper_b, 0, acc, 0);
		poke(luper_b, 0, acc, 1);
		count+=1;
  		acc+=1 % buffer_length;
		} 
	}
        else {
		erase_flag = 0;
		acc = 0;
		}

In the above example, "acc" is not wrapping around at "buffer_length". It's like the modulo is not working. The result is a buffer that is only erased between "erase_start" and "buffer_length".

I've tried something like this:

// omitting all the stuff from the above example 
// that is not being changed for this example

History erase_head(0);

if (erase_flag == 1 && count < buffer_length) {
	for (i=0; i<32; i+=1) {
		poke(luper_b, 0, erase_head, 0);
		poke(luper_b, 0, erase_head, 1);
                erase_head = acc % buffer_length;
                acc+=1;
		count+=1;
		} 

Now, "erase_head" will wrap at "buffer_length" but it would appear that it's not actually accumulating properly. The end result (I think) is that poke writes a zero at every 32 sample frames, instead of continuously writing zeroes in 32 sample frame chunks. The result is a buffer that is not properly erased.

I suspect this is due to a misunderstanding in how these loops work, and doing an equals operation as opposed to an accumulate. I also don't fully grasp the idea of a History declaration, which I think is hindering my progress. I am lacking in understand of how the values are held into the next sample frame. I'm also confused with how to get the syntax of the modulo to work with the accumulator.

How can I get "erase_head" to properly accumulate and wrap at "buffer_length"?

Any help/advice is appreciated! Thanks!

melt's icon

Of course, after having written all that out it would seem that my second version of the algorithm does in fact work. But, I have no idea why it wasn't working before, and now is.

Graham Wakefield's icon

About the always erasing in 32-sample chunks, you just need to add the termination test to the if() condition, so that it aborts before doing all 32.

Another approach to the buffer wrapping problem is to simply utilize @boundmode wrap on the pokes. In genexpr, that would be poke(luper_b, 0, erase_head, 0, boundmode="wrap"); It will automatically wrap out-of-bound indices back to the beginning. Then you can simply have erase_head run from erase_start to erase_start + dim(luper_b).

Or, another simplification could be to keep track of "how much is there still left to erase", like this:

History erase_start, erase_todo;

// (re)start erasing?
if (change(bufop_overdbub)) {
  erase_todo = dim(luper_b);  // how many samples do we need to erase?
  erase_start = odub_head;    // where to start erasing?
}

// if there's still some erasing to do:
if (erase_todo > 0) {
  for (i=0; i < 32 && erase_todo > 0; i += 1) { // erase 32 samples per real-time sample
    idx = erase_start + (dim(luper_b) - erase_todo); // erase_start, erase_start+1, erase_start+2, etc. 
    poke(luper_b, 0, idx, 0, boundmode="wrap");
    poke(luper_b, 0, idx, 1, boundmode="wrap");
    erase_todo -= 1;
  }
}

melt's icon

Thanks for much for the reply, Graham. Super helpful and elegant solutions.

When you suggested to use the boundmode attribute on the pokes I had to kick myself for not thinking of that earlier. I would have saved me so much time!

I went to implement it just now and it quickly occurred to me why it isn't ideal. Since posting my examples above, I learned that erasing the entire buffer was causing some issues.

To give more context: these buffers are long (1min) buffers which sometimes end up having only the first 10-15sec of buffer occupied. Since I need to clear the buffer as quickly as possible (instantaneously in an ideal world) and if I have a loop that occupies only the beginning of the buffer, occasionally the wrap around doesn't happen fast enough and the buffer is not cleared properly.

So I had to change the if statement to reflect loop length as opposed to buffer length. This is working great now.

The second idea of yours is very clever. This is what I appreciate so much in programming-- so many ways to skin a cat! I hope to one day be comfortable enough to think of ideas like this.

Cheers!