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秒短縮。