Using Sonic Pi snippets to explore the theory of music

Notes and tones

Some notes have a pitch

use_synth :pretty_bell
play 60 # approximately 261.63 Hz

but others do not:

sample :drum_bass_hard

The same pitch may be sounded by different instruments:

use_synth :piano
play 69
sleep 1
use_synth :pluck
play 69
sleep 1

Octaves and intervals

A frequency ratio of 2:1 is called an octave. Notes an octave apart are usually considered to be the "same" and go together well:

use_synth :tri
play :c3
sleep 1
play :c4
sleep 1
However, this is not universal, especially when we consider inharmonic sounds and even common instruments like the piano.

Consider two pure tones played together. Some intervals sound psychoacoustically consonant, while others sound dissonant:

use_synth :sine
play hz_to_midi(500), sustain: 0.95, release: 0.05
play hz_to_midi(521), sustain: 0.95, release: 0.05
sleep 2
play hz_to_midi(500), sustain: 0.95, release: 0.05
play hz_to_midi(500.0*1.06), sustain: 0.95, release: 0.05
sleep 2
play hz_to_midi(500), sustain: 0.95, release: 0.05
play hz_to_midi(500*3/2), sustain: 0.95, release: 0.05
sleep 2
play hz_to_midi(500), sustain: 0.95, release: 0.05
play hz_to_midi(500*5/4), sustain: 0.95, release: 0.05

More generally, a tone is made up of several frequencies, and when two notes are played together the amount of dissonance perceived depencs on the spectrum of the sound and the size of the interval. Typically, the most consonant intervals will be the unison (1/1), octave (2/1), fifth (3/2), fourth (4/3), major third (5/4), major sixth (5/3), etc.: ratios of small numbers. This accounts for the use of these intervals in music where consonance is desired.


One way to construct a scale, is to take the octave as a basic interval, divide it according to some system, and take some or all of those notes as a scale.

(Other classical ways: e.g. Greek, Arabic, Indian, Persian, etc…. start with a fourth!)

For example, one type of scale is constructed from octaves and fourths/fifths: starting from a note, further notes can be obtained by going up and down by perfect fifths, considering notes an octave apart as equivalent.

use_synth :tri
use_synth_defaults sustain: 0.9, release: 0.1
f = 440
7.times do
  play hz_to_midi(f) ; sleep 1
  f = f * 3.0/2
sleep 2
f = 440
7.times do
  play hz_to_midi(f) ; sleep 1
  f = f / (3.0/2.0)
Note that this process produces an infinite number of notes if continued indefinitely. In order to tune a physical instrument, we probably need to adjust this procedure and pick a finite set of notes.

Another simple way to get a scale is to divide the octave into equal intervals and use a subset of those as the basis for our scale. Using 12 pieces may be the most familiar to many readers, but depending on the tradition musicians also use 24, 53, 72, or other numbers (not to mention various unequal divisions of the octave).

# 12-step chromatic scale
use_synth :pretty_bell
r = (60..60+12).to_a
r.each  { |n| play n ; sleep 0.5 }
sleep 0.5
r.reverse.each  { |n| play n ; sleep 0.5 }

For our purposes, the primary scales will be the 7-note major or ionian scale [and different "modes" of this scale obtained by shifting the start to another note in the scale]

use_synth :tri
use_synth_defaults sustain: 0.95, release: 0.05
s = scale :c4, :ionian
s.mirror.each { |n| play n ; sleep 1 }

A random ionian "melody", to hear how it sounds:

use_random_seed (
use_synth :fm
a = 0.95
d = play :c4, sustain: 123456, amp: 0.65
s = (scale :c5, :ionian)
scalelength = s.length - 1
use_synth_defaults sustain: a, release: 1-a
def degree_to_note(deg, s)
  scalelength = s.length - 1
  d = (deg - 1) % scalelength
  oct = ((deg-1) / scalelength).floor
  return s[d]+oct*12
n = 1
increments = [1,1,1,1,1,1,1,1,2,2,2,2,3,3,4,5,7]
if scalelength < 7 then
  increments = [1,1,1,1,1,1,2,2,2,2,3,3] # fixme
dir = 1
t = 2
while (t > 0)
  play degree_to_note(n,s), sustain: a*0.5, release: (1-a)*0.5
  inc = increments.choose
  if inc > 2
    dir *= -1
    dir *= one_in(4) ? 1 : -1
  if n.abs > scalelength+1 then
    dir = -(n/n.abs).to_i
  if (dir == -1) and (inc == 1) and ((n-1)%(s.length-1)+1==2) then t -= 1 end
  n += inc*dir
  sleep 0.5
play degree_to_note(n,s)
sleep 1
sleep 0.25
Try changing :ionian to :dorian, :lydian, etc., and notice the difference!

Sonic Pi includes many built-in scales:

puts scale_names

Minor scales

There is a "minor" complication compared to the major case in that in addition to the "natural" (aeolian) minor scale there are a couple of variants:

The "melodic minor" scale

puts scale :a4, :melodic_minor_asc
puts scale :a4, :melodic_minor_desc
use_synth :chiplead
play_pattern_timed (scale :a4, :melodic_minor_asc), 1, sustain: 0.95, release: 0.05
play_pattern_timed (scale :a4, :melodic_minor_desc).reverse, 1, sustain: 0.95, release: 0.05
(note that this scale is not the same going up as doing down!!)

and the "harmonic minor" scale

use_synth :dsaw
puts (scale :d3, :harmonic_minor).inspect
play_pattern_timed (scale :d3, :harmonic_minor).mirror, 1, sustain: 0.95, release: 0.05
sleep 2
play [:e3, :bb3, :d4, :g4], sustain: 4*0.95, release:4*0.05
sleep 4
play [:a2, :g3, :cs4, :f4], sustain: 4*0.95, release:4*0.05
sleep 4
play [:d3, :f3, :b3, :e4], sustain: 4*0.95, release:4*0.05
sleep 4


For many instruments the tuning is assumed to be known and the notes are indicated on a stave or staff:

You do not need to know how to read this kind of musical notation to use Sonic Pi. However, the note names (A, B, C, etc.) are useful, because that way you need not work directly with MIDI note numbers or frequencies.

The note function converts note names to MIDI numbers; for example

puts note(:g3)
prints out 55, because that is note G in octave number 3. With most of the built-in instruments, this conversion happens automatically, so play :g3 is equivalent to play note(:g3).



A practical interlude: how best to encode and perform melodies in Sonic Pi?

One simple solution is to store the note number and duration for each note in an array.

There are also various helpful macro packages you can use. This one allows you to play a melody using a string, like so:

require "~/ziffers/ziffers.rb"
use_synth :piano
use_bpm 120
zplay "w 3 2 3 1 _#7 ^2 3 1 3 (2 1) _#7 #7 ^1 r 1 _(7 6) 5 5 ^2 1 3 5 4 (3 2) _#6 #7 ^1", key: :cs4, scale: :aeolian, degrees: true


The melody should have structure spanning several time scales, from short to long, including cells, motifs, phrases, etc.

Chords and classical harmony

Chords are when you play more than one note at the same time. In many types of popular music, chords are regarded as having to do with accompaniment as opposed to part of the main melody.

A modern "lead sheet" looks something like this:

It indicates the melody, lyrics, and harmony, implying that these make up the essence of the song. The symbols above the staff are chord symbols.

If we play a single note, considering only nominal pitch and 12 pitch classes per octave, there are 12 possibilities, for example, "G flat" and "A". If we consider only relative pitch, where all reference notes are equivalent, then it does not matter what note we start with; only intervals (described above) are taken into account. Recall that, according to the basic theory, the most consonant intervals are the unison and the octave, then the perfect fifth and the major third, and finally minor thirds and major and minor sixths. In the other direction, seconds, sevenths, ninths, etc., and diminished and augmented intervals are considered dissonances.

With three notes, there are already nearly twenty ways to start putting them together. For historical reasons, much or most Euroclassical, pop, and jazz harmony uses three-note chords constructed from thirds1 as fundamental building blocks. These serve as a vocabulary or palette you can use to colour your music, and a lifetime of reinforcement has trained your listeners to hear them functioning in particular ways.

To quickly hear some chords in Sonic Pi there is a chord function:

  (chord :c4, :major),
  (chord :fs3, :dim7),
  (chord :g3, :minor),
  (chord :a2, '7', invert: 3),
  (chord :f3, '6'),
  (chord :b2, :dim7),
  (chord :f2, :major, invert: 2),
  (chord :c3, '7', num_octaves: 2),
  (chord :f3, :major, num_octaves: 2)

LIST_OF_CHORDS.each do |c|
  play c
  sleep 1
However, it does not automatically compute the correct voicing, so there is no way, at least with the chord function, around manually spelling each set of notes exactly the way you want:
  [:c4, :g4, :e5],
  [:fs3, :c4, :a4, :eb5],
  [:g3, :bb3, :bb4, :d4],
  [:g3, :e4, :a4, :cs5],
  [:f3, :d4, :a4, :d5],
  [:b2, :d4, :ab4, :f5],
  [:c3, :f4, :a4, :c5],
  [:c3, :e4, :bb4, :c5],
  [:f3, :f4, :a4, :c5]

LIST_OF_CHORDS.each do |c|
  play c
  sleep 1
  1. As opposed to fourths and fifths, though you can, and probably should, be using those, too!