Python:ビブラートとコーラス:1

今回はFM変調を用いたビブラートとコーラスを実装していきたいと思います。

原理

遅延回路(アナログで言うところのBBD)のクロックをLFO(変調波形)で変えることにより周波数方向に揺れた音を作るエフェクトです。

基本式は以下のようになります。

  •  y(n) = Asin(2 \pi f_{c} \frac{n}{f_{s}} + Isin(2\pi f_{m}\frac{n}{f_{s}}))
  • A:原音信号振幅
  • fc:原音信号周波数
  • fs:サンプリング
  • fm:変調周波数
  • I:変調深度(エフェクターによくあるdepthつまみ)

sinの中にsinがあって.....うーんって感じがしますが、そのまま書いてグラフ出すと原理は理解しやすい
f:id:gsmcustomeffects:20180815160538p:plain

画像は上から原信号、変調波系、被変調波となっています。
時間によって周波数が変化している。
一方でトレモロの時はAM変調で単なる乗算だったので
\begin{align}
y = sin a\cdot sin b
\end{align}

な基本式が信号処理っぽく詳しく書くと
\begin{align}
y(n) = Asin(2 \pi f_{m}\frac{n}{f_{s}}) \cdot sin(2 \pi f_{c}\frac{n}{f_{s}})
\end{align}

こうなってx[n]を入力サンプルとすると

\begin{align}
x[n]\cdot Asin(2 \pi f_{m}\frac{n}{f_{s}})
\end{align}

に書き直せば一般的な問題として考えることが可能だった。

というわけで?

こいつらをどうやって実装していくかって話。
デジタル信号処理では遅延処理はメモリ(バッファ)を使う事で実現できる。
ということは入力x[n]をメモリM[n]に入れてnを変調してやればどうにかなりそう?

\begin{align}
y[n] = x[M[n]]
\end{align}

このように入れ子の構造にしてnを変調する

図で書いてみるとこんな感じになった。
f:id:gsmcustomeffects:20180815215038p:plain

次に変調ありとなしの場合のデータの流れを見ていく

f:id:gsmcustomeffects:20180815220144p:plain

変調なしの場合は順番にデータが取り出されるのでインデックス番号は一定になるが変調ありの場合はインデックスを変調したためインデックス番号が小数のところが現れる(というか整数になるとこが稀)。
まあC言語をやっている人はわかると思うが配列のインデックス番号は整数でないといけない

float buffer[10];

a = buffer[1]    // OK
b = buffer[1.1]//NG

というわけでキャストとかを使うと変調後のnは0,01,1,2,3.....のようになり複数回同じデータをyに出力することになる。
入力周波数が低い場合はいいが周波数が高いとサンプル点が少なくなるのでなめらかな波形でなくなる。
別にこの状態でもコーラスエフェクトとして使えるが一般にはこの現象を防ぐために補間を使って間のデータを補う手法が用いられる。*1

というわけで実装していきましょうか
まずは入力サンプルを作っていくので正弦波合成からですね。いきなりwaveファイルを通して実験してもいいのですがグラフだした時に確認しにくいのでこの方式にします。
正弦波の作り方はここに書いてますのでそれを参考にしてください

input = cf.create_sin(1.0,440,frame,sampling=48000)

まあこんな感じで良いでしょう
次はインデックスの変調を行う部分を作ります。

def index_mod(frame):
    n = np.zeros(frame, dtype=np.float)
    i_num = 0
    for i in range(0,frame):
        n[i] = i + depth_ms * (np.sin(i_num*(2.0 *np.pi * freq/fs)))
        i_num += 1
    return n

ここは別にどう書いてもいいですけど変調後のインデックスを返す関数を一個作っておきました。

あとはこういうふうにつかえばOK

n = index_mod(frame)#インデックスの変調
output = np.zeros(frame,dtype=np.float)#空の配列作成
for i in range(0,frame):
    output[i] = mix*input[int(n[i])] + (1-mix)*input[i] #原音と変調音を足している

サンプルサウンド

原音

エフェクト音(コーラス)

エフェクト音(えぐいビブラート)

実際のwavファイルのをつかった例

結構試行錯誤的な部分があるのでくそコードなのは勘弁していただきたい。
補間については別の記事でやりたいので今回は無補間で処理してます
無処理な割にはそこそこきれいな音になっているはず

import numpy as np
import matplotlib as pl
pl.use('TKagg')
import matplotlib.pyplot as plt
import wave
import audio_func as af
import struct
import scipy

def index_mod(frame):
    n = np.zeros(frame, dtype=np.float)
    i_num = 0
    for i in range(0,frame):
        n[i] = i + depth_ms * (np.sin(i_num*(2.0 *np.pi * freq/fs)))
        i_num += 1
    return n

wf = wave.open("GS05.wav", "r")#wav 扱うならお決まりのやつ
num_data = scipy.fromstring(wf.readframes(wf.getnframes()),dtype = "int16") / 32768.0#正規化
fs = wf.getframerate()
frame = wf.getnframes()

freq        = 9     #変調周波数
depth       = 6     #変調深度
depth_ms    = int(fs * depth /1000)
mix = 0.99          #ミックス(コーラスで言うエフェクト量高いほどヴィブラートになる)

if(wf.getnchannels() == 2):
    left = num_data[::2]#1 スライス
    right= num_data[1::2]
    in_data = left*0.5 + right*0.5#2ステレオモノラル化
    #1:スライスの説明
    #a[1,2,3,4,5]ていうリストがあったとして
    #a[::2]  -> 1,3,5
    #a[1::2] -> 2,4

    #2:ステレオ->モノラル化
    #もともとギターの録音で左右に同じ音ふってるのでLR分解して半分にして足せば同じようなもん


input = in_data

n = index_mod(frame)#インデックスの変調
output = np.zeros(frame,dtype=np.float)#空の配列作成


for i in range(0,frame-9000):#変調をあげすぎるとインデックスオーバーするから適宜調整する
    output[i] = mix*input[int(n[i])] + (1-mix)*input[i]
plt.subplot(211)
plt.plot(input[0:frame])
plt.subplot(212)
plt.plot(output[0:frame])
plt.show()

#正規化したデータを元に戻す
output = [int(x * 32768.0) for x in output]
output = struct.pack("h" * len(output), *output)
#af.play(output,wf.getsampwidth(),1,wf.getframerate()) #再生
af.save(output, fs,1,"custom_vib3.wav")

*1:音遊び!Blackfin DSP基板でディジタル信号処理初体験を参照