Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

やりたいこと

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

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

下準備

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

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

#  とりあえず音を鳴らしてみる
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オクターブの中の音を物理的にどんな高さで(周波数で)鳴らすかを定義したものです。
私が純正律でチューニングした楽器なんて弾いたことないので多くの人に馴染みがあるであろう平均律を使います。

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

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)

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

# 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は固定してしまいました)

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

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)]

旋律を鳴らしてみる

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

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

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)

和音を鳴らしてみる

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

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

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)

曲にしてみる

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

# きよしこの夜

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)

聞いてみる

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

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

参考

jxpress
技術力で「ニュースの産業革命」を起こす。言語処理・データ解析分野の専門家が集まる、News Techベンチャー。
https://jxpress.net/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away