PythonのcupyモジュールでGPUを利用して、リアルタイムfirフィルタを実装 | グラ山ギターのブログ

グラ山ギターのブログ

毎週健康のために山登りをしています。低い山ばかりですが播磨の山々は岩山が多く景観がすばらしい所が多いです。山頂などで演奏しています。 井上陽水のカバーが多いです。
暑い時期は山登りは控えています。

PythonのcupyモジュールでGPUを利用して、リアルタイムのfirフィルタ(float64bit、25601タップ)96ksps24bitステレオのリアルタイム再生を実現

 Win10のPCで、特殊フィルタをかけた音声ファイルを再生する際には、今まではwavファイルなどの音声データを読み込み、プログラムでFIRフィルタをかけて音声ファイルを作成し、その音声ファイルをGrooveミュージックなどのアプリで再生するという手順を取っていた。
 この手続きでは、変換した音声ファイルを沢山作成して、メモリに置いておく必要があり、種々のオーディオ機器にあった音を再生しようとすれば、1曲に対してオーディオ機器の数分だけファイルにしなければならず、USBスピーカーやBluetoothスピーカー、カーステレオ、卓上スピーカーなど使用する機器が多いと保存しておく音声ファイルは莫大な数になってくる。
また、このファイルは改変したファイルで個人的に楽しむのはいいが、ビジネスには著作権の問題が発生する。
 このような問題を解決するため、pythonのcupyを使ってPCのGPUを利用して、DSPのようなリアルタイムの音声処理ができたので、公開します。
 今回はwavファイル、~96kbps2ch24bitまでフィルタタップ数25,601(LchとRchは同じフィルタを適用)で実現しました。

PCのスペックは下記の通り
 i7-9750H 2.6GHz
 Win10 Home 64bit 21H1
 NVIDIA GeForce MX250

Pythonプログラムは以下の通りで、25601の中央の1tapのみ1.0で他は0.0の例(スルー)で記述しています。試してみたいフィルタはfilrfsとして1次元のnumpy.arrayのデータで設定してください。演算途中でオーバーフローする場合(24bitの値が±2**31を超える場合)は関数dfil(sdata,filrfs,chunk,1.2)の最後のパラメータ1.2を大きくしてください。

 

演算の内部処理はFloat64になっているので、高精度なFIRフィルターの効果を試すことができます。

                FIRフィルタの例

 

-------------------python プログラム開始----------------

#外部モジュール
import pyaudio
import numpy as np
import struct
import wave
import cupy as cp

#sdata(2,**) 音声データ np.array
#filrfs(**)  1次元インパルスレスポンス 
#filrfs=np.zeros(len(filrfs))
#filrfs[len(filrfs)//2]=1.0とすると全帯域通貨フィルタになる
#chunkはバッファから読み出すデータ数、24bitステレオで1024は#1024*2*3byte単位の処理
#fnorは結果がオーバーフローしないように出力を圧縮する。
#firフィルタ結果ddataが±2**31を超えるとオーバーフローする
#ので、cdataを1/fnorに圧縮する
#fnorを大きくすると出力音が小さくなる
def dfil(sdata,filrfs,chunk,fnor):
    width=len(sdata)
    sdata_cp=cp.asarray(sdata)
    filrfs_cp=cp.asarray(filrfs)
    #ステレオ時の処理
    if width==2:
        cdata0=cp.asnumpy(cp.convolve(sdata_cp[0],filrfs_cp,'valid')/fnor)
        cdata1=cp.asnumpy(cp.convolve(sdata_cp[1],filrfs_cp,'valid')/fnor)
        ddata=np.array([cdata0[:chunk],cdata1[:chunk]])
    else :
        cdata=cp.asnumpy(cp.convolve(sdata_cp,filrfs_cp,'valid')/fnor)
        ddata=cdata[:chunk]
    del sdata_cp
    del filrfs_cp
    return ddata

#float64のaoデータ(LRを交互に並べたもの)を24bitの形式に変換
#vbinwaveはbyteデータでこれを音声ストリームに出力する
def fixfl_to_paint24(ao):
    #swav = [int(x * (2**31-1)) for x in ao]
    swav = [int(x) for x in ao]
    #print(swav)
    binwave = struct.pack('i'*len(swav), *swav)
    #print(binwave)
    ubinwave= struct.unpack('x3b'*(len(binwave)//4),binwave)
    #print(ubinwave)
    vbinwave= struct.pack('3b'*(len(ubinwave)//3),*ubinwave)
    #print(vbinwave)
    return vbinwave

#firフィルタ設定(全帯域スルー特性)
tfr=12800
fsize=2*tfr+1
filrfs=np.zeros(fsize)
filrfs[tfr]=1.0
#すでにfilrfsフィルタを設定している場合は下記のように記載する
#tfr=len(filrfs)
#fsize=2*tfr+1

chunk = 1024
#フィルタサイズがfirのタップ係数より必ずchunk数大きくなるように
#sblkを決定
sblk=int(fsize/chunk)+2
print('fsize=',fsize,'sblk=',sblk)
#wav音声データを読み込む
wf = wave.open("D:/テスト用音源/山口百恵_秋桜_44x2.wav", "r")
channels=wf.getnchannels()
width=wf.getsampwidth()
# ストリームを開く
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                #output_device_index = 15,    #出力device指定の場合
                output=True)
#chunkに必要なストリームバイト数bchunkを求める
bchunk=chunk*width*channels
#sdataとしてsblk分確保する
sdata=np.zeros((channels,sblk*chunk*width))

# wavデータをストリームからchunk分だけ取り出す
data = wf.readframes(chunk)

#wavファイルから読み出し
while data != b'':
    #24bitデータを8bitの0を付加してint32に変換する
    iwave = [struct.unpack("<i",
        bytearray([0]) + data[width * k:width * (k+1)])[0]
        for k in range(int(len(data)/width))]
    #LRを分離してfloat64に変換する
    data2 = np.array(iwave, dtype='float64').reshape(-1,2).transpose()
    #sdataをchunk文シフトする
    sdata[:,:-chunk]=sdata[:,chunk:]
    #sdataの最後に読み込んだ新しいデータを付け加える
    if data2.shape[1]==chunk:
        sdata[:,-chunk:]=data2
    #wavファイルのデータの最後の端数分の処理
    else:
        sdata[:,-chunk:-chunk+data2.shape[1]]=data2
    #fsizeのタップ数のfirフィルタをかけた結果の最初の
    #chunk分をdata3に出力
    data3=dfil(sdata,filrfs,chunk,1.2)
    #1次元に変換
    data4=np.ravel(data3.transpose())
    #floatデータを24bit2chのストリームに変換
    data5=fixfl_to_paint24(data4)
    # チャンク単位分のデータをストリームに出力し音声を再生
    stream.write(data5)
    #wavファイルから次のchunk分のデータを読み込み
    data = wf.readframes(chunk)
    #i=i+1
print('演奏終了!')
wf.rewind()
stream.close()
p.terminate()

-------------------プログラム終わり----------------

 

*注意
https://qiita.com/koreyou/items/4494442eb71bea0bb5b2
「Unified Memoryを使ってGPUメモリよりも大きなモデルをChainerで扱う」
という記事があり、
import cupy as cp
pool = cp.cuda.MemoryPool(cp.cuda.malloc_managed)
cp.cuda.set_allocator(pool.malloc)

を行うとしばらく使っているとPCがフリーズしてしまうことが分かり、このメモリ割り当ての2行は実行しないでください。これを実行しなくても、GPUで使用するメモリは適宜delで解放しているので、長時間のwavファイルでもOut of Memory Errorが出ないように記述しました。