Markov chains for beginners - Part 2

In case you have read the post Markov chains for beginners and got a bit curious about what you could do with it, here is a second little lesson for you. I would like to give an example on how to create a melody that is perfectly in harmony with the underlying chord progressions but still rich and random. Actually, my intention here is to overdo it and create melodies that are as harmonic as possible, because this demonstrates the possibilities we have got using Markov chains even better. Of course, the opposite would be possible as well: create beats or sequences that are as nasty and aggressive as possible.

We start with a simple and standard chord progression that has been used in thousands of songs. Maybe you feel familiar with it when playing:

use_debug false
use_bpm 80
st = 4.0  # sleep time for progressions

progression = [
  [(chord :C3, :major, num_octaves: 2), :ionian],
  [(chord :A3, :minor, num_octaves: 2), :aeolian],
  [(chord :D3, :minor, num_octaves: 2), :dorian],
  [(chord :G3, 'dom7', num_octaves: 2), :mixolydian]
]

# organ playing chord progression
with_fx :reverb,room: 0.6, mix: 0.3 do
  with_fx :flanger, feedback: 0.5 do
    with_fx :lpf, mix: 0.5, cutoff: 70 do
      live_loop :organ, auto_cue: false  do
        p = progression.tick
        synth :tri, note: p[0], attack: 0.01, sustain: st, release: 0.1, amp: 0.8
        sleep st
      end
    end
  end
end

There are 4 chords in key C that can be played in an endless loop. The array progression contains the chords, but also the modes of scales that are associated with these chords: :ionian, :aeolian, :dorian, :mixolydian. This chord-mode association is pretty standard and can be found in Wikipedia.

For our application it is an important fact, that all notes that make up the chord are contained in the corresponding scales as well. And moreover, they are found at always the same position. In each of those scales s , the major/minor chord is built using the notes s[0]-s[2]-s[4], i.e., the indices needed to get the chord notes are always 0-2-4, independent of the chord being played. That’s very helpful when building the Markov matrix:

# Markov matrix for 8 notes of the scales
markov_matrix = [
  # 0*    1     2*    3     4*    5     6*    7*
  [0.05, 0.2,  0.75, 0.0,  0.0,  0.0,  0.0,  0.0],  # 0* base tone
  [0.1,  0.05, 0.75, 0.1,  0.0,  0.0,  0.0,  0.0],  # 1
  [0.0,  0.1,  0.1,  0.2,  0.6,  0.0,  0.0,  0.0],  # 2* third
  [0.0,  0.0,  0.2,  0.0,  0.7,  0.0,  0.0,  0.1],  # 3
  [0.0,  0.0,  0.2,  0.1,  0.2,  0.2,  0.3,  0.0],  # 4* fifth
  [0.0,  0.0,  0.0,  0.1,  0.35, 0.05, 0.4,  0.1],  # 5
  [0.0,  0.0,  0.0,  0.0,  0.4,  0.2,  0.1,  0.3],  # 6* seventh
  [0.0,  0.0,  0.0,  0.0,  0.3,  0.2,  0.5,  0.0],  # 7* octave
]

The comments above and behind each row tell us the indices in the scales. Notes belonging to the chord are marked with a *. I have marked 6 and 7 as well, because the seventh and the octave also nicely harmonize with the chord notes.

Now, the main point is to understand how the entries in the matrix have been chosen. The first row defines what notes should come next while the base note of the chord is being played. As you can see there is a 0.05 probability to play the same note again, 0.2 to move up a single step, but 0.75 to directly go to the next note that belongs to the chord. And this pattern more or less is repeated in every row: The highest probabilities are always to use the notes of the chords, and only with lower probabilities allow for the notes between. And this is what makes the created melodies strongly related to the notes of the chords and thus very harmonic.

Here’s the complete program. The function next_note_idx is exactly the same as in the previous post on that topic.

# harmonic melody using Markov chain
# written by Nechoj

use_debug false
use_bpm 80
st = 4.0  # sleep time for progressions

progression = [
  [(chord :C3, :major, num_octaves: 2), :ionian],
  [(chord :A3, :minor, num_octaves: 2), :aeolian],
  [(chord :D3, :minor, num_octaves: 2), :dorian],
  [(chord :G3, 'dom7', num_octaves: 2), :mixolydian]
]

# organ playing chord progression
with_fx :reverb,room: 0.6, mix: 0.3 do
  with_fx :flanger, feedback: 0.5 do
    with_fx :lpf, mix: 0.5, cutoff: 70 do
      live_loop :organ, auto_cue: false  do
        p = progression.tick
        synth :tri, note: p[0], attack: 0.01, sustain: st, release: 0.1, amp: 0.8
        sleep st
      end
    end
  end
end


# Markov matrix for 8 notes of the scales
markov_matrix = [
  # 0*    1     2*    3     4*    5     6*    7*
  [0.05, 0.2,  0.75, 0.0,  0.0,  0.0,  0.0,  0.0],  # 0* base tone
  [0.1,  0.05, 0.75, 0.1,  0.0,  0.0,  0.0,  0.0],  # 1
  [0.0,  0.1,  0.1,  0.2,  0.6,  0.0,  0.0,  0.0],  # 2* third
  [0.0,  0.0,  0.2,  0.0,  0.7,  0.0,  0.0,  0.1],  # 3
  [0.0,  0.0,  0.2,  0.1,  0.2,  0.2,  0.3,  0.0],  # 4* fifth
  [0.0,  0.0,  0.0,  0.1,  0.35, 0.05, 0.4,  0.1],  # 5
  [0.0,  0.0,  0.0,  0.0,  0.4,  0.2,  0.1,  0.3],  # 6* seventh
  [0.0,  0.0,  0.0,  0.0,  0.3,  0.2,  0.5,  0.0],  # 7* octave
]

# function to get next index of note in scale according to markov matrix
define :next_note_idx do |current_note_idx|
  n = 0
  r = rand
  pp = 0
  markov_matrix[current_note_idx].each do |p|
    pp += p
    break if pp > r
    n += 1
  end
  return n
end


with_fx :reverb,room: 0.8, mix: 0.4 do
  live_loop :melody, auto_cue: false do
    
    # build scale to play
    p = progression.tick
    base_note = p[0][0] # first note of current chord
    base_note += 12 if base_note < :D4.to_i  # lift up 1 octave if too low
    base_note += 12 if base_note < :D4.to_i  # lift up 1 octave if too low (again)
    mode = p[1] # mode of scale defined in progressions
    sca = scale base_note, mode
    n = [0, 2, 4, 7].choose  # index of first note in scale to play
    
    with_synth :pulse do
      with_synth_defaults amp: 0.8, cutoff: 90 do
        ng = 4  # number of chunks of notes to play with same density
        ng.times do |i|
          d = [2, 2, 4, 4, 4].choose
          density d do |dd|
            play sca[n], attack: 0.02*d, release: 0.05, sustain: st/ng-0.12
            n = next_note_idx(n)
            sleep st/ng
          end
        end
      end
    end
  end
end

There are two additional principles coded into the matrix. First is to create an upward direction of the melody when starting at low notes, downwards when starting with the high indices. Second, there are almost no probabilities for larger jumps. All probabilities in a row are placed on neighboring positions, and the 0.0 probabilities are grouped as well, marking forbidden areas. This gives a dense progression of the melody notes without larger jumps. These principles contribute to the impression of great harmony (as said above, I tried to overdo it …).

The entry point defining the note to start with when a new chord is played, is defined at random in the line

n = [0, 2, 4, 7].choose

There is a certain rhythm in the melody, because the length of the notes being played depends on the density of the time scale which is chosen at random in the line

d = [2, 2, 4, 4, 4].choose

In oder to make the melody less harmonic, lower the probabilities in the Markov matrix for the chord notes and increase e.g. the probabilities for indices 1, 3, 5. Make sure that the sum of probabilities in each row always is 1.0. Implement larger jumps in each row. And finally, use density values such as 3 or 5 to introduce uneven rhythmic patterns for the melody.

7 Likes

Here is a variation with a very different style of melody

markov_matrix = [
  # 0*    1     2*    3     4*    5     6*    7*
  [0.0,  0.1,  0.1,  0.0,  0.0,  0.0,  0.0,  0.8],  # 0* base tone
  [0.0,  0.0,  0.2,  0.0,  0.0,  0.0,  0.0,  0.8],  # 1
  [0.0,  0.0,  0.0,  0.1,  0.1,  0.0,  0.0,  0.8],  # 2* third
  [0.0,  0.0,  0.0,  0.0,  0.2,  0.0,  0.0,  0.8],  # 3
  [0.0,  0.0,  0.0,  0.0,  0.0,  0.1,  0.1,  0.8],  # 4* fifth
  [0.0,  0.0,  0.0,  0.0,  0.0,  0.0,  0.1,  0.9],  # 5
  [0.0,  0.0,  0.0,  0.0,  0.0,  0.0,  0.0,  1.0],  # 6* seventh
  [0.1,  0.2,  0.15, 0.2,  0.15, 0.1,  0.1,  0.0],  # 7* octave
]

I recommend to set the density in the melody loop to the fixed value d=4 (and not to randomly choose between 2 and 4 as with the above example). As an exercise, listen to the melody and try to relate it back to the probabilities defined in the matrix.