State machine improv synchronized to a beat

I had a lot of fun using the idea of state machines introduced to me by @nabisco - many thanks again!

Right now I´m trying to combine this approach to improvisation with regularity:
have the state machine start over new every 3 bars, for example.

I manage to approximate this, but not exactly:

use_bpm 120

set :current_state, 0


NOTES = [52, 62, 64, 52, 52, 55, 55]
INTERONSET = [1, 0.25, 0.5, 0.5, 0.25, 0.5, 0.25]


RULES = {
  0 => [1],
  1 => [2],
  2 => [3, 5],
  3 => [4],
  4 => [5],
  5 => [5, 0, 6],
  6 => [0]
}

live_loop :reset do
  Rst = tick
  puts look
  if look > 4
    tick_reset
  end
  sleep 1
end


live_loop :update_state do
  # get current state value -- 0 on the first run
  s = get[:current_state]
  play NOTES[s], amp: rrand(0.2, 0.8), release: rrand(0.1, 0.6), pan: 0
  
  # update to next state  - or reset to first state
  if Rst > 4
    set :current_state, 0
  else
    set :current_state, RULES[s].choose
  end
  sleep INTERONSET[s]
end



live_loop :drms do
  sample :drum_bass_hard
  sample :drum_cymbal_open, amp: 0.1
  sleep 1
  sample :drum_snare_soft
  sleep 1
  sample :drum_bass_soft
  sleep 1
  sample :drum_snare_soft
  sleep 1
  sample :drum_bass_soft
  sleep 1
  sample :drum_snare_soft
  sleep 1
end

The state machine does start new regularly, but delayed, for a sixteenth or an eight note.
Any ideas how to exactly start on the beat each time?

3 Likes

@Gisbert this sounds awesome! I’m glad you were able to work this into some of your pieces. I think one reason you might have your state updates out of sync is that the state change sleep n amount varies. I tend to make a kind of steady, ticker loop for state changes that’s kind of a centralized state-manager kinda like:


M = 4.0 # measure duration

# this loop ONLY changes state and plays nothing
live_loop :ticker do
  # get current state value
  s = get :current_state
  set :current_state, RULES[s].choose
  
# sleep for a steady, 16th note amount.  
  sleep M/16 
  
  # NOTE: this sleep above can be whatever interval you think makes sense.
  # In some cases, I only change the state once per measure, but that 
  # can make the piece less dynamic.
end

Then other loops will have “read-only” relationships with the state and only the above loop :ticker actually changes the state:

live_loop :inst1 do
   s = get :current_state
   play NOTES[s]
   sleep INTERONSET[s]
end

live_loop :inst2 do
   s = get :current_state
   play NOTES[s]
   sleep INTERONSET[s]
end

The two :inst1 and :inst2 loops would then just read the current state when its their time to be invoked, but not update the state when they play (which can cause the other loop to be at a different state when its called).

Hope this helps.

3 Likes

Hey @nabisco, thanks for your nice words and your input!

Your solution for synchronization is much more elegant, however, I still don´t manage to achieve the combination of the state machine process with regular phrasing.

I want the process to start with the first state at a regular intervall, say one, two or four bars.

This is me trying but it does not seem to work the way I intend it to.

use_bpm 120

set :current_state, 0

M = 4.0

live_loop :ticker do
  if tick > 15
    tick_reset
    set :current_state, 0
  else
    s = get :current_state
    set :current_state, RULES[s].choose
  end
  
  sleep M/16
  
  puts look
  
end

NOTES = [52, 62, 64, 52, 52, 55, 55]
INTERONSET = [1, 0.25, 0.5, 0.5, 0.25, 0.5, 0.25]


RULES = {
  0 => [1],
  1 => [2],
  2 => [3, 5],
  3 => [4],
  4 => [5],
  5 => [5, 0, 6],
  6 => [0]
}


live_loop :inst1 do
  s = get :current_state
  play NOTES[s]
  sleep INTERONSET[s]
end

live_loop :drms do
  sample :drum_bass_hard
  sleep 1
  sample :drum_snare_soft
  sample :drum_cymbal_open, amp: rrand(0.01, 0.07)
  sleep 1
end

Cheers

1 Like

@Gisbert:

Oh I see, so you want it to start again at state 0 at regular cycles?

Okay, the best way to achieve the resetting / cycle automatically is to use a ring (which is kind of an infinite array) made up of a range of numbers. That automatically retains the cyclical aspects I think you’re aiming for, with easily configurable lengths of cycles:

M = 4.0

PHRASE_LENGTH = 8 .   # set the desired length of your phrase in ticks

r = (0..(PHRASE_LENGTH-1)).ring .   # similar to an array of [0,1,2,3,4,5,6,7] that automatically resets when it reaches the end

live_loop :ticker do
  t = r.tick # grabs next value in ticker and regularly resets at the end
  if t != 0 # the ticker is automatically 0 again when it runs out of elements in the range, so this checks if its within the cycle.
    s = get :current_state
    set :current_state, RULES[s].choose # move the state machine
  else
    # when the else branch fires, it means its time to reset
    set :current_state, RULES[0].choose    # resets back to zeroth state
  end
  sleep M/16    # this assumes ticks are 16th notes in size
end

NOTES = [52, 62, 64, 52, 52, 55, 55]
INTERONSET = [1, 0.25, 0.5, 0.5, 0.25, 0.5, 0.25]


RULES = {
  0 => [1],
  1 => [2],
  2 => [3, 5],
  3 => [4],
  4 => [5],
  5 => [5, 0, 6],
  6 => [0]
}

live_loop :inst1 do
  s = get :current_state
  play NOTES[s]
  sleep INTERONSET[s]
end

live_loop :drms do
  sample :drum_bass_hard
  sleep 1
  sample :drum_snare_soft
  sample :drum_cymbal_open, amp: rrand(0.01, 0.07)
  sleep 1
end

lmk if this works for you.

2 Likes

Hey @nabisco, yes, that is exactly what I am after, thanks!
But I do get no sound and an error message:

Runtime Error: [buffer 3, line 5] - NameError

Thread death!

uninitialized constant SonicPi::RuntimeMethods::PHRASE_LENGTH

workspace_three:5:in `block (2 levels) in __spider_eval'

/Applications/Sonic Pi 2.app/app/server/sonicpi/lib/sonicpi/lang/core.rb:4390:in `block (2 levels) in in_thread'

I can´t work out what´s the problem there - did you actually run this and it works on your computer?

Cheers

Two extra dots seem to have crept in.
change

PHRASE_LENGTH = 8 . # set the desired length of your phrase in tick
to
PHRASE_LENGTH = 8 # set the desired length of your phrase in tick

and

r = (0..(PHRASE_LENGTH-1)).ring . # similar to an array of [0,1,2,3,4,5,6,7] that automatically resets when it reaches the end

to

r = (0..(PHRASE_LENGTH-1)).ring # similar to an array of [0,1,2,3,4,5,6,7] that automatically resets when it reaches the end

Then it should work

1 Like

Thanks @robin.newman for your input!

This code is close to where I am aiming at:

use_bpm 120

M = 4.0

PHRASE_LENGTH = 8   # set the desired length of your phrase in ticks

r = (0..(PHRASE_LENGTH-1)).ring   # similar to an array of [0,1,2,3,4,5,6,7] that automatically resets when it reaches the end

live_loop :ticker do
  t = r.tick # grabs next value in ticker and regularly resets at the end
  if t != 0 # the ticker is automatically 0 again when it runs out of elements in the range, so this checks if its within the cycle.
    s = get :current_state
    set :current_state, RULES[s].choose # move the state machine
  else
    # when the else branch fires, it means its time to reset
    set :current_state, RULES[0].choose    # resets back to zeroth state
  end
  sleep M/16    # this assumes ticks are 16th notes in size
end

NOTES = [52, 62, 64, 52, 52, 55, 55]
INTERONSET = [1, 0.25, 0.5, 0.5, 0.25, 0.5, 0.25]


RULES = {
  0 => [1],
  1 => [2],
  2 => [3, 5],
  3 => [4],
  4 => [5],
  5 => [5, 0, 6],
  6 => [0]
}

live_loop :inst1 do
  s = get :current_state
  play NOTES[s]
  sleep INTERONSET[s]
end

live_loop :drms do
  sample :drum_bass_hard
  sleep 1
  sample :drum_snare_soft
  sample :drum_cymbal_open, amp: rrand(0.01, 0.07)
  sleep 1
end

However, it´s not working exactly the way it should:

  • the note assigned to state 0 is not played when the cycle is reset, instead, the note for state 1 is played.

  • to solve this, I changed the resetting:

    else
      # when the else branch fires, it means its time to reset
      set :current_state, RULES[6].choose    # resets back to zeroth state
    end
    

…now the right note is played, unfortunately not always exactly on the first beat of the new cycle, but at varied positions…

Would be great to solve this - it´s such an interesting option to combine aspects of indeterminacy and and exact precision!

Does putting a small sleep like

sleep M/64
just before the start of live_loop :inst
(which will slightly delay both this loop and the drms loop help?
Otherwise it may be that you are trying to get the current_state at the same time as it is being changed.

I also added a line
puts "ticker, current state #{t} #{get(:current_state)}"
just before the sleep M/16
at the end of the ticker loop to help see what is going on, and tried things at a slower tempo.

Thanks @robin.newman! It was def. that extra dot that broke it. Must’ve gotten in there during the copy-paste into the forum.

@Gisbert what robin suggests is a good idea, re: letting the state get updated slightly ahead of being accessed by 1/64th note or something along those lines. That would ensure that it’s state is being updated ahead of being accessed.

1 Like

Sorry, @robin.newman & @nabisco, I got sick and momentarily lack the mental capability to participate in the forum…
Did a short test run with your suggestions, it still does not seem to work, but let me get back to this in a few days when I´m out of the bed.

Hope you get better soon.

1 Like

Hey @nabisco, Hey @robin.newman,
thanks for your patience, I´m still coughing but starting to function like a normal person again :slight_smile:

What I got from your input so far adds up to this code:

 use_bpm 120

set :current_state, 0

M = 4.0

PHRASE_LENGTH = 8   # set the desired length of your phrase in ticks

r = (0..(PHRASE_LENGTH-1)).ring   # similar to an array of [0,1,2,3,4,5,6,7] that automatically resets when it reaches the end

live_loop :ticker do
  t = r.tick # grabs next value in ticker and regularly resets at the end
  if t != 0 # the ticker is automatically 0 again when it runs out of elements in the range, so this checks if its within the cycle.
    s = get :current_state
    set :current_state, RULES[s].choose # move the state machine
  else
    # when the else branch fires, it means its time to reset
    set :current_state, RULES[0].choose    # resets back to zeroth state
  end
  puts "ticker, current state #{t} #{get(:current_state)}"
  sleep M/16    # this assumes ticks are 16th notes in size
end

NOTES = [52, 62, 64, 52, 52, 55, 55]
INTERONSET = [1, 0.25, 0.5, 0.5, 0.25, 0.5, 0.25]


RULES = {
  0 => [1],
  1 => [2],
  2 => [3, 5],
  3 => [4],
  4 => [5],
  5 => [5, 0, 6],
  6 => [0]
}

sleep M/64
live_loop :inst1 do
  s = get :current_state
  play NOTES[s]
  sleep INTERONSET[s]
end

live_loop :drms do
  sample :drum_bass_hard
  sleep 1
  sample :drum_snare_soft
  sample :drum_cymbal_open, amp: rrand(0.01, 0.07)
  sleep 1
end

The first thing that springs to my attention is that this code results in the state 1 being heard first - instead of state 0 which should be first. Any ideas on why that is? This did not occur with previous versions of the code posted above.

Cheers!