Mercurial > hg > amuse
view tools/gsharp-output.lisp @ 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 | |
children | db4acf840bf0 |
line wrap: on
line source
;; 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)))