Local variable in live loop

Hi,
I understand that there are these two idioms “in_thread do loop do whatever end end” and “live_loop do whatever end” taking care of thread creation and looping all in one fell swoop.
I am wondering: How can I create a thread-local variable in a live loop AND INITIALIZE it before looping? As it looks to me I would have to write:

in_thread(name: :thread_name) do
idx = 42
loop do
whatever
end
end

because in a live_loop, the variable would be initialised on every cycle. But the “in_thread do loop do end end” idiom doesn’t support live coding, meaning I have to stop and restart playback every time I make a change to my loop. What can I do here?
Cheers,
Martin

Most of the time, I’m sure a bunch of us haven’t really worried about that, since initialising a few variables is still very cheap!

An alternative is to work with the Time-State system.
This allows data to be safely available across threads, without the risk of race conditions etc. For example, you could create and set a variable in the main ‘body’ of the composition, and then access it from within a live_loop.
See all of chapter 10 in the tutorial for details :slightly_smiling_face:
https://sonic-pi.net/tutorial.html#section-10

I have already read about the time state but don’t think this does what I want. Let me explain again:
I have a live loop that has a local variable (invisible to any other live loop/thread). When the live loop starts, this variable must be initialised to a certain value (before entering the loop). While looping, this variable gets updated (possibly on every cycle). The following must NOT happen:

  1. the variable must not be initialised again on each iteration
  2. during playback, the variable must not be initialised again when play is pressed to update the ongoing composition

Take another look at my snippet:

in_thread(name: :thread_name) do
idx = 42 #initialize ONLY once during playback!!!
loop do
whatever
idx = idx + 3
end
end

The problem with the in_thread approach is it doesnt lend itself to live coding.

Thanks,
Martin

Sure.
In fact, Time-State is great for this situation, but (sorry) I did not mention the other piece of the puzzle: defonce. With that function, we can initialise your variable, and the function is only ever evaluated once. (Per Sonic Pi session).
Here:

defonce :once do 
  set(:idx, 42)
  puts 'test'
end

live_loop :foo do
  once
  set(:idx, get(:idx) + 3)
  puts get(:idx)
  sleep 2
end

You will notice when running this, that idx increments as expected, and ‘test’ is printed out only once.

(Incidentally, if you can when sharing code examples, it would be handy to give them code formatting as you can see here, by adding an extra blank line before and after the code, and placing three backticks ``` on these lines. Makes it easy to read :grinning_face_with_smiling_eyes: )

Hopefully the defonce clue is what you need :slight_smile:

Hmm, what am I doing wrong:

defonce :steps_once do
  set(:steps_idx, 0)
  puts 'set!!!', idx
  sleep 1
end

live_loop :steps do
  steps_once
  
  idx = get(:steps_idx)
  puts 'idx', get(:steps_idx)
  sleep 0.25
  idx = idx + 1;
  set(:steps_idx, idx)
end

This keeps printing ‘nil’ for steps_idx, and I neve see the message from the defonce

Try this. Works for me.

defonce :steps_once do
  set :steps_idx, 0
  puts 'set!!! idx 0'
  sleep 1
end
set :steps_idx,0
live_loop :steps do
  steps_once
  
  idx = get(:steps_idx)
  puts 'idx', get(:steps_idx)
  sleep 0.25
  idx = idx + 1;
  set :steps_idx, idx
end

you had an error using puts 'set!!!', idx as idx not defined.

sorry, I am tired today …

I just found out that defonce still does not do what I want: defonce will refrain from redefining even if I press play again after previously STOPPING playback. In this case, I need the variable to be reinitialized :frowning:
I only want it not to be reinitialized if I press play again DURING playback …

Does a named thread not work with live coding? In the Live Coding Fundamentals section of the tutorial, it introduces the concept with named threads.

Well as I already wrote, a named thread with a loop inside does not seem to be entirely the same as a live loop because it is not reevaluated if I press play again during playback
I don’t really understand why defonce does not execute again after playback was halted. For me, halting and restarting playback is like: ok, now everything from the top. EVERYTHING
My workaround idea is this

ovr = false

defonce :steps_once, override: ovr do
  set :steps_idx, 0
  puts 'set!!! idx 0'
  sleep 1
end

Now everytime I stop playback I need to make sure I set ovr to true and after restarting playback, I need to set it back to false again for live coding …
Not nice but seems to be working!

The key here is that yes, the contents of the thread itself are not reevaluated, but the contents of an external function that the loop calls can be.
Here is an image of a helpful example from the Lang section of the documentation, on in_thread:

Sam or others may have more to say on the subject, but as far as I see it, the define once per Sonic Pi session is a specific design decision.
(My personal thought on it: A significant aspect of Sonic Pi’s purpose is its enablement of live coding. In a live performance, things are often very much fluid, evolving over time. If, for some unexpected reason, it’s necessary to completely stop the performance, I’d say it’s reasonable that we wouldn’t want the code to, for example, suddenly reset to some default value when we get the performance running again - but rather continue on where it left off!).

1 Like

As an aside, I’m curious about the goal of your composition/code! Would love to hear a rundown of your project if you’re willing. It might help me to understand your concerns about stopping and restarting the code :slight_smile:

Maybe I got you wrong, but my experiment is not working as you describe. Suppose I want to accomplish a step sequencer-ish behaviour. I have one thread that marks every sixteenth note:

# Welcome to Sonic Pi v2.10

samples = "../etc/samples/"
set :bpm, 115
set :bdidx, 5
set :sdidx, 0
set :hidx, 7

x = (ring 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16)

define :steps_fn do
  idx = 0
  loop do
    use_bpm get[:bpm]
    
    cue x[idx];
    idx = idx + 1;
    sleep 0.25
  end
end

in_thread(name: :steps_thread) do
  sleep 0.5
  steps_fn
end

Next I want one thread to do the kick drum:

define :kick_fn do
  idx = 0
  loop do
    sync x[idx];
    sample samples, "bd_", get[:bdidx]
    idx = idx + 4;
  end
end

in_thread(name: :kick_thread) do
  kick_fn
end

If I understand you right, then changing line

idx = idx + 4;

to

idx = idx + 2;

during playback and then hitting play again should cause the kick to sound on every eighth note (instead of every fourth).
But this is not what’s happening.
What I do want is execute kick_fn from the top when playback is restarted after previously being stopped, i.e. I want idx to be reset to 0.
Hope this makes it clear.
Cheers,
Martin

By the way, I wouldn’t call this a composition. I am currently just playing around with Sonic Pi as it has made me curious for years now, and I’m just trying emulate what I’m most comfortable with, and that’s step sequencers :slight_smile:

Sure :slight_smile: Remember, ordinary loops are black holes for code execution - once the flow of control enters the loop in a particular thread, it won’t leave until the program is stopped. Looking at your example:

in_thread(name: :kick_thread) do
  kick_fn # <-- 1. Sonic Pi ends up here, and calls this function
end

Then,

define :kick_fn do
  idx = 0
  loop do # <-- 2. Sonic Pi ends up cycling through this loop endlessly
    sync x[idx];
    sample samples, "bd_", get[:bdidx]
    idx = idx + 4;
  end
end

But back in our kick thread,

in_thread(name: :kick_thread) do
  kick_fn # <-- 3. Since the loop in this function never ends, the first call to this function never exits.
end

This last point is important - since the first call to the function never exits, the thread doesn’t have a chance to pick up any changes made to the kick function!

Instead, you want the loop to be inside the thread - because you want the function to be called multiple times :slight_smile:

I think this does what you were trying to do with defonce. If you hit run whilst it’s already running :idx won’t reset, but if you stop and re-run it will. (Note that doing it this way only works with time-state and not variables.)

live_loop :foo do
  set(:idx, 60) if beat == 0
  puts "foo: #{get(:idx)}"
  set(:idx, get(:idx)+2)
  sleep 2
end

But I don’t think this counts as a “thread-local variable”. Check it out:

live_loop :foo do
  set(:idx, 60) if beat == 0
  puts get(:idx)
  set(:idx, get(:idx)+4)
  sleep 2
end

puts "Outside of thread: #{get(:idx)}"

live_loop :bar do
  sleep 4
  puts "From another thread: #{get(:idx)}"
end

So, you can get the same effect in a clearer way by initialising outside of threads, whether you use variables or Time-State.

idx = 60
live_loop :foo do
  puts idx
  idx = idx +2
  sleep 2
end

Maybe tick does what you need? I don’t think this is the way it’s supposed to be used, but it seems to tick all your boxes.

live_loop :foo do
  tick_set :idx, 60 if beat == 0
  puts look(:idx)
  tick_set :idx, look(:idx)+ 2
  sleep 2
end

puts "Outside of thread: #{look(:idx)}" #Results in 0

live_loop :bar do
  sleep 4
  puts "From another thread: #{look(:idx)}" #Results in 0
end

(By the way, tick has a step option, but when I tested the first step was still 1. Not sure what I did wrong.)

I have occasionally wanted a way to reset everything to a blank slate, without having to close and reopen Sonic Pi.

Given the focus on being deterministic (so a given piece should sound the same wherever it is played), it feels like something that would make sense. Otherwise the code could be affected by defined variables, time state, or other things left over from previously run code.

Great, now this does exactly what I want:

samples = "../etc/samples/"
set :bpm, 115
set :bdidx, 5
set :sdidx, 0
set :hidx, 7

x = (ring 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16)

define :steps_fn do |idx|
  use_bpm get[:bpm]
  
  cue x[idx];
  idx = idx + 1;
  sleep 0.25
  
  return idx
end

in_thread(name: :steps_thread) do
  sleep 0.5
  idx = 0
  loop do
    idx = steps_fn idx
  end
end

define :kick_fn do |idx|
  sync x[idx];
  sample samples, "bd_", get[:bdidx]
  idx = idx + 4;
  
  return idx
end

in_thread(name: :kick_thread) do
  idx = 0
  loop do
    idx = kick_fn idx
  end
end

By the way, is there a more elegant way to refer to the default samples other than by specifying path “…/etc/samples/” (see example)?
Cheers,
Martin

Well actually I don’t want the idx variable of a particular thread like kick or snare to be globally visible. But yes I know, what I’m doing is somehow related to tick, and I was also wondering if it could somehow be done by ticking…
The thing is of course, my idx variable is not always meant to be initialized to zero, that totally depends on the track. Think about the snare drum, which would initialize idx to 8 and increase by 8 to sound on every second beat, for example …

Fantastic! Glad it works for you :grinning_face_with_smiling_eyes:

See the Lang documentation for: sample_names and sample_groups :slight_smile: