人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

ディレイとリバーブ

Pythonで音声信号処理(2011/05/14)

今回は、サウンドエフェクトのアルゴリズムであるディレイリバーブを試してみます。私は、PCで音楽を聴くとき、いつもコンサートホールのサウンドエフェクトをかけて聴いていますがそれと同じ効果を自分で実装してみたいと思います。サウンドエフェクトをかけながら音楽を聴くと原曲とはちょっと違った感じで楽しいです。

ディレイ

まず、ディレイですが、これは現在の時刻の音に過去の音を重ね合わせるサウンドエフェクトです。やまびこみたいな効果ですね。ヤッホー、ヤッホー、ヤッホー(だんだん音は小さくなる)みたいなイメージ。実際に聴いてもらった方がイメージがつかみやすいのでサンプル音声を作ってみました。

こんな感じで最初の音声が遅れて(ディレイ)何度か再生されます。声の主は某動画サイトで大人気のゆっくりさんです。棒読みちゃんという音声合成ソフトで作って見ました。

ディレイを式で表すと

s_1(n) = s_0(n) + a s_0(n-d) + a^2 s_0(n-2d) + \cdots + a^i s_0(n-id)

ここで、s_0(n)は、オリジナルの音声のn番目のサンプルを意味し、s_1(n)は、ディレイがかかった音声のn番目のサンプルを意味します。ディレイがかかった音声のn番目のサンプルを計算するためにオリジナルのs_0(n)に加えて、dサンプル前のs_0(n-d)をa倍した値、2dサンプル前のs_0(n-2d)をa^2倍した値といったように過去の音声を重ね合わせます。iはこだまする回数です。dは何サンプルこだまを遅らせるかを表す遅延時間(単位はサンプル)です。後で試して見ますが、dを小さくしてオリジナルの音と重ねるとリバーブになります。aは減衰率です。1.0より小さいと遅れた音声が徐々に小さくなっていきます。これらのパラメータの関係を図で表すと下のようになります。

f:id:aidiary:20110619195222p:plain

では、Pythonで実装してみます。

#coding:utf-8
import wave
import pyaudio
import struct
from pylab import *

def printWaveInfo(wf):
    """WAVEファイルの情報を取得"""
    print "チャンネル数:", wf.getnchannels()
    print "サンプル幅:", wf.getsampwidth()
    print "サンプリング周波数:", wf.getframerate()
    print "フレーム数:", wf.getnframes()
    print "パラメータ:", wf.getparams()
    print "長さ(秒):", float(wf.getnframes()) / wf.getframerate()

def play (data, fs, bit):
    import pyaudio
    # ストリームを開く
    p = pyaudio.PyAudio()
    stream = p.open(format=pyaudio.paInt16,
                    channels=1,
                    rate=int(fs),
                    output= True)
    # チャンク単位でストリームに出力し音声を再生
    chunk = 1024
    sp = 0  # 再生位置ポインタ
    buffer = data[sp:sp+chunk]
    while buffer != '':
        stream.write(buffer)
        sp = sp + chunk
        buffer = data[sp:sp+chunk]
    stream.close()
    p.terminate()

def save(data, fs, bit, filename):
    """波形データをWAVEファイルへ出力"""
    wf = wave.open(filename, "w")
    wf.setnchannels(1)
    wf.setsampwidth(bit / 8)
    wf.setframerate(fs)
    wf.writeframes(data)
    wf.close()

if __name__ == "__main__":
    wf = wave.open("data/whistle.wav", "r")
    printWaveInfo(wf)

    fs = wf.getframerate()      # サンプリング周波数
    length = wf.getnframes()    # 総フレーム数
    data = wf.readframes(length)

    data = frombuffer(data, dtype="int16") / 32768.0  # -1 - +1に正規化
    a = 0.6      # 減衰率
    d = 20000    # 遅延時間(単位:サンプル)
    repeat = 6   # リピート回数

    newdata = [0.0] * length
    for n in range(length):
        newdata[n] = data[n]
        # 元のデータに残響を加える
        for i in range(1, repeat + 1):
            m = int(n - i * d)
            if m >= 0:
                newdata[n] += (a ** i) * data[m]  # i*dだけ前のデータを減衰させた振幅を加える

    # 波形を描画
#    subplot(211)
#    plot(data)
#    axis([0, 140000, -1.0, 1.0])
#    subplot(212)
#    plot(newdata)
#    axis([0, 140000, -1.0, 1.0])
#    show()

    # -32768 - +32767へバイナリ化してから再生
    newdata = [int(x * 32767.0) for x in newdata]
    newdata = struct.pack("h" * len(newdata), *newdata)
    play(newdata, fs, 16)
    save(newdata, fs, 16, "whistle_delayed.wav")

使用したオリジナルの音声ファイルは、

です。笛の音の後に3秒間無音を挿入しています(ディレイがかかった音声はオリジナルと同じサイズにしているため)。実行すると笛のあとに6回こだますることがわかります。ディレイをかけた音声ファイルも一応はっときます。

リバーブ

ディレイのパラメータでdが大きいとオリジナルの音とこだまが別々に聞こえますが、dを小さくしていくとオリジナルの音声とこだまが重なり合ってリバーブになります。リバーブはReverberationの略で残響という意味です。お風呂で歌ったような感じになり、下手な歌がうまく聞こえます(笑)リバーブのプログラムです。

#coding:utf-8
import wave
import pyaudio
import struct
from pylab import *

def printWaveInfo(wf):
    """WAVEファイルの情報を取得"""
    print "チャンネル数:", wf.getnchannels()
    print "サンプル幅:", wf.getsampwidth()
    print "サンプリング周波数:", wf.getframerate()
    print "フレーム数:", wf.getnframes()
    print "パラメータ:", wf.getparams()
    print "長さ(秒):", float(wf.getnframes()) / wf.getframerate()

def play (data, fs, bit):
    import pyaudio
    # ストリームを開く
    p = pyaudio.PyAudio()
    stream = p.open(format=pyaudio.paInt16,
                    channels=1,
                    rate=int(fs),
                    output= True)
    # チャンク単位でストリームに出力し音声を再生
    chunk = 1024
    sp = 0  # 再生位置ポインタ
    buffer = data[sp:sp+chunk]
    while buffer != '':
        stream.write(buffer)
        sp = sp + chunk
        buffer = data[sp:sp+chunk]
    stream.close()
    p.terminate()

def save(data, fs, bit, filename):
    """波形データをWAVEファイルへ出力"""
    wf = wave.open(filename, "w")
    wf.setnchannels(1)
    wf.setsampwidth(bit / 8)
    wf.setframerate(fs)
    wf.writeframes(data)
    wf.close()

if __name__ == "__main__":
    wf = wave.open("data/yukkuri.wav", "r")
    printWaveInfo(wf)

    fs = wf.getframerate()      # サンプリング周波数
    length = wf.getnframes()    # 総フレーム数
    data = wf.readframes(length)
    play(data, fs, 16)  # デフォルトの音声を再生

    data = frombuffer(data, dtype="int16") / 32768.0  # -1 - +1に正規化
    a = 0.7      # 減衰率
    repeat = 3   # リピート回数

    alldata = ''
    for d in range(1000, 3000, 500):  # 遅延時間d(sample) を変えながら音を鳴らす
        newdata = [0.0] * length
        for n in range(length):
            newdata[n] = data[n]
            # 元のデータに残響を加える
            for i in range(1, repeat + 1):
                m = int(n - i * d)
                if m >= 0:
                    newdata[n] += (a ** i) * data[m]  # i*dだけ前のデータを減衰させた振幅を加える

        # -32768 - +32767へバイナリ化してから再生
        newdata = [int(x * 32767.0) for x in newdata]
        newdata = struct.pack("h" * len(newdata), *newdata)
        play(newdata, fs, 16)
        alldata += newdata  # ファイル保存用

    # 音声をWAVEファイルに保存
    save(alldata, fs, 16, 'yukkuri_reverb.wav')

オリジナルの音声は、ゆっくりさんです。

遅延時間を500サンプルずつ長くしてどのようにリバーブの効果が現れるか聴けるようにしています。リバーブをかけた音もはっときます。

面白いのでぜひ聴いてみてください。音楽にもリバーブをかけてみましたが、けっこう処理が重いです。実際は、サウンドカードでハードウェア的に処理しているのかな?

参考文献

C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理

C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理