Closed trentgill closed 1 year ago
dang, @trentgill, this system is so powerful + such a blast to use!!
ran through the docs, only found two wonky zones with the firmware you linked:
:times
edge-case?situation: when i'm using tl.queue
, i'd like to also leverage :times
, sorta the way one might want to launch a non-looping clip of notes in Ableton Live on-demand, eg:
tl = timeline
kick = function() output[1](pulse()) end
mykick = tl.queue():loop{2, kick}:times(3)
inital success: executing mykick:play()
will send three pulses through output 1 and stop, as expected.
expectation: since mykick
has terminated, subsequent mykick:play()
calls should restart the set of three pulses
results: after the initial :times
has been performed, i can't seem to retrigger the condition for mykick
. it just continues to loop on subsequent calls unless i re-run the script.
what i tried:
mykick:play()
again -- just loopsmykick:stop()
, then mykick:play()
-- just loopsnote: i can get the desired results with timed
and score
, which each self-terminate and allow subsequent :play()
calls, eg:
tl = timeline
kick = function() output[1](pulse()) end
mysong = tl.queue():score{
0, kick
, 2, kick
, 4, kick
}
and then use mysong:play()
'reset'
times out CPUsituation: i'd like to loop a .score
, eg:
tl = timeline
kick = function() output[1](pulse()) end
snare = function() output[2](pulse()) end
tl.score{
0, kick
, s{2,1}, snare
, 2, kick
, 3, snare
, 4, 'reset'
}
results: the first 4 beats execute, but then when 'reset'
is reached, both the outputs pulse and druid returns:
CPU timed out.
error: user code timeout exceeded
Thanks @dndrks ! I'll take a look at these and certain I can find a solution for both issues.
Glad you're enjoying the system! I'm super excited for it if for no other reason than it's a bit cleaner for really simple looping things. When i've been testing CPU, it's great to just type timeline.loop{1/4, function() print(cputime()) end}
--- really quick way of running a function on a timer in a single step!
2 issues i'm seeing:
function bang() print 'a' end
timeline.loop{ 1, bang }
--> prints 'a' repeating
function bang() print 'b' end
--> continues printing 'a'
-- expect printing 'b'
i guess we're capturing the function as a closure? seems strange as i assumed it should just reference the named global function
edit
after some research, the issue is that the timeline table captures the function by value, not reference. thus when you redefine bang
the original version still exists inside the timeline table, and a new global bang
is created. i'm not sure if it's possible to make the function mutable like i was expecting, and even if it's possible, i doubt it's possible without some additional syntax.
furthermore, i could likely add a check if the function provided in the table is a global, and thus allow redirection -- however, this breaks the general scoping & val/ref dichotomy of lua. thus it could create confusion as it would be "incorrect" in the eyes of a regular lua user.
not sure if this is a deal-breaker -- there's many other ways to make the params of a timeline function change (could be via a sequins with next=0
and using the select
method to choose value. alternatively, if one wants to modify an ASL, the dyn
system could be used to change the behaviour in a stateful way).
for now, a good solution is using my_timeline:hotswap
method and just redefining the table entries in full.
i'm seeing this when using a function-table with a sequins inside:
timeline.loop{ 1, {function() output[1](oscillate(v)) end, sequins{1,2,3,4}}}
runs for a while (maybe a couple of minutes?) then simply stops. the system keeps running, but re-executing the timeline doesn't do anything.
it could be related to the "beat-duration" calculation creating some invalid time. or perhaps it's running out of memory, or having some other low-level issue. need to do some debugging to find out why. and would be good to try and minimize the test case (not sure if it's linked to the function-table, or the sequins, or both).
@dndrks
tl.loop{}:times(n)
now restarts the times
count when the timeline plays to completion. note that if you call :stop()
then :play()
the times will not be reset (so acts more like a pause). only when all repetitions are complete will it reset.
if you'd prefer :play()
to reset the times
count whenever it is called that's possible too. perhaps that would make more sense? is there any reason one would want to pause a timeline and restart it without resetting the count? i just went with the first solution that came to mind, but perhaps this alteration makes more sense?
(this is currently untested as i'm waiting for my current unit to crash to track down an issue posted above. download is here https://github.com/monome/crow/suites/11196281318/artifacts/572375412)
tl.score{ ... , 4, 'reset'}
should now work correctly now too. i believe tl.timed
with a 'reset'
key should work fine as it was. (download is same as above)
? any thoughts on whether tl.loop
should allow a 'reset'
command? this would specifically be useful for probabilistic behaviours (eg a table pair of {2, function() if math.random() > 0.5 then return 'reset' end}
). one could then have a the main part of their loop first, and the later part only execute sometimes (or based on a stateful predicate).
could this get too confusing? i think the difficulty is that :loop
works differently in that it calls the action, then waits an amount of time, whereas the :timed
and :score
methods wait first, then call the action. so the reset
action would still wait for the beat-sleep before restarting...
i'm looking at the code and wondering if timeline.timed
is a poor name? for one it's very similar to the :times
modifier, but also i'm not sure it fully communicates the use-case. loop
is obviously for making patterns in time. score
speaks directly to beat/bar numbers & events over the course of time. feels like timed
could have a better metaphor that speaks more directly to its use-case?
any thoughts here are welcome (and annoyingly short one-word names [<7 chars] preferred)!
hihi! hope you had a nice weekend :)
(this is currently untested as i'm waiting for my current unit to crash to track down an issue posted above. download is here https://github.com/monome/crow/suites/11196281318/artifacts/572375412)
just a heads up, this version didn't seem to have the fixes in it! but, can totally share thoughts re: your q's otherwise!
edit: ah! figured out how to snag the files from dev4.0
and clock_overhaul
-- but each still exhibits the same behavior.
tl.loop
everything you wrote makes sense to me, so just to summarize:
:play()
on a not-yet-started loop will play from the start and iterate through the provided n
count. once the loop has iterated according to the counter, a subsequent :play()
message will reset the count and allow another run which will terminate once the n
count is reached.
:stop()
on a running loop will pause the execution without resetting the n
count, which allows expressions like "i only want 16 of this event to happen total, but i want to selectively gate the iteration of those 16 occurrences". feels very tape-like, love it.
i'd expect a 'reset' command to follow the above formatting, so more like :reset()
.
if i have a :times(n)
modifier appended, i'd expect this to reset the n
count on-demand but otherwise not affect playing status.
in a case like this:
tl = timeline
kick = function() output[1](pulse()) end
snare = function() output[2](pulse()) end
mypairs = {1, kick, 2, snare, 0.5, snare, 0.3, kick, 0.2, snare}
timeline.loop(mypairs)
... where my loop will start with 1,kick
, i'd expect a reset command (regardless of formatting) to swing back to the 1,kick
step from wherever it is in the loop.
timeline.loop
with non-integer valuesgetting some weird behavior with this example script and the firmware you posted:
tl = timeline
kick = function() output[1](pulse()) end
snare = function() output[2](pulse()) end
mypairs = {1, kick, 2, snare, 0.5, snare, 0.3, kick, 0.2, snare}
timeline.loop(mypairs)
pulses run for a few minutes (around 4 mins), but then they stall out with no errors sent back to druid. after another few minutes of silence, a couple of pulse cycles will occasionally come through, but nothing seemingly consistent.
if i try to rerun the script, i don't get consistent behavior until i power-cycle.
re: timeline.timed
, i don't think it's terrible, but for the sake of the game:
timeline.times
(small diff, but maybe clearer?)timeline.sec
(as in seconds)timeline.real
(as in 'real' time vs. 'score' time)timeline.exact
or timeline.direct
(to differentiate between the relative timing of score
)timeline.iter
or timeline.auto
or timeline.fixed
(to suggest a process that operates without regard for other senses of time)hope that all helps! lmk if i can test with any other builds <3
thanks @dndrks!
regarding the reset idea, i wonder if we could outline the different use-cases for :play and :stop? i’m wondering if we can keep the api simpler (ie no :reset) by altering what those main 2 functions do?
if :play always restarted the whole timeline would that compromise anything? is there any real purpose for pausing and resuming a timeline? when :play is sent to an already running timeline, does it wait for the launch-quantization sync before happening? if so, do subsequent events that would occur before the launch-quant is finished need to keep running until that point (ala ableton clips)?
:stop could also just be the same as reaching the end of the timeline (resetting everything). this would remove the idea of a paused timeline which i’m probably in favour of.
not that ableton is the be-all but i cant help but think of timelines as programmatic clips. makes me think a play & stop button is all we need.
somewhat related, it would be a good idea to test whether a timeline whose actions are functions that launch other timelines works. would allow similar syntax in the small (ie drum & melody patterns) and large (ie arrangement).
—-
regarding the timelines stopping, this is the same bug as i mentioned above (long running timelines stop). it’s caused by issues in the clock library which i believe i’ve fixed in the clock-direct-sync PR. testing now, and will post a link once confimed.
—-
apologies that the linked zip wasnt right. i wish it were easier to point at the correct PR artifact on github!
that said, i’m also about to test these to confirm my changes solved both problems.
—-
re naming: i like timeline.real!
also considering timeline.sleep — ie. a sequence of events separated by clock.sleep. this might be more understandable to folks who already know the clock library, but is it less so for folks who are new to the platform?
i imagine newcomers being inclined to try timeline before learning about clocks (the syntax is simpler, and more directly speaks to musical usage).
On 27 Feb 2023, at 07:49, dan derks @.***> wrote:
hihi! hope you had a nice weekend :)
(this is currently untested as i'm waiting for my current unit to crash to track down an issue posted above. download is here https://github.com/monome/crow/suites/11196281318/artifacts/572375412)
just a heads up, this version didn't seem to have the fixes in it! but, can totally share thoughts re: your q's otherwise!
tl.loop
everything you wrote makes sense to me, so just to summarize:
:play() on a not-yet-started loop will play from the start and iterate through the provided n count. once the loop has iterated according to the counter, a subsequent :play() message will reset the count and allow another run which will terminate once the n count is reached.
:stop() on a running loop will pause the execution without resetting the n count, which allows expressions like "i only want 16 of this event to happen total, but i want to selectively gate the iteration of those 16 occurrences". feels very tape-like, love it.
i'd expect a 'reset' command to follow the above formatting, so more like :reset(). if i have a :times(n) modifier appended, i'd expect this to reset the n count on-demand but otherwise not affect playing status. in a case like this:
tl = timeline
kick = function() output[1](pulse()) end snare = function() output[2](pulse()) end mypairs = {1, kick, 2, snare, 0.5, snare, 0.3, kick, 0.2, snare} timeline.loop(mypairs) ... where my loop will start with 1,kick, i'd expect a reset command (regardless of formatting) to swing back to the 1,kick step from wherever it is in the loop.
bug?: timeline.loop with non-integer values
getting some weird behavior with this example script and the firmware you posted:
tl = timeline
kick = function() output[1](pulse()) end snare = function() output[2](pulse()) end
mypairs = {1, kick, 2, snare, 0.5, snare, 0.3, kick, 0.2, snare} timeline.loop(mypairs) pulses run for a few minutes (around 4 mins), but then they stall out with no errors sent back to druid. after another few minutes of silence, a couple of pulse cycles will occasionally come through, but nothing seemingly consistent.
re: timeline.timed naming, i don't think it's terrible, but for the sake of the game:
timeline.sec (as in seconds) timeline.real (as in 'real' time vs. 'score' time) timeline.exact or timeline.direct (to differentiate between the relative timing of score) timeline.iter or timeline.auto or timeline.fixed (to suggest a process that operates without regard for other senses of time) hope that all helps! lmk if i can test with any other downloads <3
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.
ayyy happy to help!
:play
+ :stop
totally in favor of refining these two and avoiding :reset
altogether.
summarizing my understanding from your great rundown...
if a timeline isn't running
:play
starts it (respecting any launch-quantization sync, so that any queued events keep running until the sync):stop
has no affectif a timeline is running without a times
:play
resets the loop to the first event:stop
stops itif a timeline is running with a times
:play
resets the counter to the start and resets the loop to the first event:stop
stops it and resets the countersomewhat related, it would be a good idea to test whether a timeline whose actions are functions that launch other timelines works. would allow similar syntax in the small (ie drum & melody patterns) and large (ie arrangement).
thiiiiis is cool, yeah! will test with the next download!
timeline.sleep
sorta muddies things for me -- eg. it's not legible whether the number represents the sleep before the listed event or the sleep after. but also, sleep
in clock
describes how much time should pass from its execution; whereas if timeline.sleep
is a time-based version of timeline.score
, then we're describing absolute timestamps (vs. waiting periods).
also, the idea of a score is kinda primary / above the specifics of how time within that score is measured, you know? if a score is just a collection of actions along a timeline (independent of how time moves), then maybe timeline.score
could just own both beats/seconds, depending on what the author specifies? eg.
-- musical timestamps:
timeline.score{
{0, beat, intro}
, {32, beat, verse}
, {64, beat, chorus}
, {128, beat, outro}
}
-- 'real' timestamps:
timeline.score{
{0, sec, intro}
, {6, sec, verse}
, {19, sec, chorus}
, {31, sec, outro}
}
maybe with an assumption that if no argument is provided, it's always beats?
@dndrks i really like the idea of having timing be handled universally (beats vs seconds), and even built that in my own fennel version of timeline. Nice thing about that language is there's special syntax for strings, so i used base numbers to represent seconds (0.0, 3.0
), and strings to represent beats (:2, :1/4
). in lua we could do the same but it probably looks weird to have '1/4'
and to know that if it's a string it's a 'beats' time.
i don't want to add the extra argument because it breaks how the table is parsed (and making it optional would make the parsing way more complicated, or maybe even impossible owing to the possibility that everything can be a sequins).
i think we should stick with a separate method for now simply because it avoids the need for rote knowledge (eg. "i want a beats time. should that be a number or a string?").
one option would be to use ~
as a prefix for beats time: ~2, ~1/4
but again suffers from the rote knowledge thing. it also looks like "at approximately 2 seconds".
EDIT: actually i don't think i can make it work directly right now. if both times and beats are acceptable, one would assume they can be interspersed, but timeline requires all events to be listed in order of execution which is not always apparent when switching back and forth (ie. their order depends on the tempo).
i propose we stick with what we have, and call it timeline.real
. this method is typically going to be used for a non-score-like sequence of events (i can't imagine scoring a song in terms of seconds), so feels best to treat it independently.
re the :play :stop discussion:
the only thing missing is: if a timeline is running with or without a times:
:play
restarts the timeline, respecting launch-quantization:stop
restarts the timeline, resetting any times.^^ in the :play
above, is it ok if events are cancelled during the launch-quantization period? at present there's no simple way to start the launch-quantization counter, but continue producing events until that moment. it's definitely possible, but could be quite convoluted i fear. if we can push this to 4.1 (or just not do it) i'd be happy to just get the base functionality of :play and :stop built as discussed above.
i propose we stick with what we have, and call it
timeline.real
into iiiiiit!!
^^ in the :play above, is it ok if events are cancelled during the launch-quantization period?
@trentgill i think that's fine, as it generally opens up a lot of great functionality even if there might be use for the 'keep going until the new launch' approach. this is maybe where the ableton metaphor traps things -- you're not building a clip launcher whose primary use is a ton of manual beat-juggling, you know? crow will most likely receive a :play
trigger from a well-synced source, so i can imagine someone just opting for a shorter launch-quantization period (or none at all) to retain the events leading up to the timeline restart :)
this is not production ready yet!
still need to add:
tl.prob
for probabilistic eventsshould be drop-in for norns, just need to change the aliases up top into
include
orrequire
s.the new thing!
timeline is a lua library leveraging the
clock
andsequins
libraries from norns/crow. the library is focused on a data-first representation of events in time. the intention is to enable basic (as well as complex) looping & sequencing in time, without having to write clock routines yourself.timeline comes in 3 varieties: 1)
timeline.loop
for repeating patterns, timed as durations of events (in beats) 2)timeline.score
for arranging events in a linear fashion, using beats as timestamps relative to the start 3)timeline.real
for playing events linearly with arbitrary time, timestamped relative to the start in secondssyntax is a plain table in pairs:
of course it's possible to simply have a single pair of time & event:
launch quantization
there are 2 "pre-methods" for modifying the playback & timing of timelines:
tl.launch(n)
will quantize the start of the timeline to next multiple ofn
beats, relative to the global clock. this makes it easy to bring in new elements in phase with each other:note how
tl.launch
comes before theloop
, as the function will modify the wait before the timeline begins.if you omit
tl.launch
, the timeline will default to a launch quantization of 1.playback control
you may have noticed that timelines always start as soon you execute
tl.loop()
etc. this makes it fast and easy to fire off repeating timelines. sometimes you don't want to start a timeline immediately though, and you're not sure when to queue it up with alaunch
command. here you have 2 options:1) just store the table of time/event pairs, then pass them to
tl.loop
etc lateror a more flexible option (that carries any other modifiers with it:
2) use the
queue
pre-method:note here that
:stop()
is a method that can be called on any timeline at any point.panic!
sometimes you forget to save your timelines into a variable (so they can be stopped), in which case you can stop them all by calling:
timeline.cleanup()
this actually just calls
clock.cleanup()
so beware it will also stop any running clocks if you are using them as well.duration & timestamp vs sync
in contrast to typical clock routines that use the
clock.sync(n)
function, timeline is based around durations (forloop
) and timestamps (forscore
). it's not better, but it certainly feels different.more
loop post-methods
loop
has some special "post-methods" that can be added to the end, modifying the looping behaviour. specifically, these methods allow theloop
to stop at some point in the future.unless
this is the general method. when the loop reaches the end of the table of event pairs, it will check the condition in
unless
and if it returnstrue
, the timeline will stop.tl.loop{1, kick}:unless(false) -- it'll run forever!
the condition argument to
unless
can either be a boolean value, or a function that returns a boolean. if a function, it will be re-evaluated each repetition through the timeline.times
sometimes you want a pattern or sequence to run a specific number of times. you could write your own function to do it with
unless
, but we did it for you and it's calledtimes
. simply attach the number of repetitions to a pattern:(nb:
tl:once()
has been removed. trying to focus the API and reduce overhead to learning the library. it could easily be added back in future if deemed desirable. for now usetl.:times(1)
).score
very similar to
timeline.loop
,score
is still a table of pairs, but the timing values have a different meaning:see how the numbers on the left are relative to 0. thus
verse
is called 32 beats afterintro
, andchorus
is another 32 beats afterverse
.this can make it easier to write certain kinds of rhythms compared to
loop
. examplereal
ie "realtime". very similar to
score
, the only difference here is that timing is described in seconds instead of beats. this can be super useful for things like strumming notes in a chord, or other examples:sequins
now we get to the good stuff! pretty much any value in a timeline is able to be a
sequins
element. this includes timing data, functions, method parameters, and more (no really).lots of examples
just copy them from my notes
function table
often your actions will want to accept data, rather than just do the same thing every time. of course you can wrap any function in an anonymous function inline, but that ends up baking the musical data in with the functionality.
timeline
is all about avoiding this intermingling of data & code. instead we use a table-syntax for passing parameters to a function whenever it's called. the parameters can themselves be sequins, which will be called for you each time:note how the timeline is all data. there's quite a few braces, but with some syntax highlighting it should be relatively manageable.
a bunch more here i'm tired
swing example
strum example
reset
both timeline.score & timeline.timed can be reset to the start by using the keyword
'reset'
as an action. this converts this one-shot style timelines into loops.note here that you can use a function that returns the string
reset
which allows for probabilistic repetition. something like:function() if math.random() then 'reset' end end
iter
sometimes you might want your actions to change as the timeline plays. you can use the
timeline.iter()
method to return the number of the current iteration. this value is0
if the timeline is queued and hasn't started, and/or while in the launch-quantization phase. note you'll need to refer to the timeline name in your action function.hotswap
similar to the
sequins:settable(new_seq)
method, timelines can also be swapped out on the fly, preserving the current timing & phase. hotswapping is particularly useful in a live context, or any time you want to gradually modify a timeline, without stopping the groove.note: when hot-swapping timelines, you can only change the table of times & events. specifically not the 'mode' of the timeline (ie
loop, score, real
).TODO additionally, any nested sequins in the timeline will also be hotswapped, so you can use this feature to modify the action data as well as the timing information.
TODO nested timelines will also be preserved (think a :score triggering a set of :loop s)