mdedwards / slippery-chicken

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

rationals in rhythm-seqs #58

Open Leon-Focker opened 2 years ago

Leon-Focker commented 2 years ago

There seems to be an issue with rationals as rhythm values when trying to write cmn, lilypond or xml data. Here is a minimal example that produces a weird score:

(let* ((working-title
    (make-slippery-chicken
     '+working-title+
     :title "working-title"
     :ensemble '(((oboe (oboe :midi-channel 1))))
     :set-palette '(( 1 ((c4))))
     :tempo-map '((1 (q 90)))
     :set-map `((1 (1)))
     :rthm-seq-palette '((1 ((((4 4) 4/3 4)))))
     :rthm-seq-map `((1 ((oboe (1))))))))
  (cmn-display working-title)
  (lp-display working-title)
  (write-xml working-title))

With cmn, the 4/3, which should be a dotted half note, becomes just a half note. Lilypond writes a whole note and the following quarter into a second bar, which shoudln't exist. When opening the xml file with musescore I see a whole note and a quarter in one 4/4 bar. For me there is no error or warning within sc.

mdedwards commented 2 years ago

That doesn’t surprise me, because your rthm-seqs are being entered the way sc would expect your to explicitly notate dots. It’s not so easy to catch all dots, double dots, and triple dots, in all varieties, especially when rationals can lead to complex (nested) tuplets. That’s why there’s the rqq way of entering rhythms. If you’re algorithmically creating rationals then rqq is the way to go:

(let* ((working-title (make-slippery-chicken '+working-title+ :title "working-title" :ensemble '(((oboe (oboe :midi-channel 1)))) :set-palette '(( 1 ((c4)))) :tempo-map '((1 (q 90))) :set-map ((1 (1))) :rthm-seq-palette '((1 ((((4 4) (4 (3 1))))))) ;4/3 4))))) :rthm-seq-map((1 ((oboe (1)))))))) (cmn-display working-title) (lp-display working-title) (write-xml working-title))

Of course that works out the number of dots, as you can see, so theoretically we could bang away at the code to try and catch all cases, if you’re up for that.

Best, Michael

On 18. Apr 2022, at 18:32, Leon-Focker @.***> wrote:

There seems to be an issue with rationals as rhythm values when trying to write cmn, lilypond or xml data. Here is a minimal example that produces nonsense:

(let* ((working-title (make-slippery-chicken '+working-title+ :title "working-title" :ensemble '(((oboe (oboe :midi-channel 1)))) :set-palette '(( 1 ((c4)))) :tempo-map '((1 (q 90))) :set-map ((1 (1))) :rthm-seq-palette '((1 ((((4 4) 4/3 4))))) :rthm-seq-map((1 ((oboe (1)))))))) (cmn-display working-title) (lp-display working-title) (write-xml working-title))

With cmn, the 4/3, which should be a dotted half note, becomes just a half note. Lilypond writes a whole note and the following quarter into a second bar, which shoudln't exist. When opening the xml file with musescore I see a whole note and a quarter in one 4/4 bar. For me there is no error or warning within sc.

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.

mdedwards commented 2 years ago

PS (get-rhythm-letter-for-value 4/3) ---> h.

Leon-Focker commented 2 years ago

Ah I see, I'll try and make acquaintance with rqq then!

The (get-rhythm-letter-for-value) function helps a lot for now. Do you think that some form of it should maybe be run when parsing the rhythms? In that case you could probably prevent some weird notation outputs and also get a warning as soon as rationals get a bit out of hand for notation...

Leon-Focker commented 2 years ago

Sadly rqq only solves a few of my troubles... Maybe working with a 13/16 time signature wasn't such a good idea after all :d But since the problems with odd time signatures seem to be known, I'm not going to post them here again. Instead I have cooked up some functions that attempt to turn a list of rationals into rqq notation. Even though they don't work for what I wanted to do, maybe they can help other slippery chicken users? Especially in a glorious future, where rqq can split 13 into 5 and 8... Currently I'm not sure wheter there are many usecases in which this is more usefull than (get-rythm-letter-for-value) but maybe there are.

;; *** separate-rhythms
;;; some rather 'brute-forcey' way to group and sub-group the durations
;;; by checking how they can be divided. If there is a mathematically elegant
;;; way of doing this, please let me know!
(defun separate-rhythms (rhythms &key (time-sig '(4 4)))
  ;; calculate duration of one bar relative to 4/4
  (let* ((bar-ratio (/ (car time-sig) (cadr time-sig)))
     (match '()))
    ;; find possible dividing points and save them into match:
    (loop for i from 2 to 20 until match do
     (loop for j from 1 and r in rhythms sum r into sum do
          (when (member sum
                (loop repeat (- i 1) for k from 1
                   collect (* (/ k i)
                      bar-ratio))
                :test #'=)
        (push j match))))
    ;; if we can devidie the sequence further, do so, else just return it
    (if match
    (progn (push (length rhythms) match)
           (loop for i in (reverse match) with last = 0 collect
            (let* ((ls (subseq rhythms last i))
               (len (loop for i in ls sum i)))
              ;; maybe divide the subgroups again recursively:
              (if (or (atom (cdr ls)) (apply #'= ls))
              ls
              (separate-rhythms ls
                        :time-sig (list len 1))))
          do (setf last i)))
    rhythms)))

;; *** ratio-pair-p
;;; checks if list has a certain structure: (1 (1...))
(defun ratio-pair-p (ls)
  (and (atom (car ls)) (listp (second ls)) (not (equal '() (second ls)))))

;; *** list-of-atoms-p
;;; checks if list contains lists
(defun list-of-atoms-p (ls)
  (every #'identity (mapcar #'atom ls)))

;; *** rhythmlist-to-rqq
;;; for a list of durations get rqq notation:
;;; eg.: '(1/8 1/8), time-sig '(1 4) --> (1 ((1 (1)) (1 (1))))
(defun rhythmlist-to-rqq (rhythms &key (time-sig '(4 4)))
  ;; rests will remember which notes were actually rests
  ;; (since for now we will only work with duration-values)
  (let* ((rests (loop for r in rhythms collect (if (atom r) nil t)))
     ;; divide sequence into subgroups for tuplets:
     (divisions (separate-rhythms (flatten rhythms) :time-sig time-sig))
     (result '())
     (index 0))
    ;; helper is a recursive function to return those nice and nested
    ;; rqq structures. 
    ;; I'm sure this could be more elegant but I cannot be bothered... 
    (labels ((helper (ls)
           (cond ((list-of-atoms-p ls)
              (let* ((sum (loop for i in ls sum i))
                 (gcd (gcd-of-list ls))
                 (new-ls (loop for i in ls collect (/ i gcd))))
            (list sum new-ls)))
             ((every #'identity
                 (mapcar #'ratio-pair-p ls))
              (let* ((sum (loop for i in ls sum (car i)))
                 (gcd (gcd-of-list (loop for i in ls collect
                            (car i))))
                 (new-ls (loop for i in ls collect
                      (append (list (/ (car i) gcd))
                          (cdr i)))))
                (list sum new-ls)))
             (t (let* ((new (loop for i in ls collect (helper i))))
              (if (ratio-pair-p new)
                  new
                  (helper new))))))
         ;; this function will replace certain notes with rests
         ;; by looping through the lists that correspond to actualy notes
         ;; and comparing with the rests list we created earlier
         (get-some-rest (ls)
           (if (listp ls)
           (if (list-of-atoms-p ls)
               (loop for i in ls collect
                (if (nth index rests) (list i) i)
              do (incf index))
               (loop for i in ls collect (get-some-rest i)))
           ls)))
      ;; scale first element according to time-signature:
      (setf result (cons (* (/ (car time-sig) (cadr time-sig)) 4)
             (cdr (helper divisions))))
      ;; for now we couldn't distinguish between notes and rests so
      ;; we need to replace the respective notes with rests here:
      (get-some-rest result))))

;; using the tuplets from the rqq manual as an example:
(let* ((working-title
    (make-slippery-chicken
     '+working-title+
     :title "working-title"
     :ensemble '(((oboe (oboe :midi-channel 1))))
     :set-palette '(( 1 ((c4))))
     :tempo-map '((1 (q 90)))
     :set-map `((1 (1)))
     :rthm-seq-palette `((1 ((((1 4) ,(rhythmlist-to-rqq '((1/12) (2/36) 1/36 1/36 1/36 1/36)
                                 :time-sig '(1 4)))
                  ((4 4) ,(rhythmlist-to-rqq
                       '(1/45 (1/45) 1/45 1/45 1/45 5/144 5/144 5/144 5/144
                         1/45 (1/45) 1/45 1/45 1/45 5/144 5/144 5/144 5/144
                         1/45 (1/45) 1/45 1/45 1/45 5/144 5/144 5/144 5/144
                         1/45 (1/45) 1/45 1/45 1/45 5/144 5/144 5/144 5/144)))))))
     :rthm-seq-map `((1 ((oboe (1))))))))
  (cmn-display working-title)
  (lp-display working-title)
  (write-xml working-title :file "/E/test.xml")
  )
mdedwards commented 2 years ago

Hi Leon,

The (get-rhythm-letter-for-value) function helps a lot for now. Do you think that some form of it should maybe be run when parsing the rhythms?

I think it’s something you should run when parsing your rhythms and before you make a rthm-seq or bar or whatever with them.

It’s a bit like skiing: the pistes are prepared for you. You can go this way, you can go that way. If you choose to go off-piste, don’t complain when you hit a tree ;-)

In that case you could probably prevent some weird notation outputs and also get a warning as soon as rationals get a bit out of hand for notation…

On a more serious note, see how you get along pre-processing your data and let’s talk about this. We’d need a way of reliably detecting a number (because we can’t restrict ourselves to rationals, surely?) that we know we'd want to process for possible dots and (possibly nested) tuplets—you can’t ignore those if you’re going to accept any random number and try to make it work in notation. It can be done of course but it might need a while and a lot of tests. See parse-rhythms in rthm-seq-bar.lsp for example.

Cheers, Michael