Numpyで奏でるクリスマスソング

##  やりたいこと
Pythonにも音を扱うライブラリはたくさんありますが、できるだけシンプルに、低レベルな処理を使って音楽を奏でてみます。
せっかくのAdvent Calendarなので、クリスマスソングを一曲奏でてみることをゴールとしてみます。

完成したソースはこちら
https://github.com/TatchNicolas/silent-night-py

## 下準備

### とりあえず音を鳴らしてみる

numpyで正弦波を作り、`IPython.display.Audio(wave, rate=bit_rate)`で鳴らします
とりあえず、ポピュラー音楽で一般的なチューニングの基準となるA=440Hzを鳴らしてみます。

```py
#  とりあえず音を鳴らしてみる
import numpy as np
import IPython.display 

BIT_RATE = 44100
concert_pitch = 440

A3 = concert_pitch # ピアノでいう、いわゆる「ラ」の音

duration = 3
sample_A3 = np.sin(A3 * 2 * np.pi * np.linspace(0, duration, duration * BIT_RATE))
IPython.display.Audio(sample_A3, rate=BIT_RATE)
```

### 音律を定義する

音律とは、1オクターブの中の音を物理的にどんな高さで(周波数で)鳴らすかを定義したものです。
~~私が純正律でチューニングした楽器なんて弾いたことないので~~多くの人に馴染みがあるであろう[平均律](https://ja.wikipedia.org/wiki/%E5%B9%B3%E5%9D%87%E5%BE%8B)を使います。

平均律では、1オクターブを基準音から倍の周波数までを同じ比で定義します。

```py
ratios = [2 ** (i / 12) for i in range(0, 13)]
tone_names = ['A3', 'As3', 'B3', 'C4', 'Cs4', 'D4', 'Ds4', 'E4', 'F4', 'Fs4', 'G4', 'Gs4','A4' ]

TONE_DICT = {}

# A3からA4のクロマチックスケール
for ratio, tone_name in zip(ratios, tone_names):
    duration = 1
    frequency = ratio * A3
    tmp_sound = np.sin(frequency * 2 * np.pi * np.linspace(0, duration, duration * BIT_RATE))
    audio=IPython.display.Audio(tmp_sound, rate=BIT_RATE)
    
    # あとで使えるように辞書に周波数を入れとく
    TONE_DICT[tone_name] = frequency
    # Jupyter上に表示
    print(f'tone_name:{tone_name}, frequency: {frequency}')
    IPython.display.display(audio)
```

さらに音域を広げてみましょう。

```py
# 1オクターブ上、1オクターブ下、2 オクターブ下も作る

# さきほどの1オクターブ分の周波数を作る処理を関数化
def generate_octave(start, tone_names):
    dict_to_return = {}
    for ratio, tone_name in zip(ratios, tone_names):
        frequency = ratio * start
        dict_to_return[tone_name] = frequency
    return dict_to_return

# A2からA3のクロマチックスケール
tone_names_a2_a3 = [ 'A2', 'As2', 'B2', 'C3', 'Cs3', 'D3', 'Ds3', 'E3', 'F3', 'Fs3', 'G3', 'Gs3', 'A3']
a2_a3 = generate_octave(TONE_DICT['A3'] / 2, tone_names_a2_a3)  # 基準音(440Hz)の半分の周波数 = 1オクターブ下の音から始める
TONE_DICT.update(a2_a3)

# A1からA2のクロマチックスケール
tone_names_a1_a2 = [ 'A1', 'As1', 'B1','C2', 'Cs2', 'D2', 'Ds2', 'E2', 'F2', 'Fs2', 'G2', 'Gs2', 'A2']
a1_a2 = generate_octave(TONE_DICT['A3'] / 4, tone_names_a1_a2)  # 基準音(440Hz)の1/4周波数 = 2オクターブ下の音から始める
TONE_DICT.update(a1_a2)

# A4からA5のクロマチックスケール
tone_names_a4_a5 = [ 'A4', 'As4', 'B4','C5', 'Cs5', 'D5', 'Ds5', 'E5', 'F5', 'Fs5', 'G5', 'Gs5', 'A5']
a4_a5 = generate_octave(TONE_DICT['A3'] * 2, tone_names_a4_a5)  # 基準音(440Hz)の4倍の周波数 = 2オクターブ上の音から始める
TONE_DICT.update(a4_a5)
```

### 音価を定義する

音価とは、その音符が支配する譜面上の時間の長さです。
「四分音符」「八分音符」「十六分音符」を定義してみます。(BPMは固定してしまいました）

```py
# 音価(音の長さ)をつくる

QUATER = 1/2
EIGHTH =  QUATER / 2
SIXTEENTH =  EIGHTH / 2


# 周波数と音価からsin波として音を作る関数
def generate_note(frequency, note_value):
    one_sec_note = np.sin(TONE_DICT[frequency] * 2 * np.pi * np.linspace(0, 2, 2 * BIT_RATE))
    return one_sec_note[0:int(len(one_sec_note) * note_value)]
```

### 旋律を鳴らしてみる

ここまで定義してきたものを使って、簡単な旋律(メロディ)をつくってみます。
高さと長さの情報を持つ音符を(楽譜の上で言えば横方向へ)並べていくだけです。
 「かえるの合唱」風にならべてみます。

```py
# 配列を結合する = 音を楽譜で横に並べる = メロディ

c3v4 = generate_note('C3', QUATER)
d3v4 = generate_note('D3', QUATER)
e3v4 = generate_note('E3', QUATER)
f3v4 = generate_note('F3', QUATER)

frog = np.hstack((c3v4, d3v4, e3v4, f3v4, e3v4, d3v4, c3v4))
IPython.display.Audio(frog, rate=BIT_RATE)
```

### 和音を鳴らしてみる

旋律だけでは雰囲気がでないので、伴奏をつけてみましょう。
(楽譜の上で言えば縦方向へ)音符を並べてみます。

```py
# 配列の和 = 音を楽譜で縦に並べる = ハーモニー

c3v2 = generate_note('C3', QUATER * 2)
e3v2 = generate_note('E3', QUATER * 2)
g3v2 = generate_note('G3',QUATER * 2)

c_triad_chord = np.vstack((c3v2, e3v2, g3v2))
IPython.display.Audio(c_triad_chord, rate=BIT_RATE)
```

## 曲にしてみる

あとは、上記を組み合わせて曲にしていくだけです。

```py
# きよしこの夜

melody_notes = [
    ('G3', EIGHTH + SIXTEENTH), ('A3', SIXTEENTH), ('G3',EIGHTH), ('E3', QUATER + EIGHTH),
    ('G3', EIGHTH + SIXTEENTH), ('A3', SIXTEENTH), ('G3',EIGHTH), ('E3', QUATER + EIGHTH),
    
    ('D4', QUATER), ('D4', EIGHTH), ('B3', QUATER + EIGHTH),
    ('C4', QUATER), ('C4', EIGHTH), ('G3', QUATER + EIGHTH),
    
    ('A3', QUATER), ('A3', EIGHTH), ('C4', EIGHTH + SIXTEENTH), ('B3',SIXTEENTH), ('A3', EIGHTH),
    ('G3', EIGHTH + SIXTEENTH), ('A3', SIXTEENTH), ('G3',EIGHTH), ('E3', QUATER + EIGHTH),
    
    ('A3', QUATER), ('A3', EIGHTH), ('C4', EIGHTH + SIXTEENTH), ('B3',SIXTEENTH), ('A3', EIGHTH),
    ('G3', EIGHTH + SIXTEENTH), ('A3', SIXTEENTH), ('G3',EIGHTH), ('E3', QUATER + EIGHTH),
    
    ('D4', QUATER), ('D4', EIGHTH), ('F4', EIGHTH + SIXTEENTH), ('D4',SIXTEENTH), ('B3', EIGHTH),
    ('C4', QUATER + EIGHTH), ('E4', QUATER + EIGHTH),
    
    ('C4', EIGHTH), ('G3', EIGHTH), ('E3', EIGHTH),('G3', EIGHTH + SIXTEENTH), ('F3', SIXTEENTH), ('D3',EIGHTH),
    ('C3', QUATER + EIGHTH), ('C3', QUATER + EIGHTH),

    ('C4', EIGHTH), ('G3', EIGHTH), ('E3', EIGHTH),('G3', EIGHTH + SIXTEENTH), ('F3', SIXTEENTH), ('D3',EIGHTH),
    ('C3', QUATER + EIGHTH), ('C3', QUATER + EIGHTH),
]

melody_wave = np.hstack([generate_note(tone, value) for tone, value in melody_notes])
melody_audio = IPython.display.Audio(melody_wave, rate=BIT_RATE)
IPython.display.display(melody_audio)

backing_notes = [
    ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH), ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH),
    ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH), ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH),
    
    ('D2', EIGHTH), ('F2', EIGHTH), ('A2',EIGHTH), ('G2', EIGHTH), ('B2',EIGHTH), ('D3', EIGHTH),
    ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH), ('C2', EIGHTH), ('E2', EIGHTH), ('As2',EIGHTH),

    ('F2', EIGHTH), ('A2', EIGHTH), ('C3',EIGHTH), ('F2', EIGHTH), ('A2', EIGHTH), ('C3',EIGHTH),
    ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH), ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH),

    ('F2', EIGHTH), ('A2', EIGHTH), ('C3',EIGHTH), ('Ds2', EIGHTH), ('Fs2', EIGHTH), ('A2',EIGHTH),
    ('E2', EIGHTH), ('G2', EIGHTH), ('B2',EIGHTH), ('A2', EIGHTH), ('Cs3', EIGHTH), ('E3',EIGHTH),

    ('D2', EIGHTH), ('F2', EIGHTH), ('A2',EIGHTH), ('E2', EIGHTH), ('Gs2',EIGHTH), ('B2', EIGHTH),
    ('A2', EIGHTH), ('C3', EIGHTH), ('G2',EIGHTH), ('Fs2', EIGHTH), ('C3',EIGHTH), ('E3', EIGHTH),

    ('F2', EIGHTH), ('A2', EIGHTH), ('C3',EIGHTH), ('E2', EIGHTH), ('Gs2', EIGHTH), ('B2',EIGHTH),
    ('A2', EIGHTH), ('C3', EIGHTH), ('E3',EIGHTH), ('Gs2', EIGHTH), ('B2', EIGHTH), ('D3',EIGHTH),
    ('F2', EIGHTH), ('A2', EIGHTH), ('C3',EIGHTH), ('G2', EIGHTH), ('B2', EIGHTH), ('D2',EIGHTH),
    ('C2', EIGHTH), ('E2', EIGHTH), ('G2',EIGHTH), ('C2', EIGHTH + EIGHTH + EIGHTH),
]

backing_wave = np.hstack([generate_note(tone, value) for tone, value in backing_notes])
backing_audio = IPython.display.Audio(backing_wave, rate=BIT_RATE)
IPython.display.display(backing_audio)
silent_night_wave = np.vstack((melody_wave, backing_wave))
IPython.display.Audio(silent_night_wave, rate=BIT_RATE)
```


## 聞いてみる

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">(※ 音量注意)<br>numpyとscipyで作ったお手軽「きよしこの夜」<a href="https://t.co/TwoU3xDqWQ">https://t.co/TwoU3xDqWQ</a> <a href="https://t.co/5uuOyFH5Xa">pic.twitter.com/5uuOyFH5Xa</a></p>&mdash; たっち/Tatch/它龇牙咧嘴 (@TatchNicolas) <a href="https://twitter.com/TatchNicolas/status/1074864433988419584?ref_src=twsrc%5Etfw">December 18, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


出力されたwavファイルを聞いてみましょう。

クリスマスソングをプルリクに乗せて、気になるあの人へ想いを届けてみてはいかがでしょうか。

## 参考
- https://qiita.com/mzmttks/items/13b5a67cf281215a69da