【python】モジュレーション系エフェクト・ビブラート【サウンドプログラミング】
こんにちは.1000chです. これまで空間系エフェクト(リバーブ),ダイナミクス系エフェクト(ディストーション)と作ってきました. さて今回は,モジュレーション系のエフェクトを作ってみたいと思います.
モジュレーション系エフェクト
モジュレーション系エフェクトは,その名の通り信号を変調(モジュレーション)するエフェクトです. 低周波の変調用信号を利用して,音声信号に様々なエフェクトをかけていきます(いわゆるLFO:Low Frequency Oscillator). 一言で言えば,振幅やら周波数やらを時間変化させるものです.
変調方式には様々なものが知られていますが,今回はAM / FM変調を利用したエフェクトを作ってみます.
振幅変調:トレモロ
まずはシンプルに,信号の振幅を時間変化させてみましょう. いわゆるRM変調やAM変調というやつですね. 感覚としては,音量が周期的に変化する感じです.
最もシンプルに考えると次式のようになります.
これがRM変調(Ring Modulation)の定義式になります. 書き忘れてますが,x(n)が入力値,y(n)が出力値です. A(n)が定数であればただの乗算器になりますが,それがsin関数により時間変化していますね. 通常fmは音響信号の周波数よりも小さいものを用いることは注意しておいた方が良いかもしれません.
しかしRMだと振幅が0や負値になったりしてしまいます. そこでRMをちょこっと変化させたAM変調が登場します. 定義は次式.
RMに比べるとちょっと複雑に見えますね. ただ,やってることはシンプルです. 振幅をAを中心に+- A*d分時間変化させているだけです.
RMとAMのイメージをグラフ化するとこんな感じ.
イメージは掴んでいただけたでしょうか.
本題に戻ると,このAM変調を用いたエフェクトがトレモロになります. トレモロエフェクターのクラスを作ってみます.
class Tremolo(): ''' member depth : 変調深度 freq : 変調周波数[hz] rate : サンプリングレート[hz] n : 現在フレーム ''' def __init__(self,depth=0.2, freq=2, rate=44100): self.depth = depth self.freq = freq self.rate = rate self.n = 0 def process(self,data): vfunc = np.vectorize(self.effect) return vfunc(data) def effect(self,d): d = d * (1.0 + self.depth * np.sin(self.n * ( 2 * np.pi * self.freq / self.rate))) self.n += 1 return d
使うときはこんな感じで
# in_dataにnumpy.array型の信号を格納済み tremolo = Tremolo() out_data = tremolo.process(in_data) # 後は適宜pyaudioのstreamクラスにwrite
ターゲットとして,シンプルなsin波を利用してエフェクトをかけてみましょう(audacityで生成しました).
入力
出力 (d=0.2 , f_m = 2[Hz], f_s = 44100[Hz] にて実行)
うねりっぽくなってますね.いい感じです.
周波数変調:ビブラート
では今度は,信号の周波数を時間変化させてみましょう.FM変調というやつです.
イメージはこんな感じ.(出典:wikipedia - FM変調)
上段が入力波形,中段が変調に用いる波形,下段が変調された波形です.粗密波っぽいですね.密なところは周波数が高く,粗なところは周波数が低くなっています. このように,周波数を時間で変化させるのがFM変調です.
FM変調を利用したエフェクトがビブラートです. 周波数が変わる=ピッチが変わるということなので,音の高さが揺れるコトになります.
離散信号における逐次処理でFM変調を行う場合は,時間軸をゆがめるコトで実現します.こんな感じ.
インデックスの値をsin波でゆがめるワケですね. なお,リアルタイムに処理をする場合はN(n) > nとなる場合に対応するデータが存在しないことに注意が必要です.
ただし,これを行うとN(n)が整数値を取るとは限りません. というか,整数値になる方が少ないです. その場合,x(n)の正しい値を取ることができません. そのため,線形補完を用いてy(n)を計算する必要が出てきます.
というわけで,以上をコードにしてみたのが以下です.
class Vibrato(ef.Effecter): ''' member depth : 変調具合[frame] freq : 変調周波数[hz] rate : サンプリングレート[hz] n : 現在フレーム[frame] ''' def __init__(self,depth=2, freq=2, rate=44100): self.depth = int(rate * depth / 1000) # input:[ms] self.freq = freq self.rate = rate self.n = 0 def process(self,data): # 時間軸をゆがめる frames = self.calc_frames(np.arange(data.size)) # 対応するシグナルを線形補完する data = self.calc_signal(frames,data) return data # 時間軸をゆがめる. N(n)の計算 # input : 時間軸[ 0, 1, 2, 3,..] # output : 時間軸[ 0, 0.5, 2.5, 3,..] def calc_frames(self,frames): vfunc = np.vectorize(self.calc_frame) return vfunc(frames) #return frames def calc_frame(self,n): # n = n ~ n + 2*depth n = n + self.depth * (1 + np.sin(self.n * (2 * np.pi * self.freq / self.rate))) self.n += 1 return n # N(n)を与え,線形補完してy(n)を計算 def calc_signal(self,frames,data): # frames: N(n)のリスト # framesのlimit番目以降に対し処理を行う limit = self.depth * 2 calc_data = [self.calc_interp(frame,data) for frame in frames[limit:]] calc_data = np.hstack([data[:limit],calc_data]) return np.array(calc_data) def calc_interp(self,frame,data): x = int(np.floor(frame)) d = np.interp(frame,[x,x+1],data[x:x+2]) #indexオーバーする可能性アリ return d
こちらを実行するとこのようになります.
入力(再掲)
出力1(d=2[ms],f_m=2[hz], rate=44100[hz])
出力2 (d = 2[ms], f_m = 15[Hz], rate = 44100[Hz])
出力3 (d = 20[ms], f_m = 2[Hz], rate = 44100[Hz])
音が震えている感じがしますね. パラメータ変えると結構印象が変わるのが面白いです.
まとめ
- モジュレーション系エフェクトつくった
- トレモロ
- ビブラート
- そのうちnumpy * pyaudioの使い方はまとめといた方が良いかも