mdedwards / slippery-chicken

slippery chicken: algorithmic composition software in common lisp and clos
http://michael-edwards.org/sc
71 stars 3 forks source link

Aligning events to a grid #36

Open simonbahr opened 4 years ago

simonbahr commented 4 years ago

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.

simonbahr commented 4 years ago

Arguments:

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

...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 commented 4 years ago

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 commented 4 years ago

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

simonbahr commented 4 years ago

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

mdedwards commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

Ah, I see, it is working now!

danieljamesross commented 4 years ago

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

https://github.com/mdedwards/slippery-chicken/blob/d2f7d3f25afa2209bd31b466913cff578b51d82d/import-audio.lsp#L165 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.

https://github.com/mdedwards/slippery-chicken/blob/d2f7d3f25afa2209bd31b466913cff578b51d82d/import-audio.lsp#L233 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 commented 4 years ago

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.