Musescoreで作成した譜面の移調を自動化 (Python,  Music21, MusicXML)

# 導入

## 概要

Musescoreで作成した譜面の移調を自動化するスクリプトをPythonで作成した。

## 背景

筆者は趣味でジャズギターを弾く。
練習のための譜面をMusescoreで作成することが多い。
移調はMusescore上でのマウス操作でも可能だが、12キーへの移調をすべて行うのは手間がかかるのでプログラミングで自動化することにした。

## 環境

Host OS: Windows 11
Guest OS: Ubuntu 22.04.4 LTS (WSL2)
Python version : 3.10.12
Music21 version: 9.1.0
Musescore (WSL): musescore3/jammy,now 3.2.3+dfsg2-11 amd64

# 詳細

## 使い方

スクリプト実行前のディレクトリは以下の通り。

```bash
.
├── README.md
├── change_key.py
├── jazzblues_combo.musicxml
├── read_mxl_split_write.py
├── requirements.txt
└── .venv
```
`jazzblues_combo.musicxml`が今回の転調したい譜面ファイル。
`.mscz`ではなく、`.musicxml`形式のファイルを使います。

Host OS(Windows11)上Musescore4で開くと以下の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2913763/e088809f-6a9a-4a9d-e4ee-d4138b1eb080.png)



`python change_key.py`コマンドでスクリプト実行したあとのディレクトリは以下の通り。


```bash
.
├── README.md
├── change_key.py
├── jazzblues_combo.musicxml
├── jazzblues_combo_12keys
│   ├── jazzblues_combo_00_-6_B.musicxml
│   ├── jazzblues_combo_01_-5_C.musicxml
│   ├── jazzblues_combo_02_-4_C#.musicxml
│   ├── jazzblues_combo_03_-3_D.musicxml
│   ├── jazzblues_combo_04_-2_E-.musicxml
│   ├── jazzblues_combo_05_-1_E.musicxml
│   ├── jazzblues_combo_06_00_F.musicxml
│   ├── jazzblues_combo_07_01_F#.musicxml
│   ├── jazzblues_combo_08_02_G.musicxml
│   ├── jazzblues_combo_09_03_G#.musicxml
│   ├── jazzblues_combo_10_04_A.musicxml
│   └── jazzblues_combo_11_05_B-.musicxml
├── read_mxl_split_write.py
├── requirements.txt
└── .venv
```

新しいディレクトリと12個のファイルが作成される。
11個のファイルは移調されたもの、1つのファイルは移調前と同じもの。

移調されたファイルのひとつ、`jazzblues_combo_11_05_B-.musicxml`をHost OS(Windows11)上Musescore4で開くと以下の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2913763/2a34befb-341b-052d-3f84-302c5b3e9fa5.png)


# 初期設定

順番が前後しましたが、スクリプトを実行する前の初期設定についてです。

## musescore

aptで探して手に入るものをインストール

```bash
# check available musescore  (Ubuntu)
apt list -a | grep musescore

# install musescore3 (Ubuntu)
sudo apt install musescore3
```

## Python 設定

```bash
# Create venv for Python (Ubuntu)
python3 -m venv .venv

# activate venv
source .venv/bin/activate

# install music21
python -m pip install music21

# create python package file
python -m pip freeze > requirements.txt
```



# Script

## 全体

`change_key.py`が今回のメインのスクリプト。

```python
import os
import music21
import logging

# setup log on console
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# variables
filename_body = "jazzblues_combo"
original_key_str = "F"
filename_suffix = ".musicxml"
startval = -6  # start key from the original key
steps = 12  # transporse range. If you want all key, set 12.

input_file = "./" + filename_body + filename_suffix
logger.debug(f"input_file: {input_file}")

output_dir = "./" + filename_body + "_12keys"
os.makedirs(output_dir, exist_ok=True)
logger.debug(f"output_dir: {output_dir}")


for idx_i, i in enumerate(range(startval, startval + steps)):
    score = music21.converter.parse(input_file)  # load original file.
    the_key = music21.key.Key(original_key_str)
    score.insert(0, the_key)
    score.transpose(i, inPlace=True)
    file_name = (
        filename_body
        + "_"
        + str(idx_i).zfill(2)
        + "_"
        + str(i).zfill(2)
        + "_"
        + score.keySignature.tonicPitchNameWithCase
        + filename_suffix
    )
    logger.debug(f"file_name: {file_name}")
    file_path = os.path.join(output_dir, file_name)
    score.write(fmt="musicxml", fp=file_path)
```

## 解説

### 変数

```python
# variables
filename_body = "jazzblues_combo" # 移調したいファイルの名前から拡張子を除いたもの

original_key_str = "F" 
# 移調したいファイルのキー。
# 書いておかないと譜面のキーを判定してくれない。大文字がmajor　key, 小文字が　minor key。

filename_suffix = ".musicxml"
# 譜面ファイルの拡張子。移調前と移調後で共通

startval = -6  # start key from the original key 
# この場合、オリジナルキーの-6からスタートしてstepsの値まで処理を繰り返す。

steps = 12  # 全部のキーへ移調する場合は12に設定.
```


### メイン処理

```python
for idx_i, i in enumerate(range(startval, startval + steps)):
    score = music21.converter.parse(input_file)  # 譜面のmusicxmlファイルからスコアを作成
    the_key = music21.key.Key(original_key_str) # key情報を文字列からMusic21のオブジェクトへ変換
    score.insert(0, the_key) # key情報を付与
    score.transpose(i, inPlace=True) # 移調処理
    file_name = (
        filename_body
        + "_"
        + str(idx_i).zfill(2)
        + "_"
        + str(i).zfill(2)
        + "_"
        + score.keySignature.tonicPitchNameWithCase
        + filename_suffix
    ) # パッと見で何番目に作られたどのキーの譜面なのかわかるように出力ファイル名を付与
    logger.debug(f"file_name: {file_name}")
    file_path = os.path.join(output_dir, file_name)
    score.write(fmt="musicxml", fp=file_path) # ファイル出力
```

### logger

本筋とは全然関係ないが、
printデバッグしてコメントアウトしたりするよりもlogerの方が、簡単で読みやすい。

初期設定。今回はファイル出力せずにCLIへ出力
```python
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
```

開発中はこうすると`logger.debug("何かしらの文字列")`の部分が出力される
```python
logging.basicConfig(level=logging.DEBUG)
```

終わったらこうすると余計なログを一発で止めることができる。
```python
logging.basicConfig(level=logging.INFO)
```





# おまけ 小節毎に分割

`read_mxl_split_write.py`というのも作った。
特定のPart(トラック)を小節毎に分割して、各小節を1ファイルとして保存する。
これだけだとあまり役に立たないが、練習がてら書いてみた。
```python
import os
import music21
import logging

# setup log on console
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

# variables
filename_body = "jazzblues_combo"
original_key_str = "F"
filename_suffix = ".musicxml"
part_idx = 0

startval = -6  # start key from the original key
steps = 12  # transporse range. If you want all key, set 12.

input_file = "./" + filename_body + filename_suffix
logger.debug(f"input_file: {input_file}")
logger.debug(f" ")

output_dir = "./" + filename_body + "_separated_bars"
os.makedirs(output_dir, exist_ok=True)
logger.debug(f"output_dir: {output_dir}")


score = music21.converter.parse(input_file)


the_part_mesures = score.getElementsByClass(music21.stream.Part)[
    part_idx
].getElementsByClass(music21.stream.Measure)
print(the_part_mesures)
print(len(the_part_mesures))

for idx_i, obj_i in enumerate(the_part_mesures):
    file_name = (
        filename_body
        + "_"
        + str(idx_i + 1).zfill(2)
        + "_part"
        + str(part_idx + 1).zfill(2)
        + filename_suffix
    )
    logger.debug(f"file_name: {file_name}")
    file_path = os.path.join(output_dir, file_name)
    tmp_score = music21.stream.Score()
    tmp_score.append(obj_i)
    tmp_score.write(fmt="musicxml", fp=file_path)
```

# まとめ

## 感想

オブジェクト構造をつかむまでが難しかった。
まず　大きい順にScore, Parts, Measure (小節?)
その配下に並列でNotes(音符), Rests (休符), Chord (コード)があるっぽい。

簡単に移調ができるようになってよかった!

## 今後

機械的に練習パターンを作れるようにしたい。例えば...

- 気に入ったフレーズをコード進行的に合うように加工して貼り付ける
- 特定のリズミックなモチーフをコードに合うようにして貼り付ける



異名同音(`A flat`になっていてほしいところが`G sharp`になっている)ところを調整する方法を探りたい。

musicxmlからうまいことキー情報を読み込めるようにして、キーを使用者がスクリプトに明示的に記載する必要がないようにしたい。