Simulating imitative duo improvisation


#1

When researching improvisation with other performers, we practiced a duo score, using imitation and variation. This is my take at simulating this form of interaction.

For more context, look here:
http://www.phyla.info/?p=2729

When running it for the first time, I get an error. When I run it again, it just works, any ideas on that? Also, any ideas how to achieve this with less code are very welcome.


t = Time.now.to_i#Setting the random seed according to the time
use_random_seed t#Results in different music every time
puts rand

use_synth :sine#choosing the sine synth


live_loop :variables do#defining the variables that can be changed during performance
  set :variation, 0.25#controls the amount of variation over time: 0 = no variation, 1 = maximum variation
  set :individuality, 0.5#individuality 0 results in gradual variation of one sound, maximum individuality of 1 results in two alternating sounds, each varied over time
  set :regularity, 0.6#high regularity (1 being the highest) results in less variation of inter-onset durations
  set :sprung, rrand(0.8, 1.2)#determining how far a varied pitch may differ from the previous one
  set :atk, 0.2#setting the maximum duration of the attack of the sounds
  set :rel, 2.5#setting the maximum duration of the release of the sounds
  sleep 5
end

#Setting the starting variables
#for two alternative strands of sounds
set :pitch1, rrand(45, 85)#pitch
set :lautst1, rrand(0.1, 1.2)#dynamics
set :einschwing1, rrand(0, get[:atk])#attack
set :ausklang1, rrand(0.1, get[:rel])#release
set :freq1, rrand(30, 130)#low-pass filter
set :wartezeit1, rrand(0.08, 3.8)#inter-onset duration

set :pitch2, rrand(45, 85)
set :lautst2, rrand(0.1, 1.2)
set :einschwing2, rrand(0, get[:atk])
set :ausklang2, rrand(0.1, get[:rel])
set :freq2, rrand(30, 130)
set :wartezeit2, rrand(0.08, 3.8)

set :action, [get[:pitch1], get[:pitch2]].choose#choosing which pitch to use first
set :zeit, [get[:wartezeit1], get[:wartezeit2]].choose#choosing which inter-onset intervall to use first

with_fx :reverb do
  live_loop :dialogue do
    if rand < get[:variation]#determining wether any variations on sound parameters should occur
      if rand < get[:variation]#if so, should the pitch be redifined?
        set :pitch1, rrand(45, 85)#if so, a new pitch is set randomly
      end
      if rand < get[:variation]#the same procedure applies to dynamics...
        set :lautst1, rrand(0.1, 1.2)
      end
      if rand < get[:variation]
        set :einschwing1, rrand(0, get[:atk])#...attack...
      end
      if rand < get[:variation]
        set :ausklang1, rrand(0.1, get[:rel])#...release...
      end
      if rand < get[:variation]
        set :freq1, rrand(30, 130)#...and low-pass filter.
      end
    end
    if rand > get[:regularity]#determining wether any variations regarding inter-onset intervall(that is the time between two sounds) should occur
      if rand > get[:regularity]#if so, should the first inter-onset intervall...
        if one_in(2)
          set :wartezeit1, get[:zeit]*get[:sprung]#...be multiplied...
        else
          set :wartezeit1, get[:zeit]/get[:sprung]#...or be divided?
      end
    end
    if rand > get[:regularity]#if so, should the second inter-onset intervall...
      if one_in(2)
        set :wartezeit2, get[:zeit]*get[:sprung]#...be multiplied...
      else
        set :wartezeit2, get[:zeit]/get[:sprung]#...or be divided?
        end
      end
    end
    if rand < get[:individuality]#individuality 0 results in gradual variation of one pitch, maximum individuality of 1 results in two alternating pitches
      set :action, get[:pitch1]
    end
    play get[:action], amp: get[:lautst1], attack: get[:einschwing1], release: get[:ausklang1], cutoff: get[:freq1], pan: 0.8#a sound is played
    if rand > get[:regularity]#regularity 0 results in constant redefinition of inter-onset intervalls, maximum regularity of 1 in very gradually changing inter-onset intervalls
      set :zeit, get[:wartezeit1]
    end
    sleep get[:zeit]#the inter-onset intervall is applied
    if rand < get[:variation]#the above code within the live_loop :dialogue defined the first of two possible strands of sound varied over time
      if rand < get[:variation]#what follows from here is a duplicate defining the second strand
        set :pitch1, rrand(45, 85)#I suppose there is a more efficient way to code this, any suggestions?
      end
      if rand < get[:variation]
        set :lautst1, rrand(0.1, 1.2)
      end
      if rand < get[:variation]
        set :einschwing1, rrand(0, get[:atk])
      end
      if rand < get[:variation]
        set :ausklang1, rrand(0.1, get[:rel])
      end
      if rand < get[:variation]
        set :freq1, rrand(30, 130)
      end
    end
    if rand > get[:regularity]
      if rand > get[:regularity]
        if one_in(2)
          set :wartezeit1, get[:zeit]*get[:sprung]
        else
          set :wartezeit1, get[:zeit]/get[:sprung]
      end
    end
    if rand > get[:regularity]
      if one_in(2)
        set :wartezeit2, get[:zeit]*get[:sprung]
      else
        set :wartezeit2, get[:zeit]/get[:sprung]
        end
      end
    end
    if rand < get[:individuality]
      set :action, get[:pitch2]
    end
    play get[:action], amp: get[:lautst2], attack: get[:einschwing2], release: get[:ausklang2], cutoff: get[:freq2], pan: -0.8
    if rand > get[:regularity]
      set :zeit, get[:wartezeit2]
    end
    sleep get[:zeit]
  end
end´´´

#2

Hi Gisbert,

just a short aside: This forum supports markdown mode syntax. You can create highlighted code by placing triple backticks ``` before and after the code block. This will make it much better readable; also the indentation (if there is any) will be displayed.


#3

@Martin Thanks, just forgot it…


#4

Hi there @gisbert,

if you look at the error:

Runtime Error: [buffer 6, line 23] - TypeError
Thread death!
 nil can't be coerced into Integer
/Applications/Sonic Pi.app/app/server/ruby/lib/sonicpi/lang/core.rb:2949:in `-'
/Applications/Sonic Pi.app/app/server/ruby/lib/sonicpi/lang/core.rb:2949:in `rrand'
workspace_six:23:in `block (2 levels) in __spider_eval'
/Applications/Sonic Pi.app/app/server/ruby/lib/sonicpi/runtime.rb:1043:in `block (2 levels) in __in_thread'

You’ll see that the error is on line 23. This is further emphasised with an arrow next to the line number:

If we look at this line in isolation:

set :einschwing1, rrand(0, get[:atk])#attack

We’ll see that it’s trying to get a value using the key :atk. If this particular value hasn’t been set already it will return a nil value (meaning no-known-value). This nil value will be used as the second value in the call to rrand, so in effect we’re actually trying to call the following (which clearly makes no sense :slight_smile:)

rrand(0, nil)

The question therefore is why isn’t the value with key atk being set?

If you look above line 23, you do indeed find a call to set :atk, 0.2, however it’s within a live loop. This means that the call to set is in a different thread to the thread that is calling get. In most programming languages this would produce undefined behaviour:

  • sometimes the call to set will be executed first and the get will therefore work
  • sometimes the call to get will be executed first and therefore will return nil and fail.

Given that Sonic Pi does not want undefined (or non-deterministic) behaviour like this, I have implemented the set and get system so that it does the same thing every time regardless of threads. The current implementation sees new threads as being after the parent thread in time, so as you call set in a new thread, it actually is logged as after the call to get in the original parent thread. This is why it fails the first time, but works the second, because by the time you run the code again, the set has executed (in the previous run) and the value is remembered across runs.

This is all very tricky stuff, but threads and state is actually very hard to reason about. I’m trying my best to try and make this stuff simple, and removing random behaviour is step one of this solution, but there might be ways to massage the set and get system to being a little less surprising whilst still being deterministic. I’ll definitely think hard about this - thanks for raising the issue.

However, for now, the fact that there’s reliably an error first time round is actually correct behaviour and the solution is to ensure you set all your state before you get it :slight_smile:


#5

Hey @samaaron

Many thanks for your fast and detailed explanation!

I´m still not sure how to solve the issue. I set :atk within a live_loop because I want to be able to change it during performance. So defining :atk outside of a live_loop is not an option and no change can be made at that side of the problem.

Line 23, where get[:atk]) occurs is not within a live_loop.

set :einschwing1, rrand(0, get[:atk])#attack

It´s function is to set the :einschwing1 variable just for the first time. There is no repetition required, thus there is no use for a live_loop. If I get you right, that makes it part of the parent thread, which is executed first, while the live_loop :variables is executed later. Right?

While I fully understand the diagnosis, I´m not sure how to solve the problem in this particular case. Putting line 23 (and also 24) in a different thread, thus removing it from the parent thread caused other errors…

Anyway, executing twice doesn´t bother me much.


#6

you can still set :atk in your live loop, provided that you first set an initial value in the main body of the program at the beginning. This will be picked up in line 23, so no error occurs, and subsequently if :atk is altered elsewhere in a live loop it will use the new value current when it next accesses get(:atk)

you could comment out this line at the beginning before re-running (in a performance) so that it does not alter atk again. Once atk has been set once, it will remain in existence (with the latest value set), until you quit Sonic Pi, so the initial line is not needed for subsequent runs.


#7

Thanks @robin.newman!

What you describe is exactly what I am trying to do.

Line 23 is meant to

first set an initial value in the main body of the program at the beginning.

I guess I need to aquire a more detailed knowledge of the order of execution within Sonic Pi.