synth
synth copied to clipboard
Implement proper ADSR envelope.
http://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope
Might be best to create an adsr_envelope abstraction in a separate crate.
Just thought I'd share the technique I used to handle releases in my ADSRs.
When I evaluate the ADSR at time t(in samples), if Some(tr) <= t (that is time of release <= t_now), then I calculate the curve¹ from level to 0.0 where level is established with a recursive call into the ADSR function with the same arguments, but with tr=None and t=t_release for the release phase. This snapshots the amplitude from t=t_release without using state. This way the release always descends from the correct level.
(Since I'm using floats, I could have used infinity instead of Option, still considering it to avoid the match.)
¹ using the same curves you do, I cribbed off this project to implement them because mine were klunky. ;)
I switched my project to OCaml because DSP is all math and OCaml makes that easy (and super composable.) I may port it back to Rust when I have it working. Anyway, hopefully you can get the gist of it. This code isn't quite finished, but it does the job:
let rec adsr ?(curves=(0.,0.,0.)) at dt sl rt =
let (ac, dc, rc) = curves in
fun tr t ->
let mul = match tr with
| Some tr -> (* release case *)
let self = adsr ~curves:curves at dt sl rt in (* get a copy of this ADSR *)
let delt = t -. tr in (* delt = t_now - t_release *)
if delt < 0. then self None t (* relase is in the future; evaluate up to t_now without release *)
else if delt > rt then 0. (* release is over, output 0.0 *)
else
let level = self None tr in
curve rc level 0. (delt /. rt) (* compute the curve *)
| None ->
if t < at then curve ac 0. 1. (t /. at)
else if t < dt +. at
then
let delt = t -. at in
curve dc 1. sl (delt /. dt)
else sl
in mul
Some plots with curves=(1.0,1.0,1.0) and varying t_release to test the different phases:
Release during sustain:

Release during decay:

Release during attack:

Looking at that last plot I just realized there's a bug. The falloff isn't immediately exponential. (Frustratingly, the previous version I wrote accounted for this and did it right.) Anyway, I updated the code above and here's a new plot:

I hope this helps if you need any help for the design. You might want to consider state instead for performance. I didn't bother because I would have to recurse the first time anyway to create the state and this thing is aready running quite fast even when supersampled at 16x and with a dumb plotting frontend attached to it.
There is a foible. The function blindly returns a value when the release is over so there's no indication that the note is finished. I thought about making it an Option, but instead I plan to track the detla in the voice logic and let it work out the logic. It probably makes more sense to blindly mix the voice output into the stream even after release for the remainder of the chunk and then deallocate it after the chunk is processed instead of checking at each sample.
Peace.
Thanks a lot for this! Will have a proper read through it soon :+1:
Np. And yeah... sorry about the length. When I started I didn't think I'd go on so much.
@smosher Impressive!