This has the advantage that one gets fine controls over small loop
lengths while getting more rough control over larger lengths, which is
usually what you want if you have large control ranges; from minimum
loop length to buffer size; like in this case.
The curve used is x⁴ and was calculated with a macro that inserts a
temporary variable in order to reduce the number of multiplications to
two instead of three in the naive case:
- naive: y = x*x * x*x = x⁴
- this: y = y'*y' where y' = x*x
First the loop length was wrong, set it to the minimal loop length that is eight samples.
Second, the read pointer was not set in the test. In the actual plugin
the read loop pointer is set to the read pointer when the record buffer
is copied into the loop buffer on effect activation.
This checks if the loop length is mapped into the
[minimal-loop-length:buffer-size] range and how setting the loop-length
effects the loop end position that is calculated on a length change,
e.g. for wrap-around cases where the loop start position, which is the
read pos (pointer), is less elements away from the end of the buffer
than the length of the loop. Consider this example:
[0,1,2,…,10,11,12,13,14,15]
^ ^r_pos
|loop_end
- buffer b has length 16
- read position is 10
- loop length is set to 8
→ loop end should be set to 2 now (the end is 'exclusive')
This fixes also an inaccuracy that loop lengths less than two would be
reset to 8 samples but lengths larger two were accepted as is, i.e. loop
lengths in the range 3..8 were still possible.
The ring buffer tries to avoid expensive modulo operations when wrapping
around by only allowing sizes that are powers of two and therefore the
next position of the reading pointer can be calculated by incrementing
it and then masking by the buffer's size - 1:
Let's assume that the size of the buffer is 8 and that the current read
position is 7, the next read index would be calculated as follows:
0111 + 0001 → 1000 // new read index
1000 - 0001 → 0111 // size mask
1000 & 0111 → 0000 // masked read index
One additional thing that changed is that there are now two buffers,
i.e. there is a buffer that is always filled with the input signal,
wether or not the effect is active and there is the loop buffer that is
filled with the contents of the "playback" buffer. The copying should be
pretty fast because there are no new allocations and the buffer contents
are copied via `copy_from_slice` which is a `memcpy`.
The advantage of having two buffers is that the playback buffer gets
updated while the effect is running, so switching on and off quickly
will play the latest signal and not what was read before the loop was
activated.
TODO: This badly needs some unit tests.
The VST effect already does what it is supposed to be doing, i.e.
looping small chunks of the input audio when it is active.
There are some downsides of the current implementation, e.g. that the
buffer is not filled when the effect is in active mode because there is
only one buffer and it is be read from in looping mode. This means that
the buffer does not get updated as long as the looping is active and
thus switching it off and on quickly would output old audio data.
Another approach would be to have two ring-buffers, the first is filled
with the input signal unconditionally whereas the second buffer is a copy of
the first one at the moment of switching on the effect. This would also
simplify the implementation because the second buffer is always read
from the first index.
The copy operation has to be very fast but this should be no problem as
long as `copy_from_slice` is used.
Some nice additions would be:
- set sample length in beat measures, milliseconds and samples
- set playback speed (requires interpolation (linear?))
- allow to reverse audio
PS: Cross compiling for windows by building against the x86_64-pc-windows-gnu
target showed that it runs on Windows as well.