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.
Scales
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
end
sleep 2
f = 440
7.times do
play hz_to_midi(f) ; sleep 1
f = f / (3.0/2.0)
end
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 (Time.now).to_i.freeze
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
end
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
end
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
else
dir *= one_in(4) ? 1 : -1
end
if n.abs > scalelength+1 then
dir = -(n/n.abs).to_i
end
if (dir == -1) and (inc == 1) and ((n-1)%(s.length-1)+1==2) then t -= 1 end
n += inc*dir
sleep 0.5
end
play degree_to_note(n,s)
sleep 1
sleep 0.25
d.kill
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
Notation
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)
.
Melody
Programming
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
Structure
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:
LIST_OF_CHORDS = [
(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
end
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:
LIST_OF_CHORDS = [
[: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
end
-
As opposed to fourths and fifths, though you can, and probably should, be using those, too! ↩