lilypond-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Voice switching étude


From: Dan Eble
Subject: Re: Voice switching étude
Date: Sat, 11 Apr 2015 15:50:57 +0000 (UTC)
User-agent: Loom/3.14 (http://gmane.org/)

> I'm not top-posting.

Here is one more revision of my study in combining parts without using
the partcombine iterator.  This version explores multiple styles, such as
showing unisons with multiple heads or keeping rests visible through
solo passages.

Now I have a question about architecture.  Is it worth my time to try to
move this into Lilypond and see what happens to the regression tests?
Is eliminating the partcombine iterator and doing more work in scheme
a net benefit?

Thanks,
-- 
Dan

\version "2.19.15"

% These are the parts that will be combined.
%
% The explicit \change commands demonstrate an effect similar to
% \partcombineApart, but they only affect the voice routing and not
% the texts.  They override the automated decisions, but only until
% the next automated decision affecting each voice.
one = \relative { f'4 b b b |
                  b2 \change Voice = "one" c | b b |
                  R1 | c2 \partcombineUnisono c_"(u)" }
two = \relative { b'4 b f f |
                  b2 \change Voice = "two" a_"(a)" | R1 |
                  b2 b | c2 c }

% Generate a split list using \partcombine.  Context changes in the
% input parts cause warnings, so strip them for this step.
killContextChanges =
#(define-music-function
  (parser location music) (ly:music?)
  (music-filter
   (lambda (m) (not (eq? (ly:music-property m 'name) 'ContextChange)))
   (ly:music-deep-copy music)))

combinedDefault =
  \partcombine \killContextChanges \one \killContextChanges \two

% Combine with double-stemmed unisons and seconds and a wide range for
% single stems.
combinedHymnal =
  \partcombine #'(2 . 15) \killContextChanges \one \killContextChanges \two

%
% Translate the split list to a sequence of \change commands for each part.
%

#(define default-part-one-voice-map
  ;; each entry is (split state . context id)
  '((apart . one)
    (apart-silence . one)
    (chords . shared)
    (silence1 . shared)
    (silence2 . null)
    (solo1 . solo)
    (solo2 . null)
    (unisono . shared)
    (unisilence . shared)))

#(define default-part-two-voice-map
  ;; each entry is (split state . context id)
  '((apart . two)
    (apart-silence . two)
    (chords . shared)
    (silence1 . null)
    (silence2 . shared)
    (solo1 . null)
    (solo2 . solo)
    (unisono . null)
    (unisilence . null)))

% This differs from the default by using two note heads for unisons.
% N.B. This is a poor way to achieve it because it may show an "a2"
% text where it is not appropriate.  Forcing the part combiner to
% decide on "chords" instead of "unison" would be a better way; maybe
% it could use a new state (say "near") for intervals which are not
% voice crossings but also not within the specified chord-range.
#(define alternate-part-one-voice-map
  ;; each entry is (split state . context id)
  '((apart . one)
    (apart-silence . one)
    (chords . shared)
    (silence1 . shared)
    (silence2 . null)
    (solo1 . solo)
    (solo2 . null)
    (unisono . shared)
    (unisilence . shared)))

#(define alternate-part-two-voice-map
  ;; each entry is (split state . context id)
  '((apart . two)
    (apart-silence . two)
    (chords . shared)
    (silence1 . null)
    (silence2 . shared)
    (solo1 . null)
    (solo2 . solo)
    (unisono . shared)
    (unisilence . null)))

% This differs from the default by avoiding solo and a2 states.
% The parts may still be chorded and rests may be shared.
#(define nosolo-part-one-voice-map
  ;; each entry is (split state . context id)
  '((apart . one)
    (apart-silence . one)
    (chords . shared)
    (silence1 . one)
    (silence2 . one)
    (solo1 . one)
    (solo2 . one)
    (unisono . shared)
    (unisilence . shared)))

#(define nosolo-part-two-voice-map
  ;; each entry is (split state . context id)
  '((apart . two)
    (apart-silence . two)
    (chords . shared)
    (silence1 . two)
    (silence2 . two)
    (solo1 . two)
    (solo2 . two)
    (unisono . null)
    (unisilence . null)))

voiceChanges =
#(define-music-function (parser location voice-map partcombinemusic)
  (list? ly:music?)

  (let ((m (list))
        (prevMoment ZERO-MOMENT)
        (prevSlot #f))

   (define (handle-split split)
    (let* ((moment (car split))
           (slot (assq-ref voice-map (cdr split))))
     
     (if (not (eq? prevSlot slot))
      (let ((dur (ly:moment-sub moment prevMoment)))
       (if (not (equal? dur ZERO-MOMENT))
        (set! m (cons (make-music 'SkipEvent
                       'duration (make-duration-of-length dur)) m)))
       (set! m (cons (make-music 'ContextChange
                      'change-to-id (symbol->string slot)
                      'change-to-type 'Voice) m))
       (set! prevMoment moment)
       (set! prevSlot slot)))
   ))
   
   (for-each handle-split (ly:music-property partcombinemusic 'split-list))
   (make-sequential-music (reverse! m))))

%
% Translate the split list into a sequence of events for the texts.
%

#(define part-combine-event-map
  ;; Each entry is (split state . music event).
  ;; If the event is #f, the split state has
  ;; no effect on the state machine.  If the event is 'cancel the
  ;; previous state is forgotten without emitting an event.
  '((apart . cancel)
    (apart-silence . #f)
    (chords . cancel)
    (silence1 . #f)
    (silence2 . #f)
    (solo1 . SoloOneEvent)
    (solo2 . SoloTwoEvent)
    (unisono . UnisonoEvent)
    (unisilence . #f)))

#(define default-part-combine-event-state-machine
  ;; (current-state . ((split-state-event . (output-event . next-state)) ...))
  '((Initial . ((solo1   . (SoloOneEvent . Solo1))
                (solo2   . (SoloTwoEvent . Solo2))
                (unisono . (UnisonoEvent . Unisono))))
    (Solo1   . ((solo2   . (SoloTwoEvent . Solo2))
                (unisono . (UnisonoEvent . Unisono))
                (chords  . (#f           . Initial))))
    (Solo2   . ((solo1   . (SoloOneEvent . Solo1))
                (unisono . (UnisonoEvent . Unisono))
                (chords  . (#f           . Initial))))
    (Unisono . ((solo1   . (SoloOneEvent . Solo1))
                (solo2   . (SoloTwoEvent . Solo2))
                (chords  . (#f           . Initial))))))

% This differs from the default by omitting the text when unison
% follows divisi.  It still prints the text for unison following solo.
#(define alternate-part-combine-event-state-machine
  ;; (current-state . ((split-state-event . (output-event . next-state)) ...))
  '((Initial . ((solo1   . (SoloOneEvent . Solo1))
                (solo2   . (SoloTwoEvent . Solo2))))
    (Solo1   . ((solo2   . (SoloTwoEvent . Solo2))
                (unisono . (UnisonoEvent . Unisono))
                (chords  . (#f           . Initial))))
    (Solo2   . ((solo1   . (SoloOneEvent . Solo1))
                (unisono . (UnisonoEvent . Unisono))
                (chords  . (#f           . Initial))))
    (Unisono . ((solo1   . (SoloOneEvent . Solo1))
                (solo2   . (SoloTwoEvent . Solo2))
                (chords  . (#f           . Initial))))))

% This differs from the default by omitting the text for solo passages.
#(define nosolo-part-combine-event-state-machine
  ;; (current-state . ((split-state-event . (output-event . next-state)) ...))
  '((Initial . ((unisono . (UnisonoEvent . Unisono))))
    (Solo1   . ((unisono . (UnisonoEvent . Unisono))
                (chords  . (#f           . Initial))))
    (Solo2   . ((unisono . (UnisonoEvent . Unisono))
                (chords  . (#f           . Initial))))
    (Unisono . ((chords  . (#f           . Initial))))))

partcombineEvents =
#(define-music-function (parser location state-machine partcombinemusic)
  (pair? ly:music?)

  (let ((m '())
        (prev-moment ZERO-MOMENT)
        (state 'Initial))

   (define (handle-split split)
    (let* ((moment (car split))
           (state-event-map (assq-ref state-machine state))
           (action (assq-ref state-event-map (cdr split))))
     (if action
      (let ((part-combine-event (car action))
            (next-state (cdr action)))
       (if part-combine-event
        (let ((dur (ly:moment-sub moment prev-moment)))
         (if (not (equal? dur ZERO-MOMENT))
          (set! m (cons (make-music 'SkipEvent
                         'duration (make-duration-of-length dur)) m)))
         (set! m (cons (make-music part-combine-event) m))
         (set! prev-moment moment)))
       (set! state next-state)))))
   
   (for-each handle-split (ly:music-property partcombinemusic 'split-list))
   (make-sequential-music (reverse! m))))

% Wrap each part in a "Part" context which can be moved from one Voice
% context to another.
%
% A hierarchy such as Staff/(VoiceSlot, Slot, File)/Voice might make
% more sense in musical terms than Staff/Voice/Part.  The basic
% problem seems not to be with this voice-switching concept, but that
% the Voice context already encompasses more than a single musical
% voice.
\layout {
  \context {
    \name "Part"
    \type "Engraver_group"
  }
  \context {
    \Voice
    \accepts "Part"
  }
}

newpartcombine =
#(define-music-function
  (parser location
   partOneSlotMap one
   partTwoSlotMap two
   eventStateMachine combined)
  (pair? ly:music? pair? ly:music? pair? ly:music?) #{ <<

  \new NullVoice = "text" \with {
    \consists "Part_combine_engraver"
  } <<
    \partcombineEvents #eventStateMachine #combined
    % The Part_combine_engraver wants to put texts on notes, so here
    % are some notes.
    \killContextChanges #one
    \killContextChanges #two
  >>

  \new Voice = "one" \with {
    \voiceOne
  } <<
    \new Part = "partOne" <<
      \voiceChanges #partOneSlotMap #combined
      #one
    >>
  >>

  \new Voice = "two" \with {
    \voiceTwo
  } <<
    \new Part = "partTwo" <<
      \voiceChanges #partTwoSlotMap #combined
      #two
    >>
  >>

  \new Voice = "shared" \with {
    \override CombineTextScript.color = #red
    \override NoteHead.color = #red
  } <<
    % Contexts need to be kept alive in order to change to them.
    #(skip-of-length one)
    #(skip-of-length two)
  >>

  \new Voice = "solo" \with {
    \override CombineTextScript.color = #(rgb-color 0.75 0.5 0.5)
    \override NoteHead.color = #(rgb-color 0.75 0.5 0.5)
  } <<
    #(skip-of-length one)
    #(skip-of-length two)
  >>
  
  \new NullVoice = "null" <<
    #(skip-of-length one)
    #(skip-of-length two)
  >>
>> #} )

\paper
{
  paper-width = 8.5 \in
  paper-height = 5.5 \in
  left-margin = 1 \in
}

\score {
  \new Staff \with {
    instrumentName = \markup \column { "legacy" "(w/o \\change)" }
  } <<
    \new Voice = "two" \with {
    } {}
    \new Voice = "solo" \with {
      \override CombineTextScript.color = #(rgb-color 0.75 0.5 0.5)
      \override NoteHead.color = #(rgb-color 0.75 0.5 0.5)
    } {}
    \new Voice = "shared" \with {
      \override CombineTextScript.color = #red
      \override NoteHead.color = #red
    } {}
    \combinedDefault
  >>
}

\score {
  \new Staff \with {
    instrumentName = \markup \column { "experimental" "default" }
  } <<
    \newpartcombine #default-part-one-voice-map \one
                    #default-part-two-voice-map \two
                    #default-part-combine-event-state-machine
                    \combinedDefault
  >>
}

\score {
  \new Staff \with {
    instrumentName = \markup \column { "experimental" "alternate" }
  } <<
    \newpartcombine #alternate-part-one-voice-map \one
                    #alternate-part-two-voice-map \two
                    #alternate-part-combine-event-state-machine
                    \combinedDefault
  >>
}

\score {
  \new Staff \with {
    instrumentName = \markup \column { "experimental" "hymnal" }
  } <<
    \newpartcombine #nosolo-part-one-voice-map \one
                    #nosolo-part-two-voice-map \two
                    #nosolo-part-combine-event-state-machine
                    \combinedHymnal
  >>
}





reply via email to

[Prev in Thread] Current Thread [Next in Thread]