BUFFER & GENEXPR: SAMPLE PLAYED IN PARALLEL MULTIPLE TIMES

jo geisler's icon

Dear all,

I am want to read a drum sample via a trigger out of buffer (GEN EXPR CODE BELOW. This functionality would end up in a drum sequencer.

if (Trigger_Received) Read_Sample = 1; // TRIGGER TO READ DRUM SAMPLE COMES IN
else
if (Read_Sample == 1) {
         peek (Sample_Buffer, Sample_Counter);
Sample_Counter = Sample_Counter + 1;
         if (Sample_Counter > Sample_Size) {
Read_Sample = 0;
Sample_Counter = 0;
}
                        
I have the following problem: When I speed up with the triggering and the same sample is triggered before the last one has ended playing It does not work.

I want for every trigger the sample to be played from the beginning to the end. So if I would speed up to 300 BPM the same sample would need to be played many times in parallel from the beginning to the end...

So any thoughts how to solve this?

BR

Jo

Graham Wakefield's icon

This should work. I added a Param playrate to allow playing at different speeds, so I used sample() rather than peek() to get interpolated playback. Would be simple to add a gain factor, or pan, etc. if desired.

Buffer drum;
Param playrate(1, min=0);

History samplepos;
History isplaying;

output = 0;
trigger = in1;
if (trigger) {
  samplepos = 0; 
}
if (isplaying) {
  output += sample(drum, samplepos);
  samplepos += playrate;
  if (samplepos >= dim(drum)) {
    isplaying = 0;
  }
}

out1 = output;

Graham Wakefield's icon

But actually for this you don't need to use a codebox, and even if you want to, the code can be much simpler. A patcher version shows how:

Max Patch
Copy patch and select New From Clipboard in Max.

Pop open the codebox if you really want to use genexpr for this, and it shows that it can be done without needing if() branches. This is good, because if() branches can be more expensive on the CPU than a bunch of arithmetic. So here's the more simplified version (in stereo):

Buffer clap("clap");
Param rate(1, min=0);
trigger = in1;
out1, out2 = sample(clap,
plusequals(rate, trigger), 0, index="samples", channels=2);

jo geisler's icon

Dear Graham,

thanks for providing the information. I put your performance optimized code in my looper.

It is not quiet the functionality that I was looking for. Your code is restarting the loop anytime I trigger it. As described above I am looking for a way that the sample is not restarted but that every trigger starts a new instance of the same sample that plays from the beginning to the end.

A bit background to my project to make things more clear: I have programmed an audio & midi hybrid performance looper with pure gen code (>1000 lines of code) which works fine :-)

I want now to further extend a drum sequencer functionality. The way i am implementing it is that I have one buffer where I record the drum triggers in realtime using my looper sync engine. In another buffer the drum samples are stored.

After the realtime record of the drum triggers (you hear the drums play when you do this...), I quantize the drum trigger buffer and then "read" the trigger buffer. Anytime the code reads a drum trigger "1" it reads the sample buffer and plays the sample.

As I want to speed up the looping I need this functionality.

BR

Jo

jo geisler's icon

In addition: When I speed up my looper using my Master Counter the samples are eaten up so not all triggered samples are played. This is strange to me as I thought that within gen there should be that problem as everything will be calculated in every sample..

(USE A MASTER COUNTER TO SYNC EVERYTHING => Master_Counter      = int(counter(In_Speed,Master_Restart,Master_Length));

Graham Wakefield's icon

Hi Jo, I didn't catch your meaning from the original post. To play a full sample instance for each trigger, including potentially overlapping samples, is not as straightforward. The easier way is to use an overlap-add buffer for the future, and simply dub the entire drum sample into this buffer when a trigger is received. This buffer can be played back continuously and will always contain all the active drums. The gen.ola* patchers in the examples folder both work this way. However if the drum samples are not very short though this can make the CPU usage too spiky. The alternative and trickier way is to write a voice allocator, passing each new trigger to the next available voice player, keeping track of which voices are busy or free, and probably also stealing voices if they are all busy. It can be done but it's quite a bit more complicated.

I can guess why your triggers are eaten up when setting the In_Speed above 1. If you speed it up, then Master_Counter is increasing by more than 1 for each sample of real time, which means reading a trigger buffer with peek(buf, Master_Counter) is going to skip some of the samples in the buffer. E.g. if your In_Speed == 2 then you'll be skipping every other sample in the buffer. In gen~ everything is calculated once per sample of real-time. Trying to read the buffer faster than the samplerate, i.e. reading more than one sample of the buffer for every sample of passing real-time, is doing it more than once per sample of real-time. To do this you'll need to add a for loop or while loop. Does your Master_Counter always increase, or is there any time that it jumps back (looping) or runs backward? If it only ever increases, you can simply make another variable that follows it and picks up all the triggers in between. If it ever goes backward or loops, the logic is more complicated. I haven't time to throw an example of this together right now, but if you search the forums for the ipoke gen~ threads, there's some discussion there that is very relevant.

jo geisler's icon

Dear Graham,

digested your comments ;-)

Concerning Overlap Add: I only found the pitch shift overlap ad and the slicer overlap add. Unfortunately both examples are without GENEXPR code, so I need to figure out myself how to do that. If you have an example in GEN code I would appreciate it :-) Otherwise I would have to guess how to implement as my understanding is that it needs to have a dynamic copy and play mechanism, so that anytime the sample is playing and another trigger to play the sample is coming in the sample needs to be copied into another buffer and played with another peek object. Many I am thinking too complicated:-)

Concerning Eaten Up Sample Triggers:

Before I get into this some more information about the loop player. My performance looper player has a simple architecture. A MasterCounter object is the heart and the master of everything. It drives sample accurate all loops that are recorded (8 Loops currently possible). The first loop is the loop setting the reference length. Every following loop will be a multiple or a division of the initial Loop. (e.g. Master Loop 8 samples - all slave loops could be 1,2,4,8,16,32,64... and so on long). My looper engine is then either cutting the loop after the recording or extending the recording to a according measure. So you can just push record and play, and record and play and record and play,... without thinking about the timing - it will be always in sync according the first loop :-) This works perfect and I am happy to have GEN for this :-)

Below you find the core of the simple architecture with a master counter as well as one of the 8 loop counters and 8 peek players (also declicking using Ramps :-)).

...
Master_Counter     = int(counter(In_Speed,Master_Restart,Master_Length));
playCounter1 = wrap(Master_Counter, Loop_Start1, (Loop_Start1 + Loop_Length1));
Stream_Loop1 = peek (Buffer1, playCounter1) * Ramp1 * Out_Volume1;
...

Now I want to extend that in a way that I would record not only audio samples but also sample triggers. This would mean that my buffer would be full of zeros (0) and occasionally would be a one (1) in the buffer.

You indicated that increasing the Master Counter speed results in skipped samples which I found in my patch happening. Now I know why due to your post :-) You suggested that I should have a look into Ipoke, which I did. IPoke does interpolation so my understanding is that in case one would speed up an audio sample it would e.g. jump from sample position 100 to 105 it would then guess and interpolate the sample values in between.

In my case Ipode is no help as I have a buffer full of triggers with 99,9% of the samples are zeros and the rest are the occasional drum impulse triggers (ones).

As I want to stay fully into GEN the question is how can I ensure that all triggers (represented with the sample value 1 in the buffer) are read with according position also when I speed up the reading speed of a buffer?

You indicated that I would need to use a for loop. It would mean to my understanding the following (pseudocode):

sample_counter_difference = sampleCounterCurrentSample - sampleCounterLastSample;
if (sample_counter_difference > 1) { // samples have been skipped
for (i, sampleCounterLastSample, sampleCounterCurrentSample) {
if (peek(BufferTrigger,i) = 1) { // play samples
// play the sample buffer
}
}

Unfortunately with this solution the samples would be played not at the position they have been recorded but rather close together in the duration of one sample which would make less sense....

Any thoughts how to tackle this, even suggestion to modify my architecture are welcome :-)

Jo

Graham Wakefield's icon

On public transport, will follow up with more comments soon, but regarding Overlap Add: See gen~.ola.granular and gen~.ola.pulsar examples (you might need the latest version of Max, I added them fairly recently).

Graham Wakefield's icon

Yes something like that code sample is what I'm suggesting. I can't understand why this is a problem. If your clock speed has sped up to 2x, then you need 2 samples of recorded trigger buffer to happen in 1 sample of real time.

If you really need the drum playback to be sub-sample accurate, then use @interp="linear" on the peek() and offset the playback sample index by a non-integer amount to line it up. However unless the drum triggers are coming in at what would be audible high pitch rates, you won't be able to hear the difference.

BTW if your master clock is speeding up then your looper playback via peek() will be aliasing as it skips samples -- you probably want to add interp="linear" to those.

Graham Wakefield's icon

BTW2 remember my comment above about whether the master clock only ever increases. Which it looks like it doesn't.

In your pseudocode, think through what happens when your master clock wraps around to zero.

jo geisler's icon

Hi Graham, thanks again for your feedback.

You are right when I speed up I will probably not hear the differences concerning the drums playing at high speed. I was thinking too academic ;-) I will now implement this in my project.

I had a look at the Overlap Add Buffer examples but unfortunately they are a mix of gen objects and GenExpr Code and I am not getting the idea behind. Is it as described in my last post I assume that the first impulse triggers a sample to play. If another on is coming and the sample has not finished playing, the sample is copied to another buffer and a separate peek object needs to play it?

Is this the way it works?

Jo