以前書いた記事にてコンパイルが確率的にしか通らないという問題があり、原因を解析してみた結果、「そんなのわかるかよ!」と思わず言いたくなりかねないものだったため、記事に残します。ホント、LilyPondがOSSでよかった…。
結論を先に言ってしまいますと、当たり前ではありますが、並び順に意味がある場合、崩すと予期せぬ動作を引き起こすので注意しましょう、という話です。
確率的に失敗する例
以下のコードを何度かコンパイルしてみると、たまにprogramming error: Improbable offset for stencil: -nan staff space
が発生します1:
#(define (shift-vertical-count amount)
(lambda (beam)
(let
((orig-segments (ly:beam::calc-beam-segments beam)))
(map-in-order
(lambda (segment)
`((vertical-count . ,(+ (assoc-ref segment 'vertical-count) amount))
(horizontal . ,(assoc-ref segment 'horizontal))))
orig-segments))))
\score{
\new Staff \relative c'{
\override Beam.beam-segments = #(shift-vertical-count -1)
\repeat unfold 3 {
c8[ d e f]
}
}
}
8分音符を繋げる場合、通常はvertical-count
が0の桁になるのですが、上のコード例では、無理矢理vertical-count
が-1の桁に変更しています(画像は上のコードで、2番目のbeamにてprogramming error
が出た場合)。
さて、どこが悪いのでしょうか? 結果的には-1という値が悪さをしているのですが、どんな悪さをしているのかは上のコードのみを睨んでもわからない筈です。というのは、これ、上述の-1という値がbeam.ccにあるSCM Beam::print (SCM)
関数内でvector::operator[]
経由の配列外参照(-1番目の要素へのアクセス)を引き起こす、という話だからです。
何が起きているのか
ポイントとなるのは、Beam.beam-segments
は傾きの情報や具体的なY座標等を持っていないという事実です。Beam::print
(あるいは、同じことですが、ly:beam::print
)にて、印字する際に基準となる桁を拾って各種計算が行われており、基準となる桁は、beam-segments
の先頭の要素のvertical-count
が0ならばその要素、そうでなければ、{末尾の要素のvertical-count
}番目の要素となります2。大抵の場合、基準はvertical-count
が0の桁であり、これが正しく拾われるようにbeam-segments
が適切にソートされている前提でBeam::print
は組まれていました。もちろん、存在しないと拾うことすらできません。上に上げたコード例では、先頭のvertical-count
が0ではないため、末尾のvertical-count
を見て、beam-segments
の-1番目の要素(そんな要素がなくても、vector::operator[]
なので読み出されてしまいます)を基準にしようとしています。実はfeathered beamの設定が無いと基準の桁から拾った値に0がかけられてしまうので、変な値が返ってきたところで影響はありません…運悪く無限大やら非数が返ってこなければ3。普通は全ての桁のvertical-count
が0未満ということはありませんので、最低限beam-segments
の最後の要素だけなんとかしておけば、変な落ち方はしなくなります。万一、全てのvertical-count
が0未満のBeamが必要だという場合には、Beam.stencil
に設定する関数を頑張ってこしらえてください。
で、以前のトレモロのスニペットはどう直せば良いのか
以前のスニペットは以下の形でした:
\override Beam.beam-segments
= #(lambda (beam)
(let
((segments (ly:beam::calc-beam-segments beam)))
;; fで順序が狂う
(f segments)))
なので、以下のように修正すればOKな筈です:
\override Beam.beam-segments
= #(lambda (beam)
(let
((segments (ly:beam::calc-beam-segments beam))
(direction (ly:grob-property beam 'direction)))
;; やっつけ感が酷いけれども、こんな感じで大丈夫
(sort
(f segments)
(lambda (x y)
(let*
((edge (if (eqv? direction UP) cdr car))
(lesser? (if (eqv? direction UP) > <))
(vx (assoc-ref x 'vertical-count))
(vy (assoc-ref y 'vertical-count))
(hx (edge (assoc-ref x 'horizontal)))
(hy (edge (assoc-ref y 'horizontal))))
(or
(< vx vy)
(and
(eqv? vx vy)
(lesser? hx hy))))))))
-
理論的には
\repeat
で繰り返す必要は無い筈なのですが…\repeat
なしではなかなか発現してくれませんでした。 ↩ -
興味のある方は、適当に
Beam.stencil
にbeam-segments
の中身を表示するようなコードを仕込んでいろいろと試してみると面白いと思います。なお、ソースコード的なことを言いますと、beam.cc
内のBeam::print
中のextreme
という変数が基準となる桁のインデックスを持っている形になります。 ↩ -
運悪くこれらが返ってくると計算結果が非数になり、最終的に
void Stencil::translate (Offset)
にて目出度く(?)programming error
が発生します。 ↩