Mercurial > hg > amuse
changeset 164:27e29dd5978b
Add gsharp-output. *Warning: amuse-gsharp has moved*
darcs-hash:20071221114845-40ec0-c296d15b1bb242c36febcb33f4e6266680999818.gz
author | d.lewis <d.lewis@gold.ac.uk> |
---|---|
date | Fri, 21 Dec 2007 11:48:45 +0000 |
parents | 83023a2668d2 |
children | f59787f1101e |
files | amuse-gsharp.asd implementations/gsharp/amuse-gsharp.asd tools/gsharp-output.lisp |
diffstat | 3 files changed, 351 insertions(+), 7 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/amuse-gsharp.asd Fri Dec 21 11:48:45 2007 +0000 @@ -0,0 +1,14 @@ +(asdf:defsystem amuse-gsharp + :depends-on (amuse gsharp) + :components + ((:module implementations + :components + ((:module gsharp + :components + ((:file "package") + (:file "classes" :depends-on ("package")) + (:file "methods" :depends-on ("package" "classes")) + (:file "gsharp-import" :depends-on ("package" "classes")))))) + (:module tools + :components + ((:file "gsharp-output")))))
--- a/implementations/gsharp/amuse-gsharp.asd Tue Dec 18 12:02:03 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -(asdf:defsystem amuse-gsharp - :depends-on (amuse gsharp) - :components - ((:file "package") - (:file "classes" :depends-on ("package")) - (:file "methods" :depends-on ("package" "classes")) - (:file "gsharp-import" :depends-on ("package" "classes"))))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/gsharp-output.lisp Fri Dec 21 11:48:45 2007 +0000 @@ -0,0 +1,337 @@ +;; This file is for methods creating and using gsharp score buffers +;; for output. This would generally involve rendering to screen, file, +;; printer or browser. +;; +;; The interface doesn't obviously align perfectly with the gsharp +;; data structure, so some notes on the key elements are included +;; here: +;; +;; amuse:events have no direct analogue, with their attributes divided +;; between notes and clusters: +;; +;; * notes have a pitch with a number that is 1- the diatonic +;; component of mips pitch and an accidental that is one of a list of +;; keyword options, most relevant being :sharp :natural :flat +;; :double-sharp and :double-flat +;; +;; * clusters are the rhythmic and positional aspect of one or more +;; notes. Duration is stored in terms of notehead (:long :breve :whole +;; :half :filled) and, for :filled, lbeams and rbeams for the number +;; of beams in each direction, supplemented by a dots slot. To render +;; this to a number for standard time within amuse, there is a method, +;; gsharp:duration, for which a crotchet is 1/4 +;; +;; Parts and staffs are viewed notationally, so we have largely +;; independant structures called layers (roughly = voices) and staves +;; (as they appear on the page). These have names and, at the moment, +;; are being identified by a string-getting method below. Within a +;; layer, clusters are to be found in bar objects and listed in the +;; bars slot. +;; +;; The musical entity into which these slot is a segment, and there +;; can be multiple segments in a buffer, though what this means and +;; how they relate is as yet a mystery to me. +;; +;;; IMPLEMENTATION +;; +;; We're creating a buffer. Lots of this code is adapted from +;; gsharp/mxml/mxml.lisp, but wouldn't now be recognised +;; +;; * Clef is guessed at - we need amuse:get-applicable-clef (and +;; amuse:clef) + +(in-package "AMUSE-TOOLS") + +(defstruct gsharp-duration + "Structure for specifying duration-related parameters for g-sharp" + notehead (beams 0) (dots 0) (tied-p nil)) + +;;;;;;;;;;;;;;;;;;;;; +;; Top-level methods + +(defun write-gsharp-eps (composition pathname) + ;; write a score eps from a composition. Most of this can be copied + ;; straight (this is copied already from CSR's code) + ;; Boilerplate stuff: + (let* ((frame (clim:make-application-frame 'gsharp:gsharp)) + (clim:*application-frame* frame) + (esa:*esa-instance* frame)) + (clim:adopt-frame (clim:find-frame-manager :server-path '(:null)) frame) + (clim:execute-frame-command frame '(gsharp::com-new-buffer)) + ;; Now generate the buffer + (fill-gsharp-buffer-with-constituent (car (esa:buffers frame)) composition) + ;; Refresh and process + (setf (gsharp::modified-p (car (esa:buffers frame))) t) + (gsharp::recompute-measures (car (esa:buffers frame))) + ;; Print + (clim:execute-frame-command + frame `(gsharp::com-print-buffer-to-file ,pathname)))) + +;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Big `walking through data structures' type functions + +(defgeneric fill-gsharp-buffer-with-constituent (buffer constituent) + (:documentation "Takes an empty gsharp buffer and a constituent and + fills the buffer based on the contents of constituent. Buffer is + returned, but this is not necessary, since it is modified in + place.")) +(defmethod fill-gsharp-buffer-with-constituent (buffer (composition composition)) + ;; FIXME: Throughout this, I assume that + ;; get-applicable-time-signatures isn't '() + (let ((time-signatures (get-applicable-time-signatures composition composition)) + (layers)) + (multiple-value-bind (layer-events layer-scores) + ;; Get hash-tables of events by layer and counts of events + ;; below middle C for guessing clef. + (identify-gsharp-layers composition) + ;; For each layer make clef, and one staff per layer + ;; FIXME: this is cheating + (maphash #'(lambda (name events) + (let* ((clef (if (> (gethash name layer-scores) + (/ (length events) 2)) + (gsharp::make-clef :bass) + (gsharp::make-clef :treble))) + (staff (gsharp::make-fiveline-staff :name name + :clef clef))) + ;; Make the layers + (push (gsharp::make-layer (list staff) + :body (gsharp::make-slice :bars nil) + :name name) + layers) + ;; Add the notes and bars + (add-bars-and-events (car layers) (reverse events) time-signatures))) + layer-events) + ;; Attach layers to a segment and place segment into buffer + (let* ((segment (make-instance 'gsharp::segment :layers layers :buffer buffer))) + (setf (gsharp::segments buffer) (list segment) + (gsharp::staves buffer) (mapcar #'(lambda (x) (car (gsharp::staves x))) + layers)) + buffer)))) + +(defgeneric identify-gsharp-layers (constituent) + (:documentation "Takes a composition, returns two hash tables, one + of events grouped by layer name and the other of counts of notes + below middle C by layer (for choosing clef). N.B. On two counts this + is a dodgy way of tying clef, staff and layer. Separate later.")) + +(defmethod identify-gsharp-layers ((composition composition)) + (let ((layer-events (make-hash-table :test #'equal)) + (layer-scores (make-hash-table :test #'equal))) + (sequence:dosequence (event composition (values layer-events layer-scores)) + (when (pitchedp event) + (cond + ((gethash (gsharp-layer-string event) layer-events) + (push event (gethash (gsharp-layer-string event) layer-events)) + (when (< (midi-pitch-number event) 60) + (incf (gethash (gsharp-layer-string event) layer-scores)))) + (t (setf (gethash (gsharp-layer-string event) layer-events) + (list event) + (gethash (gsharp-layer-string event) layer-scores) + (if (< (midi-pitch-number event) 60) + 1 0)))))))) + +(defun add-bars-and-events (layer events time-signatures) + "Given list of events to be attached to a layer, along with + applicable time signatures, clumsily waddle through them all and + slap an approximation to the events into place. Improve this." + (let* ((crotchet-beats (duration (crotchet (car events)))) ;; (or make into semibreves?) + (bar-starts (bar-starts time-signatures :crotchet crotchet-beats)) + (cluster) (bar (gsharp::make-melody-bar)) (cluster-time) (bar-no 0) + (body (gsharp::body layer))) + (gsharp::add-bar bar body bar-no) + (do* ((events events (cdr events)) + (event (car events) (car events))) + ((null events) layer) + (when (and (cdr bar-starts) + (>= (timepoint event) (second bar-starts))) + ;; new bar - and add 0 or more empty bars + (dotimes (i (- (or (position-if #'(lambda (x) (> x (timepoint event))) + bar-starts) + 2) + 2)) + ;; This only runs if one or more whole-bar rests is needed + ;; in a layer. + (incf bar-no) + (setf bar (gsharp::make-melody-bar)) + (gsharp::add-bar bar body bar-no) + (let* ((new-duration (quick-gs-duration (if (cdr bar-starts) + (- (second bar-starts) + (first bar-starts)) + 4))) + (rest (gsharp::make-rest (car (gsharp::staves layer)) + :notehead (gsharp-duration-notehead new-duration) + :lbeams (gsharp-duration-beams new-duration) + :rbeams (gsharp-duration-beams new-duration) + :dots (gsharp-duration-dots new-duration)))) + (gsharp-mxml::add-element-at-duration rest bar 0)) + (setf bar-starts (cdr bar-starts))) + ;; Move on to new bar + (setf bar (gsharp::make-melody-bar) + bar-starts (cdr bar-starts)) + (incf bar-no) + (gsharp::add-bar bar body bar-no)) + (when (or (not cluster-time) + (not (time= cluster-time (onset event)))) + ;; Not part of a pre-existant chord (this will have more + ;; complicated logic when I add ties). Create a new cluster. + (let* ((beat-duration (/ (duration event) crotchet-beats)) + ;; This hideous thing gives a duration that doesn't + ;; overlap with the next note or barline. Unneccessary + ;; when I fix ties and start supplying bar info to + ;; duration function. + (new-duration (quick-gs-duration (min beat-duration + (if (or (not (cdr events)) + (time= (second events) + event)) + beat-duration + (/ (- (timepoint (second events)) + (timepoint event)) + crotchet-beats)) + (if (cdr bar-starts) + (- (second bar-starts) + (/ (timepoint event) + crotchet-beats)) + beat-duration))))) + (setf cluster (gsharp::make-cluster :notehead (gsharp-duration-notehead new-duration) + :lbeams (gsharp-duration-beams new-duration) + :rbeams (gsharp-duration-beams new-duration) + :dots (gsharp-duration-dots new-duration))) + ;; This function adds cluster at a specific point in the + ;; bar. It does a lot of other things that are probably a) + ;; not necessary or b) should be within the duration logic + ;; above. Would be good not to rely on it (which is not to + ;; say that it isn't reliable) + (gsharp-mxml::add-element-at-duration cluster bar (/ (- (/ (timepoint event) + crotchet-beats) + (car bar-starts)) + 4)) + (setf cluster-time (onset event)))) + (let ((pitch (pitch-for-gsharp event))) + (gsharp::add-note cluster (make-instance 'gsharp::note + :pitch (first pitch) + :accidentals (second pitch) + :staff (car (gsharp::staves layer)))))))) + +;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Information conversion functions + +;; Pitch + +(defgeneric pitch-for-gsharp (pitch) + (:documentation "Given a pitch object, return a list of gsharp's + pitch number and accidental keyword")) +(defmethod pitch-for-gsharp ((pitch diatonic-pitch)) + ;; Easy for diatonic pitch, although behaviour for extreme or + ;; fractional alterations is unclear. + (list (1+ (diatonic-pitch-mp pitch)) + (case (diatonic-pitch-accidental pitch) + (1 :sharp) + (-1 :flat) + (2 :double-sharp) + (-2 :double-flat) + (0 :natural) + (otherwise (error "gsharp can't handle this pitch"))))) +(defmethod pitch-for-gsharp ((pitch chromatic-pitch)) + ;; Just go for line-of-fifths proximity spelling. Could always try + ;; to spell it, but... + (let* ((octave (octave pitch)) + (pitch-class (pitch-class pitch)) + (diatonic-pitch-number (aref #(0 0 1 2 2 3 3 4 4 5 6 6) pitch-class))) + (list (+ (* 7 octave) diatonic-pitch-number) + (aref #(:natural :sharp ;; C C# + :natural ;; D + :flat :natural ;; Eb E + :natural :sharp ;; F F# + :natural :sharp ;; G G# + :natural ;; A + :flat :natural) ;; Bb B + pitch-class)))) + +;; Time + +(defun quick-gs-duration (crotchets) + ;; gsharp-durations-from-beats returns a list of durations. Take the + ;; first (= largest). + (car (gsharp-durations-from-beats crotchets))) + +(defun gsharp-durations-from-beats (beats &optional (durations nil)) + ;; Takes a count of crotchets and returns a list of + ;; <gsharp-duration>s that most simply defines the attached. This + ;; is a recursive function that finds the longest simple duration + ;; that fits into beats and, if that leaves a remainder, runs again + ;; on the remainder (until hemi-demi-semi-quavers are reached). It + ;; avoids double dots and is ignorant of time-signature. It will be + ;; replaced. Soon. + ;;; FIXME: Handles quantisation fairly + ;; stupidly. Could be made slightly smarter with simple rounding + ;; (erring on the side of longer durations?) + (assert (>= beats 0)) + (push (make-gsharp-duration) durations) + ;; First find the longest simple duration that fits in beats + ;; First with notes > 1 crotchet + (loop for option in '((16 :long) (8 :breve) (4 :whole) (2 :half) (1 :filled)) + do (cond + ((= beats (* (car option) 3/2)) + (setf (gsharp-duration-notehead (car durations)) + (cadr option) + (gsharp-duration-dots (car durations)) + 1) + (return-from gsharp-durations-from-beats (reverse durations))) + ((> beats (car option)) + (setf (gsharp-duration-notehead (car durations)) + (cadr option)) + (return-from gsharp-durations-from-beats + (gsharp-durations-from-beats (- beats (car option)) durations))) + ((= beats (car option)) + (setf (gsharp-duration-notehead (car durations)) + (cadr option)) + (return-from gsharp-durations-from-beats (reverse durations))))) + (setf (gsharp-duration-notehead (car durations)) + :filled) + ;; then with short notes (beams rather than noteheads) + (do ((i 1 (1+ i))) + ((= i 4) ;; means either tuplet, very short note or unquantised data + (reverse durations)) + (cond + ((= beats (* (/ 1 (expt 2 i)) 3/2)) + (setf (gsharp-duration-beams (car durations)) + i + (gsharp-duration-dots (car durations)) + 1) + (return-from gsharp-durations-from-beats (reverse durations))) + ((> beats (/ 1 (expt 2 i))) + (setf (gsharp-duration-beams (car durations)) + i) + (return-from gsharp-durations-from-beats + (gsharp-durations-from-beats (- beats (/ 1 (expt 2 i))) durations))) + ((= beats (/ 1 (expt 2 i))) + (setf (gsharp-duration-beams (car durations)) + i) + (return-from gsharp-durations-from-beats (reverse durations)))))) + +;;;;;;;;;;;;;;;;;;;;; +;; +;; Other utility functions + +(defgeneric gsharp-layer-string (event) + (:method (e) (name-from-channel-and-patch e)) + (:documentation "Return a string that uniquely identifies the layer + to which event belongs")) + +(defun name-from-channel-and-patch (event) + "Generate layer-identifying string from the patch and channel that +would be used for midi export. For MIDI, this is guaranteed to +separate or over-separate. Tracks would possibly be better, but ?don't +exist in MIDI type 0?" + (format nil "~D/~D" + (get-patch-for-midi event) + (get-channel-for-midi event))) + +(defun bar-starts (time-signature-list &key (crotchet 1)) + (loop for time-signature in time-signature-list + nconc (loop for i from (timepoint (onset time-signature)) + to (1- (timepoint (cut-off time-signature))) + by (* (crotchets-in-a-bar time-signature) crotchet) + collect i)))