ラズパイで圧電スピーカーをMIDIで奏でる

# 概要

ラズパイと圧電スピーカーをつなぎ、タイマーや時報のためにMIDIで音を鳴らす方法を紹介します。
私は複数のタイマーや時報をラズパイで鳴らしているのですが、もっと複雑なメロディーを楽に作れたらと思いました。音程と鳴らすタイミングが必要ですが、周波数と秒数を配列としてソースコードに直書きする方法では、実際にならしてみるまで正しく指定できているかがわかりません。プレビューも難しく、気軽にメロディを試せません。そこで、DAWなどのMIDIエディタでメロディーをいったん作成してから、そのMIDIを読み込んで鳴らせることができたら楽だと思い、試しました。
以下に周波数と秒数を直書きするコード例を示します。

```py
import RPi.GPIO as GPIO
import time
import math

pin = 13  # 任意のピン番号
TIME_SLEEP = 2.5

def onkai(n):
    """ 鍵盤番号nに応じた周波数[Hz]を返します"""
    # 鍵盤番号は、ラ0（A0）を1とした連番
    # n=1（ラ0（A0））   -> 27.500Hz
    # n=2（ラ#0（A#0）） -> 29.135...Hz
    # n=3（シ0（B0））   -> 30.867...Hz
    # ...
    return 27.500 * math.pow(math.pow(2, 1 / 12), (n - 1))


# それぞれの音の周波数を定義
DO = onkai(52)
RE = onkai(54)
MI = onkai(56)
SO = onkai(59)

# メロディとリズムを配列に
mery_merody = [
    MI,
    RE,
    DO,
    RE,
    MI,
    MI,
    # ...
]
mery_rhythm = [
    0.3,
    0.6,
    0.9,
    0.6,
    0.9,
    0.3,
    # ...
]
GPIO.setmode(GPIO.BCM)
GPIO.setup(pin, GPIO.OUT, initial=GPIO.LOW)
p = GPIO.PWM(pin, 1)
p.start(50)

for i, oto in enumerate(mery_merody):
    p.start(50)
    p.ChangeFrequency(oto)
    time.sleep(mery_rhythm[i] / TIME_SLEEP)
    p.stop()
    time.sleep(0.01)

p.stop()
GPIO.cleanup()
```

## 手順

MIDIで奏でる例を示します。

### MIDIを読み込む

Pythonライブラリのmidoを使用します。以下のコマンドでインストールします。

```bash
pip install mido
```

### 圧電スピーカーで鳴らす為のMIDIを作成

圧電スピーカーで鳴らすため、少々特殊なMIDIを作成します。手元の環境がWindowsだったため、Dominoで編集しました。

- [MIDI音楽編集ソフト「Domino（ドミノ）」 | TAKABO SOFT](https://takabosoft.com/domino)


以下を満たすようなMIDIを作成します。

- 使用するのは1CHのみ
- ノートとノートが重ならないように調整する
- 拍子とテンポは変更しても問題ない
- 「End of Trackを調節」にて曲の終了を指定するのを忘れずに

以下は参考画像。

![MIDIを作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139537/ec21215b-19ee-42e2-8e54-bb5ff7051011.png)

以下の手順でMIDIファイルとしてデータを出力します。

- ファイル > SMF書き出し >
- 「format1（通常はこちら）」で保存

### MIDIをPythonで読み込み

先ほど作成したMIDIをPythonで読み込みます。MIDIをpythonで読み込むために[mido/mido: MIDI Objects for Python](https://github.com/mido/mido)を使用します。

```bash
pip3 install mido --break-system-packages
```

MIDIを読み込むときは、SysExやProgram Changeなどの制御系メッセージはすべて無視し、Note onやNote offのみを取り出します。

```py
import math
import sys
import time

import mido
import RPi.GPIO as GPIO


def midi_number_to_note_number(midi_number: int):
    """MIDIのnote numberを鍵盤番号へ変換します"""
    # 鍵盤番号: 1:ラ0（A0） = MIDIのnote number: 21
    return midi_number - 20


def onkai(n):
    """鍵盤番号nに応じた周波数[Hz]を返します"""
    # 鍵盤番号は、ラ0（A0）を1とした連番
    # n=1（ラ0（A0））   -> 27.500Hz
    # n=2（ラ#0（A#0）） -> 29.135...Hz
    # n=3（シ0（B0））   -> 30.867...Hz
    # ...
    return 27.500 * math.pow(math.pow(2, 1 / 12), n)


# GPIO XX の XXの数値でPIN番号を指定する
GPIO.setmode(GPIO.BCM)

# GPIO 13 = PWM 1 の33番PINに圧電スピーカーの+端子を接続する
GPIO.setup(13, GPIO.OUT, initial=GPIO.LOW)
p = GPIO.PWM(13, 1)

# 圧電スピーカーの-端子はGroundならどこでもよい
# 今回は34番PINに接続する


for msg in mido.MidiFile(sys.argv[1]):
    time.sleep(msg.time)
    if msg.type == "note_on":
        # 圧電スピーカー
        p.start(50)
        freq = onkai(midi_number_to_note_number(msg.note))
        p.ChangeFrequency(freq)
        print(f"note_on {freq} {msg}")
    if msg.type == "note_off":
        p.stop()
        print(f"note_off {msg}")
```

以下のコマンドで実行します。

```bash
python3 midi.py sample.mid
```

以下のログ出力とともに、圧電スピーカーから音が鳴ります。

```log
note_on 554.3652619537454 note_on channel=0 note=72 velocity=100 time=0
note_off note_off channel=0 note=72 velocity=0 time=0.25
note_on 554.3652619537454 note_on channel=0 note=72 velocity=100 time=0.25
note_off note_off channel=0 note=72 velocity=0 time=0.25
note_on 830.6093951598923 note_on channel=0 note=79 velocity=100 time=0.25
note_off note_off channel=0 note=79 velocity=0 time=0.25
note_on 830.6093951598923 note_on channel=0 note=79 velocity=100 time=0.25
note_off note_off channel=0 note=79 velocity=0 time=0.25
note_on 932.3275230361822 note_on channel=0 note=81 velocity=100 time=0.25
note_off note_off channel=0 note=81 velocity=0 time=0.25
note_on 932.3275230361822 note_on channel=0 note=81 velocity=100 time=0.25
note_off note_off channel=0 note=81 velocity=0 time=0.25
note_on 830.6093951598923 note_on channel=0 note=79 velocity=100 time=0.25
note_off note_off channel=0 note=79 velocity=0 time=0.5
note_on 739.9888454232706 note_on channel=0 note=77 velocity=100 time=0.5
note_off note_off channel=0 note=77 velocity=0 time=0.25
note_on 739.9888454232706 note_on channel=0 note=77 velocity=100 time=0.25
note_off note_off channel=0 note=77 velocity=0 time=0.25
note_on 698.4564628660095 note_on channel=0 note=76 velocity=100 time=0.25
note_off note_off channel=0 note=76 velocity=0 time=0.25
note_on 698.4564628660095 note_on channel=0 note=76 velocity=100 time=0.25
note_off note_off channel=0 note=76 velocity=0 time=0.25
note_on 622.2539674441632 note_on channel=0 note=74 velocity=100 time=0.25
note_off note_off channel=0 note=74 velocity=0 time=0.25104166666666666
note_on 622.2539674441632 note_on channel=0 note=74 velocity=100 time=0.24895833333333334
note_off note_off channel=0 note=74 velocity=0 time=0.25104166666666666
note_on 554.3652619537454 note_on channel=0 note=72 velocity=100 time=0.24895833333333334
note_off note_off channel=0 note=72 velocity=0 time=0.5
```

## 参考

- [音階周波数](https://tomari.org/main/java/oto.html)
  - 鍵盤番号と周波数について
- [Mido - MIDI Objects for Python — Mido 1.3.4.dev6+ga0158ff documentation](https://mido.readthedocs.io/en/latest/index.html)
- [MIDIのnote編集に関する基本tips【Python, mido】 #Python - Qiita](https://qiita.com/kokuren333/items/4ff17a2996b8003b665b)
- [Midoを使ってPythonでMIDIを扱ってみよう #mido - Qiita](https://qiita.com/tjsurume/items/75a96381fd57d5350971)

## 動作環境

- Raspberry Pi 4 Model B Rev 1.5
- Raspberry Pi OS Lite (64-bit)（Debian GNU/Linux 12.2.0 (bookworm)）
- Python 3.11.2
- mido 1.3.3