Open simonbahr opened 4 years ago
Thanks for summarising our discussion Simon. As you know, I began working on this a while back. There are existing functions that need to be examined and subsumed into an overall method to incorporate events from one sc object into another:
get-nearest-event (slippery-chicken.lsp) get-nearest-by-start-time (rthm-seq-bar.lsp)
This is also something of interest to Sebastian.
I'll scratch my head and get to it asap
by the way, what would we call this slippery-chicken method?
rasterise (usually just images)?
magnetise?
or just plain old quantise?
and another: I assume we'd need two sc objects (with one of them just full of rests, perhaps, that the other's events should stick to and replace with pitched events); plus a start and end bar (defaulting to the whole piece), the player mappings (if both sc objects had players with the same name then we could couple these, by default, but what if they don't have the same players and/or the same number of players?)
then there's the question of which object gets modified? I'd assume the first one (say, with all the pitches) thus using the timings of the second (say, with nothing but rests in perhaps even a single part, e.g. a bunch of 32nds)
then it would also be nice if we could skip providing a 2nd sc object and just give a rhythmic value e.g. 32 so that the first sc would quantise to this rhythm
all just thoughts at the mo.: might be worth working this up in a little mind map before implementing
by the way, what would we call this slippery-chicken method?
I think, "quantise" would be a good name, as it will basically be an abstracted version of usual quantisation algoithms, providing "standard functionality" and beyond...
and another: I assume we'd need two sc objects (with one of them just full of rests, perhaps, that the other's events should stick to and replace with pitched events); plus a start and end bar (defaulting to the whole piece), the player mappings (if both sc objects had players with the same name then we could couple these, by default, but what if they don't have the same players and/or the same number of players?)
What if the method would not be invoked on the entire sc-object, but on single players, e.g. (method sc-object-A player-A sc-object-B player-B)? One could easily use loop to quantise an entire piece or only certain players parts. That would leave the mapping-issue up to the user.
then there's the question of which object gets modified? I'd assume the first one (say, with all the pitches) thus using the timings of the second (say, with nothing but rests in perhaps even a single part, e.g. a bunch of 32nds)
The first object would be most intuitive to be modified, I suppose.
then it would also be nice if we could skip providing a 2nd sc object and just give a rhythmic value e.g. 32 so that the first sc would quantise to this rhythm
If the method is given a simple rhythmic value as grid, it should probably also be able to handle a tempo-map and time-signatures?
by the way, what would we call this slippery-chicken method?
I think, "quantise" would be a good name, as it will basically be an abstracted version of usual quantisation algoithms, providing "standard functionality" and beyond...
that's fine with me
What if the method would not be invoked on the entire sc-object, but on single players, e.g. (method sc-object-A player-A sc-object-B player-B)?
that was my assumption, but as with midi-play etc. the players would default to 'all'
One could easily use loop to quantise an entire piece or only certain players parts. That would leave the mapping-issue up to the user.
that's the way to go
then there's the question of which object gets modified? I'd assume the first one (say, with all the pitches) thus using the timings of the second (say, with nothing but rests in perhaps even a single part, e.g. a bunch of 32nds)
The first object would be most intuitive to be modified, I suppose.
agreed then
then it would also be nice if we could skip providing a 2nd sc object and just give a rhythmic value e.g. 32 so that the first sc would quantise to this rhythm
If the method is given a simple rhythmic value as grid, it should probably also be able to handle a tempo-map and time-signatures?
i was thinking that if you only pass a rhythm, then the first thing to do would be to clone the first sc object's bar structure and fill all the bars with that rhythm before then calling the 'main' method i.e. something like this (leaving out keywords for players, start-bar etc.)
(defmethod quantise ((sc1 slippery-chicken) (sc2 slippery-chicken))
;; ....
)
(defmethod quantise ((sc slippery-chicken) rhythm)
(let ((sc2 (clone-and-fill-with-rests sc rhythm)))
(quantise sc sc2)))
ps i think if you wanted to fo further and provide a rhythm, a tempo-map, and time-signatures, you'd be opening a smelly can of worms. it'd probably be just easier to create the sc object, perhaps via bars-to-sc...or did you have some cunning data structure in mind to do this more easily?
If the method is given a simple rhythmic value as grid, it should probably also be able to handle a tempo-map and time-signatures?
i was thinking that if you only pass a rhythm, then the first thing to do would be to clone the first sc object's bar structure and fill all the bars with that rhythm before then calling the 'main' method i.e. something like this (leaving out keywords for players, start-bar etc.)
(defmethod quantise ((sc1 slippery-chicken) (sc2 slippery-chicken)) ;; .... ) (defmethod quantise ((sc slippery-chicken) rhythm) (let ((sc2 (clone-and-fill-with-rests sc rhythm))) (quantise sc sc2)))
Ah, got it! That seems to be an easy way to do "standard quantisation"!
Another case: I assume that when two sc-objects (with very different bar-structures) are used, the algorithm would ignore sc1s bar-structure and only look at the absolute start-position of each event and then match the closest event in sc2? In that case, would sc1 always have to be an sc-object (meaning: data that is already in some kind of bar structure) or could it also simply be a list of sc-events, generated "by hand" from any data? Extending your example:
(defmethod quantise ((sc1 slippery-chicken) (sc2 slippery-chicken))
(let ((event-ls (get-all-events sc1)))
(quantise event-ls sc2)))
)
(defmethod quantise ((sc slippery-chicken) rhythm)
(let ((sc2 (clone-and-fill-with-rests sc rhythm)))
(quantise sc sc2)))
(defmethod quantise (event-ls (sc2 slippery-chicken))
;; ...
)
ps i think if you wanted to fo further and provide a rhythm, a tempo-map, and time-signatures, you'd be opening a smelly can of worms. it'd probably be just easier to create the sc object, perhaps via bars-to-sc...or did you have some cunning data structure in mind to do this more easily?
No, not at all. Creating a dummy-sc-object will be the easiest way to handle all applications other than simple quantisation, I guess.
(defmethod quantise ((sc1 slippery-chicken) (sc2 slippery-chicken)) ;; .... ) (defmethod quantise ((sc slippery-chicken) rhythm) (let ((sc2 (clone-and-fill-with-rests sc rhythm))) (quantise sc sc2)))
Ah, got it! That seems to be an easy way to do "standard quantisation"!
Exactly: my thought was to offer that but so much more if you pass two sc objects
Another case: I assume that when two sc-objects (with very different bar-structures) are used, the algorithm would ignore sc1s bar-structure and only look at the absolute start-position of each event and then match the closest event in sc2?
that's what i was thinking, for sure
In that case, would sc1 always have to be an sc-object (meaning: data that is already in some kind of bar structure) or could it also simply be a list of sc-events, generated "by hand" from any data?
Indeed it could be. Good thought.
Extending your example:
(defmethod quantise ((sc1 slippery-chicken) (sc2 slippery-chicken)) (let ((event-ls (get-all-events sc1))) (quantise event-ls sc2))) ) (defmethod quantise ((sc slippery-chicken) rhythm) (let ((sc2 (clone-and-fill-with-rests sc rhythm))) (quantise sc sc2))) (defmethod quantise (event-ls (sc2 slippery-chicken)) ;; ... )
So actually your proposed method would be the main workhorse, right? Meaning, if passed sc1 then all we'd do is extract a list of events from it, quantise it against sc2, sticking the events into sc2 or a clone thereof.
ps i think if you wanted to fo further and provide a rhythm, a tempo-map, and time-signatures, you'd be opening a smelly can of worms. it'd probably be just easier to create the sc object, perhaps via bars-to-sc...or did you have some cunning data structure in mind to do this more easily?
No, not at all. Creating a dummy-sc-object will be the easiest way to handle all applications other than simple quantisation, I guess.
OK, agreed then. Sounds good. Now it just needs programming :/
I made this a while back, it's kinda similar. At least it might help?
https://github.com/danieljamesross/slippery-chicken/blob/master/copy-bars-sc2sc.lsp
Reading back through this thread, why would one need a 2nd sc object? If I wanted to quantise one sc, wouldn't it be more convenient to supply a list of rhythms? Or am I missing something?
I made this a while back, it's kinda similar. At least it might help?
https://github.com/danieljamesross/slippery-chicken/blob/master/copy-bars-sc2sc.lsp
It doesn't quantise though, does it?
Reading back through this thread, why would one need a 2nd sc object? If I wanted to quantise one sc, wouldn't it be more convenient to supply a list of rhythms? Or am I missing something?
No, I think you're onto something! We could keep both arguments open: either a list or sc object. the method selected will be determined by the argument types but the heavy-lifting will be done by a method (or perhaps now a function) that handles two lists of events, aligning the first with nearest time-neighbours of the second.
So we take on essentially the rhythmic character of the 2nd arg, but the pitch character of the 1st.
I think we're going to like this.
One thing that needs to be sorted out though is: what happens if arg1 has loads of events and arg2 not nearly so many. we can always find the nearest by time in arg2, but then that same arg2 event might be the nearest by time to the next several events in arg1. so do we
1) keep overwriting, or 2) actually do things the other way around, i.e. go through arg2's events and replace their pitches with the nearest event in arg1?
The results will be quite different.
My intuition says go with 2) but should there be an option of using either approach?
ps i'm off to cut the grass (so fookin' boojwah) and might not be back around these parts for a day or two. keep thinking. this is good
Might there also be a related method that works with only sounding durations and not written?
Like adding "swing" to a midi track as you would in a DAW.
E.g. CMN display to be '(q. e) but the CLM/MIDI rendering to be somewhere between that and { 3 tq te }
My feeling is that would be a different method. We already have the :process-event-fun for cmn-display and :force-velocity for midi-play. We could make that a consistent keyword across all output methods, and even provide a 'humanise' function that varies onset times. But what we're going here is the opposite, no? Want to add an enhancement issue?
One thing that needs to be sorted out though is: what happens if arg1 has loads of events and arg2 not nearly so many. we can always find the nearest by time in arg2, but then that same arg2 event might be the nearest by time to the next several events in arg1. so do we
1. keep overwriting, or 2. actually do things the other way around, i.e. go through arg2's events and replace their pitches with the nearest event in arg1?
The results will be quite different.
I think it is important to be clear about what the "grid" is (only information to be used, nothing that really sounds in the end) and what the actual musical structure is that should be edited. In that sense, if arg1 (the piece/parts to be edited) would have a lot of events and arg2 (the grid) in the most extreme case only one single event at start-time 0, the result would be one single chord with all the pitches in arg1. That would mean:
My intuition says go with 2) but should there be an option of using either approach?
Going through arg2s events and replacing the pitches with those of the nearest event in arg1 would make the grid audible in case of standard quantisation, wouldn't it? If one would pass a simple 32 as arg2, the result would be a lot of short notes that change their pitch from time to time.
Aha, I think we're seeing this quite differently. A chat on Tuesday will help to clarify things. It's important to establish needs before programming this.
...so here comes the summary (...summery summary..? :D) of our conversation today:
The main method that handles the quantisation will receive two lists of events. The first list will be modified and returned. The start-time of each event in ls1 ("musical data") will be replaced with the start-time of the closest event in ls2 ("grid"). The end-time of each event in ls1 will be adjusted accordingly. End-times can optionally also be quantised. Replacing the start-times will be done by a function called once per event in ls1, which can optionally be replaced by a custom function. This way, other data in the events of the first list could as well be manipulated/replaced by data in the second list using this method.
Hello all,
I've worked up a procedure description below and would be curious about your thoughts, above all as to whether you think this will be fit for (your) purpose, but also whether you think it'll work and/or whether I've forgotten or misunderstood something.
Best, Michael
The events of a slippery-chicken object are quantised to the timing of the events in a second slippery-chicken object. As this is a small suite of methods and functions, the heavy lifting is done by the quantise-aux function. This quantises lists of arbitrarily timed events, e.g. those obtained by looped calls to make-event and then time-incremented by events-update-time. So, in effect, this method is independent of---though supportive of, even focused mainly upon---slippery-chicken objects. In each method though, the events of the first argument will be modified to reflect the timing of the second argument, with the pitch and other information (e.g. accents and other marks) of the first retained by default.
As the bar/meter structure of the first slippery-chicken object will adopt the bar structure of the second, it is up to the user to make sure that all players in the first are quantised similarly so that they can be output to a score. This should not present a problem when quantising to a simple rhythm, as we know well from DAWs (see method descriptions below), but might need some thought when quantising to a second slippery-chicken object.
Whilst quantising one list of events to another should present no significant problem and thus work with any given timed events, quantising with the events of slippery-chicken objects will not always work precisely. The problem is, as usual, metrical structure and score notation: once we've called the auxiliary function to quantise the event lists, we have to bring the quantised events back into an existing slippery-chicken (bar/metrical) structure. This isn't a problem when working with two slippery-chicken objects and full quantisation/stickiness (see below) but can be when working with event lists generated outside of such an object.
One problem is caused when the nearest event in the 'grid' (second argument) is outside the bar time range of the event we want to quantise. When this occurs, do we skip the event or quantise to the nearest nevertheless, allowing following events to potentially overwrite the grid event's pitch data when they too quantise to this event? I prefer the latter to skipping completely, so this is what will be implemented. This could of course result in significant event loss from the first argument but that is, I assume, what quantising to a 'sparse grid' should result in. (There was a discussion with Simon Bahr as to whether notes should coalesce into chords but given that many instruments can't play chords and these methods will be used for slippery-chicken objects, we decided against that, for now--it could become an option at some point though, as the event method add-pitches would make this quite easy.
Another problem is what to do when, even within the time frame of a common bar number, the event we want to quantise to has any arbitrary timing which may or may not be notate-able in the context of the current meter and tempo. Of course we can work out very precisely what fraction of a beat within a bar any given time might be---and because we're working in Lisp it's no problem to express that as a rational rather than floating point number e.g. (rationalize 0.9234232) = 14772/15997---but it is debatable whether this is then reasonably notate-able in the context of other nearby and arbitrarily timed events, no matter how many nested complex tuplets or complex fractions we might choose to use once we've found a common factor. Thus we take the easy way out (for now at least): not only the method which quantises to an arbitrary list of events but (because of 'stickiness' interpolations) all methods require a 'shortest-rhythm' which forms the shortest unit (or rhythmic atom) we'll quantise to. You could thus argue that we're actually double-quantising, which somehow simultaneously appeals and doesn't (for instance, with no stickiness interpolations (i.e. stickiness = 1.0) it might be better to stick to the exact rhythms of the grid, with all its potential nested tuplets, but as this is all about quantisation I'll leave that, again, for now at least).
**** Some assumptions We assume that all events have start- and end-times and are time sorted. We will check for start- and end-times (the 'every' function) but not for correct time order---that's the job of the caller.
We assume that the start-times of the grid events encompass at least the minimum and maximum start-times of the events to quantise.
We also assume that the user will call update-slots on the first slippery-chicken object once finished with calls to this/these method/s.
A short discussion with Thomas Neuhaus brought into the equation a further feature along with all its complexities: that of a 'degree' of quantisation, or in other words, how 'sticky' the grid is. We'll express this as a number between 0.0 and 1.0 where 0.0 is no quantisation (we wouldn't do that, would we?) and 1.0 is full quantisation/stickiness to the grid.
**** The methods
Blimey, this looks to be a much bigger issue that I had anticipated.
I'm still not quite sure what the intention, compositionally speaking, is behind it. @simonbahr would you be able to give me an example of a use case, please? If one is going to the trouble of creating an sc object in the first place, why would it then need quantizing?
The whole point of this exercise is not just to deplete gray matter but--beyond quantisation methods which could be useful to render e.g. a nested-tuplet heavy piece (rqq-generated perhaps) into simpler rhythms--to approach the idea of mapping one piece onto the structure of another, perhaps by a better composer ;) Imagine your latest highly-complex opus quantised to Eine Kleine Nachtmusik? Who wouldn't want that???
Blimey, this looks to be a much bigger issue that I had anticipated.
most things are, which is why you'll often see me rolling my eyes when people say "couldn't we simply...."
Imagine your latest highly-complex opus quantised to Eine Kleine Nachtmusik? Who wouldn't want that???
OK, this now makes sense. So I could generate something super complex and then provide simpler versions depending on the experience of my players. Cool.
I suppose it could work in the opposite way, too?
There's perhaps a case for doing a similar operation with pitches.
There's perhaps a case for doing a similar operation with pitches.
well, that would be handled here actually. your :copy-fun could decide to impose pitches however it wanted
Imagine your latest highly-complex opus quantised to Eine Kleine Nachtmusik? Who wouldn't want that???
OK, this now makes sense. So I could generate something super complex and then provide simpler versions depending on the experience of my players. Cool.
exactly
I suppose it could work in the opposite way, too?
i don't see why not though there's probably a limit to the complexity you could generated, especially given that 'stickiness' requires a :shortest-rhythm and I'm not sure right now how we'd avoid that--perhaps someone else has a bright idea
there's probably a limit to the complexity you could generated
I think that's acceptable. Do we ever need { 87 : 63 } tuplets?
I think that's acceptable. Do we ever need { 87 : 63 } tuplets?
well, not that one, but 87:64 for sure
Hey all! That is indeed quite a project. Thank you so much, Michael, for the detailed outline and "accepting the challenge". Everything that I personally thought of – and much more – is covered, nothing to add. Again: let me know if I can be of any assistance!
In my opinion, this is will be a highly useful functionality in many situations, especially with the :copy-fun. E.g. performing some kind of "instrumental convolution", interpreting more or less arbitrary data as musical notation and being able to easily produce a score from it, ... – generally speaking: making the "time-world" of musical notation more adaptable to the "time-world" of computers and electronics, where such things as bars and touplets don't exist and 87:63 is just as "intuitive" as 2:3. ;)
You're welcome Simon. Given the size of this I hope you'll understand that it might take a while. Not sure when I'd have the block of time to do this. Today though I've come to the conclusion that we need a general function to take any arbitrary times and turn them into notate-able rhythms. This is something we could work on together, perhaps with Sebastian as he's the guy to point out where things fail. These are my thoughts so far:
**** using rationalize to reduce arbitrary times to rhythms
So here's one approach at implementing the above reduction:
(defun rationalize-more (float &optional (warn t) (tolerance .001)
(highest-denomninator 20))
(let* ((r (rationalize float))
(n (numerator r))
(d (denominator r))
(candidates (loop for i from 1 to highest-denomninator
for div = (/ d i)
for ni = (round (/ n div))
collect (/ ni i)))
(nearest (nearest float candidates))
(diff (- nearest float)))
(when (and warn (> (abs diff) tolerance))
(warn "rationalize-more:: difference (~a) is > tolerance (~a)"
diff tolerance))
(values nearest diff)))
(RATIONALIZE-MORE .41667)
5/12
-3.33786e-6
with 100,000 random numbers < 5.0 I'm getting an accuracy of about 25 milliseconds. That's not insignificant but it's acceptable, I would say, given what we're trying to do.
(loop repeat 100000 for f = (random 5.0) with d do
(multiple-value-bind (r diff)
(rationalize-more f nil)
(setq d diff)
(format t "~&~a: ~a (diff = ~a)" f r diff))
maximize d)
......
3.5359268: 46/13 (diff = 0.002534628)
0.047169924: 1/20 (diff = 0.002830077)
0.9262544: 13/14 (diff = 0.0023170114)
0.069484115: 1/14 (diff = 0.00194446)
2.1977372: 11/5 (diff = 0.0022628307)
4.852519: 97/20 (diff = -0.0025191307)
0.024994373
Thank you, Michael! What about different tempos, maybe we need a function for deriving an optimal tempo for an arbitrary amount of arbitrarily timed events? Using such a function in combination with the above could make arbitrary time values not only notate-able at all, but also notate-able in the easiest possible way. (Or is this not necessary, because we take the bar-structure from the second event-list anyway? If so, I am not if I understand why we need notate-able rhythms at all, the second list is already notate-able, isn't it?) Maybe we can talk about this on Tuesday!
Hi Simon,
If you have some code for tempo detection in e.g. MIDI files that would be great. Otherwise I'd expect tempi to be passed by the user or simply read from the MIDI file.
Or is this not necessary, because we take the bar-structure from the second event-list anyway?
How? That's just a list of events, not bars, even though the events could have bar numbers attached to them. (Wasn't this your idea? ;)
If so, I am not if I understand why we need notate-able rhythms at all, the second list is already notate-able, isn't it?
From your original comment at the top of this thread: "It would also be usefull [sic] to make events with arbitrary time values displayable in musical notation."
So no, it's not necessarily notate-able at all, especially with Thomas's interpolation approach, which I like very much.
Let's talk about this tomorrow. In any case I'm all for a sub-project which deals with notating any given time values. It's time we had this.
Best, Michael
Here's the latest refinement. It attempts to choose the least complex rhythm that's within tolerance:
(defun rationalize-more (float &key (warn t) (tolerance .01)
(num-max 20))
(let* ((r (rationalize float))
(n (numerator r))
(d (denominator r))
(candidates (loop for i from 1 to num-max
for div = (/ d i)
for ni = (round (/ n div))
collect (/ ni i)))
diff tolerated best)
(setq candidates (remove-duplicates candidates))
(multiple-value-bind
(nearest csorted deltas)
(nearest float candidates)
;; now get all those within tolerance and choose the simplest: the one
;; with the lowest denominator
(setq tolerated (loop for d in deltas and cs in csorted
if (< d tolerance) collect cs into result
else do (return result)
finally (return result))) ; just in case they all pass
;; of those within tolerance, prefer the one with the lowest denominator
;; (less complex tuplets)
(setq best (first (sort tolerated
#'(lambda (x y)
(< (denominator x) (denominator y))))))
;; believe it or not, we shouldn't need nearest but in case we have no
;; results within tolerance it'll come in handy
(unless best (setq best nearest))
(setq diff (- best float))
(when (and warn (> (abs diff) tolerance))
(warn "rationalize-more:: difference (~a) is > tolerance (~a)"
diff tolerance))
(values best diff))))
Oooh this is a real head scratcher. Nice work!
Line 23, we don't need that second setq
, do we?
Line 24, do we need the lambda if we only call the <
anyway? I see that it is called later, but it just seems (and yes I'm being very picky here) long winded to declare lambda... Not that I could do better, mind.
Line 23, we don't need that second
setq
, do we?
cheeky bugger! ça fait encouler les mouches! mais oui, tu as le droit
line 24
we do need the lambda (afaik) to compare just the denominator, not the whole number
cheeky bugger! ça fait encouler les mouches! mais oui, tu as le droit
de rien.
we do need the lambda (afaik) to compare just the denominator, not the whole number
I'm overcomplicating it, but is there a way with back commas and eval
?
(let ((x 3/4)
(y 4/6)
best)
(setq best `(< (denominator ,x) (denominator ,y)))
(eval best))
aren't you forgetting the call to sort there?
cheeky bugger! ça fait encouler les mouches! mais oui, tu as le droit de rien.
I'm overcomplicating it, but is there a way with back commas and eval? (let ((x 3/4) (y 4/6) best) (setq best `(< (denominator ,x) (denominator ,y))) (eval best))
we do need the lambda (afaik) to compare just the denominator, not the whole number
Yes, it's an oversimplification of an overcomplication
Hah!
(sort '(262626/5342 3/4 5/6 7/8 3/5) #'< :key #'denominator)
I think that's right, and it's even simpler than I had previously thought (if indeed it is correct)
(defun rationalize-more (float &key (warn t) (tolerance .01)
(num-max 20))
(let* ((r (rationalize float))
(n (numerator r))
(d (denominator r))
(candidates (loop for i from 1 to num-max
for div = (/ d i)
for ni = (round (/ n div))
collect (/ ni i)))
diff tolerated best)
(setq candidates (remove-duplicates candidates))
(multiple-value-bind
(nearest csorted deltas)
(nearest float candidates)
;; now get all those within tolerance and choose the simplest: the one
;; with the lowest denominator
(setq tolerated (loop for d in deltas and cs in csorted
if (< d tolerance) collect cs into result
else do (return result)
finally (return result)) ; just in case they all pass
;; of those within tolerance, prefer the one with the lowest denominator
;; (less complex tuplets)
best (first (sort tolerated #'< :key #'denominator)))
;; believe it or not, we shouldn't need nearest but in case we have no
;; results within tolerance it'll come in handy
(unless best (setq best nearest))
(setq diff (- best float))
(when (and warn (> (abs diff) tolerance))
(warn "rationalize-more:: difference (~a) is > tolerance (~a)"
diff tolerance))
(values best diff))))
Now Dan, if you feel like getting your teeth into something proper, there's the matter of splitting arbitrarily timed arbitrarily long events into bars, dividing some of those into two when they tie over bars, generating the data for the rests out of those events, and using rationalise-more to get all the rhythms into bars. no pressure :)
(sort '(262626/5342 3/4 5/6 7/8 3/5) #'< :key #'denominator)
Nice, yes, that'll do the same job as my call with lambda. I never use :key but I suppose I should more often, though I doubt it's any faster (and lambda is easier to read, arguably: the problems of big languages)
Now Dan, if you feel like getting your teeth into something proper...
that was a genuine invitation...not beyond your means ;)
Oh I took it as such. Gonna have a look tomorrow!
excellent! we can talk it through perhaps also via signal
Now Dan, if you feel like getting your teeth into something proper, there's the matter of splitting arbitrarily timed arbitrarily long events into bars, dividing some of those into two when they tie over bars, generating the data for the rests out of those events, and using rationalise-more to get all the rhythms into bars. no pressure :)
Are you at this already, Dan? I am working on a function for this. Unless you got it already, I will post my attempt here soon and let you both disassemble it (can you say that like this?), ok?
It would be very usefull to have a function that can be used to align the time-values of events to a grid. The grid could either be equidistant (piano-roll-style) or could be a complex musical structure itself. This would allow for a "convolution" of events, combining the pitch, etc. values of one group of events with the time values of another group. It would also be usefull to make events with arbitrary time values displayable in musical notation.