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が出ないように記述しました。