Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aligning events to a grid #36

Open
simonbahr opened this issue May 8, 2020 · 63 comments
Open

Aligning events to a grid #36

simonbahr opened this issue May 8, 2020 · 63 comments
Assignees

Comments

@simonbahr
Copy link

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.

@mdedwards
Copy link
Owner

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

@mdedwards mdedwards self-assigned this May 8, 2020
@mdedwards
Copy link
Owner

mdedwards commented May 8, 2020

by the way, what would we call this slippery-chicken method?

rasterise (usually just images)?

magnetise?

or just plain old quantise?

@mdedwards
Copy link
Owner

mdedwards commented May 8, 2020

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

@simonbahr
Copy link
Author

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?

@mdedwards
Copy link
Owner

mdedwards commented May 8, 2020

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)))

@mdedwards
Copy link
Owner

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?

@simonbahr
Copy link
Author

simonbahr commented May 8, 2020

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.

@mdedwards
Copy link
Owner

mdedwards commented May 8, 2020


(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 :/

@danieljamesross
Copy link
Collaborator

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

@danieljamesross
Copy link
Collaborator

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?

@mdedwards
Copy link
Owner

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?

@mdedwards
Copy link
Owner

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.

@mdedwards
Copy link
Owner

mdedwards commented May 8, 2020

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?

@mdedwards
Copy link
Owner

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

@danieljamesross
Copy link
Collaborator

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 }

@mdedwards
Copy link
Owner

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?

@simonbahr
Copy link
Author

simonbahr commented May 9, 2020

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:

  • If arg1 has only unique pitches, all events in arg1 remain alive.
  • If arg1 has multiple events with equal pitches in the same players part that are nearest to the same event in arg2, the event with longest duration would survive, as the events "overlap" and one player can not play multiple notes of the same pitch at the same time, right?
  • If the instrument is not able to play multiple notes at the same time at all, there is in my point of view no really intuitive rule. One way of dealing with it would be to keep the shorter events alive in that case, in order to let as many pitches and rhythms survive as possible. But in the end, it would be up to the user to select a grid that works for the amount of events in arg1 – aligning everything to very few points in a grid would produce some kind of glitch anyway.

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.

@mdedwards
Copy link
Owner

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.

@simonbahr
Copy link
Author

simonbahr commented May 12, 2020

...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.

@mdedwards
Copy link
Owner

mdedwards commented May 14, 2020

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

  • (defmethod quantise ((sc1 slippery-chicken) (sc2 slippery-chicken) &key
    players1 start-bar1 end-bar1 player2 start-bar2
    stickiness shortest-rhythm copy-fun)
    • keyword args defaults when nil:
      • players1: all
      • start-bar1 1
      • end-bar1: last bar of sc1
      • player2: first in ensemble of sc2
      • start-bar2: 1
      • stickiness: 1.0 (full quantisation i.e. no interpolation between the
        original timing and the grid)
      • shortest-rhythm: 32nd
      • copy-fun: a function which takes two event arguments and copies over the
        required slots of the first to the second
        • i.e. not timing info but pitches, marks etc.
    • prepare the event lists from the two objects and pass to quantise-aux
      • (get-events-from-to)
      • we'll only need sc2 events up to or just beyond the end-time of end-bar1
    • the quantisation process will proceed for each player listed in players1
      (which could also be just a single player as a symbol)
    • the process will proceed for the events from start-bar1 to end-bar1 in sc1
    • what to quantise against (the grid) comes from sc2 starting at start-bar2
      • this implies that there must be enough bars/events in sc2
      • but we do not check the number of bars in sc2 as the metrical and tempo
        structures could be different
      • still we'll need to check there's enough sc2 events and issue a warning if
        not
        • quantise-aux will do this by checking the timings the first and last
          events in each list
    • after the aux function returns the list of new events, stick these back into
      the bars of sc2 and then replace the bars of sc1 with these.
      • i.e. overwrite the bars of sc1 with the returned events
    • return (modified) sc1
  • (defmethod quantise ((sc slippery-chicken) rhythm &key players1 start-bar1
    end-bar1 stickiness shortest-rhythm copy-fun)
    • (let ((sc2 (clone-and-fill-with-rests sc rhythm)))
      • a new method
      • this will issue an error if any bar of sc can't be filled exactly with
        rhythm
    • (quantise-aux events-of-sc events-of-sc2)
    • overwrite the bars of sc with the returned events
    • return (modified) sc
  • (defmethod quantise (events (sc slippery-chicken) &key player2 start-bar2
    stickiness shortest-rhythm copy-fun)
    • attacked events (i.e. skipping rests) in sc will have their pitches replaced
      by those from the first argument
      • this implies, here as well as in other methods, that some, perhaps many,
        of the events in the first argument will not be used (or, rather, their
        pitches will be overwritten by the nearest neighbour in time to the sc
        events)
    • we extract the list of events from sc as with the main method then proceed
      with quantise-aux
    • overwrite the bars of sc with the returned events
    • so we return the sc object (modified) with its metrical/bar structure intact
      but with the pitches of
  • (defmethod quantise ((sc slippery-chicken) events &key players1 start-bar1
    end-bar1 stickiness shortest-rhythm copy-fun)
    • without 'stickiness' (the great equaliser ;) this would be the most
      difficult method as there's no way of knowing what notatable rhythmic grid
      the timings of the events in the second argument would map onto
    • similar to other methods though, we extract the list of events from sc then
      proceed with quantise-aux
    • note that this and the previous method are not interchangeable i.e. you
      can't just exchange the order of sc object and the events list because in
      all these methods we impose the pitches of the first argument on the
      timings/rhythms of the second (or to see it another way, we impose the
      timings of the second on the nearest pitches of the first)
      • so if you have an existing sc object and an event list generated by
        another procedure (e.g. a simple looping call to make-event or reading in
        a midi file), then the order you pass these makes a big difference to the
        result
  • (defun quantise-aux (events1 events2 &key stickiness shortest-rhythm tempo
    sc copy-fun)
    • finally, the workhorse of the show:
    • check that the start-time of the first in events2 is <= the first in events1
    • check that the start-time of the last in events2 is >= the last in events1
    • for each event in events1 (e1):
      • get the nearest event in events2 (e2st) to e1's start time (e1st)
      • also get the nearest event (e2nd) in events2 to e1's end time (e1nd)
      • using stickiness, interpolate from e1st to e2st and e1nd to e2nd
      • if there's an sc object, get the start-time of the bar from which the
        events come
        • if the first of events1 has the bar-num slot filled (!= -1) then sc
          provided these events
        • otherwise if events2 has a bar-num, then sc provided these events
        • otherwise we've got arbitrary events and we'll assume that the tempo
          started at time 0.0 seconds
        • whichever it is, we'll call this start-time t0
      • calculate the shortest rhythm duration in seconds
        • if there's an sc (slippery-chicken object) then it will be used to get
          the tempo from:
          • (get-tempo sc (bar-num e1))
        • if not, and there's a :tempo in BPM use that
        • otherwise default to 60BPM
      • quantise to the nearest shortest rhythm using t0 to subtract from the
        events' start timings
      • generate enough rests to pad from the start of the bar or from the end of
        the last generated attack up to the next attacked event
      • calculate the number of shortest-rhythms this event occupies according to
        its stickiness-quantised start- and end-times
      • generate that number of tied events
        • copying over e1's pitch(es) to these events
        • copying over marks etc. to the first (i.e. attacked not tied) event
        • we'll leave it to the top-level caller to use (consolidate-rests) in
          order to get rid of all the ties, if desired for score notation
        • NB what actually gets copied over is left to the given function
          (:copy-fun) which defaults to copy the above but which could be replaced
          by a user-given function that does other things too (e.g. retains some
          aspects of the grid events, like marks)
      • combine these with the padding rests
    • don't forget to add padding rests at the end to fill up the last 'bar'
      • it doesn't matter whether there's an sc object to interrogate here as
        default meter of 4/4 and 60BPM would be fine too if all we're interested
        in is generating e.g. MIDI files: rest events don't matter there

@danieljamesross
Copy link
Collaborator

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?

@mdedwards
Copy link
Owner

mdedwards commented May 14, 2020

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???

@mdedwards
Copy link
Owner

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...."

@danieljamesross
Copy link
Collaborator

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?

@danieljamesross
Copy link
Collaborator

There's perhaps a case for doing a similar operation with pitches.

@mdedwards
Copy link
Owner

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

@mdedwards
Copy link
Owner

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

@danieljamesross
Copy link
Collaborator

there's probably a limit to the complexity you could generated

I think that's acceptable. Do we ever need { 87 : 63 } tuplets?

@mdedwards
Copy link
Owner

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

@mdedwards
Copy link
Owner

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))))

@danieljamesross
Copy link
Collaborator

danieljamesross commented May 19, 2020

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.

@mdedwards
Copy link
Owner

mdedwards commented May 19, 2020

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

@danieljamesross
Copy link
Collaborator

danieljamesross commented May 19, 2020

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))

@mdedwards
Copy link
Owner

mdedwards commented May 19, 2020 via email

@danieljamesross
Copy link
Collaborator

Yes, it's an oversimplification of an overcomplication

@mdedwards
Copy link
Owner

mdedwards commented May 19, 2020 via email

@danieljamesross
Copy link
Collaborator

(sort '(262626/5342 3/4 5/6 7/8 3/5) #'< :key #'denominator)

@danieljamesross
Copy link
Collaborator

I think that's right, and it's even simpler than I had previously thought (if indeed it is correct)

@danieljamesross
Copy link
Collaborator

(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))))

@mdedwards
Copy link
Owner

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 :)

@mdedwards
Copy link
Owner

(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)

@mdedwards
Copy link
Owner

Now Dan, if you feel like getting your teeth into something proper...

that was a genuine invitation...not beyond your means ;)

@danieljamesross
Copy link
Collaborator

Oh I took it as such. Gonna have a look tomorrow!

@mdedwards
Copy link
Owner

excellent! we can talk it through perhaps also via signal

@simonbahr
Copy link
Author

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?

@simonbahr
Copy link
Author

Arguments:

  • A list of lists, containing a start-time and a duration in secs each -> '((0 1) (8 .5) ...)
  • A tempo (default: 60)
  • The number of beats per bar (default: 4)

Return: A list of lists per bar, containing durations of notes (numbers) and rests (numbers in a list) -> '((1/2 (1/4) 1/4) ((1)) (1)), meaning one bar filled with a note by 50%, a rest by 25% and another note by 25%, another bar containing a whole bar rest and a third bar filled with one single note from start to end.

Would that be useful? I would leave the "slippery-chicken-side" of it to the grown-ups. ;)

@mdedwards
Copy link
Owner

Thanks Simon, looks good.

Would that be useful? I would leave the "slippery-chicken-side" of it to the grown-ups. ;)

It would. And for now I think it's fine to keep this independent of slippery chicken, in order to aid flexibility. On the other hand, we did speak of handling lists of events, which would also be fine, and perhaps convenient because that way you can create rhythm data too, attach tempo changes, meters, etc. etc. But that can also be generated after your method. Just be aware that if you start adding things to your lists that look like slots, then it might be easier to use events--they can always be converted to other object types later, if necessary.

@simonbahr
Copy link
Author

OK, I guess that's true and I will have to deal with sc-events and bars. ;) That means:
Args: list-of-events, (tempo 60), (time-sig '(4 4))
Return: A list of rhythm-seq-bars
I think the "how can I roll my own"-code will be a good template for this, once the events are sliced into bars.

There is one problem I don't seem to get around: When rationalizing the events after slicing them into bars, the returned events will not fit into the bars any more. This means, I think, that we can not simply rationalize each event by itself – we rather need another method, "rationalize-bar", that would rationalize the events in a way that they add up to the bar duration exactly, right? Now, that would already be a sort of quantisation (what's the grid?!), and maybe would lead to unexpected results.

One simple solution would be to stretch or shorten the last event accordingly, with the side effect, that this event may not have a denominator < 20 anymore. Depending on the quantisation happening afterwards, that would affect the result a lot or not at all. What do you think?

@simonbahr
Copy link
Author

...an example for the "easy way":

(defun rationalize-bar (durs)
  (let ((n-durs (length durs))
	(sum 0))
    (loop
       for dur in durs
       for n from 1
       collect
	 (let ((rational
		(rationalize-more dur)))
	   (incf sum rational)
	   (if (= n-durs n)
	       (+ rational (- 1 sum))
	       rational)))))

(defun tester (sum)
  (if (>= sum 1)
      nil
      (let ((rand (random .5)))
	(cons rand (tester (+ sum rand))))))

(loop repeat 100 do
     (print (rationalize-bar (tester 0))))

This produces results such as
(1/6 0 2/13 1/3 9/26)
(3/7 1/7 3/11 12/77)
(1/5 1/12 5/16 97/240)
(3/7 2/7 2/7)
(6/13 2/5 9/65)
(3/11 1/7 2/5 71/385)
(3/10 0 1/4 1/5 1/4)
(1/11 2/9 1/9 2/7 67/231)
(4/9 2/9 1/3)
(1/3 1/6 2/11 7/22)
(5/11 1/3 7/33)
(3/11 3/7 1/6 61/462).

The question is: do we want 61/462 ?!

@mdedwards
Copy link
Owner

Hi Simon, over lunch and after our conversation just now I had some thoughts. If you could get it so far as rationals with a common denominator I'd like to have a poke from there. If that's OK with you. Best, Michael

@simonbahr
Copy link
Author

Sure! I will finish it to that point and post the code here when I'm done.

@simonbahr
Copy link
Author

... posting the code does not work properly right now (too many lines, maybe?!) – I'll send you an email... ;)

@mdedwards
Copy link
Owner

mdedwards commented Jun 4, 2020

Thanks. You can link to code pasted here if you prefer: https://pastebin.com/

Anyway, I've had a poke and found something that seems failsafe in creating rthm-seq-bar objects. I don't think they'll be very nice to read right now, but it's a start. The problem is that all my work at getting nice individual rationals with rationalize-more ends up almost wasted when we combine them: the tuplets get more complicated, as we should expect I suppose.

If you don't have the sc regression test stuff you can find what you need here

(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
                              #'(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))))

(defun rationals-common-denominator (rationals)
  ;;                      least common multiple
  (let* ((common (apply #'lcm (mapcar #'denominator rationals))))
    (values common
            (loop for r in rationals collect (* (numerator r)
                                                (/ common (denominator r)))))))

;;; time-sig is an object or list
;;; will need to find a way of indicating rest or note and put the respective
;;; rhythms in () or not
;;; will also need to break up rhythms into ties if we're going to have anything
;;; readable, as well as handle beaming, somehow (I don't fancy auto-beam here)
(defun rationals-to-rsb (rationals time-sig &optional verbose)
  (when verbose (format t "~&*** rationals-to-rsb: processing ~a" rationals))
  (let* ((ts (make-time-sig time-sig))
         ;; time-sig duration in whole notes
         (ts-dur (/ (rationalize (duration ts)) 4))
         ;; strangely enough, we're not interested here in the denominator,
         ;; rather, we have to sum the numerators to find the tuplet
         (nums (nth-value 1 (rationals-common-denominator rationals)))
         (tuplet-num (apply #'+ nums))  ; how many (numerator) in the time of
         (rthm-value (/ tuplet-num ts-dur))
         ;; this will be the basic rhythmic unit (e.g. 16th 32nd 64th ... ) to
         ;; be multiplied if we start looking at the numbers and trying to
         ;; divide e.g 5 tuplets into 4+1 for better notation
         (npow2 (nearest-power-of-2 rthm-value))
         (tuplet-denom (* ts-dur npow2))
         (tuplet (/ tuplet-num tuplet-denom))
         (rthms (loop for n in nums collect (/ rthm-value n)))
         (rsb (cons (data ts) (if (= 1 tuplet) ; got lucky (or maybe not)?
				  rthms
				  (append (list '{ tuplet)
					  rthms
					  '(}))))))
    (when verbose
      (format t "~&tuplet: ~a, rthm-value: ~a, npow2: ~a, ~%nums: ~a~&bar: ~a"
              tuplet rthm-value npow2 nums rsb))
    (make-rthm-seq-bar rsb)))

;;; test with lists of random floats (between 2 and 7 with a range of .1 and
;;; 2.5) and random time sigs ranging from 2/32  to 13/2
(sc-deftest test-rationals-to-rsb (&optional (repeat 100) (tolerance .01)
					     verbose)
  (flet ((get-some (how-many)
           (loop repeat how-many collect (between .1 2.5 nil)))
         (fs2rs (floats)
           (loop for f in floats collect
                (rationalize-more f :tolerance tolerance))))
    (let* ((lotsa-floats (loop repeat repeat collect
			      (get-some (+ 2 (random 6)))))
           (lotsa-time-sigs
            (loop repeat repeat collect
                 (list (+ 2 (random 12))
                       (nearest-power-of-2 (+ 2 (random 35)) t)))))
      (loop for fs in lotsa-floats
         for ts in lotsa-time-sigs
           do (rationals-to-rsb (fs2rs fs) ts verbose)))))

@simonbahr
Copy link
Author

Thank you, Michael! I executed the deftest a couple of times, and every once in a while I get this error:

rhythm::get-tuplet-ratio: unhandled tuplet: 1
   [Condition of type SIMPLE-ERROR]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [*ABORT] Return to SLIME's top level.
 2: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1002FE8413}>)

Backtrace:
  0: ((FLET TERR :IN GET-TUPLET-RATIO))
      [No Locals]
  1: ((:METHOD FIX-NESTED-TUPLETS (RTHM-SEQ-BAR))  ..) [fast-method]
      Locals:
        ON-FAIL = NIL
        POS = 0
        R#1 = RHYTHM: value: 6.400, duration: 0.625, rq: 5/8, is-rest: NIL, 
                      is-whole-bar-rest: NIL, 
                      score-rthm: 6.4, undotted-value: 32/5, num-flags: 0, num-dots: 0, 
                      is-tied-to: NIL, is-t..
        RESULT = 1
        RSB = 
              RTHM-SEQ-BAR: time-sig: 48 (7 16), time-sig-given: T, bar-num: -1, 
                            old-bar-nums: NIL, write-bar-num: NIL, start-time: -1.000, 
                            start-time-qtrs: -1.0, is-rest-bar: NIL, mu..
  2: ((LAMBDA (SB-PCL::|.P0.| SB-PCL::|.P1.|) :IN "/home/simon/sc/test-suite/sc-test-suite.lsp") #<unavailable argument> #<unavailable argument>)
      [No Locals]
  3: (TEST-RATIONALS-TO-RSB 0.01)
  4: (SB-INT:SIMPLE-EVAL-IN-LEXENV (TEST-RATIONALS-TO-RSB) #<NULL-LEXENV>)
      Locals:
        SB-KERNEL:LEXENV = #<NULL-LEXENV>
        SB-IMPL::ORIGINAL-EXP = (TEST-RATIONALS-TO-RSB)
  5: (EVAL (TEST-RATIONALS-TO-RSB))
      Locals:
        SB-IMPL::ORIGINAL-EXP = (TEST-RATIONALS-TO-RSB)
 --more--

Do you have an idea why this comes up?

@mdedwards
Copy link
Owner

Yes, that arises when no tuplet is necessary, but I fixed that just after initially posting the code. Apparently 100 tests wasn't enough. When did you copy the code above? If you evaluate what's there now it shouldn't issue this error.

@simonbahr
Copy link
Author

Ah, I see, it is working now!

@danieljamesross
Copy link
Collaborator

@simonbahr you might be interested to see some of the new files I wrote in import-audio.lsp

(defun event-list-to-bar-list (event-list &key (tempo 60) (time-sig '(4 4)))

In particular, event-list-to-bar-list at l.165. It take a list of events of arbitrary length and pushes them into a list of lists, with each sublist adding up to the total time of an arbitrary time sig.

(defun bar-list-to-bars (bar-list &key (time-sig '(4 4)) (tempo 60)

Once I have this list, I use bar-list-to-bars at l. 233 to create actual bars, before passing to bars-to-sc.

@simonbahr
Copy link
Author

Thanks, @danieljamesross, these functions may be useful! I will get back to this issue soon and continue to work with Michael on something to make arbitrarily timed events notateable, as Michael told me that such a thing may serve your needs, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants