]> git.donarmstrong.com Git - lilypond.git/commitdiff
Create engravers for merging rests
authorJay Anderson <horndude77@gmail.com>
Mon, 19 Jun 2017 11:58:17 +0000 (12:58 +0100)
committerJames Lowe <pkx166h@gmail.com>
Mon, 19 Jun 2017 11:58:17 +0000 (12:58 +0100)
Issue 1228

This commit includes engravers
for merging rests and multimeasure
rests among multiple voices.

Documentation/notation/simultaneous.itely
input/regression/merge-rests-engraver.ly [new file with mode: 0644]
scm/define-context-properties.scm
scm/scheme-engravers.scm

index c9810d62c305f127fb1255ba48188372b4f518a6..1ad24d94b9f7a260d3f653659f466b8ac92c949f 100644 (file)
@@ -389,6 +389,7 @@ multiple staves.
 * Single-staff polyphony::
 * Voice styles::
 * Collision resolution::
+* Merging rests::
 * Automatic part combining::
 * Writing music in parallel::
 @end menu
@@ -909,6 +910,39 @@ are at the same time differently dotted are not clear.
 @end ignore
 
 
+@node Merging rests
+@unnumberedsubsubsec Merging rests
+
+When using multiple voices it is common to merge rests which occur in both
+parts. This can be accomplished using @code{Merge_rests_engraver}.
+
+@lilypond[quote,verbatim]
+voiceA = \relative { d''4 r d2 | R1 | }
+voiceB = \relative { fis'4 r g2 | R1 | }
+\score {
+  <<
+    \new Staff \with {
+      instrumentName = "unmerged"
+    }
+    <<
+      \new Voice { \voiceOne \voiceA }
+      \new Voice { \voiceTwo \voiceB }
+    >>
+    \new Staff \with {
+      instrumentName = "merged"
+      \consists #Merge_rests_engraver
+    }
+    <<
+      \new Voice { \voiceOne \voiceA }
+      \new Voice { \voiceTwo \voiceB }
+    >>
+  >>
+}
+@end lilypond
+
+Setting the context property @code{suspendRestMerging} to @code{##t} allows for
+turning off rest merging temporarily.
+
 @node Automatic part combining
 @unnumberedsubsubsec Automatic part combining
 
diff --git a/input/regression/merge-rests-engraver.ly b/input/regression/merge-rests-engraver.ly
new file mode 100644 (file)
index 0000000..688fc31
--- /dev/null
@@ -0,0 +1,79 @@
+\version "2.19.60"
+
+\header {
+  texidoc = "Test for merging rests in different voices."
+}
+
+\paper {
+  ragged-right = ##f
+}
+
+voiceA = \relative {
+  % no rest merges
+  c''4 r c c |
+
+  % does not combine differently written rests
+  c4 r r2 |
+
+  % all rests merged
+  r2^"Up" r4 r8 r16 r32 r64 r128 r |
+
+  % multi-measure rests are combined
+  R1^"Upper text" |
+
+  % compressed multi-measure rests are combined
+  R1*3 |
+
+  % combining between beams, slurs
+  c8[( r c]) r c16[( r c] r c[ r c]) r |
+
+  % combining in tuplets
+  \tuplet 3/2 { c8 r r } r4 \tuplet 3/2 { c4 r r } |
+
+  % accents on rest, dynamics still aligned
+  r4->\f\> r-. r r\! |
+
+  % Non-multimeasure whole rests merged at the correct vertical position
+  \time 8/4
+  r1 r1
+
+  % Ensure when suspending merging rests are in their usual positions
+  \time 4/4
+  \set Staff.suspendRestMerging = ##t
+  r4 r8
+  \set Staff.suspendRestMerging = ##f
+  r8 r2 |
+
+  % Don't merge pitched rests
+  c4\rest d\rest e\rest f\rest |
+}
+
+voiceB = \relative {
+  r2 c'4 r |
+  c4 r r r |
+  r2_"Down" r4 r8 r16 r32 r64 r128 r |
+  R1_"Lower text" |
+  R1*3 |
+  c8[( r c]) r c16[( r c] r c[ r c]) r |
+  \tuplet 3/2 { c8 r r } r4 \tuplet 3/2 { c4 r r } |
+  r4-> r-. r r |
+  r1 r1 |
+  r4 r8 r r2 |
+  r4 r r r |
+}
+
+voiceC = \relative {
+  s1*2 |
+  r2 r4 r8 r16 r32 r64 r128 r | % Combines rests from more than 2 voices
+}
+
+\score {
+  \new Staff \with {
+      \consists #Merge_rests_engraver
+  } <<
+    \compressFullBarRests
+    \new Voice { \voiceOne \voiceA }
+    \new Voice { \voiceTwo \voiceB }
+    \new Voice { \voiceThree \voiceC }
+  >>
+}
index c79d7c94ab44a461ccf8e568358fa6cb275508b2..04b57afaceef21496507df5ab01a7a0d8ae9a759 100644 (file)
@@ -673,6 +673,8 @@ Example:
 @noindent
 This will create a start-repeat bar in this staff only.  Valid values
 are described in @file{scm/bar-line.scm}.")
+     (suspendRestMerging ,boolean? "When using the Merge_rest_engraver do not
+                         merge rests when this is set to true.")
      )))
 
 
index f4902cb94b99b2c31b05f064efca75bb8df13c01..b2966d79e61281e985f8af35d4ca475ef074a2b8 100644 (file)
@@ -117,3 +117,87 @@ receive a count with @code{\\startMeasureCount} and
    (properties-read . ())
    (properties-written . ())
    (description . "Connect cross-staff stems to the stems above in the system")))
+
+(define-public (Merge_rests_engraver context)
+"Engraver to merge rests in multiple voices on the same staff.
+
+This works by gathering all rests at a time step. If they are all of the same
+length and there are at least two they are moved to the correct location as
+if there were one voice."
+
+  (define (is-single-bar-rest? mmrest)
+    (eqv? (ly:grob-property mmrest 'measure-count) 1))
+
+  (define (is-whole-rest? rest)
+    (eqv? (ly:grob-property rest 'duration-log) 0))
+
+  (define (mmrest-offset mmrest)
+  "For single measures they should hang from the second line from the top
+  (offset of 1). For longer multimeasure rests they should be centered on the
+  middle line (offset of 0).
+  NOTE: For one-line staves full single measure rests should be positioned at
+  0, but I don't anticipate this engraver's use in that case. No errors are
+  given in this case."
+    (if (is-single-bar-rest? mmrest) 1 0))
+
+  (define (rest-offset rest)
+    (if (is-whole-rest? rest) 1 0))
+
+  (define (rest-eqv rest-len-prop)
+    "Compare rests according the given property"
+    (define (rest-len rest) (ly:grob-property rest rest-len-prop))
+    (lambda (rest-a rest-b)
+      (eqv? (rest-len rest-a) (rest-len rest-b))))
+
+  (define (rests-all-unpitched rests)
+    "Returns true when all rests do not override the staff-position grob
+    property. When a rest has a position set we do not want to merge rests at
+    that position."
+    (every (lambda (rest) (null? (ly:grob-property rest 'staff-position))) rests))
+
+  (define (merge-mmrests rests)
+  "Move all multimeasure rests to the single voice location."
+    (if (all-equal rests (rest-eqv 'measure-count))
+      (merge-rests rests mmrest-offset)))
+
+  (define (merge-rests rests offset-function)
+    (let ((y-offset (offset-function (car rests))))
+      (for-each
+        (lambda (rest) (ly:grob-set-property! rest 'Y-offset y-offset))
+        rests))
+    (for-each
+      (lambda (rest) (ly:grob-set-property! rest 'transparent #t))
+      (cdr rests)))
+
+  (define has-one-or-less (lambda (lst) (or (null? lst) (null? (cdr lst)))))
+  (define has-at-least-two (lambda (lst) (not (has-one-or-less lst))))
+  (define (all-equal lst pred)
+    (or (has-one-or-less lst)
+        (and (pred (car lst) (cadr lst)) (all-equal (cdr lst) pred))))
+
+  (let ((curr-mmrests '())
+        (mmrests '())
+        (rests '()))
+    (make-engraver
+      ((start-translation-timestep translator)
+        (set! rests '())
+        (set! curr-mmrests '()))
+      (acknowledgers
+        ((rest-interface engraver grob source-engraver)
+          (cond
+            ((ly:context-property context 'suspendRestMerging #f)
+              #f)
+            ((grob::has-interface grob 'multi-measure-rest-interface)
+              (set! curr-mmrests (cons grob curr-mmrests)))
+            (else
+              (set! rests (cons grob rests))))))
+      ((stop-translation-timestep translator)
+        (if (and
+              (has-at-least-two rests)
+              (all-equal rests (rest-eqv 'duration-log))
+              (rests-all-unpitched rests))
+          (merge-rests rests rest-offset))
+        (if (has-at-least-two curr-mmrests)
+          (set! mmrests (cons curr-mmrests mmrests))))
+      ((finalize translator)
+        (for-each merge-mmrests mmrests)))))