I’ve recently been toying with Sonic-Pi - a code based music creation tool created by Sam Aaron. It’s written in Ruby on top of SuperCollider and is designed to handle timing, threading and synchronization specifically for music creation.

An older music creation library Overtone, authored by Sam Aaron as well, included the famous THX Surround Test sound simulation example: Code. This inspired to try replicate a different interesting sound - the Shepard tone - using Sonic-Pi.

Shepard tone

Shepard tone has an interesting property, in that it feels like it is always increasing in pitch. The effect can be achieved by playing multiple tones with an increasing pitch at the same time, one octave (or a double in frequency) apart. Each tone starts at a low frequency and fades-in in volume, gradually increasing in pitch, and fades-out upon reaching a high pitch as another tone takes its place.

Shepard tone spectrum The spectrum diagram by Grin (CC BY-SA) illustrates the construction well. Note that the red lines are going up over time in pitch, the thickness of a line - volume - increases as it fades in at a low pitch, and decreases as it fades out at a high pitch.

Constructing the tone

Sonic-Pi has a neat sliding mechanism, allowing us to start from one pitch and slide it up to another pitch:

synth = play note(:C3), note_slide: 1
control synth, note: note(:C4)
sleep 1

Here it starts by playing a C3 note using a sine wave synthesizer, and slides it up by one octave to a C4 note over one beat (or 1 second at a 60bpm default tempo): Rising pitch example spectrogram

Extending this example would allow us to build a tone with an increasing pitch, but it plays always at max volume.

The second building block is volume control. The tone needs to fade in at the beginning, reach a maximum volume, and fade out at some high pitch. Sonic-Pi provides an ADSR envelope generator we can use for this purpose.

ADSR stands for Attack, Decay, Sustain, Release and describes how sound changes over time, or in this case - its volume.

ADSR Envelope diagram

Abdull’s diagram on Wikipedia illustrates the use well. Imagine pressing a piano key. If you press it quickly and forcefully, it reaches the full volume quickly (the Attack stage), then decays to some sustainable volume (the Decay stage) that is sustained for a while (the Sustain stage), after you release the key - the sound quickly diminishes to zero.

This mechanism can be conveniently used to build the fade-in and fade-out of our sine wave:

synth = play note(:C3), note_slide: 5, attack: 1, decay: 0, sustain: 3, release: 1
control synth, note: note(:C4)
sleep 5

I’ve increased the duration of the wave to 5 beats to illustrate the example. But following the ADSR envelope diagram above, the sound will go from 0 to 100% volume in 1 beat. Decay is 0, so we can ignore it. It will stay at maximum volume for 3 beats, and then go from 100% to 0% volume in 1 beat. All the while the pitch of the sound is increasing from C3 to C4. Fade-in and out amplitude over time

The last building block is to play multiple tones at the same time. Sonic-Pi handles threading for us very well, and in Sonic-Pi commands are executed in parallel. E.g. if we try to play two tones at once, the code would look as follows:

play note(:C3)
play note(:C4)
sleep 1

Both notes C3 and C4 would be played at the exact same time for 1 second.

Multiple simultaneous notes - spectrogram

In a similar vain, the idea would be to sequentially start multiple waves we created above in independent threads, sleeping for a bit to start another wave just as others have reached octaves. This can be done roughly as follows:

total_waves = 10
total_waves.times do
  in_thread do
    synth = play note(:C3), note_slide: 10, attack: 1, decay: 0, sustain: 8, release: 1
    control synth, note: note(:C5)
    sleep 10
  end
  sleep 5
end

I’ve further increased the duration of a single sine wave to 10 seconds, and the pitch to rise from C3 to C5, or two octaves. The code above will play 10 such sine waves, start of each separated by 5 beats. That way:

  • At second 0 the first sine wave will start playing at C3, fading-in in volume
  • At second 5, the first sine wave would have reached C4 note at full volume, then a second wave would start playing a C3 note also fading-in in volume;
  • At second 10, the first wave would have reached C5 and would’ve finished fading out. The second wave would have reaced C4 note, and a third wave would’ve started playing a C3 note, and so on.

Multiple rising waves spectrogram

All 3 construction components put together, cleaned up and synchronized using the cue/sync resulted in the following final snippet:

starting_note = note(:C2)
total_octaves = 4
octave_duration = 10
total_waves = 20
wave_duration = octave_duration * total_octaves
fade_in_out_duration = wave_duration * 0.2

target_notes = octs(starting_note, total_octaves + 1).drop(1)

in_thread do
  total_waves.times do
    sync :new_octave
    in_thread do
      with_synth :sine do
        instrument = play starting_note,
          note_slide: octave_duration,
          attack: fade_in_out_duration, release: fade_in_out_duration,
          decay: 0, sustain: (wave_duration - 2 * fade_in_out_duration)

        total_octaves.times { |octave|
          cue :new_octave
          control instrument, note: target_notes[octave]
          sleep octave_duration
        }
      end
    end
    sleep octave_duration
  end
end

cue :new_octave

And the end result would sound like this:

Shepard tone spectrogram

Overall I’m rather impressed by Sonic-Pi’s ability to handle threading, synchronization and sliding. It resulted in a fairly short piece of code, and could likely be made smaller still by replacing the octave-target loop with a single control call to linearly slide the starting note to the highest note the sine wave is supposed to reach.