# Generative Piano using Markov Chaining

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

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.

8 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:

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 `knit`s 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.

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
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
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``````

@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.

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