Skip to content

Commit a8e4d5b

Browse files
committed
Lang - reluctantly roll back support for changing the FX context of a live loop
This unfortunately introduced too many subtle bugs. I think I either need to have another go from scratch - or more likely - the whole threading model needs *properly* re-designing and re-building :-(
1 parent c0fe82f commit a8e4d5b

File tree

6 files changed

+37
-284
lines changed

6 files changed

+37
-284
lines changed

CHANGELOG.md

+1-65
Original file line numberDiff line numberDiff line change
@@ -38,75 +38,12 @@
3838

3939
## version 5 'Live Loop'
4040

41-
This new release of Sonic Pi introduces a breaking change in the interaction between `live_loop`s and `with_fx` that significantly improve using them together in a live performance. There has also been a significant overhaul of the shortcuts in the GUI to allow you to change them. You can now choose between the default (called Emacs Live), Windows or Mac modes as well as customise them yourself. There's also a wonderful new function for working with tuplets designed by Dago Sondervan.
42-
43-
Before we get to the full breakdown list of all the changes, let's take a moment to explore the breaking change with `live_loop` and `with_f`:
44-
45-
### Live Loop Now Behaves like Live Audio
46-
47-
Consider this code:
48-
49-
```ruby
50-
with_fx :reverb do
51-
live_audio :moog
52-
end
53-
```
54-
55-
When you run this code, the audio from the sound card is played through reverb. Now, what if you're in the middle of a performance and wanted to change it to distortion? Easy, you just change the code to this and hit run again:
56-
57-
```ruby
58-
with_fx :distortion do
59-
live_audio :moog
60-
end
61-
```
62-
63-
The live_audio magically moves out of the Reverb context and into the Distortion. There's no need to stop and start the code - it all happens live and is completely seemless. As an added bonus, behind the scenes the Reverb figures that the live_audio has moved on and automatically garbage collects itself freeing the resources. The crowd dances to the distorted Moog synth.
64-
65-
Now consider this:
66-
67-
```ruby
68-
with_fx :reverb do
69-
live_loop :foo do
70-
play 70
71-
sleep 0.5
72-
end
73-
end
74-
```
75-
76-
As with the live_audio - this works as expected, the sound from the live loop is played through Reverb. However, if you changed the reverb to Distortion and hit run again, it did not change the FX over to the Distortion. The live loop wass stuck in the Reverb FX it was originally created in. Forever. This wasn't ideal and confused a lot of people.
77-
78-
It unfortunately stayed like this for many years because it was an incredibly hard problem to fix due to the architecture of Sonic Pi's internals. It required deep changes to the thread management system, the live loop implementation, and with_fx and its auto FX garbage collection system. Each of these is tricky enough to work on independently - but the intersection of them all is a thing of nightmares.
79-
80-
Well, now more because the work is done and this behaves like live loop. Swap the code to this, hit the run whilst it's still playing and you'll hear the `:foo` `live_loop` played through distortion:
81-
82-
```ruby
83-
with_fx :distortion do
84-
live_loop :foo do
85-
play 70
86-
sleep 0.5
87-
end
88-
end
89-
```
90-
91-
You can even change the FX to one with a control parameter and it will still just work:
92-
93-
```ruby
94-
with_fx :lpf do |fx|
95-
live_loop :foo do
96-
control fx, cutoff: rrand(70, 120)
97-
synth :square, note: :e3, release: 0.2
98-
sleep 0.25
99-
end
100-
end
101-
```
102-
103-
When you're working with fast spinning `live_loop`s it's now generally better to put your FX outside the `live_loop` as it's much more efficient. You just have one FX running the whole time, rather than creating a new FX every time the loop spins round.
41+
This new release of Sonic Pi introduces a significant overhaul of the shortcuts in the GUI to allow you to change them. You can now choose between the default (called Emacs Live), Windows or Mac modes as well as customise them yourself. There's also a wonderful new function for working with tuplets designed by Dago Sondervan.
10442

10543
### GUI
10644
* Complete overhaul of the shortcut system.
10745

10846
### Improvements
109-
* `live_loop` now moves to new FX contexts similar to `live_audio`. You can therefore now use `with_fx` both within and outside of `live_loop` and be able to change things live in a performance.
11047
* Where supported, you can now indepedently specify different input and output sound cards in the `audio-settings.toml` config file. This is done with the new options `input_sound_card_name = ""` and `output_sound_card_name = ""`. Note, you still have to ensure that the sample rate is the same for input and output.
11148
* Incoming OSC bundles are now supported. Timestamps are ignored (if OSC scheduling is a commonly requested feature this could be added in the future). This increases compatability with software which exlusively sends OSC in budle format such as TouchDesigner.
11249

@@ -118,7 +55,6 @@ When you're working with fast spinning `live_loop`s it's now generally better to
11855
* Two new ride cymbals `:ride_tri` and `ride_via`.
11956
* New hi-hat `:hat_len`.
12057

121-
12258
### Translations
12359

12460
* Improvements to the French, German, Korean, Polish, Portuguese, Portguguese (Brazil), Russian and Turkish, translations.

app/server/ruby/lib/sonicpi/lang/core.rb

+12-121
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
require_relative "../version"
1616
require_relative "../util"
1717
require_relative "../runtime"
18-
require_relative "../promise"
1918
require_relative "western_theory"
2019

2120
## TODO: create _* equivalents of all fns - for silent (i.e computation) versions
@@ -2297,130 +2296,22 @@ def live_loop(name=nil, *args, &block)
22972296
raise ArgumentError, "Live loop block must only accept 0 or 1 args"
22982297
end
22992298

2300-
start_live_loop = Promise.new
2301-
parent_thread = Thread.current
2302-
mk_live_loop = lambda do
2303-
in_thread(name: ll_name, delay: delay, sync: sync_sym, sync_bpm: sync_bpm_sym) do
2304-
2305-
start_live_loop.get
2306-
__system_thread_locals.set_local :sonic_pi_local_live_loop_auto_cue, auto_cue
2307-
if args_h.has_key?(:init)
2308-
res = args_h[:init]
2309-
else
2310-
res = 0
2311-
end
2312-
use_random_seed args_h[:seed] if args_h[:seed]
2313-
2314-
loop do
2315-
new_bus = nil
2316-
new_group = nil
2317-
new_parent_t = nil
2318-
completed_prom = nil
2319-
new_thread_moved_prom = nil
2320-
new_thread_moved_ack_prom = nil
2321-
__system_thread_locals.get(:sonic_pi_local_spider_thread_move_mut).synchronize do
2322-
new_bus, new_group, new_parent_t, completed_prom, new_thread_moved_prom, new_thread_moved_ack_prom = __system_thread_locals.get :sonic_pi_local_live_loop_move_to_bus_and_parent_t
2323-
2324-
# immediately reset for the next move
2325-
__system_thread_locals.set_local :sonic_pi_local_live_loop_move_to_bus_and_parent_t, nil
2326-
end
2327-
if new_bus
2328-
moved_prom = __system_thread_locals.get :sonic_pi_local_spider_thread_moved
2329-
ack_prom = __system_thread_locals.get :sonic_pi_local_spider_thread_moved_ack
2330-
# update the context
2331-
# reset tracker and send old tracker with ack
2332-
tracker = __current_tracker
2333-
__system_thread_locals.set_local(:sonic_pi_local_tracker, nil)
2334-
__system_thread_locals.set(:sonic_pi_mod_sound_synth_out_bus, new_bus)
2335-
__system_thread_locals.set(:sonic_pi_mod_sound_job_group, new_group)
2336-
moved_prom.deliver! tracker
2337-
ack_prom.get
2338-
__system_thread_locals.set_local :sonic_pi_local_spider_thread_moved, new_thread_moved_prom
2339-
__system_thread_locals.set_local :sonic_pi_local_spider_thread_moved_ack, new_thread_moved_ack_prom
2340-
__system_thread_locals.set_local :sonic_pi_local_live_loop_move_to_bus_and_parent_t, nil
2341-
new_job_id = __system_thread_locals(new_parent_t).get(:sonic_pi_spider_job_id)
2342-
job_subthread_move_named(Thread.current, new_job_id, ll_name)
2343-
__remove_thread_from_parent_subthreads!(Thread.current)
2344-
__move_thread_to_new_parent!(Thread.current, new_parent_t)
2345-
completed_prom.deliver! :moved
2346-
end
2347-
__live_loop_cue name if __system_thread_locals.get :sonic_pi_local_live_loop_auto_cue
2348-
res = send(ll_name, res)
2349-
end
2299+
in_thread(name: ll_name, delay: delay, sync: sync_sym, sync_bpm: sync_bpm_sym) do
2300+
__system_thread_locals.set_local :sonic_pi_local_live_loop_auto_cue, auto_cue
2301+
if args_h.has_key?(:init)
2302+
res = args_h[:init]
2303+
else
2304+
res = 0
2305+
end
2306+
use_random_seed args_h[:seed] if args_h[:seed]
2307+
loop do
2308+
__live_loop_cue name if __system_thread_locals.get :sonic_pi_local_live_loop_auto_cue
2309+
res = send(ll_name, res)
23502310
end
23512311
end
23522312

2353-
live_loop_thread = mk_live_loop.call
23542313
st = sthread(ll_name)
2355-
if live_loop_thread.alive?
2356-
2357-
# live loop was created - this was the first time
2358-
# let it start normally
2359-
start_live_loop.deliver! :go
2360-
else
2361-
new_out_bus = current_out_bus
2362-
new_synth_group = current_group
2363-
in_thread do
2364-
new_thread_moved_prom = __system_thread_locals.get(:sonic_pi_local_spider_thread_moved)
2365-
new_thread_moved_ack_prom = __system_thread_locals.get(:sonic_pi_local_spider_thread_moved_ack)
2366-
move_holding_thread_prom = Promise.new
2367-
Thread.new do
2368-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "ll swapper and holding thread for #{name} #{st}"
2369-
completed_prom = Promise.new
2370-
2371-
2372-
ct1 = Thread.new do
2373-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "ll ct1 mv waiter for #{name} #{st.object_id} #{__system_thread_locals(st).get(:sonic_pi_local_thread_group)}"
2374-
__system_thread_locals(st).get(:sonic_pi_local_spider_thread_move_mut).synchronize do
2375-
2376-
_b, _g, t, p = __system_thread_locals(st).get(:sonic_pi_local_live_loop_move_to_bus_and_parent_t)
2377-
if p
2378-
## another live loop already registered a move, but didn't manage to swap in time - so we're going to clobber it
2379-
p.deliver! :clobbered, false
2380-
end
2381-
__system_thread_locals(st).set_local :sonic_pi_local_live_loop_auto_cue, auto_cue if st
2382-
## register our move
2383-
2384-
__system_thread_locals(st).set_local :sonic_pi_local_live_loop_move_to_bus_and_parent_t, [new_out_bus, new_synth_group, parent_thread, completed_prom, new_thread_moved_prom, new_thread_moved_ack_prom]
2385-
end
2386-
end
2387-
2388-
ct2 = Thread.new do
2389-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "ll ct2 join waiter for #{name} #{st.object_id} #{__system_thread_locals(st).get(:sonic_pi_local_thread_group)}"
2390-
st.join
2391-
completed_prom.deliver! :joined, false
2392-
end
2393-
2394-
moved_or_clobbered = completed_prom.get
2395-
ct1.kill
2396-
ct2.kill
2397-
2398-
if moved_or_clobbered == :moved
2399-
st_joined_or_moved_again = Promise.new
2400-
mt1 = Thread.new do
2401-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "ll join waiter for #{name} #{st.object_id} #{__system_thread_locals(st).get(:sonic_pi_local_thread_group)}"
2402-
st.join
2403-
st_joined_or_moved_again.deliver! true, false
2404-
end
2405-
2406-
mt2 = Thread.new do
2407-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "ll move waiter for #{name} #{st.object_id} #{__system_thread_locals(st).get(:sonic_pi_local_thread_group)}"
2408-
2409-
ack_p = __system_thread_locals(st).get(:sonic_pi_local_spider_thread_moved_ack)
2410-
ack_p.get
2411-
st_joined_or_moved_again.deliver! true, false
2412-
end
2413-
st_joined_or_moved_again.get
2414-
mt1.kill
2415-
mt2.kill
2416-
end
2417-
move_holding_thread_prom.deliver! true
2418-
end
2419-
tracker = new_thread_moved_prom.get
2420-
tracker.get
2421-
move_holding_thread_prom.get
2422-
end
2423-
end
2314+
__system_thread_locals(st).set_local :sonic_pi_local_live_loop_auto_cue, auto_cue if st
24242315
st
24252316
end
24262317
doc name: :live_loop,

app/server/ruby/lib/sonicpi/lang/sound.rb

+5-25
Original file line numberDiff line numberDiff line change
@@ -1814,31 +1814,9 @@ def with_fx(fx_name, *args, &block)
18141814
else
18151815
kill_delay = args_h[:kill_delay] || 1
18161816
end
1817-
1818-
subthreads.map do |st|
1819-
Thread.new do
1820-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "wfx subthread waiter for #{fx_name} #{new_bus}"
1821-
subthread_completed_or_moved = Promise.new
1822-
wt1 = Thread.new do
1823-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "wfx join waiter for #{fx_name} #{new_bus}"
1824-
st.join
1825-
__system_thread_locals(st).get(:sonic_pi_local_spider_subthread_empty).get
1826-
subthread_completed_or_moved.deliver! true
1827-
end
1828-
wt2 = Thread.new do
1829-
__system_thread_locals.set_local :sonic_pi_local_thread_group, "wfx move waiter for #{fx_name} #{new_bus}"
1830-
p = __system_thread_locals(st).get(:sonic_pi_local_spider_thread_moved)
1831-
tracker_from_moved_thread = p.get
1832-
__system_thread_locals(st).get(:sonic_pi_local_spider_thread_moved_ack).deliver! true, false
1833-
tracker_from_moved_thread.get
1834-
subthread_completed_or_moved.deliver! true
1835-
end
1836-
subthread_completed_or_moved.get
1837-
wt1.kill
1838-
wt2.kill
1839-
end
1840-
end.each do |jt|
1841-
jt.join
1817+
subthreads.each do |st|
1818+
st.join
1819+
__system_thread_locals(st).get(:sonic_pi_local_spider_subthread_empty).get
18421820
end
18431821
tracker.block_until_finished
18441822
Kernel.sleep(kill_delay)
@@ -1847,6 +1825,8 @@ def with_fx(fx_name, *args, &block)
18471825

18481826
gc_init_completed.deliver! true
18491827
end ## end gc collection thread definition
1828+
1829+
18501830
end
18511831

18521832
## Trigger new fx synth (placing it in the fx group) and

0 commit comments

Comments
 (0)