Sonic Pi: Envelopes

  • Sonic Pi
  • Raspberry Pi
  • Ruby

This post continues from the last one, which covered the basics of making sound with Sonic Pi. In addition to setting an individual sound's basic volume, panning, and synth, Sonic Pi allows us to fine tune the duration and amplitude of a sound with an ADSR envelope.

All audible sounds start and end in silence and contain some non-silent part in between. Envelopes allow you to control the length and volume of the non-silent parts of the sound. The most common envelope for doing this is the ADSR envelope, which is the envelope that Sonic Pi enables you to use. ADSR is an acronym for the four phases of a sonic envelope: Attack, Decay, Sustain, and Release.

  • Attack: the amount of time it takes your sound to go from 0 (silent) to attack_level; this is the start of the note
  • Decay: the amount of time it takes your sound to go from attack_level to decay_level
  • Sustain: the amount of time it takes your sound to go from decay_level to sustain_level
  • Release: the amount of time it takes your sound to go from sustain_level to 0 (silent); this is the end of the note

Attack Phase

You can think of the attack phase as controlling how the sound of a note starts. You can control the attack phase duration using the attack opt like so:

play 60, attack: 0

Given no other opts, attack is the amount of time it takes your sound to go from 0 (silent) to the amp value (1 by default).

attack's default value is 0, which means it moves from 0 amplitude to 1 immediately with no fade in. This also means that play 60 is the same as play 60, attack: 0.

You can give the sound a longer fade in by increasing the attack opt's value:

play 60, attack: 2

You give the sound a shorter fade in by decreasing the attack opt's value:

play 60, attack: 0.5

You can also pass an optional attack Level with the attack_level opt, which is the amplitude that your sound attacks toward, or the sound's volume at the end of the attack phase. This will typically be between 0 and 1, and represents that portion of the sound's overall amp opt value at the end of the attack phase.

For example, if amp's value is 1 and attack_level's value is 0.5, then the attack phase will fade to 50% of the overall note's volume:

play 60, amp: 1, attack: 2, attack_level: 0.5

You'll notice that after the 2 beat attack phase, the amplitude jumps up to 1. We'll sort out how to deal with that jump later in this post.

attack_level's default value is 1, or 100% of the note's amplitude.

Just to recap, attack is the amount of time it takes your sound to go from 0 (silent) to attack_level, starting the sound.

Decay Phase

The decay phase is the second portion of the sound, which comes after the attack phase. You control the duration of this phase using the decay opt.

play 60, decay: 0

This sets the amount of time between the attack phase and either the sustain phase or release phase (depending on whether you include sustain; more on that later).

decay's default value is 0, which means it moves from the attack phase to either the sustain phase or the release phase immediately. This also means that play 60 is the same as play 60, decay: 0.

You can give the sound a longer decay phase by increasing the decay opt's value:

play 60, decay: 2

You can give the sound a shorter decay phase by decreasing the decay opt's value:

play 60, decay: 0.5

You can also pass an optional decay Level with the decay_level opt, which is the amplitude that your sound decays toward, or the sound's volume at the end of the decay phase. This will typically be between 0 and 1, and represents that portion of the sound's overall amp opt value at the end of the decay phase.

For example, if amp's value is 1 and decay_level's value is 0.5, then the decay phase will fade to 50% of the overall note's volume:

play 60, decay: 1, decay_level: 0.5

Now the sound is a little bit strange; it almost sounds like the same note playing twice, even though we're only playing a single note. That's fine for now, the next phase will help us fix that.

decay_level's default value is the value of sustain_level. Again, we haven't gotten there yet, but if sustain_level's value is 0.8, then decay_level's default value is 0.8. sustain_level's default value is 1, or 100% of the note's amplitude. If sustain_level and decay_level are left to their default values, then they both default to 1, or 100% of the note's amplitude.

Before moving on, it's also important to note that all of the phase opts we're discussing here can be combined to offer you complete control of the envelope. You can put attack, attack_level, decay, and decay_level together to control how the note starts:

play 60, amp: 1, attack: 1.2, attack_level: 0.8, decay: 0.8, decay_level: 0.5

To recap this section, decay is the amount of time it takes your sound to go from attack_level to decay_level. decay_level is probably the most optional of the envelope opts covered in this post, but is available to give a full range of control over your note.

Sustain Phase

The sustain phase is the third portion of the sound, which comes after the decay phase. You control the duration of this phase using the sustain opt.

play 60, sustain: 0

This sets the amount of time between the decay phase and the release phase. Given no other opts, this is the time for which the sound is maintained at full amplitude between the attack and release phases.

sustain's default value is 0, which means it moves from the decay phase to the release phase immediately. This also means that play 60 is the same as play 60, sustain: 0.

You can give the sound a longer sustain phase by increasing the sustain opt's value:

play 60, sustain: 2

You can give the sound a shorter sustain phase by decreasing the sustain opt's value:

play 60, sustain: 0.5

You can also pass an optional sustain Level with the sustain_level opt, which is the amplitude that your sound sustains toward, or the sound's volume at the end of the sustain phase. This will typically be between 0 and 1, and represents that portion of the sound's overall amp opt value at the end of the sustain phase.

For example, if amp's value is 1 and sustain_level's value is 0.5, then the sustain phase will fade to 50% of the overall note's volume:

play 60, sustain: 1, sustain_level: 0.5

You'll also notice that, unlike when we adjusted the attack_level and decay_level there's no weird jump or repeating of the note when we adjust the sustain_level; that's because the note's amplitude always fades to the sustain_level's value before fading to 0 in the release phase.

To fix our issues from earlier, we can combine the sustain phase opts with the attack phase opts:

## compare this:play 60, amp: 1, attack: 2, attack_level: 0.5sleep 5
## with this:play 60, amp: 1, attack: 2, attack_level: 0.5, sustain: 1, sustain_level: 0.8

The second note here is a great deal smoother than the first note, but we can still do better. Let's add in the decay phase to really fix it:

## compare this:play 60, amp: 1, attack: 2, attack_level: 0.5sleep 5
## with this:play 60, amp: 1, attack: 2, attack_level: 0.5, sustain: 1, sustain_level: 0.8sleep 5
## and now this:play 60, amp: 1, attack: 2, attack_level: 0.5, decay: 0.8, decay_level: 0.8, sustain: 1, sustain_level: 0.8

Much smoother! But also longer. This leads us to an important note: that as you add on attack, decay, and sustain duration opts, those add to the length of the note.

A default note represents a single beat. At 60 bpm, it plays for one second; or more accurately, releases for 1 beat...more on that in the Release Phase section.

play 60

If we add attack: 1, it attacks for 1 beat; decay: 1 decays for 1 beat; sustain: 1 sustains for 1 beat:

play 60, attack: 1, decay: 1, sustain: 1

To recap this section, sustain is the amount of time it takes your sound to go from decay_level to sustain_level.

Release Phase

The last phase in the ADSR envelope is the release phase, which controls how a note ends. All synths have a default release value of 1, which means that by default they have a duration of 1 beat.

play 60, release: 1

play 60 is therefore the same as play 60, release: 1.

You give the sound a longer duration by increasing the release opt's value:

play 60, release: 2

Or you can give the sound a shorter duration by decreasing the release opt's value:

play 60, release: 0.5

Since release ends the sound at 0 amplitude (silence), there is no release_level opt available.

You can combine release with attack in various ways:

play 64, attack: 0.7, release: 3sleep 2.5
play 58, attack: 3, release: 0.7sleep 5.2
play 65, attack: 0.5, release: 0.5

You can also set both attack and release to 0 and just use sustain to have absolutely no fade in or fade out to the sound:

play 60, attack: 0, sustain: 3, release: 0

However, release: 0 can produce clicks in the audio and it's often better to use a very small value, such as 0.2:

play 60, attack: 0, sustain: 3, release: 0.2

To recap this section, release is the amount of time it takes your sound to go from sustain_level to 0 (silent), ending the sound.

All Defaults

Here is a scale that starts with no opts at all, but gradually adds each opt (at its default value) that we've covered both in this post and in Sonic Pi: Basics:

play 60sleep 2
play 62, amp: 1sleep 2
play 64, amp: 1, pan: 0, attack: 0sleep 2
play 65, amp: 1, pan: 0, attack: 0, attack_level: 1sleep 2
play 67, amp: 1, pan: 0, attack: 0, attack_level: 1, decay: 0sleep 2
play 69, amp: 1, pan: 0, attack: 0, attack_level: 1, decay: 0, decay_level: 1sleep 2
play 71, amp: 1, pan: 0, attack: 0, attack_level: 1, decay: 0, decay_level: 1, sustain: 0sleep 2
play 72, amp: 1, pan: 0, attack: 0, attack_level: 1, decay: 0, decay_level: 1, sustain: 0, sustain_level: 1sleep 2
play 74, amp: 1, pan: 0, attack: 0, attack_level: 1, decay: 0, decay_level: 1, sustain: 0, sustain_level: 1, release: 1

By default:

  • attack is 0
  • attack_level is 1
  • decay is 0
  • decay_level is the same as sustain_level
  • sustain is 0
  • sustain_level is 1
  • release is 1

Put It All Together

A scale with nothing but the default values isn't any fun though. To do something more interesting, I'm going to play with the first part of the melody from Radiohead's song, Idioteque.

As an aside, this song has a pretty interesting history that's very intertwined with computer music synthesis and composition. Check out the Recording section of the song's Wikipedia article if you're curious.

Anyway, let's start with the basic notes:

play 58sleep 1.2
play 62sleep 1.2
play 70sleep 1.2
play 67

To give it a little bit of character, I'll add in some amplitude so that the volume builds toward the third note and then decrescendos out:

play 58, amp: 0.62sleep 1.2
play 62, amp: 0.76sleep 1.2
play 70, amp: 1sleep 1.2
play 67, amp: 0.69

Now I want to adjust the attack for each note so they can fade in. I like the idea of increasing the attack duration parallel to the amplitude, but I'll actually drop the attack to its lowest point on the third note to throw a bit of a change in:

play 58, amp: 0.62, attack: 0.6sleep 1.2
play 62, amp: 0.76, attack: 0.8sleep 1.2
play 70, amp: 1, attack: 0.433sleep 1.2
play 67, amp: 0.69, attack: 0.6

Now I'll play with the attack_level. I'll just have that decrescendo all the way through the phrase:

play 58, amp: 0.62, attack: 0.6, attack_level: 1sleep 1.2
play 62, amp: 0.76, attack: 0.8, attack_level: 0.9sleep 1.2
play 70, amp: 1, attack: 0.433, attack_level: 0.8sleep 1.2
play 67, amp: 0.69, attack: 0.6, attack_level: 0.7

I don't want to mess too much with the decay phase, so I'll set that pretty much the same for all four notes, but I'll make the decay duration a little bit shorter on the fourth note:

play 58, amp: 0.62, attack: 0.6, attack_level: 1, decay: 0.8, decay_level: 0.75sleep 1.2
play 62, amp: 0.76, attack: 0.8, attack_level: 0.9, decay: 0.8, decay_level: 0.75sleep 1.2
play 70, amp: 1, attack: 0.433, attack_level: 0.8, decay: 0.8, decay_level: 0.75sleep 1.2
play 67, amp: 0.69, attack: 0.6, attack_level: 0.7, decay: 0.6, decay_level: 0.75

Now I'll do the sustain phase, and for the most part this will mirror each note's amplitude:

play 58, amp: 0.62, attack: 0.6, attack_level: 1, decay: 0.8, decay_level: 0.75, sustain: 0.56, sustain_level: 0.8sleep 1.2
play 62, amp: 0.76, attack: 0.8, attack_level: 0.9, decay: 0.8, decay_level: 0.75, sustain: 0.43, sustain_level: 0.9sleep 1.2
play 70, amp: 1, attack: 0.433, attack_level: 0.8, decay: 0.8, decay_level: 0.75, sustain: 0.67, sustain_level: 1sleep 1.2
play 67, amp: 0.69, attack: 0.6, attack_level: 0.7, decay: 0.6, decay_level: 0.75, sustain: 0.4, sustain_level: 0.7

Finally, I'll set the duration for each note to fade out with decay. Here's my final result!

play 58, amp: 0.62, attack: 0.6, attack_level: 1, decay: 0.8, decay_level: 0.75, sustain: 0.56, sustain_level: 0.8, release: 1.2sleep 1.2
play 62, amp: 0.76, attack: 0.8, attack_level: 0.9, decay: 0.8, decay_level: 0.75, sustain: 0.43, sustain_level: 0.9, release: 0.8sleep 1.2
play 70, amp: 1, attack: 0.433, attack_level: 0.8, decay: 0.8, decay_level: 0.75, sustain: 0.67, sustain_level: 1, release: 0.6sleep 1.2
play 67, amp: 0.69, attack: 0.6, attack_level: 0.7, decay: 0.6, decay_level: 0.75, sustain: 0.4, sustain_level: 0.7, release: 1.2

I hope you enjoyed this post; I'm certainly having a lot of fun toying around in Sonic Pi and plan to keep doing so for a little while. Obviously there's way, way more going on with the original song's melody, but this is a fun starting place. It would be great to adjust the synth or panning, or add some chords and bass notes in. If you're so inspired, I urge you to do so. Otherwise, I'll see you in the next post.