🤖

Pythonで音を鳴らす

2022/01/28に公開
5

はじめに

なにか音を鳴らすプログラムを組みたくなる時があります。以下ではJupyter Notebook (Google Colab)上で音を鳴らすサンプルです。

ソースコードは以下においてあります。

kaityo256/python_play_sound

Google Colabで開いてそのまま試すこともできます。

Open In 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のサインカーブ\sin(2 \pi f t)を書き込んだ配列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等分するので、隣の音の周波数は2^{1/12}倍になります。

上記で言えば、Aの音を基準の440Hzとして、一つ下の音(G#)は415.3Hz、一つ上の音(A#)は466.2Hzになります。平均律では隣合う音は2^{1/12}倍なので、Cの半音上げたおとC#(ドのシャープ)と、Dの半音下げた音Db(レのフラット)は全く同じ音になります。これを異名同音と言います。ちなみに、ピタゴラス音律では異名同音の周波数が微妙に変わります。

この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を鳴らしたり、ピアノロールっぽいものを作って音に変換したりしてみました。生音を作るシーンはほとんど無いと思いますが、もし何かデータを音に変換したくなったりしたときに、この記事が参考になれば幸いです。

GitHubで編集を提案

Discussion

ピヨンツクピヨンツク

初めまして。記事を読ませていただきました。書いてくださってありがとうございます。

突然ですが、質問させてください。
#の音を出す時、エラーになってしまいます。。どのように処理したら良いでしょうか?

どうやら、2文字以上が来るとplayするときにエラーになってしまうようなんです。88音定義したくてA1、C5などとやったらならなくて気がつきました。ロボ太さんのコードで、きらきら星のドを#に変換しただけでも、エラーとなってしまいました。

Pythonを始めて1ヶ月(しかも専門は森林)のため、よくわからないことが多いのですが、どなたか教えてくださると大変助かります。

ロボ太ロボ太

これは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([])
    notes = mml.split(',')
    for s in notes:
        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 = "C,C,G,G,A,A,G,R,C#,C#,G#,G#,A#,A#,G#,R"
play_mml(mml_twinkle_star)

を実行すると、きらきら星の最初は普通に、次は半音上げて音を鳴らすことができます。

ただし、このコードはオクターブの指定や、四分音符以外の指定、スタッカートなど、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.0
np.pift))
return IPython.display.Audio(music, rate=rate, autoplay=True)

mml_ = "E4,E5,D5,C5,B4,A4"
play_mml(mml_)


このようなところで勝手をしてしまい、大変恐縮ですが、よろしくお願い致します。。

ピヨンツクピヨンツク

ご迷惑をおかけいたしました。大変感謝いたします。
ありがとうございました!