PythonのwaveモジュールでのPCMデータ読み出しを高速化する

RIFF WAVEファイルからリニアPCMのデータを取り出して、フレーム毎に(正確には各フレーム内のサンプル毎に)加工して出力するツールを作った。

諸事情によりPython 2.xの標準ライブラリのwaveモジュールを使ったのだが、Wave_readオブジェクトの扱い方によって速度に差が出たので、メモを残しておく。

環境はCygwin x64上のPython 2.7.12だ。

最初のコード(遅かった)

フレーム毎にデータを加工したかったため、フレーム毎にreadしようとして最初に書いたのは、こんなコードだった。

wr = wave.open(wavfile, 'rb')

for _ in range(wr.getnframes()):
    frame = wr.readframes(1)
    samples = [frame[i:i+wr.getsampwidth()] for i in range(0, len(frame), wr.getsampwidth())]
    # XXX 各サンプルごとに、何らかの処理を行う。

wr.close()

このコードは結構遅くて、4MB弱のWAVEファイルを流し込んで処理を行おうとすると、手元の環境では7〜8秒ほどかかっていた(上記の「各サンプルごとに、何らかの処理を行う」を含めた時間なので注意)。

小手先の改良コード(ちょっと速くなった)

で、小手先の策を弄したコードがこれ。

wr = wave.open(wavfile, 'rb')

nframes = wr.getnframes()
sampwidth = wr.getsampwidth()

for _ in range(nframes):
    frame = wr.readframes(1)
    samples = [frame[i:i+sampwidth] for i in range(0, len(frame), sampwidth)]
    # XXX 各サンプルごとに、何らかの処理を行う。

wr.close()

ループ中で何度もWave_read.getsampwidth()を呼ぶのではなく、ループ前に変数に格納しておいて、それを参照するようにしただけ。しかしこれでも1秒近く短縮された。

もうちょっと改良したコード(もっと速くなった)

もう少し速くならないかと考えて、ループ中で毎度Wave_read.readframes()で1フレームずつreadするのではなく、一度にガッとreadした上で、スライス演算でフレームごとにアクセスするジェネレータ式を使うようにしてみた。

wr = wave.open(wavfile, 'rb')

sampwidth = wr.getsampwidth()
framewidth = sampwidth * wr.getnchannels()

frames = wr.readframes(wr.getnframes())
it = (frames[i:i+framewidth] for i in range(0, len(frames), framewidth))
for frame in it:
    samples = [frame[i:i+sampwidth] for i in range(0, len(frame), sampwidth)]
    # XXX 各サンプルごとに、何らかの処理を行う。

wr.close()

これでさらに2秒ほど短縮された。

ダメ押しの改良コード(さらに速くなった)

もう高速化は無理だろうと思っていたところ、ふと「そういえば、何で1フレーム取り出してサンプルごとに分割してるのだろう?」と気づいてしまった。

今回の加工処理は、「同一フレーム中の各サンプル」という括りで何かする必要はなかった。なので、最初からサンプル単位でアクセスしても問題なかった。

wr = wave.open(wavfile, 'rb')

frames = wr.readframes(wr.getnframes())
sampwidth = wr.getsampwidth()
it = (frames[i:i+sampwidth] for i in range(0, len(frames), sampwidth))
for sample in it:
    # XXX サンプルごとに、何らかの処理を行う。

wr.close()

ダメ押しの1秒短縮。

まとめ

  • 最終的に、最初のコードの2倍ほど高速になった。
  • C/C++の感覚からすると、メソッド呼び出しのコストは少し高めかも。*1
  • スライス演算とジェネレータ式! そういうのもあるのか。

*1:念のため書いておくと、これはPythonをdisっているわけではなく、Pythonを使っていたにもかかわらず暗黙のうちに他の言語(C/C++)の感覚で判断を行っていた自分への戒めである。PythonPythonとして扱わなければ失敗する、当然のことだ。