Outils pour utilisateurs

Outils du site


article_4

[ 2017 ]

by Yann Ics

by.cmsc@gmail.com

http://experimental.mus-ics.net

Cite this article:
Yann Ics, Convert a midi file to a SuperCollider score, 2017
Date retrieved: 17/10/18
Permanent link: http://experimental.mus-ics.net/wiki/doku.php?id=article_4

Presentation

This article belongs to the development of enkode. From now on, enkode allows converting a midi file in order to be interpreted inside the SuperCollider environment. This work allows creating a bridge between the traditional score notation and the algorithmic environment of SuperCollider. Then, this article focus on the full description of the methods used to convert a midi file and its integration inside the command line enkode to a SuperCollider score1).

Conversion

This implies a work which can be exported as a midi file. This is generally the case with most of the score editor.

CSV file

The first step from a midi file is to convert it as a readable tabular data file in a plain text called comma-separated value (CSV). This is done with the command line Unix midicsv.

midicsv <midi_file> out.csv

From the CVS file, it is possible to extract notes with their respective position in time as Note_on and Note_off. Thus, the difference between the two last is the duration of the note involved.

grep 'Note' out.csv | awk '{print $1 " " $2 " " $5}' | sed 's/,//g' > data

Also, the Tempo is extracted and computed as 60.106/Tempo in order to get the real tempo in beats/minute. If the Tempo changes during the score, then the tempo of the score is set to zero. Anyway, this is purely indicative, because the clock of the timing is constant.

grep Tempo out.csv | awk '{print $4}' > tempo_file
tmptempo=`head -n 1 tempo_file`
while read line
do
if [[ $tmptempo != $line ]] 
then tempo=0 
fi
done < tempo_file
if [[ $tempo != 0 ]]
then tempo=$tmptempo
     tempo=$(echo "60000000/$tempo" | bc)
fi

Duration

In time

The duration in time is the difference between the value of the Note_on and the value of the Note_off.

(defun get-note-on-off (track &optional res)
  (if (null track) res
      (let ((al (loop for i in (cdr track) when (equalp (cadar track) (cadr i)) collect i)))
	(push (list (caar track) (- (caar al) (caar track)) (cadar track)) res)
	(get-note-on-off (cdr (remove (car al) track :count 1 :test #'equalp)) res))))
 
(defun add-duration (data)
  (mapcar #'(lambda (track) (reverse (get-note-on-off track))) data))

For the current work, the bias is to fit the duration between two notes or two chords. If the duration is too long, then it is clipped at the beginning of the next event. If the duration stops before the next event, a silence is set in between. Note that a silence is set to zero as a midi value.

This is done as follow:
Let t be the position in time of a chord as a group of notes and d the duration.
Let Ai be the dataset [ ti, di, chord ] at the position i.
Assume that δi = ti+1 − ti and Δi = δi − di.
Then,
if Δi = 0 → Ai
if Δi > 0 → Ai + [ ti + di, δi − di, rest ]
if Δi < 0 → [ ti, δi, chord ]

(defun add-silence-start (track) 
  (if (= 0 (caar track)) track (cons '(0 0) (cons (list (caar track) 0) track))))
 
(defun add-silence-end (lst)
  (let ((tmp (loop for i in lst collect (apply #'+ (mapcar #'cadr i)))))
    (loop for track in lst
	 collect
	 (if (= (apply #'max tmp) (apply #'+ (mapcar #'cadr track)))
	     (mapcar #'cdr track)
	     (reverse (cons (list (- (apply #'max tmp) (apply #'+ (mapcar #'cadr track))) '(0)) (mapcar #'cdr (reverse track))))))))
 
(defun add-rest (track &optional r)
  (loop for a in (butlast track)
     for i from 1
     do
       (let* ((tdiff (- (car (nth i track)) (car a)))
	      (rdiff (- tdiff (cadr a))))
	 (cond
	   ((= rdiff 0) (push a r))
	   ((> rdiff 0) (push a r) (push (list (+ (car a) (cadr a)) (- tdiff (cadr a)) '(0)) r))
	   (t (push (list (car a) tdiff (caddr a)) r)))))
  (reverse (append (last track) r)))

Out time

All the durations computed just below has to be reduced in order to get the minimal value as integer. This is done by calculating the greatest common divisor using the method of prime factorisations.

(defun factor (n)
  "Return a list of factors of n."
  (when (> n 1)
    (loop with max-d = (isqrt n)
       for d = 2 then (if (evenp d) (+ d 1) (+ d 2)) do
	 (cond ((> d max-d) (return (list n))) ; n is prime
	       ((zerop (rem n d)) (return (cons d (factor (truncate n d)))))))))
 
(defun decomposition (n)
  "Decomposition of n as the product of prime numbers.
The result retains only the exponents."
  (let* ((f (factor n))
	 (ser (loop for i from 1 to (apply #'max f) collect i))
	 (cil (mapcar #'reverse (count-item-in-list f))))
    (mapcar #'(lambda (x) (let ((r (assoc x cil))) (if r (cadr r) 0))) ser)))
 
(defun complete-list (lst n)
  (if (= n (length lst)) lst
      (let ((l lst))
	(loop until (= n (length l))
	   do
	     (setf l (reverse (cons 0 (reverse l))))) l)))
 
(defun pgcd (lst)
  (let* ((df (mapcar #'decomposition lst))
	 (n (apply #'max (mapcar #'length df)))) 
    (apply #'* (mapcar #'expt (loop for i from 1 to n collect i)
		       (apply #'mapcar #'min 
			      (loop for a in df collect (complete-list a n)))))))
 
(defun scoring-duration (durations-list)
  (loop for i in durations-list collect (/ i (pgcd durations-list))))

Histogram

The histogram is computed as the summation of all durations in time of a given midi note divided by the number of occurrences of this midi note, and multiplied by 1000 before to be rounded in order to qualify the rounding.

(defun count-item-in-list (lst &optional r)
  (dolist (e (remove-duplicates lst :test #'equalp) r)
    (push (list (count e lst :test #'equalp) e) r)))
 
(defun flat (lst)
  (if (endp lst)
      lst
      (if (atom (car lst))
	  (append (list (car lst)) (flat (cdr lst)))
	  (append (flat (car lst)) (flat (cdr lst))))))
 
(defun sum-dur (pitch track)
  (loop for i in track when (equalp pitch (caddr i)) sum (cadr i)))
 
(defun mean-dur (histogram)
  (let ((m (/ (loop for i in histogram sum (caddr i)) (loop for j in histogram sum (cadr j)))))
    (loop for h in histogram collect (list (car h) (round (* 1000 (cadr h) m)))))) 
 
(defun score2hist (s)
  (loop for i in s append (loop for j in (cadr i) collect (list 0 (car i) j))))
 
(defun histogram (data)
  (let* ((len (loop for track in data collect (sort (count-item-in-list (mapcar #'caddr track)) #'< :key #'cadr)))
	 (res (mapcar #'(lambda (a b) (mapcar #'(lambda (x) (list (cadr x) (car x) (sum-dur (cadr x) b))) a)) len data)))
    (mapcar #'mean-dur res)))

Cmd enkode

This conversion is integrated from the version 4.2 of enkode with to the option --midi. Thus, the standard ouput is the score itself, and the histogram file is written in the output folder. Also, the option --merge allows to merge all tracks of a midi file in one.

;; read midi file
(defun read-text-lines (file)
  (with-open-file (in-stream file
			     :direction :input
                             :element-type 'character)
    (loop with length = (file-length in-stream)
       while (< (file-position in-stream) length)
       collect (read-line in-stream))))
 
(defun string-to-list (string)
  (let ((the-list nil) 
        (end-marker (gensym))) 
    (loop (multiple-value-bind (returned-value end-position)
	      (read-from-string string nil end-marker)
            (when (eq returned-value end-marker)
              (return the-list))
            (setq the-list 
                  (append the-list (list returned-value)))
            (setq string (subseq string end-position))))))
 
;; split list by track ...
(defun split-track (lst &optional track res)
  (if (null lst) (reverse (cons (reverse (mapcar #'cdr track)) res))
      (if (equalp (caar lst) (if track (caar track) (caar lst)))
	  (split-track (cdr lst) (cons (car lst) track) res)
	  (split-track lst nil (cons (reverse (mapcar #'cdr track)) res)))))
 
;; merge tracks ...
(defun merge-data (data)
  (list (sort (loop for i in data append i) #'< :key #'car)))
 
;; group note(s) as chord
(defun group-notes (track &key (fun #'max)) ;; fun allows to select one duration among the durations of each note of the chord.
  (let ((tmp (list (car track))) r)
    (loop for n in (reverse (butlast (cons '(0 0 0) (reverse track))))
       for i from 1
       do	 
	 (cond
	   ((= (car n) (caar tmp)) (push n tmp))
	   (t (push tmp r) (setf tmp (list (nth i track))))))
    (mapcar #'(lambda (x) (list (caar x) (apply fun (mapcar #'cadr x)) (remove-duplicates (mapcar #'caddr x)))) (reverse r))))
 
;; write result ...
(defun mat-trans (lst)
  (apply #'mapcar #'list lst))
 
(defun group-list (lst len-lst)
  (let ((tmp lst) (res nil))
    (catch 'it
      (loop for segment in len-lst
	 while tmp
	 do (push (loop for i from 1 to segment
		     when (null tmp)
		     do  (push sublist res) (throw 'it 0)
		     end
		     collect (pop tmp) into sublist
		     finally (return sublist))
		  res)))
    (nreverse res)))
 
(defun mk-res-file (str-dir lst tempo) 
  (with-open-file (stream (make-pathname :directory (pathname-directory str-dir)
					 :name (pathname-name str-dir)
					 :type "score")
			  :direction :output
			  :if-exists :supersede
			  :if-does-not-exist :create) 
    (loop for i in lst
       for j in (group-list (scoring-duration (apply #'append (mapcar #'car lst))) (mapcar #'length (mapcar #'car lst)))
       do
	 (format stream "~{~a~^ ~}~&" j)
	 (format stream "~{[~{~a~^,~}]~^ ~}~&" (cadr i)))
    (format stream "~a 0 1" tempo)))
 
(defun mk-histogram-file (str-dir histogram)
  (with-open-file (stream (make-pathname :directory (pathname-directory str-dir)
					 :name (pathname-name str-dir)
					 :type "histogram")
			  :direction :output
			  :if-exists :supersede
			  :if-does-not-exist :create)
    (loop for i in histogram do (format stream "~{~{~a~^ ~}~&~} ~&" i))))
;; end of block code lisp.
 
;;;; written in the script shell
;;(defparameter data (add-duration (mapcar #'add-silence-start (split-track (mapcar #'string-to-list (read-text-lines <path_to_data>))))))
;;(defparameter score (add-silence-end (mapcar #'add-rest (mapcar #'group-notes (if (equalp 1 <merge>) (merge-data data) data)))))
;;(mk-res-file <midi_file_name> (mapcar #'mat-trans score) <tempo>)
;;(mk-histogram-file <midi_file_name> (mapcar #'mat-trans (histogram (list (score2hist (apply #'append score))))))

Score

The score and the histogram generated are used inside the performance IMP K540.
Note that for convenience, the files score and histogram has to be in the same directory of the SuperCollider patch.

// load files
~score = FileReader.readInterpret("".resolveRelative +/+ "<midi_file_name>.score",true, true);
~histogram = FileReader.readInterpret("".resolveRelative +/+ "<midi_file_name>.histogram",true, true);
 
// scale the duration of the first phrase
~duration=~score[0]*0.5;

Thus, the score is interpreted as an array and therefore can be read inside a stream as array iteration in the Routine or part of Pattern – using Pbind for instance – as events patterns. In the following piece of code, the event pattern Pwrand allows choosing randomly a note according to its weight defined by the histogram.

(
p = Pseq(
	~histogram.clump(2).collect({ arg item, i;
		Pbind(
			\instrument, \reed,
			\amp, 0.6,
			\dur, Pseq(~duration,inf),
			\pan, Pwhite(-0.5,0.5,inf),
			// Note that the sum of the list of weight should be equal to 1. 
                        // This is done with  the instance method normalizesum.
			\midinote, Pwrand(item[0], item[1].normalizeSum, inf)
		)
}), 1).play
)

Discussion

This conversion has been done to serve the purpose of the last chapter. Of course, in certain context, it will be not enough de reduce some durations as described in the chapter duration in time. Also, more information can be selected from the midi file, such as the level and the midi instrument. Probably this will be part of further development of enkode.

1) See also the article Writing score for SuperCollider
article_4.txt · Dernière modification: 2018/08/20 15:58 (modification externe)