LilyPondでテキストから楽譜を作るTips

#まえがき

LilyPond及びそれを用いたライブラリやツールを使うと、
テキストで文字を書くことによって五線譜やMIDIファイルを作ることができます。
今回はLilypond周辺ツールを使ってより効果的に楽譜を作成することにフォーカスします。
- こちらは、[音楽ツール・ライブラリ・技術 Advent Calendar 2019](https://adventar.org/calendars/3997) 12/19分の記事です。

# ２行で
- 既存の仕組みとしてLilyPondアプリケーションのFrescobaldiで設定をいじる
- 別でより簡単にカタカナ全角で扱うためにちょっとしたGUIアプリケーションを作りました。

# 目標
「ドレミファミレド」みたいな入力だけで楽譜を作れるようにする。

# 手段１：LilyPond及びFrescobaldiを使う
## 環境
- Lilypondをダウンロードする。
http://lilypond.org/index.ja.html

- Frescobaldiをダウンロードする。
https://github.com/frescobaldi/frescobaldi/releases

いずれも開発版で最新バージョンがおすすめです。
古いものでは私の環境では文字化けなどが発生しました。

- Lilypond 2.19.83
- Frescobaldi 3.0.0


## 書式を考える
警告対策のため、現状のLilypondのバージョンを明記します。

```
\version "2.19.83"
```

そのままでの入力は`c d e f g a b`のようにアルファベット記法です。
個人的な好みとしてドレミで表現したいため、includeに下記を加えます。

```
\include "italiano.ly"
```
これで、`do re mi fa sol la si`で音階が表現できます。
ソで`sol`、ラをLはじまりの`la`で表すことに気を付ければ直感的に表現が可能です。
フラットは`b`を、シャープは`d`を加えます。紛らわしいですが分別するコツとしては、フラットの形が`b`に似ている。シャープはその逆で`d`とします。


シンプルなテンプレートとしては下記が便利です。

```
\version "2.19.83"
\include "italiano.ly"
\score{
  \new Staff{
    \tempo 4 = 144
    \relative do'{
      do4. re8 mi4. do8
      mi4 do mi2
    }
  }
  \layout {}
  \midi {}
}
```

### おすすめの設定
- `Lilypond -> Automatic Engrave`にチェックを入れる。
編集時に自動で楽譜pdfとMIDIファイルを更新してくれます。
- `Tools`で頻繁に使用する機能を表示する。
簡単なMIDI確認などは`Tools->MIDI Player`で可能です。

## 備考
記法の詳細は [公式ドキュメント](http://lilypond.org/doc/v2.18/Documentation/notation/index.ja.html) を参考に、`do re mi` のような階名を中心に楽譜を作ることができました。


# 手段２：より直感的に扱うためアプリケーションを作成する。
こちらは少しやりすぎですが、追加で実装してみました。

## 環境
手段１にくわえ、GUIアプリケーション`pyside(Qt For Python)`をインストールして、
「ドレミファ」など入力すると、自動的に楽譜を作成する手順までを実装しました。
(別にPysideにこだわる必要はありません。)


## ねらい
Frescobaldiで作成しても、Lilypond記法の理解がある程度必要なのと、
プログラムになれてない人にも使えるように全角への対応をある程度よくしたい。

## デモ
機能は大幅に絞られてますが、カタカナ全角入力に対応してすぐ楽譜にしていくサンプルです。
![scores2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89004/8749b5e3-f415-fb7b-a868-1430f4f61ded.gif)

デモのように、ある程度その人にあわせた簡略化は考えられそうです。

## （参考）作成したコード

```
#!/usr/bin/env python

import os
import sys

from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtSvg import *

import subprocess

# 下記、環境に応じて変えてください。
cmd_path = 'C:\\Program Files (x86)\\LilyPond\\usr\\bin'
base_filepath =　'YOUR FILE PATH'
exe_lilypond_name = 'lilypond.exe'


code_dict = {'１':'1', '２':'2', '４':'4', '８':'8', '１６':'16', '３２':'32',
             'ド': ' do', 'レ': ' re','ミ': ' mi', 'ファ': ' fa', 'ソ': ' sol','ラ': ' la', 'シ': ' si','ッ':' r',
            '（':' <','）':'>','#':'d','♭':'b','↑':'\'','↓':','}

class UIWindow(QWidget):
    def __init__(self):
        super(UIWindow, self).__init__()

        layout = QHBoxLayout()

        w_code = QWidget()
        w_code.setFixedWidth(500)
        w_layout = QGridLayout()

        self.chkbox_run = QCheckBox()
        self.chkbox_run.setText("Run")
        w_layout.addWidget(self.chkbox_run, 0, 0)

        str_dict = ""
        for i, c in enumerate(code_dict.items()):
            str_dict += str(c) + ' '
            if i % 4 == 3:
                str_dict += "\n"
        label_dict = QLabel()
        label_dict.setText(str_dict)
        w_layout.addWidget(label_dict, 2, 0)
        
        self.edit_txt = QTextEdit()
        w_layout.addWidget(self.edit_txt, 1, 0 )

        self.txt_header = QTextEdit()
        header_template = '''\
\\version "2.19.83"
\\include "italiano.ly"
\\score{
  \\new Staff{
    \\tempo 4 = 144
    \\relative do'{
'''
        self.txt_header.setText(header_template)
        w_layout.addWidget(self.txt_header, 0 ,1 )
        self.txt_code = QTextEdit()
        w_layout.addWidget(self.txt_code, 1, 1)
        self.txt_footer = QTextEdit()
        footer_template = '''
    }
  }
  \\layout { }
  \\midi { }
}
'''
        self.txt_footer.setText(footer_template)
        w_layout.addWidget(self.txt_footer, 2, 1)
        w_code.setLayout(w_layout)
        layout.addWidget(w_code)

        self.svg_gen = QSvgWidget()
        self.svg_gen.setMinimumSize(QSize(400, 600))
        self.svg_gen.setStyleSheet("background-color:white;")
        layout.addWidget(self.svg_gen)
        self.setLayout(layout)

    def getKanaStr(self):
        return self.edit_txt.toPlainText()

    def setConvedStr(self, str_code):
        self.txt_code.setText(str_code)

    def pressOut(self):

        full_str = self.txt_header.toPlainText() + \
            self.txt_code.toPlainText() + \
            self.txt_footer.toPlainText()

        with open(f'{base_filepath}.ly', 'wt') as f:
            f.write(full_str)
        
        assert os.path.isdir(cmd_path)
        os.chdir(cmd_path)
        subprocess.Popen([exe_lilypond_name, '-dbackend=svg',f'-o{base_filepath}', f'{base_filepath}.ly'])
        self.svg_gen.load(f'{base_filepath}.svg')

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.uiWindow = UIWindow()
        self.setCentralWidget(self.uiWindow)

        self.timer = QTimer()
        self.timer.timeout.connect(self.run)
        self.timer.start(5000)

        self.old_kanastr = ""
    

    def conv_txt(self, old_str):
        new_str = ''
        for index in range(len(old_str)):
            for code_key in code_dict.keys():
                if old_str[index:].startswith(code_key):
                    new_str += code_dict[code_key]
                    # 複数文字対策
                    if len(code_key) > 1:
                        index += len(code_key)-1
        return new_str

    def run(self):
        if self.uiWindow.chkbox_run.isChecked():
            self.new_kanastr = self.uiWindow.getKanaStr()
            if self.new_kanastr != self.old_kanastr:
                self.old_kanastr = self.new_kanastr
                self.uiWindow.setConvedStr(self.conv_txt(self.new_kanastr))
            self.uiWindow.pressOut()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

```