Morikatron Engineer Blog

モリカトロン開発者ブログ

Pythonと音楽と...(1)音を鳴らす

モリカトロンのチーフエンジニア松原です、こんにちは!
今回から「Pythonと音楽と...」と題して連載します(全部で四回くらいの予定)。第一回は「音を鳴らす」。音のデータ化や録音・再生の仕組みから、Pythonで指定した周波数の音を発音するところまで、となります。Pythonで音を鳴らしたい人、Pythonでメロディを再生したい人、PythonでMIDIを扱ってみたい人などが対象読者です。どうぞよろしくお願いします。

そもそも音ってなんなの?

まずは音をデジタルデータとして扱うにあたっての基本的な知識をざっくり説明します。「もう知ってる勢」「興味ない勢」は読み飛ばしてください。

「音」とは「振動」です。ある「振動」が空気や液体や固体の中を伝わり、人間の鼓膜や骨を経由して蝸牛内の毛を揺らして聴神経が変換した電気インパルスが脳に届いたところで、音が認識される仕組みです。

http://www.njha.co.jp/guidance/mechanism/images/g01_img.gif
聴こえのしくみ | 補聴器 上手な付合い方 | NJHさんのサイトより)

したがって「音データ」とはすなわち「振動をデータ化したもの」と言えます。自然の音を録音する場合、音→マイク→電気信号と変換し、この信号を毎秒4万回みたいな頻度で計測して記録するなどします。こうした方法をパルス符号変調(PCM、英語: pulse code modulation)と呼びます。記録時のパラメータには、サンプリング周波数(秒間何回計測したか=時間の分解能)、ビット数(何ビットで計測するか=音量の分解能)、チャンネル数(何本のマイクで拾ったかと考えるとわかりやすい)などがあります。で、こうしてデジタルデータに変換した「音」はwavファイルというフォーマットで記録するとプログラムで扱いやすいです(もちろんほかにもいろいろな保存方法や圧縮方法があります)。

一方、数式などを使ってプログラムで直接生成した音をどう録音するかというと、PCMと同じフォーマットで保存することで、自然音と同じように再生できます(あたりまえですね)。wavファイルは単純に言うと振動の幅を時系列に記録したシリアルデータですから、プログラムでもしごく簡単に生成できます。たとえばPythonのライブラリにNumpyという超有名な数値演算用のライブラリがあるのですが、そのNumpyを使うとサインカーブを使った波形などしごく簡単に生成できます。またScipyというこれまた超有名な科学技術演算用ライブラリを使うと、この波形データをwavファイルに簡単に保存できます。

音データのデジタル化と、デジタル化された音データの復元(発音)経路をまとめると、次のようになります。 f:id:morika-ma2:20200727115653p:plain

Pythonで音を出すには

さて、音とそのデジタルデータの基本的な扱いがわかったところで、その音を「鳴らす」には具体的にどうすればいいのでしょうか。
自作のプログラムから「音を鳴らす」には、そのプログラムが動作中のハードウェア(PCとかゲーム機とか仮想マシンとか)に接続されたスピーカーを駆動する必要があります。その駆動方法はハードウェアやOSごとにちがう仕組みとなりますので、ふつうに考えると面倒な手続きが必要そうです。たとえばごく簡単なビープ音を鳴らしたいと思って「python ビープ音」でググると、いきなり「Windows用」だとか「Windows/Mac対応」みたいな記事がいろいろとヒットします。ビープ音鳴らしたいだけなのにOSごとに分岐を作って個別の処理をしなくちゃいけないなんて……ねえ。

でも安心してください。OSやハードウェアの違いを吸収し、別OSでも同一スクリプトで同じ音を再生してくれるすぐれたライブラリが、実はいくつもあるのです。たとえば……

などなど。これらのライブラリを使うことで、誰でも簡単に音を鳴らすことが出来ます(ありがたい)。この記事では、pygameとPyAudioを使ってみましょう。

サイン波を作って鳴らしてみる

まずは、単純な音声波形(指定した周波数で振動するサイン波)を生成し、それをwavファイルに保存し、pygameで鳴らす、というサンプルプログラムを紹介します。なお、この記事で紹介するPythonスクリプトはPython 3.7で動作します。ライブラリとしてNumpy、Scipy、pygame、PyAudioを使いますので、たとえばanacondaをお使いの環境でしたら次のようなコマンドでインストールしてやってください(anacondaを使っていない場合はpipでもすべてインストールできます)。

$ conda install numpy
$ conda install scipy
$ conda install pyaudio
$ pip install pygame
sample1_play_file.py

ドの音程で1秒の長さのwavファイルを保存し、再生するサンプルコードです。
https://github.com/morikatron/snippet/blob/master/python_and_music/sample1_play_file.py

import time
import numpy as np            # install : conda install numpy
from scipy.io import wavfile  # install : conda install scipy
from pygame import mixer      # pip install pygame

# パラメータ
FREQ = 261.626          # 生成するサイン波の周波数(note#60 C4 ド)
SAMPLE_RATE = 44100     # サンプリングレート
# 16bitのwavファイルを作成
wavfile.write("do.wav", SAMPLE_RATE,
              (np.sin(np.arange(SAMPLE_RATE) * FREQ * np.pi * 2 / SAMPLE_RATE) * 32767.0).astype(np.int16))

# wavファイルをロードして再生
mixer.init()  # mixerを初期化
mixer.music.load("do.wav")  # wavをロード
mixer.music.play(1)  # wavを1回再生

# 1秒(音がおわるまで)待つ
time.sleep(1)

コメントでnote#60とあるのはMIDI(演奏データのデジタル規格 参考:https://ja.wikipedia.org/wiki/MIDI)のノート番号です。C4のCは音名、4はオクターブです。たとえば一口に「ド」といっても1オクターブ上にも下にも理論上無限に「ドの音」が存在するので周波数が決定できませんが、note#60とかC4という言い方をすると、その音の周波数は261.626Hzとわかりますというか決まっています(ノート番号と音名と周波数の関係を詳しく知りたい人は音階の周波数を計算する方法とMIDIノート番号の関係をどうぞ。一覧表も掲載されています)。

さて、このサンプルでは、Numpyのnp.sin関数で261.626ヘルツのサイン波を生成し、16bit、44.1kHz、モノラル(1チャンネル)のwavファイルとして保存しています。それがぜんぶwavfile.writeのところのたった1行の仕事です。すごいですね。これスクラッチから書き起こすと大変な行数が必要ですが、優秀なライブラリさんたちのおかげでたった1行で済んでしまいます。感涙。

ちなみにこのとき生成されているサイン波がどのようなものかを見るため、波形を作る部分のコードを取り出してGoogle Colaboratoryで実行してみると、このようなグラフが表示できます。このサイン波の波の間隔が短くなると高音に、間隔が長くなると低音になるわけですね。 f:id:morika-ma2:20200722141839p:plain

こうして保存されたwavファイル"do.wav”を、pygameのmixerにロード&再生を指示すると、音を鳴らすことができます。

pygameはPythonでゲームを作るときに便利なユティリティ群。このmixerも、ゲームでBGMや効果音を出したいときにとても使いやすい関数が用意されています。たとえばパンチ音、キック音、衝撃音、うめき声といったwavファイルをあらかじめ用意し、ゲーム開始時にロードしておいて、あとはゲーム中タイミングよくplay()を呼び出せば音が鳴る、みたいな使い方ができます。上記サンプルで使っているmixier.music.load()とmixier.music.play()はBGMを鳴らすとき用の関数ですが、これもたいへん使いやすいです。

メモリ上の音を直接鳴らす

しかしながらたとえばシンプルに「ドの音を出したい」とき、上記サンプル(sample1_play_file.py)みたいにいちいちwavを保存して、loadして、playって……というのはなんか違いますよね。メモリ上に作成した波形を、そのままダイレクトに鳴らしたい。また、ドだけじゃなくミもソも出したい、みたいなときに、もっと音が扱いやすいライブラリPyAudioというのがありますので、これを使ってみましょう。

sample2_play_buf.py

numpyでメモリ上に作ったサイン波を、PyAudioのstreamで順次再生するコードです。これでドミソドーっと鳴ります。
https://github.com/morikatron/snippet/blob/master/python_and_music/sample2_play_buf.py

import numpy as np  # install : conda install numpy
import pyaudio      # install : conda install pyaudio

# サンプリングレートを定義
SAMPLE_RATE = 44100


# 指定ストリームで、指定周波数のサイン波を、指定秒数再生する関数
def play(s: pyaudio.Stream, freq: float, duration: float):
    # 指定周波数のサイン波を指定秒数分生成
    samples = np.sin(np.arange(int(duration * SAMPLE_RATE)) * freq * np.pi * 2 / SAMPLE_RATE)
    # ストリームに渡して再生
    s.write(samples.astype(np.float32).tostring())


# PyAudio開始
p = pyaudio.PyAudio()

# ストリームを開く
stream = p.open(format=pyaudio.paFloat32,
                channels=1,
                rate=SAMPLE_RATE,
                frames_per_buffer=1024,
                output=True)

# ドミソドーを再生
play(stream, 261.626, 0.3)  # note#60 C4 ド
play(stream, 329.628, 0.3)  # note#64 E4 ミ
play(stream, 391.995, 0.3)  # note#67 G4 ソ
play(stream, 523.251, 0.6)  # note#72 C5 ド

# ストリームを閉じる
stream.close()

# PyAudio終了
p.terminate()

PyAudioを使うには、ストリームをopenし、writeし、closeするだけです。wavファイルに保存する必要もなく、メモリ上の音データを直接ストリームにwriteすれば音が鳴ります。

このサンプルでは、複数の音を連続して生成&再生させたかったので、一つの音を生成&再生するための関数 play() を定義しています。play()にstream、音の周波数、音の長さ(秒数)を渡すと、np.sinで指定周波数のサイン波を指定秒数分生成し、streamにwriteします。

以上のようなシンプルなプログラムで、ドミソドーという音を鳴らすことができました。楽しいですね。

今後の予定

今回作ったサンプルコードをさらに改造して、トラック単位に波形を作って再生したり、複数のトラックの波形を合成してマルチトラック再生ができるようにして行きます。第3回目ではSMF(Standard MIDI File)を読み込んで解析&再生、第4回目では読み込んだSMFに変換をかけて、あのキューブ型ロボットで再生するところまでやりたいと思っています。お楽しみに!

参考にさせていただいたサイト(ありがとうございます)

ゼロからはじめるPython(55) Pythonで音楽 - PyAudioとNumPyで楽器を作ろう | マイナビニュース

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

mixer - Pygameドキュメント 日本語訳

PyAudio Documentation — PyAudio 0.2.11 documentation

音階の周波数を計算する方法とMIDIノート番号の関係