「指揮付きの伴奏動画を作る:指揮の表示」の続きです。画面にスコアを表示して、現在位置のガイドをつけたいのです。
実は、LilyPond の派生プロジェクトとして ly2video というのがあります。これを使えば、スクロールする楽譜にガイドをつけることができます。しかしながら、インストールが妙に複雑なのと、やりたいことが微妙に違っているので、ガイドをつける技法を参考にさせてもらって、コーディングは自前でやることにしました。
LilyPond を使います。まず、\paper
セクションに以下のように指定して、1ページに1段ずつ楽譜を組版します。ヘッダ・フッタはすべて非表示にします。
systems-per-page = 1
oddHeaderMarkup = ##f
evenHeaderMarkup = ##f
bookTitleMarkup = ##f
scoreTitleMarkup = ##f
tagline = ##f
拍の位置を出力させる方法を探すのに苦労しました。作戦は以下の通りです。
- 拍の位置に
\markup
テキストを置いて、stencil
を##f
にして、表示されないようにする。 TextScript
のafter-line-breaking
コールバックを設定する。このコールバック関数中で、ly:grob-relative-coordinate
を呼ぶと、現在の譜表 (System
) に対する相対座標が求められる(これを見つけるのに苦労した)。- 楽曲の先頭からの位置は、
(grob::when grob)
で取得できる。ly:moment-main
で全音符の長さの倍数に変換できて、タイムベース×4 (1536
) を掛けると MIDI ティックに変換できる。 - 小節線の位置も同様に取得できる。コールバックを設定するとき、
BarLine
はVoice
コンテキストではなくStaff
コンテキストに属するので、Staff.BarLine.after-line-breaking
と記述する必要がある。
これに基づいて、下のようにコーディングしました。\bsclA
に音楽の本体が入っている想定です。
#(define output-port (open-output-file "part_movie.out"))
#(define (calc-XY-position grob)
(let* ((system (ly:grob-system grob))
(x (ly:grob-relative-coordinate grob system X))
(y (ly:grob-relative-coordinate grob system Y)))
(format output-port "~a ~a ~a\n" (grob::name grob) x y (* 1536 (ly:moment (grob::when grob))))))
beat = #(define-event-function () () #{ -\tweak TextScript.stencil $##f ^\markup "!" #})
\score {
\new Staff = "bsclar" {
%lt;%lt; { \transpose c d' \bsclA }
{ \override TextScript.after-line-breaking = #calc-XY-position
\override Staff.BarLine.after-line-breaking = #calc-XY-position
\beat \beat % two empty beats before beginning of the score
\repeat unfold 111 { s4.\beat s4.\beat } }
>>
}
}
part_movie.out
には下のような内容が出力されます。数字の1つめが System
の左端からの位置(単位は譜線間の距離 = staff-size を4で割ったもの。Staff-size の単位は point = 1/72.27 inch)。数字の3つ目は先頭からのティック値です。
TextScript 11.129561264437905 -1.2233492063492082 0
TextScript 11.129561264437905 -1.2233492063492082 0
TextScript 11.129561264437905 -1.2233492063492082 0
TextScript 14.633786412225351 -1.2233492063492082 576
TextScript 19.26695255310438 -1.2233492063492082 1152
BarLine 18.1380115600128 -3.777000000000002 1152
…
System
の紙面内の位置を取得する方法が、どうしてもわかりませんでした。1ページのレイアウトが終わった時点で決まるはずなのですが、どこにコールバックを引っ掛ければいいのかがわからない。結局、ly2video のソースを参照して、1ページ分を png 形式にした画像を左上から1ピクセルずつ見ていって、「白でないピクセルが真横に一定の長さ並んでいる」ところを探して、それを一番上の譜線の位置にすることにしました。力技です。Python で書いています。
from PIL import Image
def find_staff_rect(image): # image は Image 型の画像オブジェクトとする
lines = []
width, height = image.size
for y in range(height):
for x in range(width):
if image.getpixel((x,y)) != 255:
for len in range(width - x):
if image.getpixel((x+len,y)) == 255:
lines.append((y, x, len))
break
break
lines.sort(key = lambda a: -a[2] * height + a[0]) # sort by len (descending order) and then by y (ascending order)
# The y range of the staff
staff_y0 = 1000000
staff_y1 = -1
staff_len = lines[0][2]
staff_x = lines[0][1]
for a in lines:
if a[2] < staff_len - 3:
break # This is no longer part of the staff
staff_y0 = min(staff_y0, a[0])
staff_y1 = max(staff_y1, a[0])
return (staff_x, staff_y0, staff_len, staff_y1 - staff_y0)
1ページ1段で組版した楽譜の PDF から1ページごとに画像を切り出して、それぞれの譜線の位置を計算するところまでコーディングしました。PDF のページ数を求めるのに pdfinfo
、PDF から画像に変換するのに ImageMagick
を使っています。どちらも Homebrew でインストールしたものです。
import pygame
basename = "part_movie"
titlefile = "title.png"
WIDTH = 1280 # Screen width
HEIGHT = 720 # Screen height
PPI = 154 # Pixels per inch in the page image
def create_png_files():
global page_images, staff_rects, max_height, title_image
staff_rects = [] # List of (x, y, width, height) corresponding to the staff in each pages
page_images = [] # List of pygame surfaces
np = subprocess.check_output(["bash", "-c", "pdfinfo {}.pdf | grep 'Pages:' | awk '{{ print $2 }}'".format(basename)])
np = int(np)
max_height = 0
for p in range(np):
pngname = "page{:04}.png".format(p)
if (not os.path.exists(pngname)) or os.path.getmtime(pngname) < os.path.getmtime(basename + ".pdf"):
print("Creating {}.".format(pngname))
os.system("magick -density {} {}.pdf[{}] -alpha remove -define trim:edges=south,north -trim +repage" \
" -gravity west -extent {} +repage -gravity northwest -splice 0x10 -gravity southwest -splice 0x10 +repage" \
" {}".format(PPI, basename, p, WIDTH, pngname))
image = Image.open(pngname)
staff_rects.append(find_staff_rect(image))
image = image.convert("RGB")
if max_height < image.size[1]:
max_height = image.size[1]
page_images.append(pygame.image.fromstring(image.tobytes(), image.size, image.mode).convert())
print(page_images[-1])
title_image = pygame.image.load(titlefile)
これで、一番大変なところを突破しました。あとは、先日検討した指揮の表示と合わせて動画を作成します。長くなったので、別記事で書きます。