Essaytial Machine Learning

機械学習は随想だ

サウンドプログラミング 雑音編

 Python でノイズを生成しよう。

概要

 小休止を飛ばすと前回記事の続き。波形、周波数による決定的音生成でカバーできない確率的な雑音の生成方法について述べる。

雑音

 一般に雑音、ノイズというと不要な除去すべき情報といったニュアンスを含むが、ここでは乱数を軸に生成する音の高さが定義できない音を指す。サウンドプログラミングにおいて雑音はパーカッション代わりに使う音として重宝する。

 ただし完全に乱数頼みだと音色を変えにくいので、乱数で生成した原音を加工して周波数特性にパラメトリックな特徴を持つ雑音を生成する方針を取る。原音の乱数生成器には色々な分布に従うものを使っても面白いかもしれないが、ここではそこまで拘らず基本的な一様分布または正規分布に従う乱数生成器を使う。

カラーノイズによる雑音の表現

 周波数  f に対してパワースペクトル密度が  f^{-\alpha} \alpha \in [-2, 2])に大体比例して変化する雑音をカラーノイズと呼ぶ。大体の比例になるのは乱数+離散化によって周波数特性にもブレが生じるためである。

 Python では乱数による原音に対してフーリエ変換で周波数を取り出し、 f^{-\alpha} で傾向付けてフーリエ逆変換で雑音を復元するのが速い。生演奏等でリアルタイムにカラーノイズを生成するにはここ等で解説されている時系列モデルを使った逐次手法を利用するが Python (+NumPy+SciPy)では分が悪い。

  \alpha=2 で生成される雑音(ブラウンノイズと呼ばれる)はランダムウォークと同じ構造を持ち、 \alpha=2 に近い値を取るほど音量が安定しなくなるため低周波数帯をカットする処理を加える*1

import numpy as np
from scipy.fft import rfft, irfft, rfftfreq

# カラーノイズを生成する
# thr でカットする最高周波数を指定
# mode で原音の乱数生成器を指定
def makecnoise(alpha, T, rate, thr=1, mode='normal'):
    n = int(T*rate)
    if mode=='normal':
        x = np.random.normal(size=n)
    if mode=='uniform':
        x = np.random.uniform(-1, 1, size=n)
    
    # 原音 x のフーリエ変換 y とその周波数配列 f
    y = rfft(x)
    f = rfftfreq(n, 1/rate)
    # f の thr 以下の低周波数帯を thr に切り上げて y を加工する
    f[f<thr] = thr    
    y = np.exp(np.log(y)-0.5*alpha*np.log(f))
    # 逆変換して正規化して返す
    x = irfft(y)
    return x/np.max(abs(x))

rate = 44100
T = 1
alphas = [2, 1, 0, -1, -2]
wave1 = np.hstack([makecnoise(alpha, T, rate) for alpha in alphas])

 Jupyter Notebook で再生するには前回と同じく次を実行。

from IPython.display import Audio
Audio(wave1, rate=rate)

サンプリングレートによる雑音の表現

 みんな大好きファミコン音源ではサンプリングレートを変えることで雑音の音色を変えているらしい。原音の生成にも専用のアルゴリズムを使っているようだがそちらには踏み込まない*2

# サンプリングレートによるノイズを生成する
# mode で原音の乱数生成器を指定
def makernoise(arate, T, rate, mode='uniform'):
    # 目標のサンプリングレートで乱数を生成
    na = int(T*arate)
    if mode=='normal':
        _x = np.random.normal(size=na)
    if mode=='uniform':
        _x = np.random.uniform(-1, 1, size=na)
    
    # 本来のサンプリングレートに従う配列に上で生成した乱数を格納
    n = int(T*rate)
    # na を n 分割して格納元を指定する配列 ii を生成
    ii = np.linspace(0, na, n, False).astype(int)
    x = _x[ii]
    # 正規化して返す
    return x/np.max(abs(x))

rate = 44100
T = 1
mods = [64, 32, 16, 4, 1]
wave2 = np.hstack([makernoise(rate/mod, T, rate) for mod in mods])

*1:低周波数帯をカットすることで音量が安定する証明はしていない。たぶん低周波数帯がランダムウォークの局所的な平均の変動を表現しているからだと思う。

*2:こっそりデフォルト mode を uniform にしているのはこの辺に配慮してのこと。