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 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:
the variable must not be initialised again on each iteration
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.
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 )
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
I only want it not to be reinitialized if I press play again DURING playback …
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!).
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
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
Sure 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
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.
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 …