-
Notifications
You must be signed in to change notification settings - Fork 1
Planning: Delay Effect
All the other stages of the synth are essentially memoryless, except for the delay effect, which makes it a bit more difficult to achieve.
We can create a simple delay by just buffering the previous signal values in the time domain and adding a weighted buffer[idx-N] value (where N is the number of samples to delay by) to the current output.
But we want the ability to delay each partial differently, which makes the above buffering strategy not feasible. By putting it at the end of the effect chain, we do get the ability to cast the partial into the time domain at this point - if we need to.
One approach to delaying is to write the amplitude and spacial location of this partial as a weighted sum of the previous amplitude at a specific time(s). Let Ain(t)
be the amplitude at time t of the partial before undergoing a delay effect and Aout(t)
the post-processed amplitude.
Then classical delay works with Aout(t) = Ain(t) + k*Aout(t-t0)
. Or we can achieve something similarly without the recursion: Aout(t) = Ain(t) + k1*Ain(t-t0) + k2*Ain(t-2*t0) + ...
(up to the beginning of the note, or some arbitrary cut-off). Avoiding recursion may be a good idea to make the application more parallelizable.
The difficulty here lies in efficiently keeping track of the previous values of Ain. Especially because each parameter can be altered during a note, we must store the historic states of every parameter if we wish to recompute the Ain at an arbitrary value of t. Furthermore, recomputing N of these might effectively require N times as much processing power.
An alternative is to buffer Aout(t) for each partial and reference this in the future calculations. This gets expensive fast. To delay just 2 seconds between each delay voice, and with 256 partials, we require 256 partials * 2 seconds * 44100 samples/sec * 4 bytes / float = 90 mb of buffers.
We could also do a bit of cheating, rather than making this a 100% accurate delay. For many parameters, changing them during a note play is ambiguous. So we could have the delay effect ignore the past state of parameters, and just compute the output based on the current state of all parameters. The downside is that we can't do cool things like filter sweeps followed by delays if the filter cutoff was modified externally (this would cause the delayed output to also appear filtered). On the other hand, filter sweeps controlled by internal ADSR followed by delays would still work fine. But, this approach still consume excessive cpu by doing many recomputations.
If the delay effect is put at the end of the chain, then we can perform it after casting the wave into the time domain by buffering future time-domain outputs, and adding our current voice into specific future frames. Say we want to create 5 delay voices, each 2 seconds apart. Then we need to buffer 10 seconds ahead * 44100 frames / sec * 2 channels * 4 bytes / float = 3 mb.