Music is absolutely fascinating. However, very few people are skilled in its composition. Computers may not produce the most pleasing music, but they can produce interesting music, using permutations of a scale, and random numbers.
The creation of a score is the primary feature of this program. To do this, it builds on other parts of the program, first, generating a score description, from (generate-score-notes)
, formatting the notes for output, and generating a header. After that, it will assemble a file in GNU LilyPond syntax, and run the lilypond
program to produce a PDF.
(defun generate-score-contents (&optional (title "Algorithmic Composition") (composer "Common Lisp") (key :c-major) (measures 25)) (let* ((score (generate-score-notes measures)) (notes (format nil "~{~a~}" (loop for note-pair in score collect (generate-note (car note-pair) (cdr note-pair) key)))) (header (format nil "\\header {title = \"~a\" composer = \"~a\" tagline = \"\"}~&\\score {" title composer))) (with-output-to-string (output-file) (format output-file "\\version \"2.18.2\"~&#(set-default-paper-size \"letter\")~&") (format output-file "~a~&" header) (format output-file "\\new Staff {~a ~a\\bar \"|.\"}\\layout {}\\midi{}}" (getf *key-signature* key) notes)))) (defun create-score (file &key (title "Algorithmic Composition") (composer "Common Lisp") (key :c-major) (measures 25)) (with-open-file (output-file file :direction :output :if-exists :overwrite :if-does-not-exist :create) (format output-file "~a" (generate-score-contents title composer key measures)) (run-program (list "lilypond" file))))
To generate proper syntax for a note from a note number and a duration, this function is used. It takes the note number, the duration and the key. It then looks up the note, based on the key, and raises 2 to the duration, and formats the note.
Durations are found using the following table:
Duration | Beats | |
---|---|---|
! | dur | beats |
# | 0 | 1 |
# | 1 | 1:2 |
# | 2 | 1:4 |
# | 3 | 1:8 |
# | 4 | 1:16 |
(defun generate-note (number &optional (duration 2) (key :c-major)) (declare (type (integer 1 8) number) (type (integer 0 4) duration) (keyword key)) (let ((note (getf (getf *numbers-to-notes* key) number)) (true-duration (expt 2 duration))) (format nil "~a~d " note true-duration)))
Actually generating notes is perhaps one of the more difficult parts. To do so, the number of measures must first be known, and is used to generate a list of rhythms. After that, a set of changes is generated, which is used for the picking of notes. For each rhythm, a number, from 0 to 8 is chosen. If the number chosen is 0, then the system will go through a single permutation of the octave for the next 8 rhythms, otherwise, that note will be used for the next rhythm. Notes and rhythms are consed and pushed on to a list, which is the reversed and returned.
(defun generate-score-notes (number-of-measures) (let ((score '()) (durations (generate-durations number-of-measures)) (changes (steinhous-johnson-trotter 8))) (do () ((null durations)) (let ((note-source (random 9))) (if (= note-source 0) (loop for note in (pop changes) if (not (null durations)) do (push (cons note (pop durations)) score)) (push (cons note-source (pop durations)) score)))) (reverse score)))
One of the more important parts of generating music is creating sensible rhythms. To do so, a number between 0 and 4 is chosen, and then checked to see if it makes sense, if it does, it is pushed on to a list of durations, and then the appropriate number of beats is deducted from the beats-left count. Finally, the duration list is reversed and returned.
(defun generate-durations (number-of-measures) (let ((durations '()) (n-left (* 16 number-of-measures))) (labels ((try-make-duration () (let ((dur (random 5))) (case dur (0 (if (= 0 (mod n-left 16)) (progn (push dur durations) (decf n-left 16)) (try-make-duration))) (1 (if (= 0 (mod n-left 8)) (progn (push dur durations) (decf n-left 8)) (try-make-duration))) (2 (decf n-left 4) (push dur durations)) (3 (decf n-left 2) (push dur durations)) (4 (decf n-left) (push dur durations)))))) (do () ((= 0 n-left)) (try-make-duration))) (reverse durations)))
One of the more important parts of generating music is the ability to actually have correct note output, in key. To do this, I use a key lookup alist, and a note lookup alist.
(defvar *numbers-to-notes* '(:a-major (1 "a" 2 "b" 3 "cis'" 4 "d'" 5 "e'" 6 "fis'" 7 "gis'" 8 "a'") :b-major (1 "b" 2 "cis'" 3 "dis'" 4 "e'" 5 "fis'" 6 "gis'" 7 "ais'" 8 "b'") :c-major (1 "c'" 2 "d'" 3 "e'" 4 "f'" 5 "g'" 6 "a'" 7 "b'" 8 "c''") :d-major (1 "d'" 2 "e'" 3 "fis'" 4 "g'" 5 "a'" 6 "b'" 7 "cis''" 8 "d''") :e-major (1 "e'" 2 "fis'" 3 "gis'" 4 "a'" 5 "b'" 6 "cis''" 7 "dis''" 8 "e''") :f-major (1 "f'" 2 "g'" 3 "a'" 4 "bes'" 5 "c''" 6 "d''" 7 "e''" 8 "f''") :g-major (1 "g'" 2 "a'" 3 "b'" 4 "c''" 5 "d''" 6 "e''" 7 "fis''" 8 "g''") :a-minor (1 "a" 2 "b" 3 "c'" 4 "d'" 5 "e'" 6 "f'" 7 "g'" 8 "a'") :b-minor (1 "b" 2 "cis'" 3 "d'" 4 "e'" 5 "fis'" 6 "g'" 7 "a'" 8 "b'") :c-minor (1 "c'" 2 "d'" 3 "ees'" 4 "f'" 5 "g'" 6 "aes'" 7 "bes'" 8 "c''") :d-minor (1 "d'" 2 "e'" 3 "f'" 4 "g'" 5 "a'" 6 "bes'" 7 "c''" 8 "d''") :e-minor (1 "e'" 2 "fis'" 3 "g'" 4 "a'" 5 "b'" 6 "c''" 7 "d''" 8 "e''") :f-minor (1 "f'" 2 "g'" 3 "aes'" 4 "bes'" 5 "c''" 6 "des''" 7 "ees''" 8 "f''") :g-minor (1 "g'" 2 "a'" 3 "bes'" 4 "c''" 5 "d''" 6 "ees''" 7 "f''" 8 "g''"))) (defvar *key-signature* '(:a-major "\\key a \\major" :b-major "\\key b \\major" :c-major "\\key c \\major" :d-major "\\key d \\major" :e-major "\\key e \\major" :f-major "\\key f \\major" :g-major "\\key g \\major" :a-minor "\\key a \\minor" :b-minor "\\key b \\minor" :c-minor "\\key c \\minor" :d-minor "\\key d \\minor" :e-minor "\\key e \\minor" :f-minor "\\key f \\minor" :g-minor "\\key g \\minor")) (defvar *chords* (map 'list #'(lambda (chord) (concatenate 'string "<<" chord ">>")) '("c e g" "cis eis gis" "des f aes" "d fis a" "dis fisis ais" "ees g bes" "e gis b" "f a c" "fes aes ces")))
One of the more interesting ways this produces music is through the use of Plain Changes, a type of permutation based on single transpostions. To do this, I use the Steinhaus-Johnson-Trotter algorithm, explained in Figure steinhaus-johnson-trotter-img.
digraph { start [label = "Start"]; stop [label = "End"]; swap [label = "Swap around change"]; swapdone [label = "Changes left?", shape = rectangle]; if_three [label = "Length == 3?", shape = rectangle]; subgraph cluster_generate_changes { generate_changes_previous [label = "Generate Changes For Number - 1"]; subgraph cluster_inner_change_loop { previous_left [label = "Previous changes left?", shape = rectangle]; loop_from_1_to_num [label = "Loop from 1 to Number"]; insert_from_previous_1_plus [label = "Insert from previous change plus 1"] loop_from_1_minus_num_to_1 [label = "Loop from Number - 1 to 1"]; insert_from_previous [label = "Insert from previous change"]; previous_left -> loop_from_1_to_num [label = "True"]; loop_from_1_to_num -> insert_from_previous_1_plus; insert_from_previous_1_plus -> loop_from_1_minus_num_to_1; loop_from_1_minus_num_to_1 -> insert_from_previous; insert_from_previous -> previous_left; } generate_changes_previous -> previous_left; } start -> if_three; if_three -> stop [label = "True,\n(2 1 2 1 2)"]; if_three -> generate_changes_previous [label = "False"]; previous_left -> swap [label = "False"]; swap -> swapdone; swapdone -> swap [label = "True"]; swapdone -> stop [label = "False"]; }
(defun plain-changes (n) (if (= n 2) '(1) (if (= n 3) '(2 1 2 1 2) (let ((up (xrange (1- n))) (down (xrange (- n 2) t)) (recur (remove-if #'null (plain-changes (1- n)))) (changes '())) (do () ((null recur)) (loop for x in down do (push x changes)) (push (1+ (pop recur)) changes) (loop for x in up do (push x changes)) (push (pop recur) changes)) (remove-if #'null changes))))) (defun steinhous-johnson-trotter (n) (let ((permutations '())) (push (xrange n) permutations) (loop for i in (plain-changes n) do (let* ((old-permutation (pop permutations)) (old-a (nth (1- i) old-permutation)) (old-b (nth i old-permutation)) (new-permutation (copy-list old-permutation))) (push old-permutation permutations) (setf (nth (1- i) new-permutation) old-b (nth i new-permutation) old-a) (push new-permutation permutations))) permutations))
To correctly generate the permutations, an xrange
function is needed, similar to the one built into Python. It is implemented using the loop
macro, although, I had originally planned on using iter
.
(defun xrange (length &optional (reverse-p nil)) (let ((range (loop for i from 1 to length collect i))) (if reverse-p (reverse range) range)))
The full logic of the music generation program is shown in Figure figure:overview.
digraph { entry [label = "Program Entry"]; exit [label = "Program Exit"]; subgraph cluster_generate_rhythms { rank = same; beat_count [label = "Multiply Measure number by 4"]; continue [label = "Beat Count = 0?", shape = rectangle] reverse [label = "Reverse Rhythms"] subgraph cluster_check_valid { pick_random [label = "Pick Random number\n0 -- 4"]; case_0 [label = "Number is 0\n(Whole Note)"]; case_1 [label = "Number is 1\n(Half Note)"]; case_2 [label = "Number is 2\n(Quarter Note)"]; case_3 [label = "Number is 3\n(Eighth Note)"]; case_4 [label = "Number is 4\n(Sixteenth Note)"]; mod_4 [label = "Beats mod 4 == 0", shape = rectangle]; mod_2 [label = "Beats mod 2 == 0", shape = rectangle]; push_to_rhythm [label = "Push correct number to the rhythms list."]; decrement_beat_count [label = "Decrement number of beats."]; pick_random -> {case_0, case_1, case_2, case_3, case_4}; case_0 -> mod_4; case_1 -> mod_2; {mod_4, mod_2} -> pick_random [label = "False"]; {mod_4, mod_2} -> push_to_rhythm [label = "True"]; {case_2, case_3, case_4} -> push_to_rhythm; push_to_rhythm -> decrement_beat_count; } beat_count -> pick_random; decrement_beat_count -> continue; continue -> pick_random [label = "False"]; continue -> reverse [label = "True"]; } subgraph cluster_generate_notes { rank = same; for_rhythms [label = "For Each Rhythm", shape = rectangle, rank = top]; pick_random2 [label = "Pick a random number\n0--8"]; if_0 [label = "Number == 0", shape = rectangle]; reverse_notes [label = "Reverse Note Order"]; push_note [label = "Push Note"]; do_changes [label = "Push Set of Changes."]; rhythms_left [label = "Rhythms-Count == 0", shape = rectangle]; decrement_rhythms [label = "Decrement Rhythm Count"]; merge_rhythms_notes [label = "Merge Rhythms and Notes"]; for_rhythms -> pick_random2; pick_random2 -> if_0; if_0 -> do_changes [label = "True"]; if_0 -> push_note [label = "False"]; {do_changes, push_note} -> decrement_rhythms; decrement_rhythms -> rhythms_left; rhythms_left -> for_rhythms [label = "False"]; rhythms_left -> reverse_notes [label = "True"]; reverse_notes -> merge_rhythms_notes; } subgraph cluster_generate_score { rank = same; for_rhythm_note_pair [label = "For Each Rhythm", shape = rectangle]; format_header [label = "Format Header"]; format_note [label = "Format Note"]; process_lilypond [label = "Process Lilypond File"]; notes_left [label = "Notes-left == 0", shape = rectangle]; format_header -> for_rhythm_note_pair; for_rhythm_note_pair -> format_note; format_note -> notes_left; notes_left -> for_rhythm_note_pair [label = "False"]; notes_left -> process_lilypond [label = "True"]; } entry -> beat_count; reverse -> for_rhythms; merge_rhythms_notes -> format_header; process_lilypond -> exit; }
(defpackage #:muzic (:use #:cl #:uiop) (:export #:create-score)) (in-package #:muzic) <<keys-and-mappings>> <<helper-functions>> <<generate-permutations>> <<generate-durations>> <<generate-set-of-notes>> <<generate-note>> <<create-a-score>>