Skip to content

Commit 6543abb

Browse files
committed
Lang - introduce language support for Ableton Link
`use_bpm :link` When using the :link BPM, Sonic Pi switches all timing algorithms for that specific thread to use the shared Ableton Link metronome. Ableton Link is GPL software that implements a metronome that is automatically shared and synchronised with other pieces of link-enabled software running on the same local network. This means that with `use_bpm :link` the BPM effectively becomes dynamic and can be modified from any piece of link-enabled software on the local network. With use_bpm *all* timing functions for that thread now use Link as their ground truth with respect to time. However, whilst this does mean that the BPM becomes dynamic, functions such as `at`, `time_warp`, `density` and `use_bpm_mul` *should* continue to work as expected as we now also internally capture a notion of time density which works as a multiplier on the dynamic BPM value. Note that this commit and other related ones introduce a lot of changes to the core timing functionality. However, by default when not using the `:link` option for the BPM, it is expected that functionality should be identical. Any differences should be considered a bug. There are also likely to be quite a few kinks remaining - this has only been tested on Windows and I'm yet to do any co-located jamming with multiple link-powered audio apps so there's also a good chance that the schedule ahead functionality needs to be tweaked and possibly new latency compensation routines to be added to get everything in actual audio sync. Also, the Link functionality has to be extended before a release can be contemplated (resetting, changing BPM, etc). There also needs to be some representation in the GUI (likely using the Ableton Live metrome widget as inspiration to improve knowledge transfer and familiarity). Finally, the Link beat quantum needs to be explored and added to the API. Perhaps something like `use_bpm :link, quantum: 4`
1 parent 5b6a380 commit 6543abb

File tree

4 files changed

+147
-89
lines changed

4 files changed

+147
-89
lines changed

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

Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3563,7 +3563,7 @@ def with_cue_logging(v, &block)
35633563

35643564
def use_bpm(bpm, &block)
35653565
raise ArgumentError, "use_bpm does not work with a block. Perhaps you meant with_bpm" if block
3566-
raise ArgumentError, "use_bpm's BPM should be a positive value. You tried to use: #{bpm}" unless bpm > 0
3566+
raise ArgumentError, "use_bpm's BPM should be a positive value or :link. You tried to use: #{bpm}" unless bpm == :link || (bpm.is_a?(Numeric) && bpm > 0)
35673567
__change_spider_bpm!(bpm)
35683568
end
35693569
doc name: :use_bpm,
@@ -3674,11 +3674,10 @@ def with_bpm(bpm, &block)
36743674
def with_bpm_mul(mul, &block)
36753675
raise ArgumentError, "with_bpm_mul must be called with a do/end block. Perhaps you meant use_bpm_mul" unless block
36763676
raise ArgumentError, "with_bpm_mul's mul should be a positive value. You tried to use: #{mul}" unless mul > 0
3677-
current_bpm = __get_spider_bpm
3678-
new_bpm = current_bpm * mul.to_f
3679-
__change_spider_bpm!(new_bpm)
3680-
res = block.call
3681-
__change_spider_bpm!(current_bpm)
3677+
res = nil
3678+
__with_spider_time_density(mul) do
3679+
res = block.call
3680+
end
36823681
res
36833682
end
36843683
doc name: :with_bpm_mul,
@@ -3710,7 +3709,7 @@ def use_bpm_mul(mul, &block)
37103709
raise ArgumentError, "use_bpm_mul must not be called with a block. Perhaps you meant with_bpm_mul" if block
37113710
raise ArgumentError, "use_bpm_mul's mul should be a positive value. You tried to use: #{mul}" unless mul > 0
37123711
new_bpm = __get_spider_bpm * mul.to_f
3713-
__change_spider_bpm!(new_bpm)
3712+
__layer_spider_time_density!(mul)
37143713
end
37153714
doc name: :use_bpm_mul,
37163715
introduced: Version.new(2,3,0),
@@ -3738,9 +3737,7 @@ def density(d, &block)
37383737
raise ArgumentError, "density must be called with a do/end block." unless block
37393738
raise ArgumentError, "density must be a positive number. Got: #{d.inspect}." unless d.is_a?(Numeric) && d > 0
37403739
reps = d < 1 ? 1.0 : d
3741-
prev_density = __thread_locals.get(:sonic_pi_local_spider_density) || 1.0
3742-
__thread_locals.set_local(:sonic_pi_local_spider_density, prev_density * d)
3743-
with_bpm_mul d do
3740+
__with_spider_time_density(d) do
37443741
if block.arity == 0
37453742
reps.times do
37463743
block.call
@@ -3751,7 +3748,6 @@ def density(d, &block)
37513748
end
37523749
end
37533750
end
3754-
__thread_locals.set_local(:sonic_pi_local_spider_density, prev_density)
37553751
end
37563752
doc name: :density,
37573753
introduced: Version.new(2,3,0),
@@ -4138,31 +4134,18 @@ def sleep(beats)
41384134
__change_spider_beat_and_time_by_beat_delta!(beats)
41394135

41404136
sat = current_sched_ahead_time
4141-
new_vt = __get_spider_time
4142-
now = Time.now
4137+
new_vt = __get_spider_time.to_r
4138+
now = Time.now.to_f
41434139

41444140
in_time_warp = __system_thread_locals.get(:sonic_pi_spider_in_time_warp)
41454141

4146-
if now - (sat + 0.5) > new_vt
4147-
raise TimingError, "Timing Exception: thread got too far behind time"
4142+
if (now - (sat + 0.5)) > new_vt
4143+
4144+
# raise TimingError, "Timing Exception: thread got too far behind time
4145+
__delayed_serious_warning "Serious timing error. Too far behind time..."
41484146
elsif (now - sat) > new_vt
4149-
# TODO: Empirical tests to see what effect this priority stuff
4150-
# actually has on typical workloads
4151-
4152-
# Hard warning, system is too far behind, expect timing issues.
4153-
p = Thread.current.priority
4154-
p += 10
4155-
p = 100 if p < 100
4156-
p = 150 if p > 150
4157-
Thread.current.priority = p
41584147
__delayed_serious_warning "Timing error: can't keep up..."
41594148
elsif now > new_vt
4160-
# Soft warning, system should work correctly, but is currently behind
4161-
p = Thread.current.priority
4162-
p += 5
4163-
p = 50 if p < 50
4164-
p = 150 if p > 150
4165-
Thread.current.priority = p
41664149
## TODO: Remove this and replace with a much better silencing system which
41674150
## is implemented within the __delayed_* fns
41684151
unless __thread_locals.get(:sonic_pi_mod_sound_synth_silent) || in_time_warp
@@ -4175,7 +4158,8 @@ def sleep(beats)
41754158
# However, do make sure the vt hasn't got too far ahead of the real time
41764159
# raise TimingError, "Timing Exception: thread got too far ahead of time" if (new_vt - 17) > now
41774160
else
4178-
Kernel.sleep new_vt - now
4161+
t = (new_vt - now).to_f
4162+
Kernel.sleep t
41794163
end
41804164
end
41814165

@@ -4317,13 +4301,17 @@ def sync_event(*args)
43174301

43184302
__system_thread_locals.set(:sonic_pi_spider_synced, true)
43194303

4320-
## only need to use se for beat and time if not in :link or :metro bpm
43214304
__change_spider_time_and_beat!(se.time, se.beat)
43224305
__system_thread_locals.set_local :sonic_pi_local_last_sync, se
43234306

43244307
if bpm_sync
4325-
bpm = se.bpm <= 0 ? 60 : se.bpm
4326-
use_bpm bpm
4308+
if se.bpm == :link
4309+
use_bpm :link
4310+
elsif se.bpm.is_a?(Numeric)
4311+
use_bpm se.bpm
4312+
else
4313+
raise StandardError, "Incorrect bpm value. Expecting either :link or a number such as 120"
4314+
end
43274315
end
43284316

43294317
run_info = ""

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3909,7 +3909,7 @@ def validate_if_necessary!(info, args_h)
39093909

39103910
def ensure_good_timing!
39113911
return true if __thread_locals.get(:sonic_pi_mod_sound_disable_timing_warnings)
3912-
raise "Timing Exception: thread got too far behind time." if time_diff > 1.1
3912+
# raise "Timing Exception: thread got too far behind time." if time_diff > 1.1
39133913
end
39143914

39153915
def time_diff
@@ -3918,7 +3918,7 @@ def time_diff
39183918
vt = __get_spider_time
39193919
sat = current_sched_ahead_time
39203920
compensated = (Time.now - sat)
3921-
compensated - vt
3921+
(compensated - vt).to_f
39223922
end
39233923

39243924
def in_good_time?(error_window=0)

app/server/ruby/lib/sonicpi/runtime.rb

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -89,58 +89,127 @@ def load_snippets(path=snippets_path, quiet=false)
8989
end
9090
end
9191

92+
93+
9294
### Start - Spider Time Management functions
9395
###
94-
def __change_spider_time_and_beat!(new_time, new_beat)
95-
__system_thread_locals.set :sonic_pi_spider_time, new_time.freeze
96-
__system_thread_locals.set(:sonic_pi_spider_beat, new_beat)
96+
def __layer_spider_time_density!(density)
97+
new_density = __get_spider_time_density * density
98+
__change_spider_time_density!(new_density)
99+
end
100+
101+
def __change_spider_time_density!(new_density)
102+
__thread_locals.set(:sonic_pi_spider_time_density, new_density.to_f)
103+
end
104+
105+
def __with_spider_time_density(density, &blk)
106+
prev_density = __get_spider_time_density
107+
__layer_spider_time_density!(density)
108+
blk.call
109+
__change_spider_time_density!(prev_density)
110+
end
111+
112+
def __in_link_bpm_mode
113+
__system_thread_locals.get(:sonic_pi_spider_bpm) == :link
114+
end
115+
116+
def __change_spider_time_and_beat!(new_time = nil, new_beat = nil)
117+
__system_thread_locals.set :sonic_pi_spider_time, new_time.to_r
118+
__system_thread_locals.set(:sonic_pi_spider_beat, new_beat.to_f)
97119
end
98120

99121
def __reset_spider_time_and_beat!
100-
t = Time.now.freeze
101-
__change_spider_time_and_beat!(t, 0)
102-
__system_thread_locals.set :sonic_pi_spider_start_time, t
122+
if __in_link_bpm_mode
123+
new_time, new_beat = @tau_api.link_current_time_and_beat(0)
124+
__system_thread_locals.set :sonic_pi_spider_time, new_time.to_r
125+
__system_thread_locals.set(:sonic_pi_spider_beat, new_beat.to_f)
126+
__system_thread_locals.set :sonic_pi_spider_start_time, new_time.to_r
127+
else
128+
t = Time.now.to_r
129+
__change_spider_time_and_beat!(t, 0)
130+
__system_thread_locals.set :sonic_pi_spider_start_time, t
131+
end
103132
end
104133

105134
def __change_spider_bpm!(new_bpm)
106-
__system_thread_locals.set :sonic_pi_spider_bpm, new_bpm.to_f
135+
case new_bpm
136+
when :link
137+
if __in_link_bpm_mode
138+
# do nothing, we're already in link bpm mode
139+
else
140+
__system_thread_locals.set :sonic_pi_spider_bpm, :link
141+
__reset_spider_time_and_beat!
142+
end
143+
144+
when Numeric
145+
__system_thread_locals.set :sonic_pi_spider_bpm, new_bpm.to_f
146+
else
147+
raise StandardError, "Unknown BPM value. Expecting either a number e.g. 120 or :link."
148+
end
107149
end
108150

109151
def __reset_spider_bpm!
110152
__change_spider_bpm!(60.0)
111153
end
112154

113155
def __change_spider_beat_and_time_by_beat_delta!(beat_delta)
114-
sleep_time = beat_delta * __get_spider_sleep_mul
115-
new_time = __get_spider_time + sleep_time
116-
new_beat = __get_spider_beat + beat_delta
117-
__change_spider_time_and_beat!(new_time, new_beat)
156+
new_beat = __get_spider_beat + (beat_delta / __get_spider_time_density)
157+
158+
if __in_link_bpm_mode
159+
__system_thread_locals.set(:sonic_pi_spider_beat, new_beat.to_f)
160+
else
161+
sleep_mul = __get_spider_sleep_mul
162+
sleep_time = beat_delta * sleep_mul
163+
new_time = __get_spider_time + sleep_time
164+
__change_spider_time_and_beat!(new_time, new_beat)
165+
end
118166
end
119167

120168
def __get_spider_time
121-
__system_thread_locals.get(:sonic_pi_spider_time)
169+
if __in_link_bpm_mode
170+
@tau_api.link_get_clock_time_at_beat(__get_spider_beat)
171+
else
172+
__system_thread_locals.get(:sonic_pi_spider_time)
173+
end
174+
end
175+
176+
def __get_spider_time_density
177+
__thread_locals.get(:sonic_pi_spider_time_density, 1)
122178
end
123179

124180
def __get_spider_schedule_time
125-
__system_thread_locals.get(:sonic_pi_spider_time) + current_sched_ahead_time
181+
__get_spider_time + current_sched_ahead_time
126182
end
127183

128184
def __get_spider_sleep_mul
129-
60.0 / __system_thread_locals.get(:sonic_pi_spider_bpm)
185+
(60.0 / __get_spider_bpm)
130186
end
131187

132188
def __get_spider_bpm
133-
__system_thread_locals.get(:sonic_pi_spider_bpm)
189+
# take into account density
190+
if __in_link_bpm_mode
191+
@tau_api.link_tempo * __get_spider_time_density
192+
else
193+
__system_thread_locals.get(:sonic_pi_spider_bpm) * __get_spider_time_density
194+
end
134195
end
135196

136197
def __get_spider_beat
137-
__system_thread_locals.get(:sonic_pi_spider_beat)
198+
__system_thread_locals.get :sonic_pi_spider_beat
138199
end
139200

140201
def __get_spider_start_time
141202
__system_thread_locals.get :sonic_pi_spider_start_time
142203
end
143204

205+
def __current_run_time
206+
(__get_spider_time - @global_start_time).round(6)
207+
end
208+
209+
def __current_local_run_time
210+
(__get_spider_time - __get_spider_start_time).to_f.round(6)
211+
end
212+
144213
def __with_preserved_spider_time_and_beat(&blk)
145214
time = __get_spider_time
146215
beat = __get_spider_beat
@@ -323,7 +392,7 @@ def __schedule_delayed_blocks_and_messages!
323392
:thread_name => __current_thread_name}
324393
last_vt = __get_spider_time
325394
sched_ahead_sync_t = last_vt + __current_sched_ahead_time
326-
sleep_time = sched_ahead_sync_t - Time.now
395+
sleep_time = sched_ahead_sync_t.to_f - Time.now.to_f
327396

328397
Thread.new do
329398
Kernel.sleep(sleep_time) if sleep_time > 0
@@ -390,14 +459,6 @@ def __error(e, m=nil)
390459
__msg_queue.push({type: :error, val: res, backtrace: e.backtrace, jobid: __current_job_id, jobinfo: __current_job_info, line: line})
391460
end
392461

393-
def __current_run_time
394-
(__get_spider_time - @global_start_time).round(6)
395-
end
396-
397-
def __current_local_run_time
398-
(__get_spider_time - __system_thread_locals.get(:sonic_pi_spider_start_time)).round(6)
399-
end
400-
401462
def __current_thread_name
402463
__system_thread_locals.get(:sonic_pi_local_spider_users_thread_name) || ""
403464
end
@@ -410,14 +471,6 @@ def __current_job_info
410471
__system_thread_locals.get(:sonic_pi_spider_job_info) || {}
411472
end
412473

413-
def __sync(id, res)
414-
@cue_events.event("/sync", {:id => id, :result => res})
415-
end
416-
417-
def __cue_events
418-
@cue_events
419-
end
420-
421474
def __stop_start_cue_server!(stop)
422475
@tau_api.start_stop_cue_server!(stop)
423476
end
@@ -948,11 +1001,6 @@ def __gui_cue_log_idxs
9481001
@gui_cue_log_idxs
9491002
end
9501003

951-
952-
953-
954-
955-
9561004
def __in_thread(*opts, &block)
9571005
args_h = resolve_synth_opts_hash_or_array(opts)
9581006
name = args_h[:name]
@@ -1323,13 +1371,9 @@ def normalise_buffer_name(name)
13231371
end
13241372
return norm
13251373
end
1326-
1327-
1328-
13291374
end
13301375

13311376

1332-
13331377
class Runtime
13341378

13351379
include Util
@@ -1393,8 +1437,9 @@ def initialize(ports, msg_queue, user_methods)
13931437
@system_state.set 0, 0, osc_cue_server_thread_id, 0, 0, 60, :sched_ahead_time, default_sched_ahead_time
13941438
@gui_cue_log_idxs = Counter.new
13951439
@osc_cue_server_mutex = Mutex.new
1396-
@register_cue_event_lambda = lambda do |t, p, i, d, b, m, address, args, sched_ahead_time=0|
13971440

1441+
@register_cue_event_lambda = lambda do |t, p, i, d, b, m, address, args, sched_ahead_time=0|
1442+
t = t.to_r
13981443
sym = nil
13991444
address, sym = *address if address.is_a?(Array)
14001445

@@ -1408,7 +1453,8 @@ def initialize(ports, msg_queue, user_methods)
14081453
:cue => address })
14091454

14101455
sched_ahead_sync_t = t + sched_ahead_time
1411-
sleep_time = sched_ahead_sync_t - Time.now
1456+
1457+
sleep_time = sched_ahead_sync_t - Time.now.to_r
14121458
if sleep_time > 0
14131459
Thread.new do
14141460
Kernel.sleep(sleep_time)

0 commit comments

Comments
 (0)