Audio From Scratch With Go: ADSR

With everything that we have added to our library so far we are almost capable of generated small tunes. One thing that’s missing to make it sound more ‘natural’ is a way for the notes to start and stop.

In this post we will implement a type of envelope called “ADSR”, for “Attack, Decay, Sustain, Release”. Which will make the notes sound more natural as they are played in sequence.

To see why we need this, listen to this sound generated without an ADSR envelope around the generated frames.

If you want to read the (not pretty) code that generated this, check out this github gist.

ADSR

The Attack, Decay, Sustain and Release envelope is a common type of envelope. Schematically this can be represented as below (from wikipedia):

Wikipedia schematic of ADSR envelope

As we apply this envelope to a signal, the signal will change in amplitude depending on which phase we are in of the ADSR envelope. In the image it is visible that the amplitude rises during the attack step, reaches a peak amplitude before decreasing a bit. After decreasing it reached the sustain amplitude, where it will stay until the note is released, and after release it decays until the ampltide is zero.

For our parameters, three will relate to time:

  • attack (time to rise)
  • decay (time to fall to sustain level)
  • release (time to decay from sutan to zero)

So the Sustain parameter does not refer to time, but rather to the amplitude we will maintain.

Turning this schematic into code, we get :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func ADSR(maxamp, duration, attacktime, decaytime, sus, releasetime, controlrate float64, currentframe int) float64 {
	dur := duration * controlrate
	at := attacktime * controlrate
	dt := decaytime * controlrate
	rt := releasetime * controlrate
	cnt := float64(currentframe)

	amp := 0.0
	if cnt < dur {
		if cnt <= at {
			// attack
			amp = cnt * (maxamp / at)
		} else if cnt <= (at + dt) {
			// decay
			amp = ((sus-maxamp)/dt)*(cnt-at) + maxamp
		} else if cnt <= dur-rt {
			// sustain
			amp = sus
		} else if cnt > (dur - rt) {
			// release
			amp = -(sus/rt)*(cnt-(dur-rt)) + sus
		}
	}

	return amp
}

One parameter in this function that you don’t find in the schematic is the need for a control rate. The control rate will be used to turn a duration in seconds into an amount of frames. The control rate could just be sample rate but this does not necessarily have to be the case. One such use-case us sub-audio modulation, whereby the modulating oscillator is running below 20Hz. You can check this chapter for a bit more on that.

Application

To apply the ADSR envelope to a signal, for example to one that was generating using the oscillator we created we have to iterate over each frame, pass the current frame to the ADSR function, and modify the amplitude of the frame with the result. For example, this is the complete example program included in GoAudio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"flag"
	"fmt"

	synth "github.com/DylanMeeus/GoAudio/synthesizer"
	"github.com/DylanMeeus/GoAudio/wave"
)

func main() {
	flag.Parse()
	osc, err := synth.NewOscillator(44100, synth.SINE)
	if err != nil {
		panic(err)
	}

	sr := 44100
	duration := sr * 10

	frames := []wave.Frame{}
	var adsrtime int
	for i := 0; i < duration; i++ {
		value := synth.ADSR(1, 10, 1, 1, 0.7, 5, float64(sr), adsrtime)
		adsrtime++
		frames = append(frames, wave.Frame(value*osc.Tick(440)))
	}

	wfmt := wave.NewWaveFmt(1, 1, sr, 16, nil)
	wave.WriteFrames(frames, wfmt, "output.wav")
	fmt.Println("done writing to output.wav")
}

In this example, notice that our control rate is the same as our sample rate, and the adsrtime increases together with the frames that we have processed. (We could thus pass the i iterating variable to the function, but I thought making it explit was clearer).

Resources


If you liked this and want to know when I write new posts, the best way to keep up to date is by following me on twitter.