Essaytial Machine Learning

機械学習は随想だ

サウンドプログラミング

 Python (Jupyter Notebook)で音を出そう。

概要

 DAW ソフトをいくつか試してみると音階と拍で構成される近代音楽制作に特化し過ぎていて、何より GUI の再現困難な操作でカチカチしないといけないのが非常に使い辛い。MIDI キーボードはキーボードで弾ける曲しか再現できないし。なのでテキストベースのプログラミングで曲を作ろう。主目的は波形データの作成で、.wav 等の音声ファイルへの変換は各種ツールに任せることとする。

単音の生成

 まずは単音データを作ろう。単音は  w(t) = w(t+T) を満たす周期関数で表されて、パラメータとして単位波形、周波数、音の長さ、さらにデータの場合はサンプリングレートを持つ*1。単位波形は一周期を表す関数で、 [0, T] で定義して周波数の情報も含めることもあるがここでは切り離して  [0, 1] で定義しておく。周波数は1秒当たりの周期関数の反復回数で、人間には  [20, 20000] ぐらいの範囲で聞こえることになっているが  [100, 1000] ぐらいだと耳の負担にならなくていいと思う。音の長さは音の長さだ。サンプリングレートは1秒当たりのデータ数で、 44100 とか  48000 とかよく使われる値があるので各自調べて必要に応じて選ぶといい。

 以下は単位波形 unit、周波数 f、音の長さ T、サンプリングレート rate から単音データを生成する Python コードだ。NumPy はいいぞ。一旦  [0, 1] を周波数 f で周期的に動く配列を作り(x)、単位波形の関数に渡している。np.linspace 内の endpoint=False は指定しないと約 1/n だけ周波数がずれる。単位波形は後々合成したり調整したり最終的に正規化したりすることを考えると、平均か中央値が  0 になるようにしておくといいと思う。左右の音形を変えてステレオで出力したいならサイズ (2, len(x)) の配列を返すようにしておこう。

import numpy as np

# 単音を生成する
def makewave(unit, f, T, rate):
    n = int(T*rate)
    x = f*np.linspace(0, T, n, endpoint=False)%1
    return unit(x)

# 以下テスト

# いわゆるsin波
def unit_sin(x):
    return np.sin(2*np.pi*x)

# いわゆる矩形波
def unit_square(x):
    return np.sign(x-0.5)

# 単位波形は基本的には1変数1価の関数なら何でもいい
# 一応中央値が0になるようにしておく
def unit_exp(x):
    return np.exp(x)-0.5*(np.exp(0)+np.exp(1))

# ステレオの場合
def unit_sin_stereo(x):
    return np.tile(np.sin(2*np.pi*x), [2, 1])

unit = unit_sin
f = 300
T = 2
rate = 44100
wave1 = makewave(unit, f, T, rate)

音列の作成

 音を時間方向に並べてメロディ等を作る場合は np.hstack 等で配列を結合すればいい。パラメータ辞書をリストで持ってループを回すとそれっぽくなって楽しい。無音は周波数を  0 にすればそのまま makewave に渡せる。

# 単音データのパラメータ辞書のリストを用意
# サンプリングレートは共通なので除外する
rate = 44100
melo_dct = [{'unit': unit_sin, 'f': 300  , 'T': 0.5  }, 
            {'unit': unit_sin, 'f': 450  , 'T': 0.5  }, 
            {'unit': unit_sin, 'f': 337.5, 'T': 0.5  }, 
            {'unit': unit_sin, 'f': 450  , 'T': 0.5  }, 
            {'unit': unit_sin, 'f': 360  , 'T': 0.5  }, 
            {'unit': unit_sin, 'f': 400  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 450  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 400  , 'T': 0.5  }, 
            {'unit': unit_sin, 'f': 500  , 'T': 0.5  }, 
            {'unit': unit_sin, 'f': 600  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 450  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 675  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 720  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 675  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 720  , 'T': 0.125}, 
            {'unit': unit_sin, 'f': 675  , 'T': 0.125}, 
            {'unit': unit_sin, 'f': 600  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 540  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 450  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 540  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 400  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 450  , 'T': 0.25 }, 
            {'unit': unit_sin, 'f': 360  , 'T': 0.5  }, 
            {'unit': unit_sin, 'f':   0  , 'T': 0.5  }]

melo = np.hstack([makewave(**d, rate=rate) for d in melo_dct])

音の合成

 和音を作ったり複数パートを統合したりする場合は時間軸を合わせた配列を足し合わせればいい。これも辞書リストから生成したデータを np.sum に渡すといい感じ。配列サイズを int(T*rate) で決定する関係で、音の長さとしては同じでも配列サイズが異なる場合があるので要調整。音の長さでなく開始時刻と終了時刻で指定すると調整せず合わせられるが今回は割愛する。パート毎の音量バランスは定数をかけて調整しよう。

# 和音
rate = 44100
harm_dct = [{'unit': unit_square, 'f': 300, 'T': 2}, 
            {'unit': unit_square, 'f': 360, 'T': 2}, 
            {'unit': unit_square, 'f': 450, 'T': 2}]

harm = np.sum([makewave(**d, rate=rate) for d in harm_dct], axis=0)

# 複数パートの合成
bass_dct = [{'unit': unit_square, 'f': 120  , 'T': 1.0}, 
            {'unit': unit_square, 'f': 135  , 'T': 1.0}, 
            {'unit': unit_square, 'f': 150  , 'T': 1.0}, 
            {'unit': unit_square, 'f': 112.5, 'T': 1.0}, 
            {'unit': unit_square, 'f': 120  , 'T': 1.0}, 
            {'unit': unit_square, 'f': 135  , 'T': 1.0}, 
            {'unit': unit_square, 'f': 150  , 'T': 1.5}, 
            {'unit': unit_square, 'f':   0  , 'T': 0.5}]

bass = np.hstack([makewave(**d, rate=rate) for d in bass_dct])

# int(T*rate) の誤差で配列サイズがずれる
wave2 = melo+0.1*bass[:-1]

Jupyter Notebook での音声データ埋め込み

 GUI は嫌だと書いたが作成した音声データの聴覚的確認ぐらいは GUI に頼っていいだろう。Jupyter Notebook でそれができる(cf. Jupyter で音声データを埋め込む - Qiita )。.wav 等の音声ファイルに変換する前に np.max(abs(wave2)) で割る等して正規化を施した方がいいと思うが IPython.display.Audio を使うと自動で正規化されるようだ*2。最悪の場合はスピーカーの破損等にも繋がるため、他のツールで音声ファイルを作成するなら正規化について必ず確認しておきたい。

# Jupyter Notebookに音声データを埋め込む
from IPython.display import Audio
Audio(wave2, rate=rate)

 Gist経由でJupyter Notebookを埋め込もうと思ったが音声データ埋め込みが反映されなかったので断念した。上のコードを実行すると下のようにはてなブログに埋め込むのと同じ形でJupyter Notebookの出力セルにプレイヤーが表示される。

*1:ビット深度もパラメータではあるがデータの型の方に関わるパラメータなのでスルーする。

*2:正確には normalize オプションで制御されていて、デフォルトで True になっている。