;;; display-lily.scm -- Display music expressions using LilyPond notation ;;; ;;; ;;; ;;; Copyright (C) 2005--2015 Nicolas Sceaux ;;; ;;; - This file defines the procedures used to define display methods for each ;;; music type: define-display-method and define-extra-display-method. ;;; See scm/define-music-display-methods.scm ;;; Display methods are stored in the `display-methods' property of each music ;;; type. ;;; ;;; - `music->lily-string' return a string describing a music ;;; expression using LilyPond notation. The special variables *indent* ;;; and *omit-duration* influence the indentation level and the ;;; display of music durations. ;;; ;;; - `with-music-match' can be used to destructure a music expression, extracting ;;; some interesting music properties. (define-module (scm display-lily) #:use-module (ice-9 optargs) #:use-module (ice-9 format) #:use-module (ice-9 regex) #:use-module (ice-9 pretty-print) #:use-module (srfi srfi-1) #:use-module (srfi srfi-13) #:use-module (srfi srfi-39) #:use-module (lily)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; ;;; Display method definition and call ;;; (define-macro (define-display-method music-type vars . body) "Define a display method for a music type and store it in the `display-methods' property of the music type entry found in the `music-name-to-property-table' hash table. Print methods previously defined for that music type are lost. Syntax: (define-display-method MusicType (expression) ...body...))" `(let ((type-props (hashq-ref music-name-to-property-table ',music-type '())) (method (lambda ,vars ,@body))) (set! type-props (assoc-set! type-props 'display-methods (list method))) (hashq-set! music-name-to-property-table ',music-type type-props) method)) (define-macro (define-extra-display-method music-type vars . body) "Add a display method for a music type. A primary display method is supposed to have been previously defined with `define-display-method'. This new method should return a string or #f. If #f is returned, the next display method will be called." `(let* ((type-props (hashq-ref music-name-to-property-table ',music-type '())) (methods (assoc-ref type-props 'display-methods)) (new-method (lambda ,vars ,@body))) (set! type-props (assoc-set! type-props 'display-methods (cons new-method methods))) (hashq-set! music-name-to-property-table ',music-type type-props) new-method)) (define* (tag->lily-string expr #:optional (post-event? #f)) (format #f "~{~a ~}" (map (lambda (tag) (format #f "~a\\tag #'~a" (if post-event? "-" "") tag)) (ly:music-property expr 'tags)))) (define* (tweaks->lily-string expr #:optional (post-event? #f)) (format #f "~{~a ~}" (map (lambda (tweak) (let ((addr (car tweak)) (val (cdr tweak))) (format #f "~a\\tweak ~a #~a" (if post-event? "-" "") (cond ((symbol? addr) (format #f "~a" addr)) ((symbol? (cdr addr)) (format #f "~a.~a" (car addr) (cdr addr))) (else (format #f "~{~a~^.~}" (if (symbol? (car addr)) addr (cdr addr))))) (scheme-expr->lily-string val)))) (ly:music-property expr 'tweaks)))) (define-public (music->lily-string expr) "Print @var{expr}, a music expression, in LilyPond syntax." (if (ly:music? expr) (let* ((music-type (ly:music-property expr 'name)) (procs (assoc-ref (hashq-ref music-name-to-property-table music-type '()) 'display-methods)) (result-string (and procs (any (lambda (proc) (proc expr)) procs)))) (if result-string (format #f "~a~a~a" (tag->lily-string expr (post-event? expr)) (tweaks->lily-string expr (post-event? expr)) result-string) (format #f "%{ Print method not implemented for music type ~a %}" music-type))) (format #f "%{ expecting a music expression: ~a %}" expr))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; ;;; Music pattern matching ;;; (define (var? x) (and (symbol? x) (char=? #\? (string-ref (symbol->string x) 0)))) (define (music? x) (and (pair? x) (eqv? (car x) 'music))) (define (music-list? x) (and (pair? x) (every music? x))) (define (music-or-var-list? x) (and (pair? x) (every (lambda (e) (or (music? e) (var? e))) x))) (define (key-val-list->alist lst) (define (key-val-list->alist-aux lst prev-result) (if (null? lst) prev-result (key-val-list->alist-aux (cddr lst) (cons (cons (first lst) (second lst)) prev-result)))) (reverse! (key-val-list->alist-aux lst (list)))) (define (gen-condition expr pattern) "Helper function for `with-music-match'. Generate an form that checks if the properties of `expr' match thoses described in `pattern'." (let* (;; all (property . value) found at the first depth in pattern, ;; including a (name . ) pair. (pat-all-props (cons (cons 'name (second pattern)) (key-val-list->alist (cddr pattern)))) ;; all (property . value) pairs found in pattern, where value is not ;; a ?var, a music expression or a music list. (prop-vals (remove (lambda (kons) (or (var? (cdr kons)) (music? (cdr kons)) (music-or-var-list? (cdr kons)))) pat-all-props)) ;; list of (property . element) pairs, where element is a music expression (element-list (filter (lambda (kons) (music? (cdr kons))) pat-all-props)) ;; list of (property . (e1 e2 ..)) pairs, where (e1 e2 ...) is a ;; list a music expressions (elements-list (filter (lambda (kons) (music-or-var-list? (cdr kons))) pat-all-props))) `(and ;; a form that checks that `expr' is a music expression ;; before actually accessing its properties... (ly:music? ,expr) ;; a form that checks that `expr' properties have the same ;; values as those given in `pattern' ,@(map (lambda (prop-val) (let ((prop (car prop-val)) (val (cdr prop-val))) `(and (not (null? (ly:music-property ,expr ',prop))) (equal? (ly:music-property ,expr ',prop) ,val)))) prop-vals) ;; build the test condition for each element found in a (property . element) pair. ;; (typically, property will be 'element) ,@(map (lambda (prop-element) (gen-condition `(ly:music-property ,expr ',(car prop-element)) (cdr prop-element))) element-list) ;; build the test conditions for each element found in a (property . (e1 e2 ...)) pair. ;; this requires accessing to an element of a list, hence the index. ;; (typically, property will be 'elements) ,@(map (lambda (prop-elements) (let ((ges (gensym)) (index -1)) `(and ,@(map (lambda (e) (set! index (1+ index)) (if (music? e) (gen-condition `(and (> (length (ly:music-property ,expr ',(car prop-elements))) ,index) (list-ref (ly:music-property ,expr ',(car prop-elements)) ,index)) e) #t)) (cdr prop-elements))))) elements-list)))) (define (gen-bindings expr pattern) "Helper function for `with-music-match'. Generate binding forms by looking for ?var symbol in pattern." (let* (;; all (property . value) found at the first depth of pattern, ;; including a (name . ) pair. (pat-all-props (cons (cons 'name (second pattern)) (key-val-list->alist (cddr pattern)))) ;; all (property . ?var) pairs (prop-vars (filter (lambda (kons) (var? (cdr kons))) pat-all-props)) ;; list of (property . element) pairs, where element is a music expression (element-list (filter (lambda (kons) (music? (cdr kons))) pat-all-props)) ;; list of (property . (e1 e2 ..)) pairs, where (e1 e2 ...) is a ;; list a music expressions (elements-list (filter (lambda (kons) (music-or-var-list? (cdr kons))) pat-all-props))) (append ;; the binding form for the ?var variable found in pattern (first depth). ;; ?var is bound to the value of `expr' property (map (lambda (prop-var) `(,(cdr prop-var) (ly:music-property ,expr ',(car prop-var)))) prop-vars) ;; generate bindings for each element found in a (property . element) pair. ;; (typically, property will be 'element) (append-map (lambda (prop-element) (gen-bindings `(ly:music-property ,expr ',(car prop-element)) (cdr prop-element))) element-list) ;; generate bindings for each element found in a (property . (e1 e2 ...)) pair ;; (typically, property will be 'elements) (append-map (lambda (prop-elements) (let ((index -1)) (append-map (lambda (e) (set! index (1+ index)) (if (var? e) `((,e (list-ref (ly:music-property ,expr ',(car prop-elements)) ,index))) (gen-bindings `(list-ref (ly:music-property ,expr ',(car prop-elements)) ,index) e))) (cdr prop-elements)))) elements-list)))) (defmacro-public with-music-match (music-expr+pattern . body) "If `music-expr' matches `pattern', call `body'. `pattern' should look like: '(music property value property ?var1 element (music ...) elements ((music ...) ?var2 (music ...))) The properties of `music-expr' are checked against the values given in the pattern (the name property being the symbol after the `music' keyword), then all music expression found in its properties (such as 'element or 'elements). When ?var is found instead of a property value, ?var is bound that property value, as read inside `music-expr'. ?var may also be used to refere to a whole music expression inside an elements list for instance. These bindings are accessible inside body." (let ((music-expr (first music-expr+pattern)) (pattern (second music-expr+pattern)) (expr-sym (gensym))) `(let ((,expr-sym ,music-expr)) (if ,(gen-condition expr-sym pattern) (let ,(gen-bindings expr-sym pattern) ,@body) #f)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; ;;; Special parameters ;;; ;;; indentation (define-public *indent* (make-parameter 0)) ;;; set to #t to omit duration printing (define-public *omit-duration* (make-parameter #f)) ;;; Set to #t to force a line break with some kinds of expressions (eg sequential music) (define *force-line-break* (make-parameter #t)) (define *max-element-number-before-break* (make-parameter 6)) ;; \times factor (used in durations) (define *time-scale* (make-parameter 1)) (define *current-context* (make-parameter 'Bottom)) (define *explicit-mode* (make-parameter #t)) (define (new-line->lily-string) (format #f "~%~v_" (max 0 (1- (*indent*))))) ;;; ;;; music type predicate maker ;;; (define (make-music-type-predicate . music-types) (define make-music-type-predicate-aux (lambda (mtypes) (lambda (expr) (if (null? mtypes) #f (or (eqv? (car mtypes) (ly:music-property expr 'name)) ((make-music-type-predicate-aux (cdr mtypes)) expr)))))) (make-music-type-predicate-aux music-types)) (ly:load "define-music-display-methods.scm")