Python で音楽を作って楽しもう

# 概要
この記事では、Python を使って シンセっぽい音を合成したり、MIDI 入出力をいじったり、和音を扱ったりして、音楽を楽しむ方法を紹介します。音楽や MIDI に関する基本的な知識は持っているけどプログラムから制御する方法は分からないという方や、Python で音が出せるのって面白そうだと思う方に読んでいただけると嬉しいです。

# Python でシンセっぽい音を合成する

Python でアナログシンセのような波形を生成して再生したり、音声ファイルとして書き出したりできる [synthesizer](https://github.com/yuma-m/synthesizer) というライブラリを作りました(macOS と Ubuntu しかサポートしていません、Windows の方、ごめんなさい)。このライブラリは Python から簡単に波形を生成・編集できるのが特徴ですが、サポートしている機能は現在のところ非常に限定的です。オープンソースのアナログシンセシミュレータとしては、 [amSynth](https://github.com/amsynth/amsynth) など、波形の編集パラメータが充実していて、入出力IFもリッチな優れたライブラリがありますので、本格的な利用にはそちらがオススメです。

## Python で波形を生成してみる

まずは、数値計算ライブラリを使って波形を生成し、オーディオファイルに書きだしてみます。numpy, scipy の機能を使うだけでも、正弦波(サイン波)や矩形波、鋸歯状波(ノコギリ波)等を作ることができます。

### 440Hzのサイン波を作ってみる

```py
import numpy as np
from scipy.io import wavfile

frequency = 440.0  # 生成するサイン波の周波数
seconds = 1.0      # 生成する音の秒数
rate = 44100       # 出力する wav ファイルのサンプリング周波数

phases = np.cumsum(2.0 * np.pi * frequency / rate * np.ones(int(rate * seconds)))
# 波形を生成
wave = np.sin(phases)  # -1.0 〜 1.0 の値のサイン波
# import scipy.signal して、
# wave = scipy.signal.sawtooth(phases) とすると鋸歯状波、
# wave = scipy.signal.square(phases) とすると矩形波になる

# 16bit の wav ファイルに書き出す
wave = (wave * float(2 ** 15 - 1)).astype(np.int16)  # 値域を 16bit にする
wavfile.write("sine.wav", rate, wave)
```

上記を実行すると、以下のようなファイルが生成されます(音量注意)。

- [サイン波](https://drive.google.com/file/d/1kQDEDFGUrxAgYcjz365E6w9MQjXmz-op/view?usp=sharing)
- [ノコギリ波](https://drive.google.com/file/d/1NZJOf8KGSGj0EoTBX6ZcAL-bDP4SRRBL/view?usp=sharing)
- [矩形波](https://drive.google.com/file/d/1rB2saESec4Sn6s6jITa_Mvi-iMSAB7w4/view?usp=sharing)

## synthesizer の簡単な使い方

上記のように、シンプルな波形の音を作るだけならば numpy や scipy で簡単に実装できます。これだけでもファミコン風の 8bit 音源としては十分かもしれません(そういう音源で作られた曲をチップチューンと呼びます)。しかし、実際のユースケースでは、もっと様々な音色を重ねあわせて使いたいと思います。synthesizer ライブラリを使うと、(少し)簡単に実現できるようになります。

### インストール

```bash
$ pip install synthesizer
```

### サイン波の再生と書き出し

先程 numpy を使ってサイン波を生成した例は、synthesizer では以下のように書けます。

```py
from synthesizer import Synthesizer, Waveform, Writer

# ここの説明は 音作り を参照
synth = Synthesizer(osc1_waveform=Waveform.sine, osc1_volume=1.0, use_osc2=False)
# 一定音程の波形を生成する
wave = synth.generate_constant_wave(frequency=440.0, length=1.0)
# オーディオファイル出力用クラス
writer = Writer()
writer.write_wave("sine.wav", wave)
```

実際には numpy, scipy のメソッドをラップしているだけなのですが、カンタンに書けますよね。
また、オーディオデバイスへの出力も可能です。

```py
from synthesizer import Player, Synthesizer, Waveform

# オーディオデバイス出力用クラス
player = Player()
# 引数を指定しなければデフォルトの出力デバイスが選択されます
player.open_stream()

# player.enumerate_device() でデバイスの一覧を表示できます
# player.open_stream(device_name="UA-25EX") のように出力デバイスを指定可能です

synth = Synthesizer(osc1_waveform=Waveform.sine, osc1_volume=1.0, use_osc2=False)
player.play_wave(synth.generate_constant_wave(440.0, 1.0))
```

正しくオーディオデバイスが開けていれば、スピーカーやヘッドフォンからサイン波が鳴るはずです。

### 音作り

synthesizer では2つの音源生成器(オシレータと呼びます)からの音を重ねあわせることで、新たな音色を作ることできます。

```py
synth = Synthesizer(
    osc1_waveform=Waveform.sawtooth,  # 1つ目のオシレータの波形
    osc1_volume=1.0,                  # 1つ目のオシレータの音量
    use_osc2=True,                    # 2つ目のオシレータを利用する場合は True
    osc2_waveform=Waveform.square,    # 2つ目のオシレータの波形
    osc2_volume=0.6,                  # 2つ目のオシレータの音量
    osc2_freq_transpose=2.0,          # 2つ目のオシレータの再生時の周波数を指定された周波数の何倍にするか
)
writer.write_wave("overtone.wav", synth.generate_constant_wave(frequency=440.0, length=1.0))
```

例えば上記の設定で音を鳴らすと、[こんな音](https://drive.google.com/file/d/10xl_d53eiEcAOXrxLbK12Lo0KqlJBKHJ/view?usp=sharing) になります。ノコギリ波の音をベースに、矩形波の倍音を少し重ねたような音色です。

他には、例えばハモったような音色を作ることもできます。

```py
# 長3度のハモリ
# osc2_freq_transpose の値がポイント
synth1 = Synthesizer(
    osc1_waveform=Waveform.sine, osc1_volume=1.0,
    use_osc2=True, osc2_waveform=Waveform.sine,
    osc2_volume=0.8, osc2_freq_transpose=1.25,
)
writer.write_wave("third.wav", synth1.generate_constant_wave(frequency=440.0, length=1.0))

# 完全5度のハモリ
synth2 = Synthesizer(
    osc1_waveform=Waveform.sine, osc1_volume=1.0,
    use_osc2=True, osc2_waveform=Waveform.sine,
    osc2_volume=0.8, osc2_freq_transpose=1.5,
)
writer.write_wave("fifth.wav", synth2.generate_constant_wave(frequency=440.0, length=1.0))
```

それぞれ、[長3度](https://drive.google.com/file/d/1KN-_VE-f_ziAP6HqNzC8fS-sUN9XCeMd/view?usp=sharing), [完全5度](https://drive.google.com/file/d/1xBejvucM3wQOOegwGt7yYSrd2ZnxgsGX/view?usp=sharing) のような音色になります。

応用としてローパスフィルター等のフィルター処理をかけてあげると、音色をさらに変えることができます。

```py
import scipy
synth = Synthesizer(
    osc1_waveform=Waveform.sawtooth, osc1_volume=1.0,
    use_osc2=True, osc2_waveform=Waveform.sawtooth,
    osc2_volume=0.4, osc2_freq_transpose=4.0,
)
wave = synth.generate_constant_wave(frequency=440.0, length=1.0)

# ローパスフィルタのパラメータ
nyquist_freq = 44100 / 2.0
cutoff_freq = 2000.0
cutoff = cutoff_freq / nyquist_freq
# ローパスフィルタを適用
n, wn = scipy.signal.buttord(cutoff , cutoff / 0.7, 6.0, 40.0)
b, a = scipy.signal.butter(n, wn)
wave = scipy.signal.filtfilt(b, a, wave)

writer.write_wave("filter.wav", wave)
```
[フィルターなし](https://drive.google.com/file/d/1ttBj2yZV-rxrO8muRiVSMc3Sjr7G51Fb/view?usp=sharing)と[フィルター適用後](https://drive.google.com/file/d/1VAR_tum_ew9ay4GhK_MpcD_IF0wzKXPG/view?usp=sharing)を聴き比べると、ローパスフィルタによって丸い音色に変わっていることが分かると思います。ローパスフィルタはその名の通り、低周波数域を通して高周波数域を削るようなフィルターなので、ノコギリ波や矩形波に対して適用することで、倍音成分を削って音の特性を変化させることができます。

### 和音を合成

synthesizer では和音を合成することもできます。

```py
BASE = 261.626  # C4
synth = Synthesizer(osc1_waveform=Waveform.sine, osc1_volume=1.0, use_osc2=False)

# ド・ミ・ソ(C major) を生成
chord = [BASE,  BASE * 2.0 ** (4 / 12.0), BASE * 2.0 ** (7 / 12.0)]
writer.write_wave("C.wav", synth.generate_chord(chord, 1.0))

# ド・ミ♭・ソ・シ♭(Cm7) を生成 
chord = [BASE,  BASE * 2.0 ** (3 / 12.0), BASE * 2.0 ** (7 / 12.0), BASE * 2.0 ** (10 / 12.0)]
writer.write_wave("Cm7.wav", synth.generate_chord(chord, 1.0))
```

それぞれ、[C major](https://drive.google.com/file/d/1TMu-7xeKTOSWmHPGpBJvD5tQBPDG1oyX/view?usp=sharing), [Cm7](https://drive.google.com/file/d/1dXQGbKi7FZs0EYAMmeaGqwJm-0SN94KR/view?usp=sharing) のような和音が生成されます。

現状は周波数をリスト型で渡せて自由度が高いのですが、使い勝手がイマイチなので、この辺りは改善していきたいです。

# Python で MIDI 入出力を行う

さて、Python でシンセっぽい音が合成できることは分かりましたが、音楽を作って再生するのはまだまだ厳しそうです。そこで、Python から MIDI を制御して MIDI 入力に対応したシンセサイザーを鳴らしてみると、もっと音楽らしいことができるようになります。

[python-midi](https://github.com/vishnubob/python-midi) というライブラリを使うと、 Python から MIDI ファイルや MIDI ポートへの入出力を行うことができます。python-midi は割と上級者向けのライブラリで親切な感じではないのですが、その分細かいコントロールの指定や制御ができるライブラリです。MIDI のコントロール等に詳しくなくて挫折しそうな方は、[pretty_midi](https://github.com/craffel/pretty-midi) や [EasyMIDI](https://github.com/daviddekleer/EasyMIDI) 等を使うと幸せになるかもしれません。

## python-midi の使い方

### インストール

pip で手に入るのはバージョンが違うので、github から直接取得します。

```bash
$ pip install git+https://github.com/vishnubob/python-midi.git
```

### MIDI 出力デバイスを列挙

まずは出力可能な MIDI デバイスを探してみます。何にも出力できるデバイスがない場合は、TiMidity を入れてみましょう。 Ubuntu なら `apt install timidity` で、 macOS ならば `brew install timidity` でインストールできます。Ubuntu なら `timidity -iA` で、macOS なら `timidity -id` でデーモン起動できるかと。

```py
from midi import sequencer as sequencer
hardware = sequencer.SequencerHardware()
for client in hardware._clients:
    print(client)
# MIDIデバイス名が出力される
# TiMidity
# Midi Through
```

### MIDI ファイルを再生する

出力デバイスを指定して MIDI ファイルを再生をしてみましょう。例えば MIDI ファイルを python-midi でロードして、 TiMidity に MIDI 信号として送信する例です。MIDI ファイルなんて持ってないよという方は、頑張って検索してみてください。[童謡](http://www.asahi-net.or.jp/~HB9T-KTD/music/Japan/Studio/midi.html)や[懐かしの洋楽](http://www.geocities.jp/nobuo_takenaka/beatlesj.htm)等のMIDIファイルなら結構見つかるものです。

```py:play.py
import time
import midi
from midi import sequencer as sequencer


def play_pattern(client, port, pattern):
    # python-midi のサンプル midiplay.py を参考にちょっと修正
    seq = sequencer.SequencerWrite(sequencer_resolution=pattern.resolution)
    seq.subscribe_port(client, port)
    pattern.make_ticks_abs()

    events = []
    for track in pattern:
        for event in track:
            events.append(event)
    events.sort()

    seq.start_sequencer()
    for event in events:
        buf = seq.event_write(event, tick=True)
        if buf is None:
            continue
        if buf < 1000:
            time.sleep(.5)
    while event.tick > seq.queue_get_tick_time():
        seq.drain()
        time.sleep(.5)



midi_device = "TiMidity"
midi_port = "TiMidity port 0"

# MIDI ファイルをロード
pattern = midi.read_midifile("MIDIファイル名")

# MIDI の再生デバイス, ポートを取得
hardware = sequencer.SequencerHardware()
client, port = hardware.get_client_and_port(midi_device, midi_port)

# MIDI ファイルを再生
play_pattern(client, port, pattern)
```

MIDI の再生周りがラップされていないので、ちょっと分かりづらいですね。また、 python-midi の問題かは分からないのですが、ピッチベンドやCCイベント等を細かい周期で送った際に、再生のもたつきを感じることがありました。このあたりの最適化は DAW には敵わない領域かもしれません。

### MIDI のパターンを作ってみる

既存の MIDI ファイルをロードするだけでなく、プログラム中で自由に MIDI を生成することもできます。

```py:generate.py
import midi

CC_MASTER_VOL = 7

# パターンを作成
pattern = midi.Pattern()

# トラックを作成しパターンに追加
track = midi.Track()
pattern.append(track)

# MIDI の CC イベントでボリュームを設定
volume = 100
event = midi.ControlChangeEvent(tick=120, control=CC_MASTER_VOL, value=volume, channel=1)
track.append(event)

# C4 と C5 のノートを追加
velocity = 100
note_on = midi.NoteOnEvent(tick=240, velocity=velocity, pitch=midi.C_4, channel=1)
note_off = midi.NoteOffEvent(tick=480, pitch=midi.C_4, channel=1)
track.append(note_on)
track.append(note_off)
note_on = midi.NoteOnEvent(tick=480, velocity=velocity, pitch=midi.C_5, channel=1)
note_off = midi.NoteOffEvent(tick=720, pitch=midi.C_5, channel=1)
track.append(note_on)
track.append(note_off)

# トラック終了イベントを追加
eot = midi.EndOfTrackEvent(tick=1)
track.append(eot)

# あとは上記の例と同様
hardware = sequencer.SequencerHardware()
client, port = hardware.get_client_and_port(midi_device, midi_port)
play_pattern(client, port, pattern)
```

上記の例が正しく動けば、2つの音が再生されます。MIDI のイベント周りも特にラップされていないので、ノートを追加する際に NoteOn と NoteOff の両方のイベントを追加しないといけなかったり、 tick 数の管理をきちんとしないといけなかったりという不便さはあります。このあたりは、先に紹介した[pretty_midi](https://github.com/craffel/pretty-midi) や [EasyMIDI](https://github.com/daviddekleer/EasyMIDI) 等を使うと、もうちょっとスマートに記述できます。

# Python で和音を扱う

さて、Python で音を合成して、好きな曲を再生できたら、次は音楽理論を Python で扱いたくなりますよね！(ボケです)
そんな音楽大好きな皆様のために、Python で和音を扱える [pychord](https://github.com/yuma-m/pychord) という自作ライブラリを公開しています。

誰が何に使うの？って感じだと思いますが、 [ディープラーニングで曲のコード進行を生成するウェブサービスを作りました - Qiita](https://qiita.com/yura/items/bb7e0767813fff6fe74f) みたいなことをするのに使えて、github の PR もいくつかもらいました。

## pychord の簡単な使い方

### インストール

```bash
$ pip install pychord
```

### 和音を作る

C, Am, Gsus4 のようなコードネームを Chord のコンストラクタに渡すことで、Chord オブジェクトを作ることができます。

```py
from pychord import Chord
c = Chord("C")
```

`components()` で構成音のリストを取得できます。

```py
am7 = Chord("Am7")
print(am7.components())
# ['A', 'C', 'E', 'G'] ように構成音が表示される
```

スラッシュコードも扱うことができます。

```py
slash = Chord("F/G")
print(slash.components())
# ['G', 'F', 'A', 'C']
```

### 転調する

`transpose` メソッドを利用することでコードを転調することができます。

```py
c = Chord("C6")
c.transpose(2)  # C6 を +2 転調するので D6 になるはず
print(c.chord)
# 確かに D6 と表示される 
```

また、コード同士は比較することができます。

```py
c == Chord("D6")  # True
c == Chord("C6")  # False
```

### 構成音からコードを探す

`note_to_chord` メソッドを使うことで構成音から和音を取得できます。

```py
from pychord import note_to_chord

print(note_to_chord(["F", "A", "C"]))
# [<Chord: F>]

print(note_to_chord(["A", "C", "F"]))  # 分数コードもOK
# [<Chord: F/A>]

print(note_to_chord(["F", "Ab", "B", "D"]))  # dim6 なら転回形が全て出てくる
# [<Chord: Fdim6>, <Chord: Abdim6/F>, <Chord: Bdim6/F>, <Chord: Ddim6/F>]

print(note_to_chord(["A", "B", "C#", "D#", "F"]))  # コードにならない場合は空リストが返ってくる
# []
```

ある程度音楽に慣れていると、コードネームを聞いただけで構成音が分かったり、構成音を聞いただけで和音が分かったりするのですが、そうでなくても大丈夫！ちなみにコードネームから構成音を取得するのは、上記で紹介した `Chord.components` メソッドです。

### コード進行を作る

単一のコードだけでなく、`ChordProgression` クラスを使うことでコード進行を扱うことができます。

```py
from pychord import ChordProgression

cp = ChordProgression(["C", "Am", "F"])
print(cp)
# C | Am | F

print(cp[0])  # 要素にもアクセス可能
# C
```

リスト型のように append メソッドでコードを追加することができます。

```py
cp.append("F/G")  # コードを追加
print(cp)
# C | Am | F | F/G
```

また、`Chord` クラスと同様に `transpose` メソッドで転調できます。ラスサビで転調するケースとか、そういう時に役立ちそうな予感がしますね。

```py
cp.transpose(-3)  # コード進行ごと転調
print(cp)
#  A | Gbm | D | D/E
```

## pychord の目的

pychord は音楽理論でいうところのコード(和音)を扱うためのライブラリなので、それ単体で音が出たり、音を解析させたりすることはできません。では、どういうことに利用できるかと言いますと、例えば、コード譜をいっぱいパースしてコード進行の解析をしてみたいとか、既存の曲のコードを転調させたり、比較したりしたいとか、そういった目的に使うことを想定しています。また、上記で紹介した MIDI の入出力ライブラリと組み合わせたりすれば、シンセを使って音を出すこともできます(頑張れば...)。

# さいごに

この記事では、Pythonで音楽するための方法と、そのための自作ライブラリの紹介を行いました。音楽には専用のソフトが必要で敷居が高いと思われがちなのですが、意外とカンタンにプログラムからいじることができるので、皆様是非試してみてください！