LoginSignup
0

More than 5 years have passed since last update.

近代フランス系で時々使うあのトレモロを書く

Last updated at Posted at 2017-12-09

ノーテーションの流儀が、一部、LilyPondのデフォルトから外れているせいで困る、なんてことがあったりしますよね。運良く、ちょいちょいと\setなり\overrideなりを仕込むことで実現できれば良いのですが、そうもいかない場合は結構大変ですよね。

せっかくですので、「そうもいきそうでそうもいかない、ちょっとだけそうもいく場合」に該当する割と単純なスニペットではありますが、記事にしてみます。LilyPondを使い始めたばかりであるとか、LilyPondを使わない人達には、LilyPond使いがどんな風に考えているのかが謎という向きが多そうですので(意外と資料が転がっていない気がします)、私の例で恐縮ですが、考え方の雰囲気が多少なりとも伝わるような順序で書いてみたつもりです。
なお、楽譜第一で、midiのアウトプットについては考慮しません。また、gitでソースを落としてきて野良ビルドした、version表示が2.21.0のLilyPondのを使用して譜例を作成していますが、大概は2.18.2でも動くと思います。

ゴール

以下の譜例は、近代フランスの作曲家、M. Ravel (1875-1937)の、バレエ音楽の方の《Ma Mère l'Oye》から採ったものです。
target.cropped.png
今回は、このトレモロの記譜を目標とします。

デフォルトだと上手くいかない理由

Beamには何本だけ「切る」かを設定するgap-countというプロパティがbeam-interfaceにあるので、それをいじれば少なくとも後者についてくらいは上手く行くと思うかもしれませんが、以下の譜例の通り、残念ながらそうでもないんですね。
ng-example.cropped.png
ちなみに目標としている譜例の場合、記譜の流儀を変更して良いとしても後者が曲者で、32分に開かない限りはどうやっても一筋縄にはいかないでしょうし、そもそもカッチリ32分で弾く場面でもなさそうなので開くわけにもいかなそう、という問題があります。

実装案

一般論として、こういうちょっと特殊な調整が必要な場合、次の3つあたりがよくある選択肢ではないかと思います:
1. Markupでどうにかする。
2. 見えない音符を重ねてどうにかする。
3. 諦めてSchemeコードを書く。

Markupで(無理矢理)どうにかする案

私自身がそこまで普段からmarkupでやんちゃしてはいないこともあり、仮にやってみようとしたらトライアンドエラーで洒落にならないレベルで時間を使うことが明白ですので、今回はボツです。

\override Beam.stencil = #ly:text-interface::print
\override Beam.text = \markup{ チョー凄いmarkup }
c8[ c c c]

markup-example.cropped.png

余談ですが、以下のような、描くもののサイズがもっと小さな一点ものに関して言えば、時間が無いとか面倒だとかいう際には悪くない選択肢だと思います:

\relative c''{
  \once\override Beam.gap-count = 2
  \once\override TupletNumber.stencil = #ly:text-interface::print
  \once\override TupletNumber.text = \markup{
    \general-align #Y #CENTER \fermata
  }
  \times 1/1 {
    \repeat tremolo 8 {
      <g c,>32 <c e,>
    }
  }
}

fermata-example.cropped.png

見えない音符を重ねてどうにかする案

余程の特殊記譜でなければ、重ね打ちにより幅広い対応が可能です。何と何をどう重ねるか、発想が命となりますが、思考の足し算なのでラクはラクですね。

s4*0^"これと"
\once\override Beam.positions = #'(0 . 0)
c8[ c c16 c]

s4*0^"これを"
\once\override Beam.positions = #'(0 . 0)
\once\override Beam.color = #red
\once\override Beam.gap-count = 1
c16*2[
  \set stemRightBeamCount = 1
  c
  \set stemLeftBeamCount = 1
  c16 c]

s4*0^"重ねる"
<<{
  \oneVoice
  \once\override Beam.positions = #'(0 . 0)
  c8[ c c16 c]
}\\{
  \oneVoice
  \override NoteColumn.ignore-collision = ##t
  \once\override Beam.positions = #'(0 . 0)
  \once\override Beam.color = #red
  \once\override Beam.gap-count = 1
  c16*2[
    \set stemRightBeamCount = 1
    c
    \set stemLeftBeamCount = 1
    c16 c]
}>>

poormansbold-example.cropped.png

声部があまりにも増えるとこのままでは破綻しますが、Beam.positionsも定数決め打ちですので、適当なプロパティに即値でも指定してズラせば問題ないですね。
…何、マジックナンバーは悪ですって? 痛いところを突きますね。
後で触れますが、一時凌ぎであるとか、最後の最後に仕上げとしてゴリゴリと即値で微調整しているだとか、そういった場合を除くとこの方法論では落とし穴にハマる可能性があります。

諦めてSchemeコードを書く案

この場合も、ly:stencil-ナントカ系の関数を使用して適当に重ね打ちするという手段がありますが、それでは芸が無いので、なんとかして元々の出力を変形してみたいところです。ところが、今回のゴールは、運が悪いことに、いい感じのヒントがLSRにありませんでした。当初、割と困ってしまったと思ったのですけれども、適当にstencilに、渡ってきたgrobのプロパティをprintするコードを仕込んで出力を眺めているといろいろなことわかるものですね…どうにか思い付きました。
さて、ドキュメントにあまり細かいことが書いてないのですが、beam-interfaceのbeam-segmentsというプロパティに桁が1本1本入っています。きちんと書きますと、beam-segmentsは「n番目の高さにx座標pからx座標qまで引く桁」のリストで、そのような桁情報のそれぞれは次のような連想リストになっています:((vertical-count . n) (horizontal p . q))
なので、デフォルトのbeam-segmentsに対して、短くしたい桁に相当する部分のp, qの値を変更してしまえば、特定の桁だけ短くできそうですね。ついでに、beam-segmentsに勝手な桁情報を追加すれば、余計な桁を書くこともできそうです。例えば、今回のゴールの前半については、イメージ的には次のようなbeam-segmentsの変形をすればよい、ということになります1

  • いじる前(インプット) orig-segments.png

(((vertical-count . 0) (horizontal 0 . 3))
((vertical-count . 1) (horizontal 0 . 1))
((vertical-count . 1) (horizontal 2 . 3)))

  • いじった後(アウトプット) alt-segments.png

(((vertical-count . 0) (horizontal 0 . 3))
((vertical-count . 1) (horizontal 0.25 . 0.75))
((vertical-count . 1) (horizontal 2 . 3)))

あとは、この変換関数を書いて、beam-segmentsを差し替えてしまえばOKですね!2

今回のゴール(割と長大なため注意)3
%% for tremolo
#(use-modules
  (srfi srfi-1)
  (srfi srfi-8))

#(define (shorten-interval delta pair)
  (cons
   (+ (car pair) delta)
   (- (cdr pair) delta)))

#(define (with-gaps list-of-list)
  "(() (#t #f) (#t))を渡すと,
  |====|====|====|
  |====|    |====|
  |    |    |====|
  |    |    |    |
  を
  |====|====|====|
  | == |    |====|
  |    |    | == |
  |    |    |    |
  にする,
  |====|====|====|
  |====|    |====|
  |====|    |====|
  |    |    |    |
  を
  |====|====|====|
  | == |    |====|
  | == |    | == |
  |    |    |    |
  にする,等々。
  "
  (lambda (beam)
   (let*
    ((direction (ly:grob-property beam 'direction))
     (orig-segments (ly:beam::calc-beam-segments beam))
     (lesser?
      (if (eqv? direction DOWN)
       > <))
     (gap (ly:grob-property beam 'gap)))
    (let vertical-loop
     ((count 0)
      (rest-modifications list-of-list)
      ;; direction次第で順序が変わるので,ソートする
      (source
       (sort orig-segments
        (lambda (x y)
         (let
          ((vx (abs (assoc-ref x 'vertical-count)))
           (vy (abs (assoc-ref y 'vertical-count)))
           (lx (car (assoc-ref x 'horizontal)))
           (ly (car (assoc-ref y 'horizontal))))
          (or
           (< vx vy)
           (and
            (eqv? vx vy)
            (< lx ly)))))))
      (modified-segments '()))
     (if
      (or
       (null? rest-modifications)
       (null? source))
      (append modified-segments source)
      (let
       ((modifications (car rest-modifications)))
       (receive (source- modified-segments+)
        ;; 各要素(list)に対する処理
        (let horizontal-loop
         ((lst modifications)
          (src source)
          (dest modified-segments))
         (if (or (null? lst) (null? src))
          (values src dest)
          (let
           ((add-gap (car lst))
            (segment (car src)))
           ;; gapを付与しないsegmentは素通し
           (if (not add-gap)
            (horizontal-loop (cdr lst) (cdr src) (cons segment dest))
            (receive (target rest)
             (partition
              ;; 16分にgapが付与されるならば,そこの32分以降にもgapを付与,etc.
              (lambda (x)
               (let*
                ((interval (assoc-ref x 'horizontal))
                 (midpoint (/ (+ (car interval) (cdr interval)) 2))
                 (left-edge (car (assoc-ref segment 'horizontal)))
                 (right-edge (cdr (assoc-ref segment 'horizontal))))
                (or
                 (eq? segment x)
                 (and
                  (lesser?
                   (assoc-ref x 'vertical-count)
                   (* direction count))
                  (> right-edge midpoint)
                  (> midpoint left-edge)))))
              src)
             (horizontal-loop
              (cdr lst)
              rest
              (append
               (map
                (lambda (x)
                 (let
                  ((vertical-count
                    (assoc-ref x 'vertical-count))
                   (horizontal
                    (assoc-ref x 'horizontal)))
                  `((vertical-count . ,vertical-count)
                    (horizontal . ,(shorten-interval gap horizontal)))))
                target) dest)))))))
        ;; 16分の処理が終わった際に,まだ残っている16分があれば,素通し
        (receive (source-- modified-segments++)
         (partition
          (lambda (x)
           (lesser?
            (assoc-ref x 'vertical-count)
            (* direction count)))
          source-)
         (vertical-loop
          (1+ count)
          (cdr rest-modifications)
          source--
          (append modified-segments++ modified-segments+))))))))))

#(define (with-extra-floating-segments number direction)
  (lambda (beam)
   (let*
    ((gap (ly:grob-property beam 'gap))
     (orig-segments
      (sort (ly:beam::calc-beam-segments beam)
       (lambda (x y)
        (<
         (assoc-ref x 'vertical-count)
         (assoc-ref y 'vertical-count)))))
     (interval
      (shorten-interval
       gap
       (assoc-ref
        (car orig-segments)
        'horizontal)))
     (start-count
      (if (eqv? direction UP)
       (1+ (assoc-ref (last orig-segments) 'vertical-count))
       (1- (assoc-ref (car orig-segments) 'vertical-count)))))
    (append orig-segments
     (map
      (lambda (x)
       `((vertical-count . ,x)
         (horizontal . ,interval)))
      (iota number start-count  direction))))))

%%
\score{
  \new PianoStaff <<
    \new Staff = "up" \relative c'''{
      \key es\major
      \tempo\markup\left-column{
        "Danse du Rouet"
        \concat{
          "m. 94, 1"
          \raise #0.5 \small{"ers"}
          " V"
          \raise #0.5 \small{"ons"}
        }
      }
      \time 6/8
      \repeat unfold 2 {
        \once\override Beam.beam-segments
        = #(with-gaps '(() (#t #f)))
        a16*2[(
      \set stemRightBeamCount = 1
      bes)
      \set stemLeftBeamCount = 1
      b16(-> bes])
      }
      \bar"||"
      \key c\major
      \tempo\markup\left-column{
        "Le jardin féerique"
        \concat{
          "m. 55,  Célesta"
        }
      }
      \time 3/4
      \hideNotes
      \grace e1^(
      \unHideNotes
      g8)
      \stemDown
      \once\override Beam.beam-segments =
      #(with-extra-floating-segments 1 DOWN)
      <g, e c g>16[(
        \change Staff = "dn"
        \stemUp
        <a, e c>
      ]
      \repeat tremolo 8 {
    \change Staff = "up"
    \stemDown
    <g c e g>32
    \change Staff = "dn"
    \stemUp
    <a e c>
      } |
      \change Staff = "up"
      \stemNeutral
      <g c e g>4)\fermata r r |
      \bar".."
    }
    \new Staff = "dn" \relative c'''{
      \key es\major
      \repeat unfold 2 {
        \once\override Beam.beam-segments
        = #(with-gaps '(() (#t #f)))
        g16*2[(
      \set stemRightBeamCount = 1
      as)
      \set stemLeftBeamCount = 1
      a16(-> as])
      }
      \key c\major
      \grace s1
      r8 s8 s2 |
      R2. |
    }
  >>
  \layout{
    ragged-last = ##f
  }
}


もっと綺麗に書ければよかったのですが(特に、前半については内側のnamed letがなかなか酷い)、使う側からすれば割と有難い感じである、「プロパティの変更だけ」で目的を達成できました4。一応、stable版(2.18.2)でも大丈夫な筈。
ちなみに、以下の1小節目で言えば、向きに応じて符頭側2本のvertical-countが2, 3または-2, -3になります。Beamに限らないことではありますが、使う際に向きを意識しないで済むようにするためには、関数内でどうにかしないといけません。

other-examples.cropped.png

これについて、こういうことができないと辛い5
%% 上述の面倒なSchemeコードをここに貼るか、どうにか別ファイルから読み込む前提

subdivide =
#(define-music-function
  (parser location cut-at keep-count music)
  (integer? integer? ly:music?)
  (make-music 'SequentialMusic 'elements
   (let loop
    ((i 1)
     (src (ly:music-property music 'elements))
     (dest '()))
    (cond
     ((null? src) dest)
     ((eqv? i cut-at)
      (append
       dest
       (list
    (make-music
     'ContextSpeccedMusic
     'context-type
     'Bottom
     'element
     (make-music
      'PropertySet
      'value
      2
      'symbol
      'stemRightBeamCount))
    (car src)
    (make-music
     'ContextSpeccedMusic
     'context-type
     'Bottom
     'element
     (make-music
      'PropertySet
      'value
      2
      'symbol
      'stemLeftBeamCount)))
       (cdr src)))
     (else (loop (1+ i) (cdr src) (append dest (list (car src)))))))))
%%
\score{
  \new PianoStaff <<
    \new Staff = "up" \relative c''{
      \time 4/4
      %% 向きとか気にしたくない
      \override Beam.beam-segments =
      #(with-gaps '(() () (#t #t)))
      \subdivide #2 #2 {
        <gis cis,>64*4 [ <cis eis,> <a d,> <d fis,> ]
      }
      \subdivide #2 #2 {
        <bes es,> [ <es g,> <b e,> <e gis,> ]
      }
      \subdivide #2 #2 {
        <c f,> [ <f a,> <cis fis,> <fis ais,> ]
      }
      \subdivide #2 #2 {
        <d g,> [ <g b,> <dis gis,> <gis bis,> ]
      }
      \revert Beam.beam-segments
      \time 3/8
      \key b\major
      \once\override Beam.beam-segments =
      #(with-extra-floating-segments 2 UP)
      %% gapはこっちで設定したい
      \once\override Beam.gap = 1.25
      \change Staff = "dn"
      \stemUp
      <fisis dis gis,>8.[
        \change Staff = "up"
        \stemDown
        <cisis e gis cisis e>]
    }
    \new Staff = "dn" {
      R1 |
      \key b\major
      s4. |
    }
  >>
}

困ったことに、Schemeコードを書く案でも面倒なものを踏み抜いています。これについては最後に触れます。

重ね打ちの問題点について

さて、後で触れると言った話題について。重ね打ちする際、Beamのpositionsみたいなものについて、即値(というか、マジックナンバー)がとっ散らかっているのはよろしくないですので、なんとかして即値を使わないようにしたいですよね。一方で、2つのBeamのpositionsをきっちり合わせないことには目的が果たせません。何とかして重ねる側で重ねられる側のBeam自体を、あるいは、せめて相手のBeamの適当なプロパティを見ることはできないものでしょうか。
2つのBeamの間には特に親子関係があるというわけでもなさそうですので、安直で簡単なSchemeコードに留めるには、グローバルな変数でも用意して、先に描画される方で必要な値を拾ってその変数に放り込んで、後から描画される側から読み出せば良さそうです。ところが、残念なお知らせがあります。
重ねる例ではわかりやすさのためにBeamの色を変えていましたが、その時の色で話を進めることにしましょう。重ねている<< ... >>の部分について、必ず赤の後に黒が描画されると言えるでしょうか?
以下のコード5で試してみました。

passage =
#(define-music-function
  (parser location count)
  (number?)
  #{
  s4*0^$(format "~a" count)
  <<{
  \oneVoice
  \override NoteColumn.ignore-collision = ##t
  \once\override Beam.positions = #'(0 . 0)
  \once\override Beam.stencil =
  #(lambda (grob)
    (print "~a:black beam\n" count)
    (ly:beam::print grob))
  c8[ c c16 c]
  \revert NoteColumn.ignore-collision
}\\{
  \oneVoice
  \override NoteColumn.ignore-collision = ##t
  \once\override Beam.positions = #'(0 . 0)
  \once\override Beam.color = #red
  \once\override Beam.stencil =
  #(lambda (grob)
    (print "~a:red beam\n" count)
    (ly:beam::print grob))
  \once\override Beam.gap-count = 1
  c16*2[
    \set stemRightBeamCount = 1
    c
    \set stemLeftBeamCount = 1
    c16 c]
  \revert NoteColumn.ignore-collision
}>> r4 r8 r4 r8 |
  #})

\score{
  \new Staff \relative c'{
    \time 9/8
    \passage #1
    \passage #2
    \break
    \passage #3
    \passage #4
  }
}

ウチの現在の野良ビルドLilyPondの結果は以下の通りです:
poormansbold-example3.cropped.png

ちなみに、以下がその時の出力ログの一部になります:

GNU LilyPond 2.21.0
Processing `poormansbold-example3.ly'
Parsing...
poormansbold-example3.ly:1: warning: no \version statement found, please add

\version "2.21.0"

for future compatibility
Interpreting music...
Preprocessing graphical objects...
Finding the ideal number of pages...
Fitting music on 1 page...
Drawing systems...4:red beam
4:black beam
3:red beam
3:black beam
1:black beam
1:red beam
2:black beam
2:red beam

なんということでしょう、恐しい描画順6になっているではありませんか!
おまけに、\breakを抜いたら以下の始末でした:
poormansbold-example3.cropped.png

GNU LilyPond 2.21.0
Processing `poormansbold-example3.ly'
Parsing...
poormansbold-example3.ly:1: warning: no \version statement found, please add

\version "2.21.0"

for future compatibility
Interpreting music...
Preprocessing graphical objects...
Finding the ideal number of pages...
Fitting music on 1 page...
Drawing systems...1:black beam
1:red beam
2:black beam
2:red beam
3:black beam
3:red beam
4:black beam
4:red beam

重ね打ちの安直な方法による部品化は、何も考えずに重ねても問題ないものに限った方がよさそうです。描画順がどちらが先になろうとも大丈夫なようにできなくもないですが、そんな分岐書きたくないですし、その分岐をSchemeで書くのは、諦めてSchemeコード書くのと大差無いと思いますので。

ついでに…Scheme案の既知の問題と警告

何故か確率的にしか正常にコンパイルが通らない

こう書けないと辛い、と例に上げたものについて、ウチの環境では結構な確率で、以下のログと共に表示が下の例のような感じでおかしくなることがあります。

programming error: Improbable offset for stencil: -nan staff space
Setting to zero.
continuing, cross fingers

other-examples.cropped.png
lyファイルを何度かコンパイルしていると問題なく通ることも多く、こちらもこちらで結構な地雷のようです。仮にLilyPondのバグだとしても、再現できる最低限のSnippetがまだ作れていないですので、解析にも報告にも困る…。

2018/01/15追記:こういうことでした。

浮いている桁を細くしたいとか、斜めにしたいとか、そういうのはさらに高度なSchemeでの調整が必要

もっとも、そうは言っても既にx座標は取れそうだということがわかっているわけですので、割となんとかなりそうですね。ここまで来てしまうと、ly:stencil-ナントカ系の出番です。

その他、未知の問題と警告

もし変な問題が潜んでいたらごめんなさい。


  1. 以下の2つの図だけはGimpで慌てて書いたものです。微妙に汚なくて申し訳ありません。 

  2. あたかもとても簡単であるように言ってのけていますが、あまりLilyPondとは関係のない純粋なSchemeプログラミングの問題になってしまうので、できれば今回は触れたくないだけだったりします。Qiitaなのに!とか言われかねませんが、話がブレるので…。 

  3. 変形に際して、どういうパラメータを与えることでどうやって制御するのが良いか、意見が割れると思います。個人的には前半の方について、最初の要素に()(空リスト)か何かを入れる必要がある点が微妙に不満足ですが、そういう事を言い出すと再帰の形を崩して分岐を増やすか、ラップする関数を追加することになり、いずれにしても1クッション挾む形となって記事の方向性的に微妙なので今回はやめました。 

  4. 他のプロパティがちゃんと生きていますので、例えばpositionをいじれば上手いこと動きますし、gap-countなんかを設定してしまうとそれ相応の変な表示になります。 

  5. Unstable版だと、define-music-function等でparserとlocationが不要になっています。 

  6. 仮にlayerプロパティを設定すればどちらが上に印字されるか制御できますが、ここで気にしている描画順というのはstencil等に仕込んだコールバックが呼ばれる順序です。いくら上に印字されようが、必要な値がその時点で取れないと困りますので。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0