Pythonで音を鳴らす
はじめに
なにか音を鳴らすプログラムを組みたくなる時があります。以下ではJupyter Notebook (Google Colab)上で音を鳴らすサンプルです。
ソースコードは以下においてあります。
Google Colabで開いてそのまま試すこともできます。
音の鳴らし方
音を鳴らすには、波形データをNumPy配列で作ってIPython.display.Audio
に突っ込むのが簡単です。例えばサンプリングレート48kHz、長さ1秒で、基準となるラの音(440Hz)を鳴らすには、以下のようなコードになります。
import numpy as np
import IPython
rate = 48000
duration = 1.0
t = np.linspace(0., duration, int(rate*duration))
x = np.sin(2.0*np.pi*440.0*t)
IPython.display.Audio(x, rate=rate, autoplay=True)
サンプリングレートは1秒間にあるデータの数なので、それに秒数をかけたものが総データ数になります。サンプリングレートは44.1kHzか、48kHzにすることが多いようです。
そのデータ数だけの波形データを用意すれば良いわけですが、今回は440Hzのサインカーブx
を、IPython.display.Audio
に突っ込めばOKです。例えばGoogle Colabで実行すると以下のような画面が出て、音がなります。
後のために、テンポから4分音符の長さを求めて、その長さだけ演奏するようにしましょう。4分音符の音の長さはBPM (Beat Per Minutes)から決まります。BPMは1分あたりの4分音符の数です。なのでBPM=60なら4分音符は1秒、120なら0.5です。ここではBPM=120、つまり4分音符の長さは0.5秒としましょう。
rate = 48000
BPM = 120
qn_duration = 60.0/BPM
t = np.linspace(0., qn_duration, int(rate*qn_duration))
x = np.sin(2.0*np.pi*440.0*t)
IPython.display.Audio(x, rate=rate, autoplay=True)
MMLから音を鳴らす
Music Macro Language (MML)という、音楽のためのDSLがあります。古の時代、BASICのPLAY文で音を鳴らすことができました。CDEFGABがそれぞれドレミファソラシド、Rが休符です。例えば4分音符はC4
、2分音符はC2
と数字を続けますが、今回は全部4分音符ということにして音階だけ表現することにしましょう。
音階ですが、「ラ(A)」の音を基準とし、周波数を2倍にすると1オクターブ上がり、半分にすると1オクターブ下がります。1オクターブを12個の音に分け、それぞれ以下のように名前をつけましょう。
- C
- C#
- D
- D#
- E
- F
- F#
- G
- G#
- A
- A#
- B
これを対数スケールで均等に分けるのが平均律、すべての音を周波数比3:2でわけていくのがピタゴラス音律です。ここでは平均律を採用しましょう。ラ(A)の音を440Hzとします。1オクターブ上がると周波数が2倍になり、それを対数スケールで12等分するので、隣の音の周波数は
上記で言えば、Aの音を基準の440Hzとして、一つ下の音(G#)は415.3Hz、一つ上の音(A#)は466.2Hzになります。平均律では隣合う音は
この12個の音と、休符をあわせて13個の周波数を作りましょう。
freqs = [0] + [440.0 * 2.0**((i-9)/12.0) for i in range(12)]
音階の文字列と周波数を辞書に入れておきます。
notes = ["R", "C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
dic = {}
for i, s in enumerate(notes):
dic[s] = i
今回は使いませんが、C#
なんかも一応定義しておきます。
さて、これでMMLを音に直す準備ができました。MMLを受け取って音を鳴らす関数play_mml
は以下のように書けるでしょう。
def play_mml(mml):
rate = 48000
BPM = 120
qn_duration = 60.0/BPM
t = np.linspace(0.0, qn_duration, int(rate*qn_duration))
music = np.array([])
for s in list(mml):
f = freqs[dic[s]]
music = np.append(music, np.sin(2.0*np.pi*f*t))
return IPython.display.Audio(music, rate=rate, autoplay=True)
「キラキラ星」を鳴らしてみましょうか。
mml_twinkle_star = "CCGGAAGRFFEEDDCRGGFFEEDRGGFFEEDRCCGGAAGRFFEEDDCR"
play_mml(mml_twinkle_star)
「キラキラ星」が演奏されたはずです。
「かえるのうた」も同様に演奏できます。
mml_frog_song = "CDEFEDCREFGAGFERCRCRCRCRCDEFEDCR"
play_mml(mml_frog_song)
BPMを変えれば演奏速度を変えることができます。
ピアノロールからの演奏
ピアノロールが画像として与えられた時、それを音に変換したいことがあります。まずはMMLからピアノロールを作成し、逆にピアノロールから音を鳴らすルーチンも作ってみましょう。
MMLからピアノロール
まずはMMLからピアノロールを作成する関数を作ります。ここでは、音は1オクターブ(12音)だけを扱い、二次元のNumPy配列でピアノロールを表現することにしましょう。画像を扱うためのライブラリPIL
をインポートして、4分音符のピアノロール上での長さqn_length
も定義しておきます。ここでは8ドットにしておきしょう。
from PIL import Image, ImageDraw, ImageFont
qn_length = 8
MMLの文字列を受け取って、ピアノロール用のNumPy配列を返す関数はこんな感じになります。
def mml2data(mml):
data = np.zeros((12, qn_length*len(mml)), dtype=np.uint8)
for i, s in enumerate(list(mml)):
if s == "R":
continue
j = notes.index(s) - 1
data[11-j, (i*qn_length):((i+1)*qn_length)] = 255
return data
キラキラ星のMMLを食わせてNumPy配列を作り、それを画像として可視化してやりましょう。
data = mml2data("CCGGAAGRFFEEDDCRGGFFEEDRGGFFEEDRCCGGAAGRFFEEDDCR")
Image.fromarray(data)
こんな画像が得られます。
「かえるのうた」も同様です。
data = mml2data("CDEFEDCREFGAGFERCRCRCRCRCDEFEDCR")
Image.fromarray(data)
ピアノロールなら音を複数同時に鳴らす表現が可能なので、既存のデータにMMLを追加する関数を作りましょう。
def mml2data_append(data, mml):
for i, s in enumerate(list(mml)):
if s == "R":
continue
j = notes.index(s) - 1
data[11-j, (i*qn_length):((i+1)*qn_length)] = 255
return data
これを使うと、「かえるのうた」の輪唱を作ることができます。
data = mml2data("CDEFEDCREFGAGFERCRCRCRCRCDEFEDCR")
data = mml2data_append(data, "RRRRRRRRCDEFEDCREFGAGFERCRCRCRCR")
data = mml2data_append(data, "RRRRRRRRRRRRRRRRCDEFEDCREFGAGFER")
Image.fromarray(data)
面倒なので、最初の人が歌い終わったらおしまいにしています。
ピアノロールから音を鳴らす
次に、このピアノロールから音を鳴らす関数を作りましょう。画像を行ごとに走査して、音のなり始めと終わりを検出し、その場所に対応する周波数でサインカーブを乗せるだけです。
def data2audio(img):
_, length = img.shape
rate =48000
BPM = 120
qn_duration = 60.0/BPM
x = np.zeros(int(length / qn_length * qn_duration * rate))
for i in range(12):
note_on = False
start = 0
for j in range(length):
if note_on:
if img[i][j] == 0:
note_on = False
start = start / qn_length
end = j / qn_length
note_length = end - start
note_len_r = int(note_length*qn_duration*rate)
t = np.linspace(0.0, note_length*qn_duration, note_len_r)
start_r = int(start * qn_duration * rate)
x[start_r:start_r+note_len_r] += np.sin(2.0*np.pi*freqs[12-i]*t)
else:
if img[i][j] == 255:
note_on = True
start = j
return IPython.display.Audio(x, rate=rate, autoplay=True)
キラキラ星を鳴らしてみましょう。
data = mml2data("CCGGAAGRFFEEDDCRGGFFEEDRGGFFEEDRCCGGAAGRFFEEDDCR")
IPython.display.display(Image.fromarray(data))
data2audio(data)
以下のように、食わせたピアノロールが表示されつつ、音もなったはずです。
「かえるのうた」の輪唱版も鳴らしてみましょう。
data = mml2data("CDEFEDCREFGAGFERCRCRCRCRCDEFEDCR")
data = mml2data_append(data, "RRRRRRRRCDEFEDCREFGAGFERCRCRCRCR")
data = mml2data_append(data, "RRRRRRRRRRRRRRRRCDEFEDCREFGAGFER")
IPython.display.display(Image.fromarray(data))
data2audio(data)
「かえるのうた」が聞こえてきたでしょうか?
まとめ
Pythonで音を鳴らしてみました。基本的には音に対応する一次元のNumPy配列を作ってIPython.display.Audio
につっこむだけです。ローカルのJupyter NotebookやGoogle Colabで音がなるのでちょっと楽しいかもしれません。応用例として、簡易MMLを鳴らしたり、ピアノロールっぽいものを作って音に変換したりしてみました。生音を作るシーンはほとんど無いと思いますが、もし何かデータを音に変換したくなったりしたときに、この記事が参考になれば幸いです。
Discussion
初めまして。記事を読ませていただきました。書いてくださってありがとうございます。
突然ですが、質問させてください。
#の音を出す時、エラーになってしまいます。。どのように処理したら良いでしょうか?
どうやら、2文字以上が来るとplayするときにエラーになってしまうようなんです。88音定義したくてA1、C5などとやったらならなくて気がつきました。ロボ太さんのコードで、きらきら星のドを#に変換しただけでも、エラーとなってしまいました。
Pythonを始めて1ヶ月(しかも専門は森林)のため、よくわからないことが多いのですが、どなたか教えてくださると大変助かります。
これは
play_mml
が、一文字ずつ処理することを前提に書いてあるからです。この関数をと、カンマ区切りの文字列を処理するように修正し、例えば
を実行すると、きらきら星の最初は普通に、次は半音上げて音を鳴らすことができます。
ただし、このコードはオクターブの指定や、四分音符以外の指定、スタッカートなど、MMLの他の機能は全く入れていませんので、それらを使いたい場合はご自身で
play_mml
を修正する必要があります。ロボ太さん、丁寧にお答えいただき、本当にありがとうございます。
そして、ここで大変申し訳ないのですが、お時間がありましたら、下のコードでなぜ音が出ないのか教えていただきたいです。
#音高
import numpy as np
import IPython
freqs = [0] + [440.0 * 2.0**((i-9)/12.0) for i in range(-39,49,1)]
notes = ["R", "A0","A#0","B0", "C1","C#1","D1","D#1","E1","F1","F#1","G1","G#1","A1","A#1","B1"
, "C2","C#2","D2","D#2","E2","F2","F#2","G2","G#2","A2","A#2","B2"
, "C3","C#3","D3","D#3","E3","F3","F#3","G3","G#3","A3","A#3","B3"
, "C4","C#4","D4","D#4","E4","F4","F#4","G4","G#4","A4","A#4","B4"
, "C5","C#5","D5","D#5","E5","F5","F#5","G5","G#5","A5","A#5","B5"
, "C6","C#6","D6","D#6","E6","F6","F#6","G6","G#6","A6","A#6","B6"
, "C7","C#7","D7","D#7","E7","F7","F#7","G7","G#7","A7","A#7","B7"
,"C8"]
dic = {}
for i, s in enumerate(notes):
dic[s] = i
def play_mml(mml):
rate = 48000
BPM = 120
qn_duration = 60.0/BPM
t = np.linspace(0.0, qn_duration, int(rateqn_duration))
music = np.array([])
notes = mml.split(',')
for s in notes:
f = freqs[dic[s]]
music = np.append(music, np.sin(2.0np.pift))
return IPython.display.Audio(music, rate=rate, autoplay=True)
mml_ = "E4,E5,D5,C5,B4,A4"
play_mml(mml_)
このようなところで勝手をしてしまい、大変恐縮ですが、よろしくお願い致します。。
上記を試したところ、問題なく実行できています。インデントなど、別のエラーが原因ではないでしょうか?
ご迷惑をおかけいたしました。大変感謝いたします。
ありがとうございました!