Sharing Code: Polyrhythm

Hello everyone!

I’d like to share some code I wrote, and tell a little story.

Do you know polyrhythms? It’s really fun to play them with your hands on random sound-making things.

I wanted to practice them and watched some cool Videos by Adam Neely to get into it again:

After some practicing of the three-against-four (3:4) rhythm I had an idea:
Let’s say, we play the slower 1/3 notes with the left hand and the faster 1/4 notes with the right hand.
Then we stop playing with the left had and just continue to play with the right hand.
The right hand continues in the same tempo for some time and we forget everything we played so far.
We just hear the steady beat. Now we interpret the movement of the right hand as 1/3 notes, but the tempo of the right hand is still the same. Then we play 1/4 notes with the left hand. The left hand is faster than the right hand, because 1/4 notes are faster than 1/3 notes.

Now we can repeat the whole process, but the roles of the right and left hand are switched.
That way we get faster with every new cycle – faster and faster.
To compensate that, we slowly get slower while playing with both hands.

I tried to put my idea into practice, but I failed. I thought: “I have to listen to this to practice it.” And the easiest method for doing this, was to implement it in Sonic-Pi. And that’s what I did.

The code can be found in this git repo on github: polyrhythm/three-against-four.rb at main · j0le/polyrhythm · GitHub

via the perma-link of the current version: polyrhythm/three-against-four.rb at 2187a2748134caf83456fdc2c42ce976350569ea · j0le/polyrhythm · GitHub

or here:

define :my_play do |bpm, sample_sym, repeats|
  in_thread do
    with_bpm bpm do
      repeats.times do
        sample sample_sym
        sleep 1.0/repeats
      end
    end
  end
end


repeats_x = 3
repeats_y = 4

state = 1
start_bpm = 40.0
end_bpm = start_bpm*1.0*repeats_x/repeats_y
bpm = start_bpm

sound_a = :perc_snap
sound_b = :perc_snap2

live_loop :polyrythm do
  with_bpm bpm do
    
    if state == 1
      
      if bpm == start_bpm
        sample :perc_bell, amp: 0.5
      end
      
      my_play bpm, sound_a, repeats_x
      my_play bpm, sound_b, repeats_y
      
      getting_faster = start_bpm < end_bpm
      
      if (getting_faster && (bpm >= end_bpm)) || (!getting_faster && (bpm <= end_bpm))
        state = 2
        bpm = end_bpm
      else
        bpm += (end_bpm - start_bpm)/6.0
      end
      
    elsif state == 2
      sample :perc_bell, amp: 0.5
      sample sound_a
      my_play bpm, sound_b, repeats_y
      
      bpm = start_bpm
      state=3
      
    elsif state == 3
      sample :perc_bell, amp: 0.5
      my_play bpm, sound_b, repeats_x
      
      # swap sounds
      helper = sound_a
      sound_a = sound_b
      sound_b = helper
      
      state=1
    else
      # alarm
      my_play bpm*2, :perc_bell, 7
      
    end
    
    sleep 1
  end
end

It’s a state machine. What do you think of it?
Maybe I can use some other techniques. For example rings.

Anyway – I have some plans: I want to add some more states and fade the sounds in and out.

And that’s it for now. Maybe I add some more thoughts later.

— Ole

1 Like

The ’ use_bpm ’ and ’ live_loop ’ used together
make SPi a natural for polyrhythms …
i.e…

use_bpm A
Live_loop :a do
blah

use_bpm B
Live_loop :b do
blah

Just for fun: :slight_smile:

speedy = (line Math::log(90), Math::log(900), steps: 128).map {|x| Math.exp(x)}
use_bpm :link
in_thread do
  loop do
    set_link_bpm! speedy.ring.reflect[tick]
    sleep 1
  end
end

CHORD = (
  [55, 58, 62]*4+
  [54, 60, 57, 62]*3+
  [55, 58, 62]*4+
  [53, 57, 60]*4+
  [53, 58, 62]*4+
  [53, 57, 60]*4+
  [55, 58, 62]*4+
  [60,54,57,62]*3
).ring
set :foo, 0

def gensym()
  "#{tick(:g)}"
end

define :launch do |n|
  live_loop gensym do
    t = 4.0
    synth :bass_foundation, amp: 0.6, note: CHORD[get(:foo)], sustain: 0.3, release: 0.1
    set :foo, get(:foo)+1
    sleep t/n
  end
end

with_fx :flanger do
  with_fx :reverb do
    with_fx :bitcrusher, cutoff: 100 do
      with_fx :pan, pan: -1 do
        launch(4.05)
      end
      with_fx :pan, pan: 1 do
        launch(3)
      end
    end
  end
end

You can get different rhythms by changing the parameters:

use_bpm 300
CHORD = (
  [55, 58, 62]*4+
  [50,54,57,60]*3+
  [55, 58, 62]*4+
  [53, 57, 60]*4+
  [58, 53, 62]*4+
  [53, 57, 60]*4+
  [55, 58, 62]*4+
  [50,54,57,60]*3
).ring
set :foo, 0

def gensym()
  "#{tick(:g)}"
end

define :launch do |n|
  live_loop gensym do
    t = 4.0
    synth :bass_foundation, amp: 0.6, note: CHORD[get(:foo)], sustain: 0.3, release: 0.1
    set :foo, get(:foo)+1
    sleep t/n
  end
end

with_fx :flanger do
  with_fx :reverb do
    with_fx :bitcrusher, cutoff: 100 do
      launch(6)
      with_fx :pan, pan: -1 do
        launch(6.01)
      end
      with_fx :pan, pan: 1 do
        launch(3)
      end
    end
  end
end
use_bpm 100
CHORD = (
  [55, 58, 62]*4+
  [50,54,57,60]*3+
  [55, 58, 62]*4+
  [53, 57, 60]*4+
  [58, 53, 62]*4+
  [53, 57, 60]*4+
  [55, 58, 62]*4+
  [50,54,57,60]*3
).ring
set :foo, 0

def gensym()
  "#{tick(:g)}"
end

define :launch do |n|
  live_loop gensym do
    t = 4.0
    synth :bass_foundation, amp: 0.6, note: CHORD[get(:foo)], sustain: 0.3, release: 0.1
    set :foo, get(:foo)+1
    sleep t/n
  end
end

with_fx :flanger do
  with_fx :reverb do
    with_fx :bitcrusher, cutoff: 100 do
      
      with_fx :pan, pan: -1 do
        launch(7.01)
      end
      with_fx :pan, pan: 1 do
        launch(11.01)
      end
    end
  end
end

would you mind explain this part of you code ?

I just did not want to type out each live_loop separately, so repeatedly calling gensym (it’s just used as a kind of macro there) outputs "0", "1", etc. Therefore launch(11), launch(4) creates live_loop_0, live_loop_1 and so on, just a different name every time you run it. Since this is not really “live” code you are right, you do not necessarily need named live_loops.