Closed catfact closed 12 months ago
...phase getter should probably just lock the mutex and do a full query of micros / phase at that moment.. I suppose.
(means moving link
object and session state definition out of clock thread.)
...or: this will sound silly, but could be totally fine to just add a fudge factor to phase. (of, say, twice the sleep period which is 0.1ms, so 0.2ms * tempo / 60
)
the reasons i'd consider it are (1) blocking to retreive all the link and session state could be heavy, (2) usually only care about integer component of phase anyway, so priority on correct result after math.floor
Here's a test script I used with your PR: https://gist.github.com/ambv/293fa69b38e63dc468b092f217b4d67c
It allows us to start the clock using K2, and stop it using K3. On the first beat the left side of the Grid blinks, on other beats the right side of the Grid blinks.
I observe that indeed quantum
is now honored, and the clock only starts when Live starts.
One weirdness I see is that with this test script I always see Norns starting clock at the 2nd beat of Ableton Live. Is there something wrong in the test script?
i modified the example from here, like so
function clock.transport.start()
print("clock.transport.start")
id = clock.run(clock_loop)
transport_running = true
redraw()
end
function clock.transport.stop()
print("clock.transport.stop")
clock.cancel(id)
transport_running = false
pulse_count = 0
redraw()
end
function clock_loop()
clock.sync(1)
while true do
print("------")
local phase = _norns.clock_link_get_phase()
-- fudge because the reported phase is 0.1ms old:
-- (fudge value is large to accomodate tempo)
-- (assuming quantum is 4)
phase = math.fmod((phase + 0.1), 4)
local iphase = math.floor(phase)
print("my link phase: " .. iphase .. " (" ..phase..")")
local hz
if iphase == 0 then hz = 330
else hz = 220 end
engine.hz(hz)
pulse_count = util.wrap(pulse_count + 1,1,4)
redraw()
clock.sync(1)
end
end
only issue i'm seeing is the wonkiness with old phase reporting as noted.
i don't have strong opinion about when to fire the transport callback but this seems to work for me.
@catfact iirc, any sync(x) calls will be delayed until beat 0 anyway on the (local) link timeline.
if we want scripts to be link-aware, we can toggle this behavior, so sync(1) will also yield at -3, -2, -1 beats (given that quantum is 4), but then coroutines will need extra checks around sync() calls as below:
b = clock.sync(1)
if b > 0:
...
right now it's rather working as follows:
b = clock.sync(1) # will pause until beat 0
does your change solve a different use case?
unrelated to the above question, we shouldn't need phase in scripts, as we can always calculate that from current_beat / quantum, but since we have don't have a global "sync rate" and each coroutine inside a script can run at its own (variable) rate, a global phase accessor shouldn't be necessary.
for the same reason, we don't need (to know) quantum in most cases too, in norns it only affects the time beat 0 is aligned to (if 2 peers are running with the same quantum it will be identical).
any sync(x) calls will be delayed until beat 0 anyway on the (local) link timeline.
unless i misunderstand, that's not what i'm seeing. clock.transport.start
fires on the next beat boundary (? anyway, doesn't wait for next bar), and the clock coro starts running, before beat=0 is reached. (and sync(n)
syncs to the next multiple of n
beats regardless of quantum setting, which is as i'd expect.)
i agree that being able to explicitly query the phase is less important. with or without that, scripts can implement "quantized start" in various ways, but only by specifying different logic when the clock source is Link.
the expected user story is that launching a script without link-specific logic (like awake
or whatever), the script should start playing at the same time Ableton does. can you suggest a way to make this work without delayingclock.transport.start
?
its absolutely true that i don't entirely know what i'm doing here and have little stake in the outcome, so happy to step out of the discussion.
anyways thanks for looking.
unless i misunderstand, that's not what i'm seeing.
clock.transport.start
fires on the next beat boundary (? anyway, doesn't wait for next bar), and the clock coro starts running, before beat=0 is reached.
right, that's definitely not by design. perhaps this was broken at some point. clock.transport.start
firing time actually doesn't matter, as normally we'll only schedule other coroutines in this callback, but clock.sync
timing certainly does.
the expected user story is that launching a script without link-specific logic (like awake or whatever), the script should start playing at the same time Ableton does. can you suggest a way to make this work without delayingclock.transport.start?
yeah, the solution is to never fire a SYNC_RESUME event at any beat before 0 (consequentially, any active clock.sync(x) for any x should be resumed at beat 0).
i would simply add a condition at https://github.com/monome/norns/blob/main/matron/src/clocks/clock_scheduler.c#L96
maybe something like (clock_beat > event->sync_clock_beat && clock_beat >= 0)
. would that solve the problem?
if i read correctly, that would mean coro is never run/resumed before beat 0 right?
if we want to be really nice and proper, ideally we would still run the coro during the "count-in" part. that's so that if a script does want to be link-aware, it can implement a count-in animation or whatever just as Ableton Live does.
hence the thought of effectively decoupling transport.start
from the start of the coro.
@Dewb shared this useful document from Ableton that sort of covers user-story expectations. https://github.com/Ableton/link/blob/master/TEST-PLAN.md
if i read correctly, that would mean coro is never run/resumed before beat 0 right?
yes.
we can still query beat value and show count-in animations in redraw()
if beat is below 0 (that is, if we don't use sync() to schedule redraws, and we shouldn't mustn't). in the end, either method requires some extra logic in the scripts.
what i'm suggesting was basically the original intent in this api. it also requires no extra effort for the use-case you describe: if any peer restarts the transport in a link session - awake will restart playback from beat 0 with no script modifications.
I like the idea to fire early but allow reading that it's count-in and reacting to that per script. Docs would need to be updated to highlight this, and old scripts wouldn't gain this feature without changes, but ultimately rendering the count-in in UI is indeed preferable and makes up for the downsides.
OK, so I poked at the issue with my example and I found the culprit. Ezra's example doesn't demonstrate the problem I'm describing because it relies on Ableton Live starting the transport. If we add just this to the example script:
function key(k, z)
if z == 0 then
if k == 2 then
clock.link.start()
end
if k == 3 then
clock.link.stop()
end
end
end
then we can start the transport from Norns and observe what I'm talking about.
...
new iphase 3 (3.004399)
new iphase 0 (0.000616)
new iphase 1 (1.012156)
new iphase 2 (2.008676)
new iphase 3 (3.005130)
new iphase 0 (0.001494)
starting transport from link
clock.transport.start
------
my link phase: 1 (1.097804)
new iphase 1 (1.012937)
------
my link phase: 2 (2.094081)
new iphase 2 (2.009198)
------
my link phase: 3 (3.090432)
new iphase 3 (3.005571)
------
my link phase: 0 (0.086847)
new iphase 0 (0.001981)
...
Note how the first "----" appears at phase "1" and not phase "0". That's because the current documentation and examples all tell us to run clock.sync()
at the start of any while
loop. Now, with this PR in place, clock.transport.start
happens at beat 1 so if there is a clock.sync(1)
right there, it will create an off-by-one.
Certainly, user scripts could work around this but the count-in idea will be a much better solution.
it's true that i had really only looked at the case where norns is follower for transport.
yes, there is some wonky logic going on (it's WIP, for real), but that description doesn't match the issues i'm seeing and i don't think the initial clock.sync
is the issue. with or without it, my phases are in sync between norns and Live. (which is actually what the print statements above are showing.)
here is the messy current state of the test script. i'm playing a different pitch on beat 0. https://gist.github.com/catfact/aac3fd79c333721ad761bda4a949b2a9
when Live is leader, i find the initial clock.sync(1)
useful in the particular case that Live starts transport in the middle of beat 0. the scheduler will fire transport.start
right away and norns will fire first beat late - seems preferable for it to pick up on beat 1 or (probably) the scheduler logic should wait til next bar cycle to fire.
when norns is leader, i actually don't see a predictable difference with clock.sync or not (it continues "immediately" i think) but there are some sporadic inconstiencies with when Live picks up the transport, and also with when tempo is applied. omitting the initial sync seems more stable in that case.
anyways, i would be perfectly happy if the answer was to do nothing. if @artfwo considers the current behavior (before this change) to be not what was intended, then i would follow their lead for sure.
ok, so i've updated my norns today and confirmed that transport restart in awake isn't working 100% as it should (without ezra's change).
for testing transport syncing i'm using LinkHut example and reference implementation bundled with Ableton Link, see https://github.com/Ableton/link/tree/master/examples/linkhut
it doesn't have a lot of dependencies and needs only cmake and some platform-specific headers (portaudio or jack on linux, coreaudio on mac). if using that for testing, make sure that both "link" and "start/stop sync" are enabled, it must display something like this in the status bar:
enabled | num peers | quantum | start stop sync | tempo | beats | metro
yes | 1 | 4 | yes [stopped] | 75.00 | 11.55 | XXXX
^ here ^ and here
when stopping the transport from LinkHut, awake stops playback - this is expected.
when starting the transport from LinkHut, awake starts playback when the LinkHut metronome begins to tick - this is also expected (provided that both norns and LinkHut quantum is identical).
awake won't reset the step position, but i'd say this is an awake issue and should be an easy fix. my immediate thought was to add reset()
to clock.transport.start
callback or, better, restart the coroutine afresh in that callback.
there's another issue with awake, however - it increments the step position immediately after clock.sync:
https://github.com/tehn/awake/blob/main/awake.lua#L147-L154
so, when testing with reset()
in the transport start callback, it skips the first step immediately after syncing to the link beat and plays the 2nd step.
restructuring the step
function to 1) sync 2) play a note, and 3) advance forward in the loop (syncing twice or advancing just before the end of the loop) should solve the aforementioned use-case without any changes in the clock subsystem.
i'd also move the resetting logic into the beginning of step
function and get rid of "running" flag completely, just stopping the coroutine in the transport stop callback for slightly cleaner code. step position could also be calculated by rounding the return value of clock.sync
and divmodding it by loop length.
that said, it looks like we can leave the transport resetting logic on script's conscience completely here, as (transport) syncing seems to be working correctly to me. adding transport hooks fixes it for awake, if the coroutine is fixed. does anything above make any sense? :) @catfact @ambv
Ezra, your code reproduces the problem I'm describing. See this video: https://www.dropbox.com/scl/fi/og4yy2trj18cfj07owh7b/ableton-link-norns-sequencer-starting-from-second-beat.mov?rlkey=gjfzoid38m85fdptgexvf62n3&dl=1
I'm starting and stopping transport here from Norns. You can see reproducibly that the first audible sound after the transport starts is the second beat, not the first beat. When @artfwo says:
b = clock.sync(1) # will pause until beat 0
then clearly this isn't currently the case. It pauses until beat 1.
In your example, the phases are aligned but that's because you're relying on the new _norns.clock_link_get_phase()
function. Before your PR this function didn't exist so sequencers in existing scripts assume that the first iteration of the while loop in a clock coroutine is playing the first beat, keeping an internal measure of the number of beats played. This leads to the off-by-one shift I'm observing.
For the record, relevant settings I used in the video are: on Norns, link quantum is 4; on Live you can see in the top bar that Global Launch Quantization is set to "1 Bar".
My current preference to how to solve this would be what @artfwo suggested above:
if we want scripts to be link-aware, we can toggle this behavior, so sync(1) will also yield at -3, -2, -1 beats (given that quantum is 4), but then coroutines will need extra checks around sync() calls
This would allow Norns scripts to implement count-in indication, which would ensure people aren't confused as to why their Norns isn't starting playback right away. And hopefully this would also make it less tricky to actually align sequencers on Norns to start playback at beat 0.
On top of this, internal/crow/MIDI clocks to continue working as they are today as they don't have the concept of quantum
.
I would prefer if we all focused on the same example. It feels like I'm playing a simultaneous exhibition with all the examples you're both running. Ezra's last example is simple and synthetic, which will allow us to focus on the actual issue.
there's another issue with awake, however - it increments the step position immediately after clock.sync
No, the bug is different in Awake. Look at the original values of one.pos
and two.pos
in the global. They are set to 0, which is out of bounds in Lua. The first increment in the clock loop will start playing at the first step, as you can clearly hear when you first start the script.
So the bug really is that reset()
should set one.pos
and two.pos
again to 0 and not to 1.
I would prefer if we all focused on the same example. It feels like I'm playing a simultaneous exhibition with all the examples you're both running. Ezra's last example is simple and synthetic, which will allow us to focus on the actual issue.
good point, i'll have a look at that later today as well.
there's another issue with awake, however - it increments the step position immediately after clock.sync
No, the bug is different in Awake. Look at the original values of
one.pos
andtwo.pos
in the global. They are set to 0, which is out of bounds in Lua. The first increment in the clock loop will start playing at the first step, as you can clearly hear when you first start the script.So the bug really is that
reset()
should setone.pos
andtwo.pos
again to 0 and not to 1.
thanks, good catch. apologies for getting back to the awake example :) so i have updated reset()
to set both positions to 0, and clock.transport.start
handler to call reset()
and restart the step
coroutine, here are my current handlers in awake:
function stop()
running = false
all_notes_off()
clock.cancel(coro_id)
end
function start()
running = true
reset()
coro_id = clock.run(step)
end
function reset()
one.pos = 0
two.pos = 0
end
this seems to work with stock maiden without any modifications when syncing with LinkHut:
https://www.youtube.com/watch?v=3OsS8ckx17o
does it? am i missing anything here?
I'm starting and stopping transport here from Norns. You can see reproducibly that the first audible sound after the transport starts is the second beat, not the first beat.
hmm, you're right. in this example, when starting from norns, the initial clock.sync(1)
needs to be skipped to start on the bar. i think if you comment out the initial clock.sync(1)
(or skip it via some other mechanism) you will get the behavior you're looking for. (sorry, not sure why i was confused looking at it last night, possibly was running the wrong state of the script.)
(with the change, clock.transport.start fires "immediately" when running the routine, but always after the transition to beat zero.)
anyways, i don't actually like the delayed transport either. it feels broken as a user expeirence (hit the "start" button, wait with no feedback for an unknown amount of time before actually starting.).(ironically, i suggested from the begining that we not implement this, but was convinced it was wanted.)
does it? am i missing anything here?
yes, it seems to work here too - awake
start is delayed until the barline. thanks for demonstrating.
the other behavior important to link users is norns-as-leader. this seems trickier to me but maybe it's not.
small point
. Before your PR this function [get_phase] didn't exist
as artem points out, it is (or should be) equivalent to calling clock.get_beats()
and dividing by taking the remainder of division by the quantum. i did want to see what we were getting according to the link session state, to compare. (that's also why i added the prints for integer phase in the scheduler.)
so, is what i'm hearing that we don't in fact need to make any scheduling changes, yeah? maybe we close this PR then (discussion can continue.)
and maybe best thing we could do is add some documentation / sample code demonstrating best practice a little better.
You mean something like:
local quantum = params:get('link_quantum')
local pulse_count = math.fmod(math.floor(clock.get_beats()), quantum)
if pulse_count == 0 then
-- new bar
else
-- other beat
end
That seems to work but I admit it's somewhat cryptic. So if we're going that way then maybe instead of your PR we want a different PR with just a clock.link.get_beat()
utility function that packages the first two lines above (and maybe is 1-indexed to be better compatible with the rest of Lua)?
I can make that PR for you, if you want.
yeah, something like that. and i am not sure if there are also other places where we would want the system to insert a clock.sync(quantum)
anywhere. (i guess not.)
as a suggestion, and regarding indexing:
@function get_link_phase
@return phase: fractional phase since bar started. this begins at zero since it is a normalized duration, not an index
function get_link_phase()
local quantum = params:get('link_quantum') -- not ideal to have a param access in a utility fn...
local pulse_count = math.fmod(clock.get_beats(), quantum)
end
@function get_link_beat_in_bar
@return beat: 1-based index of current beat in bar
function get_link_beat_in_bar()
return math.floor(get_link_phase()) + 1
end
use phase
if you are, say, building a count-in ramp or otherwise want normalized duration, beat_in_bar
for 1-based index / musical beat number. avoids philosophical disputes.
You mean something like:
local quantum = params:get('link_quantum') local pulse_count = math.fmod(math.floor(clock.get_beats()), quantum) if pulse_count == 0 then -- new bar else -- other beat end
That seems to work but I admit it's somewhat cryptic. So if we're going that way then maybe instead of your PR we want a different PR with just a
clock.link.get_beat()
utility function that packages the first two lines above (and maybe is 1-indexed to be better compatible with the rest of Lua)?I can make that PR for you, if you want.
that shouldn't be necessary, if coroutines are restarted in the transport start callback. also the first sync()
call in a coroutine will resume at beat 0 then.
the other behavior important to link users is norns-as-leader. this seems trickier to me but maybe it's not.
link is fully peer-to-peer by design, there are no assigned leaders in a session. if start/stop sync is enabled on all peers, it doesn't matter who starts the transport, each peer will have the callbacks triggered and the local timelines realigned.
that shouldn't be necessary, if coroutines are restarted in the transport start callback. also the first sync() call in a coroutine will resume at beat 0 then.
You keep repeating this, but this is not what I'm observing. Run this and see for yourself: https://gist.github.com/ambv/54cfaf2a5824cc6fb37cec4aef1f21c0
It's Ezra's example modified to run on the current shipped version of Matron without this PR.
Try to start the transport from Norns (press K3) when your other Link device is in phase 2 or 3. You will notice that it immediately starts playing on the next beat, ignoring quantum
entirely. The sequence will stay in phase (in the sense that the 330 Hz E note will play on beat 0) but that's due to the clock.get_beats()
we discussed in the latest comments. It won't be the first note that played after you started the transport from Norns, even though that's what quantum
was supposed to enforce. This is what this issue is about.
To work around this, you need to implement a count-in in the coroutine, like:
-- Link count in
local clock_source = params:string('clock_source')
if clock_source == "link" then
local quantum = params:get('link_quantum')
while math.fmod(math.floor(clock.get_beats() + 0.4), quantum) ~= 0 do
clock.sync(1)
end
else
clock.sync(1)
end
while true do
-- do work first, and then...
clock.sync(1)
end
I still need to fiddle with the details here (the magic number "0.4" for example) but that's the only thing that worked for me.
Please confirm that you reproduce this, otherwise we must be talking about some two different setups.
@ambv could be, here's what i'm observing with your unmodified script under unmodified matron:
https://www.youtube.com/watch?v=aM-UO7Hpa_U
the behavior looks correct to me, is it not?
do you have start/stop sync enabled in both norns and Live as described here? specifically this:
Try to start the transport from Norns (press K3) when your other Link device is in phase 2 or 3. You will notice that it immediately starts playing on the next beat, ignoring
quantum
entirely. The sequence will stay in phase (in the sense that the 330 Hz E note will play on beat 0) but that's due to theclock.get_beats()
we discussed in the latest comments. It won't be the first note that played after you started the transport from Norns, even though that's whatquantum
was supposed to enforce. This is what this issue is about.
i am able to make this work as well by modifying the clock_loop
function as follows:
function clock_loop()
local quantum = params:get('link_quantum')
local beat = clock.sync(1)
while true do
local hz
pulse_count = math.floor(beat) % quantum
print(pulse_count)
if pulse_count == 0 then hz = 330
else hz = 220 end
engine.hz(hz)
redraw()
beat = clock.sync(1)
end
end
we aren't getting negative beat values here to show remaining beats for count-in, but timing-wise it looks fine.
ok, so after multiple attempts i was able to get several skips with the K3 case. a few times it began at beat 1 and a few times at beat 3. i think i've forgotten to rewind the local timeline in clock_link, looking into it...
thanks for looking into it @artfwo
again i think it would be wonderful to just have a clearer demonstration of correct practice. and thus maybe identify reasons/ways to encapsulate that. to be clear, my example here is just lightly adapted from the clocks study which is an important reference point and should be fixed if it needs it.
link is fully peer-to-peer by design, there are no assigned leaders in a session.
yes thank you for the clarification. what i meant is that i find myself having to do different things depending on whether norns initiates the change to the session transport state or not.
closing this PR to make it ultra-clear that it's not the right solution, but comments are still open.
right, so the above example seems to work with the K3 case after the following change in clock_link.c:
diff --git a/matron/src/clocks/clock_link.c b/matron/src/clocks/clock_link.c
index e9ba389a..289cc809 100644
--- a/matron/src/clocks/clock_link.c
+++ b/matron/src/clocks/clock_link.c
@@ -44,7 +44,7 @@ static void *clock_link_run(void *p) {
if (clock_link_shared_data.transport_start) {
- abl_link_set_is_playing(state, true, 0);
+ abl_link_set_is_playing_and_request_beat_at_time(state, true, micros, 0, clock_link_shared_data.quantum);
abl_link_commit_app_session_state(link, state);
clock_link_shared_data.transport_start = false;
}
@ambv can you apply this and see if it fixes the problem for you?
Looking.
YES! This makes clock.sync(1)
behave like you said: the first callback in the coroutine waits to align with beat 0. This is exactly the fix I wanted initially. Thank you!
Ezra was skeptical of a solution like this because it potentially waits for close to 4 beats before you can hear playback. I personally think this solution is good because:
quantum
to 1 to restore previous behavior;clock.sync(1)
completes -- this is possible because clock.transport.start()
is fired right away, it's clock.sync(1)
that waits until the next beat 0.Ah, forgot to mention, this solution also works better than my count-in attempts with magic numbers to cover for rounding edge cases.
ah, nice, i think i can prepare a PR for this then.
on a slightly offtopic matter, @ambv your profile badge shows you're a python core dev. this summer i've released a similar clock lib for python compatible with asyncio (with only link backend supported due to platform-specific complexities of midi configuration).
it's a bit more convenient with python given that you can program coroutine cancellation behavior using exceptions, await for other clock-synced coroutines, use recursion, etc.
check it out, if you're using python for any music stuff here: https://github.com/artfwo/aalink
@artfwo we actually collaborated already: https://github.com/artfwo/pymonome/issues/11 😄
just to be clear, i was in fact skeptical of the situation where we delayed transport start callback. that was presented to me as the solution adopted by seamstress. but i think that is inaccurate and seamstress is also using abl_link_set_is_playing_and_request_beat_at_time
:
https://github.com/ryleelyman/seamstress/blob/main/src/clock.zig
i think the change addresses all our concerns and brings parity between the two environments.
if i understand correctly, that's the only place its needed, other places just need to set clock_link_shared_data.transport_start
right? (i did not properly grok the point of this data structure initially.)
still checking. on the 2nd look it might be that we only need to correct the time in this line:
https://github.com/monome/norns/blob/main/matron/src/clocks/clock_link.c#L60
it's right that we only attempt to rewind the local link timeline if start/stop sync is enabled locally (and call transport.start in any case), but it might be that link internals have changed, requiring actual (wall clock) time to be specified when calling abl_link_request_beat_at_start_playing_time, so the actual change will probably be:
diff --git a/matron/src/clocks/clock_link.c b/matron/src/clocks/clock_link.c
index e9ba389a..25255fa0 100644
--- a/matron/src/clocks/clock_link.c
+++ b/matron/src/clocks/clock_link.c
@@ -57,7 +57,7 @@ static void *clock_link_run(void *p) {
if (clock_link_shared_data.start_stop_sync) {
if (!clock_link_shared_data.playing && link_playing) {
- abl_link_request_beat_at_start_playing_time(state, 0, clock_link_shared_data.quantum);
+ abl_link_request_beat_at_start_playing_time(state, micros, clock_link_shared_data.quantum);
clock_link_shared_data.playing = true;
// this will also reschedule pending sync events to beat 0
if i understand correctly, that's the only place its needed, other places just need to set clock_link_shared_data.transport_start right? (i did not properly grok the point of this data structure initially.)
that should be already set by clock.link_start(), the purpose of the structure was to keep data shared between threads in the same basket.
The last diff is invalid.
The API is:
abl_link_request_beat_at_start_playing_time(abl_link_session_state, beat, quantum)
You're trying to put time as a beat. If you want to change 0 into micros, that should happen in abl_link_set_is_playing
, like:
diff --git a/matron/src/clocks/clock_link.c b/matron/src/clocks/clock_link.c
index e9ba389a..82b71997 100644
--- a/matron/src/clocks/clock_link.c
+++ b/matron/src/clocks/clock_link.c
@@ -44,7 +44,7 @@ static void *clock_link_run(void *p) {
if (clock_link_shared_data.transport_start) {
- abl_link_set_is_playing(state, true, 0);
+ abl_link_set_is_playing(state, true, micros);
abl_link_commit_app_session_state(link, state);
clock_link_shared_data.transport_start = false;
right, that should be abl_link_request_beat_at_time(state, 0, micros, quantum);
sorry, been a while since i touched this code :sweat_smile:
and the change to abl_link_set_is_playing
of course.
Cool, I'll test the PR when it's up.
PR is up: https://github.com/monome/norns/pull/1740
it seems that the only change required here is fixing the start/stop time (micros instead of 0), and abl_link_request_beat_at_start_playing_time
then picks up the effective time correctly.
@ambv can you confirm this patch fixes the problem with your most recent script too?
quick attempt to do two things related to ableton link:
EVENT_CLOCK_START
(which triggersclock.transport.start
) until phase 0. this implements the recommended "quantized start" behavior without requiring scripts to track phase (which is a link-specific parameter.)idea is that scripts can still do stuff during "count-in" if they want to specifically be link-aware; if they don't they can still start at the same time as the Live transport.
known issue: there's a problem with this as it stands: the phase is updated in the main link clock routine, which updates every 10ms... and with the way the scheduler works, lua "sees" a 10-ms-old phase value on clock sync. need to find some other time to update the cached link phase.
there could well be other issues, i don't totally grok all the intent in the ableton API even after reading their user-story documentation.