This is going to be a rather long post and may be interesting for music theory enthusiasts.
The program implements a random walk through all diatonic chords in all major keys, i.e, 12 x 7 = 84 chords. This is done with the help of a markovian matrix that defines the transition probabilities. However, the transitions are guided by rules in order to achieve a ‘good sounding’ result.
First, the transitions from key to key prefer neighboring keys where the major scales differ by only a few notes. The keys are organized in a ring containing the differences to a given midi base note:
keys = ring(0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5)
If we assume 0 = C
, then this sequence would be C-G-D-A-E-H-Gb-Db-Ab-Eb-B-F
, which is a sequence of increasing fifth, or, falling fourth in the reverse direction.
Second, the transitions from chord degree to chord degree are organized in this ring:
ring(:i, :iv, :vii, :iii, :vi, :ii, :v)
Again, this sequence provides for falling fourth when ticking along. Neighboring chords share as many notes as possible.
Third, the probabilities in the markovian matrix restrict transitions from one key to another in such a way, that only specific chord transitions are used to change the key. For example, the line
markov[:i][[-2, :ii]] = prop_key_change[-2]
allows for changing the key by a -2
step in ring keys
only if a :i
chord is played in the previous key, and the transition results in a :ii
chord in the new key. Example transition: Cmaj7 in key C → Cminor7 in key Bb. This mechanisms establishes ‘doors’ that the random walk will use when changing the key. And the used chords will be related to each other because they share some notes. Therefore, the key transition will ‘sound good’.
All probabilities can be set with parameters. The probabilities for stepping forward in ring degrees
are set by map prop_deg_change
, and the propabilities to change keys by stepping back and forth within ring keys
are set by the map prop_key_change
.
Example for a setting that realises a pretty standard ‘functional’ chord progression without key change. The chords just tick along by falling fourth. Such very smooth progressions have been used during the baroque period a lot:
prop_deg_change = {
1 => 1.0,
2 => 0.0,
3 => 0.0,
4 => 0.0,
5 => 0.0,
6 => 0.0
}
prop_key_change = {
-3 => 0.0,
-2 => 0.0,
-1 => 0.0,
1 => 0.0,
2 => 0.0,
3 => 0.0,
}
In contrast, the following setting implements a lot of key changes and also wilder jumps within the ring of degrees:
prop_deg_change = {
1 => 0.3,
2 => 0.2,
3 => 0.1,
4 => 0.0,
5 => 0.0,
6 => 0.1
}
prop_key_change = {
-3 => 0.05,
-2 => 0.05,
-1 => 0.15,
1 => 0.01,
2 => 0.05,
3 => 0.05,
}
When experimenting with these settings, a rule of thumb is that those propabilities roughly sum up to 1 or a bit less than 1. If the sum is less than 1, the remaining probability is used to simply repeat the same chord.
Here is the complete program:
# harmonic random walk in major
# written by Nechoj
use_debug false
use_random_seed 42
# chord degree sequence
degrees = ring(:i, :iv, :vii, :iii, :vi, :ii, :v)
# prepare hash map to get index in degrees if value is given
degrees_idx = Hash.new
degrees.each_with_index do |k, i|
degrees_idx[k] = i
end
# key sequence: delta midi notes relative to a base key like :C3
# example when 0=C: C-G-D-A-E-H-Gb-Db-Ab-Eb-B-F
keys = ring(0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5)
# prepare hash map to get index in keys if value is given
keys_idx = Hash.new
keys.each_with_index do |k, i|
keys_idx[k] = i
end
# probabilities for changing the index within ring degrees
# prop_deg_change[n]: probability for transition degrees[i] -> degrees[i+n]
prop_deg_change = {
1 => 0.4,
2 => 0.2,
3 => 0.1,
4 => 0.0,
5 => 0.0,
6 => 0.1
}
# probabilities for changing the index within ring keys
# prop_key_change[n]: probability for transition keys[i] -> keys[i+n]
prop_key_change = {
-3 => 0.0,
-2 => 0.05,
-1 => 0.15,
1 => 0.03,
2 => 0.0,
3 => 0.0,
}
# markovian matrix = map of maps for transition probabilities
markov = Hash.new
degrees.each do |deg|
markov[deg] = Hash.new
end
# set transition probabilities for key changes
# Interpretation of markov[:i][[-2, :ii]]:
# if last played chord was degree :i and in key keys[n], then
# with probability given by markov[:i][[-2, :ii]]
# change to chord with degree :ii played with key keys[n-2]
markov[:i][[-2, :ii]] = prop_key_change[-2]
markov[:i][[-1, :v]] = prop_key_change[-1]
markov[:i][[1, :iv]] = prop_key_change[1]
markov[:ii][[-3, :vii]] = prop_key_change[-3]
markov[:ii][[-2, :iii]] = prop_key_change[-2]
markov[:ii][[-1, :vi]] = prop_key_change[-1]
markov[:ii][[1, :v]] = prop_key_change[1]
markov[:iii][[-1, :vii]] = prop_key_change[-1]
markov[:iii][[1, :vi]] = prop_key_change[1]
markov[:iii][[2, :ii]] = prop_key_change[2]
markov[:iii][[3, :v]] = prop_key_change[3]
markov[:iv][[-2, :v]] = prop_key_change[-2]
markov[:iv][[-1, :i]] = prop_key_change[-1]
markov[:iv][[1, :vii]] = prop_key_change[1]
markov[:v][[-2, :ii]] = prop_key_change[-2]
markov[:v][[-1, :ii]] = prop_key_change[-1]
markov[:v][[-1, :v]] = prop_key_change[-1]
markov[:vi][[-2, :vii]] = prop_key_change[-2]
markov[:vi][[-1, :iii]] = prop_key_change[-1]
markov[:vi][[1, :ii]] = prop_key_change[1]
markov[:vi][[2, :v]] = prop_key_change[2]
markov[:vii][[-1, :iv]] = prop_key_change[-1]
markov[:vii][[-1, :vii]] = prop_key_change[-1]
markov[:vii][[3, :v]] = prop_key_change[3]
# set transition probabilities for chord changes without key change
degrees.each do |deg|
this_idx = degrees_idx[deg]
range(1, 7, 1).each do |i|
markov[deg][[0, degrees[this_idx + i]]] = prop_deg_change[i.to_i]
end
end
# finally set probabiltities for no change (play same chord again)
# adjust row sum of markov map equal to 1.0
degrees.each do |deg|
cum_probs = 0.0
markov[deg].each_value do |v|
cum_probs += v
end
if cum_probs > 1
puts "warning: probabilties greater than 1, normalizing", deg
markov[deg][[0, deg]] = 0.0
markov[deg].each_key do |k|
markov[deg][k] /= cum_probs
end
else
markov[deg][[0, deg]] = 1 - cum_probs
end
end
# function performing the random walk
# inputs: current key, current chord degree
# outputs: new key and degree
define :get_next_chord do |k_idx, deg|
r = rand
cum_prob = 0
next_key = nil
markov[deg].each do |k, v|
next_key = k
cum_prob += v
if cum_prob > r
break
end
end
# result
return keys[keys_idx[k_idx] + next_key[0]], next_key[1]
end
# helper for printing notes
define :ntos do |n|
note_info(n).to_s.split(" ")[1][1..-2]
end
####################
### playing the walk
####################
base = :A2 # first note defining key and octave to start with
ch = [0, :i] # first chord degree to start with (Amaj7 in this case)
st = 1.6 # overall sleep time between chord progressions
live_loop :walk, auto_cue: false, delay: 0.2 do
ch = get_next_chord(ch[0], ch[1])
set :ch, ch
puts "key: " + ntos(base + ch[0])
puts "deg: " + ch[1].to_s
sleep st
end
## bass
live_loop :bass, auto_cue: false do
sync :ch
ch = get :ch
b = base + ch[0]
deg = ch[1]
my_chord1 = (chord_degree deg, b, :major, 3)
my_chord1 -= 12 if my_chord1[0] > base + 6
with_synth :fm do
with_synth_defaults release: 0.1 do
# basic note
play my_chord1[0], sustain: st/2
play my_chord1[0] + 12, sustain: st/2, amp: 0.6 # overtone
sleep st/2
# play 5th
play my_chord1[2] - 12, sustain: st/2
play my_chord1[2], sustain: st/2, amp: 0.6 # overtone
end
end
end
# arpeggio
live_loop :arp, auto_cue: false do
sync :ch
ch = get :ch
b = base + ch[0]
deg = ch[1]
my_chord2 = (chord_degree ch[1], b, :major, 5)
my_chord2 = (chord_invert my_chord2, 1) if [:iv, :v].include? deg
my_chord2 = (chord_invert my_chord2, 2) if deg == :vii
my_chord2 -= 12 if my_chord2[0] > base + 10
sca = my_chord2 + my_chord2.reverse[1..3]
nn = sca.length
with_synth :dpulse do
with_synth_defaults amp: 0.6, detune: 0.15, sustain: st/nn, release: 0.05, cutoff: 95 do
sca.each_with_index do |n, i|
play n
sleep st/nn if i < nn - 1
end
end
end
end
Using the same idea, my goal is to implement other domains in music theory. The minor keys are the next logical step, but much more difficult to do.