From 212ca268e813cd72eca8c07e714e1b6669cba747 Mon Sep 17 00:00:00 2001 From: Jay Anderson Date: Mon, 19 Jun 2017 12:58:17 +0100 Subject: [PATCH] Create engravers for merging rests Issue 1228 This commit includes engravers for merging rests and multimeasure rests among multiple voices. --- Documentation/notation/simultaneous.itely | 34 +++++++++ input/regression/merge-rests-engraver.ly | 79 +++++++++++++++++++++ scm/define-context-properties.scm | 2 + scm/scheme-engravers.scm | 84 +++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 input/regression/merge-rests-engraver.ly diff --git a/Documentation/notation/simultaneous.itely b/Documentation/notation/simultaneous.itely index c9810d62c3..1ad24d94b9 100644 --- a/Documentation/notation/simultaneous.itely +++ b/Documentation/notation/simultaneous.itely @@ -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 index 0000000000..688fc31f79 --- /dev/null +++ b/input/regression/merge-rests-engraver.ly @@ -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 } + >> +} diff --git a/scm/define-context-properties.scm b/scm/define-context-properties.scm index c79d7c94ab..04b57aface 100644 --- a/scm/define-context-properties.scm +++ b/scm/define-context-properties.scm @@ -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.") ))) diff --git a/scm/scheme-engravers.scm b/scm/scheme-engravers.scm index f4902cb94b..b2966d79e6 100644 --- a/scm/scheme-engravers.scm +++ b/scm/scheme-engravers.scm @@ -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))))) -- 2.39.2