Generative Piano using Markov Chaining

This is my very first SonicPi video/performance/composition:

(Gist here)

It uses a technique for generate note states for a “left” and “right” hand of a piano part using markov chains for the left hand and a scale + cos function for the right hand. The technique is inspired by Andrew Sorensen’s “The Concert Programmer” but is here adapted for SonicPi / ruby from the scheme-based language he is using in the video.

9 Likes

This is nice! Interesting code.
I think there is an end missing at the end of the live_loop :right code (line 69)

thanks @robin.newman! I added the missing end statement to the gist.

This is wonderful! I’ve been following Sorensen’s work for years and I’m excited to look into your code.

Thanks!

2 Likes

To build on this concept, I tried to add a more interesting beat and a vocal sample from The Weeknd. This is a live coding video, but still fairly non-extemporaneous and precomposed:

(Gist here)

A few lessons I learned:

By making a smarter markov chain that’s more likely to follow sensible next notes than just choose random things (as I kind of did last time). It makes a much more listenable result.

For example, my first approach used randomization and a cos function to generate the “right hand” part and this might’ve been a little too random. In this example, I used the following markov chain:

H = {
  8 => [0],
  7 => [8],
  6 => [2],
  5 => [2, 2, 0, 0, 7, 7],
  4 => [1, 1, 2, 2, 6, 6],
  3 => [5],
  2 => [4, 4, 0],
  1 => [3, 3, 3, 0, 0, 0],
  0 => [-2, 2, 4, 4, 0],
  -1 => [-3, -3, 0, 0],
  -2 => [-4, -4, 0],
  -3 => [-5],
  -4 => [-1, -1, -2, -2, -6, -6],
  -5 => [-2, -2, 0, 0, -7, -7],
  -6 => [2],
  -7 => [8],
  -8 => [0],
}

This same markov chain that chooses the root notes also chooses the right hand “lead” notes. It tends to favor following triads and still (probabilistically) shows a strong preference for 0 or the root note (even having entirely deterministic state changes at the ends of the scale). This has been mostly tested on and expected to work best 8-note scales but doesn’t sound too bad on pentatonic ones either.

live_loop :right do
  use_synth :pretty_bell
  n1 = S[markov(gg, H)] + 12 # choose 1st random note in scale
  n2 = S[markov(gg, H)] + 12 # choose 2nd random note in scale
  d = d1.choose # choose random duration
  play n1, release: M/2, sustain: M/2
  play n2 if (bools 1, 0, 0, 0).tick # plays a chord on leading edge of measures
  sleep M/4
  sleep d.abs
  2.times do
    pplay S[markov(gg, H)] + 12, d
    sleep (M/4 - d.abs)
  end
end

After some great advice from @mrbombmusic re: ways to program beats, I also added some (very basic) randomized rhythm and duration generation to the hihats. I used a mixture of matrix/midi-grid-style kick/snare and knits for the hihats, from which durations are plucked randomly.

M = 4.0

# Rhythm Patterns
p1 = (bools 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0) # kick drums
p2 = (bools 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0) # snare drums
p3 = (knit, M/16, 4, M/24, 6, M/16, 4, M/24, 6, M/16, 4, M/64, 8) # hihats

# Random durations
d1 = (knit M/8, 8, M/16, 4, M/-8, 2, M/-16, 2)

# Rhythm Components
live_loop :kicks do # kick drum loop
  16.times do # using 16 times here makes the grid loops better synced to measures
    with_fx :level, amp: 1 do
      sample :bd_tek if p1.tick
      sleep M / 16
    end
  end
end

live_loop :snares do # snares loop
  16.times do  # using 16 times here makes the grid loops better synced to measures
    with_fx :level, amp: 1 do
      sample :sn_dolf if p2.tick
      sleep M / 16
    end
  end
end

live_loop :hats do # hi-hats loop
  16.times do
    with_fx :level, amp: 1 do
      with_fx :eq, high: -0.5, mid: -0.9 do
        with_fx :distortion, mix: 0.9 do
          sample :elec_tick, rate: 1, amp: 3.8 + rrand(-0.5, 0.5) if p3.tick >= 0
          sleep p3.look.abs
        end
      end
    end
  end
end

The next challenge, though, is generating kick/snare patterns randomly that are listenable and still remain structured like “hip hop” or “trap” style beats. :man_shrugging:

5 Likes

This is great stuff! I look forward to having a play with it tomorrow. Many thanks for the code, and the song sounds very nice with your accompaniment.

1 Like

Thanks @robin.newman! It should work as is, except for the vocal sample itself which is just a place holder in the code. But if you want to use the same one, here’s where I got it from: https://www.youtube.com/watch?v=kphpS6T-8Js

Hi Nabi,

First off… so cool. Absolutely gorgeous beat…

Sadly, so complex and obfuscated… I’m afraid its too far away
from the coding /.music intersection for me to understand.

,,,,,,,,,,,,,,,,,,,, <- Programming   ||   Music ->      ,,,,,,,,,,,,,,,,,,,,

                              <-[    Me   ]->
<-[ ]->
This code... 

Regards,

Eli…

@Eli thanks for checking it out and the kind words!

I admit there’s a bit too much setup code here that might be better abstracted away as library or a utility class. My next steps on this might be refactoring or thinking about how to make the markov chain boilerplate a bit more invisible and require less setup code, perhaps. Would be really cool to seamlessly convert Hash or Array objects into these using methods like [].ring and just drop them into loops as little patterns. I think that’s one of the more elegant parts of SonicPi now.

There are some interesting articles on the web about this process for note selection and “algorithmic composition” (below). The only downside is that this system is often associated with machine learning and uses MIDI files or existing note data for generating. I kinda took it out of that context a bit, though.

https://hackernoon.com/generating-music-using-markov-chains-40c3f3f46405

3 Likes

Sorry Nabi,

I can’t help it… I’m a compulsive code twiddler… I think this version
would work well in an online game… just spent a couple of hours
playing WoW with this as background and it wasn’t irritating in the least.

use_bpm 60 # global bpm
M = 4.0 # measure size
R = 59 # root note
S = scale(R, :minor) # working scale
set :root, 0 # initialize root note (relative)

SAMPLE = "path/to/vocal/sample/vocals.wav"

# A hash to control loop levels in a single place
LEVELS = {
  hats: 0.0,
  snares: 1.75,
  kicks: 3.0,
  right: 0.15,
  left: 0.5,
  subbass: 0.5,
  vox: 2.75
}

# This hash simulates a markov chain.
# Each key is the state and the array
# value represents the next state from
# which to choose at random.
H = {
  8 => [0],
  7 => [8],
  6 => [2],
  5 => [2, 2, 0, 0, 7, 7],
  4 => [1, 1, 2, 2, 6, 6],
  3 => [5],
  2 => [4, 4, 0],
  1 => [3, 3, 3, 0, 0, 0],
  0 => [-2, 2, 4, 4, 0],
  -1 => [-3, -3, 0, 0],
  -2 => [-4, -4, 0],
  -3 => [-5],
  -4 => [-1, -1, -2, -2, -6, -6],
  -5 => [-2, -2, 0, 0, -7, -7],
  -6 => [2],
  -7 => [8],
  -8 => [0],
}

# Rhythm Patterns
p1 = (bools 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0) # kick drums
p2 = (bools 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0) # snare drums
p3 = (knit, M/16, 4, M/24, 6, M/16, 4, M/24, 6, M/16, 4, M/64, 8) # hihats
sb = (50..100).ring.mirror # simulate fade-up -> fade-down
ds = (0..9).map {|n| n * 0.1}.ring
# Random durations
d1 = (knit M/8, 8, M/16, 4, M/-8, 2, M/-16, 2)

# State machine utility functions
define :markov do |a, h| h[a].sample; end # Chooses the next state at  random from hash
define :gg do get[:root]; end # simplified root note in scale getter
define :g do S[R + get[:root]]; end # simplified raw root note getter
define :s do |n| set :root, n; end # simplified root note setter

# play function with duration baked in and the ability to play rests using negative (-) time (i.e. M/-4 would be a quarter rest).
define :pplay do |n, d|
  play n if d >= 0
  sleep d.abs
end

# Rhythm Components
live_loop :kicks do # kick drum loop
  l = LEVELS[:kicks] || 0 # keeping this outside of the do...end block ensures a consistent level across the measure
  16.times do # using 16 times here makes the grid loops better synced to measures
    with_fx :level, amp: l do
      sample :bd_tek if p1.tick
      sleep M / 16
    end
  end
end

live_loop :snares do # snares loop
  l = LEVELS[:snares] || 0 # keeping this outside of the do...end block ensures a consistent level across the measure
  16.times do  # using 16 times here makes the grid loops better synced to measures
    with_fx :level, amp: l do
      sample :sn_dolf if p2.tick
      sleep M / 16
    end
  end
end

live_loop :hats do # hi-hats loop
  l = LEVELS[:hats] || 0 # keeping this outside of the do...end block ensures a consistent level across the measure
  16.times do
    with_fx :level, amp: l do
      with_fx :eq, high: -0.5, mid: -0.9 do
        with_fx :distortion, mix: 0.9 do
          sample :drum_cymbal_closed, sustain: 0, release: 0.01 , rate: 1, amp: 2 + rrand(-0.5, 0.5) if p3.tick >= 0
          sleep p3.look.abs
        end
      end
    end
  end
end

# Vox samples
live_loop :vox do
  with_fx :level, amp: LEVELS[:vox] || 0 do
    with_fx :reverb, mix: ds.tick, room: 0.4 do
      with_fx :echo, mix: ds.look * 0.2, decay: 0.25 do
        # sample SAMPLE, rate: 1.0, amp: 2.5 # play your vocal sample
      end
    end
  end
  sleep M * 16 # I chose a 16-measure vocal sample
end

# Melodic Components
live_loop :right do
  with_fx :level, amp: LEVELS[:right] || 0 do
    with_fx :reverb, mix: 0.9, room: 0.8 do
      with_fx :echo, mix: 0.5 do
        use_synth [:fm, :prophet, :piano].choose
        ##| use_synth :fm
        n1 = S[markov(gg, H)] + 12 # choose 1st random note in scale
        n2 = S[markov(gg, H)] + 12 # choose 2nd random note in scale
        d = d1.choose # choose random duration
        play n1, release: M/2, sustain: M/2
        play n2 if (bools 1, 0, 0, 0).tick # plays a chord on leading edge of measures
        sleep M/4
        sleep d.abs
        2.times do
          pplay S[markov(gg, H)] + 12, d
          sleep (M/4 - d.abs)
        end
      end
    end
  end
end

live_loop :left do
  use_synth :sine
  with_fx :level, amp: LEVELS[:left] || 0 do
    with_fx :reverb, mix: 0.8, room: 0.8 do
      with_fx :echo, mix: 0.5 do
        use_synth [:pluck, :blade].choose
        s markov(gg, H) # update the state using markov chaining
        pplay (S[gg] - 12), M/8 # down one octave from root
        sleep (M / 4) + (M / 8)
        pplay (S[gg - 5] - 12), M/8 # down a fifth and an octave from the root
        sleep (M / 4) - (M / 8)
      end
    end
  end
end

live_loop :subbass do
  n = g - 24 # down two octaves from current root
  d = (sb.tick * 0.01) - 0.01
  with_fx :level, amp: LEVELS[:subbass] || 0 do
    with_fx :reverb, mix: 0.8 do
      with_fx :distortion, distort: 0.2 do
        use_synth :pretty_bell
        play n, release: 4, sustain: 4, amp: d
        sleep M / 1
      end
    end
  end
end

comment do
  
  use_synth [:pulse, :fm].choose
  notes = (ring, 72, 74, 75, 77, 79)
  
  live_loop :r do
    with_transpose -12 do
      if one_in(5) then
        sleep [0.5,0.25].choose
        next
      else
        if rand() > 0.75 then
          notes = (ring, 72, 74, 75, 77, 79)
        else
          notes = notes.shuffle
        end
        play notes.tick, release: 2, amp: 0.125
        sleep [0.5,0.25].choose
        
      end
    end
  end
  
end
1 Like

@Eli ha:

just spent a couple of hours
playing WoW with this as background and it wasn’t irritating in the least.

Listenability is pretty key for me.

The additional voices in the commend do block def. beef up the harmony. It’s I like how it switches between :pulse and :fm sound sources too. This adds more variety. I was doing that manually during my live editing, but having it automatically switch based on a set of rules is an interesting concept.

I’ll spend more time tinkering throughout the next few days and hopefully iterate on these concepts.

Erk… I was going to delete that comment - do before posting… it added too much
for my taste, and was more distracting than the simpler tune…

Mind you… if you take out the :left, :right and :subbass loops, and then use a

with_fx :ixi_techno do
with_fx :flanger do

on the pulse/fm stuff, it does sound nice again…

Oh well, each to his own. :slight_smile:

Eli…

So after spending more time refining this approach to generative music, I came up with a few “lessons learned” about how better generalize this system. I turned the whole thing into a blog post:

TLDR: making each state in the state machine a “pattern” or a “phrase” or a “chord” tends to result in more listenable music at the end of the day.

Here’s a SonicPi script that better showcases this idea of markov chaining between pattern/chord/phrase states:


T = 4.0

# State machine utility functions
define :markov do |a, h| h[a].sample; end # Chooses the next state at  random from hash
define :g do |k| get[k]; end # simplified root note in scale getter
define :s do |k, n| set k, n; end # simplified root note setter
define :mnote do |key,chain| s key, (markov (g key), chain); g key; end

# Initializes states for all state machines
set :k, 1
set :b, 0
set :s, 0
set :y, 0

# Scale
sc_root = :F2
sc_type = :major
sc = scale(sc_root, sc_type)

# Chords in scale -- chords are defined here.
chords = (1..7).map {|n| chord_degree n, sc_root, sc_type }.ring

K = {
  1 => [1,1,4,5],
  4 => [5],
  5 => [1,4]
}

B = {
  0 => [0,1],
  1 => [0,1]
}

S = {
  0 => [0,0,0,0, 0,0,0,1], # 1/8 chance of choosing snare pattern 2
  1 => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1] # 1/16 chance of choosing snare pattern 2
}

kick_patterns = [
  (bools, 1,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0), # Kick Pattern 1 / C
  (bools, 1,0,0,0, 0,0,1,0, 0,1,1,0, 0,1,1,0) # Kick Pattern 2 / C
].ring

snare_patterns = [
  (bools, 0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0), # Snare Pattern 1 / G
  (bools, 1,0,1,0, 1,0,0,0, 0,0,0,0, 1,0,1,0)  # Snare Pattern 2 / G
].ring

# Melody Maker -- makes a single melody pattern of length len and moves away from root in range 2
define :make_melody do |len = 16, rng = 2|
  (1..len).map{|n| ((rng*-1)..rng).to_a.sample }.ring  # This uses a .sample and a range, but can also be done with cosine functions.
end

# Generates 4 melody patterns
melodies = (1..4).map{|n| make_melody(16,2)}.ring

# Melodies -- markov chain for switching patterns
Y = {
  0 => [1],
  1 => [0, 1, 2],
  2 => [1, 2],
  3 => [1]
}

live_loop :kicks do
  pat = kick_patterns[mnote :b, B] # markov select pattern
  pat.length.times do
    sample :bd_mehackit if pat.tick
    sleep T/16
  end
end

live_loop :snares do
  pat = snare_patterns[mnote :s, S] # markov select pattern
  pat.length.times do
    sample :sn_dolf if pat.tick
    sleep T/16
  end
end

live_loop :chords do
  use_synth :fm
  chr = chords[mnote :k, K]
  dur = T/1
  play chr[0], release: dur
  play chr[1], release: dur
  play chr[2], release: dur
  play chr[3], release: dur

  melody = melodies[mnote :y, Y]
  use_synth :pretty_bell
  4.times do
    play sc[melody.tick] + 36
    sleep T/4
  end
end
4 Likes