問題
上記の実装の拡張として、Python の waveモジュールを使って32-bit or 64-bit float形式のwavファイル読込を追加しようとして試してみたところ、以下のようなエラーがでました。
>wave.Error: unknown format: 3
てっきりwaveモジュールはfloat形式にも対応しているかと思いきや、そうではないようです。
CPythonのWaveモジュールのソースコードを確認してみました。
87行目には、
WAVE_FORMAT_PCM = 0x0001
のみで、WAVE_FORMAT_IEEE_FLOAT
などはありません。
また、254-273行目を見てもPCM以外の形式はunknown format
としてエラー扱いになっていることが分かります。。
def _read_fmt_chunk(self, chunk): try: wFormatTag, self._nchannels, self._framerate, dwAvgBytesPerSec, wBlockAlign = struct.unpack_from('<HHLLH', chunk.read(14)) except struct.error: raise EOFError from None if wFormatTag == WAVE_FORMAT_PCM: try: sampwidth = struct.unpack_from('<H', chunk.read(2))[0] except struct.error: raise EOFError from None self._sampwidth = (sampwidth + 7) // 8 if not self._sampwidth: raise Error('bad sample width') else: raise Error('unknown format: %r' % (wFormatTag,)) if not self._nchannels: raise Error('bad # of channels') self._framesize = self._nchannels * self._sampwidth self._comptype = 'NONE' self._compname = 'not compressed'
ということで、Python (CPython) の wave モジュールではFloat形式のWAVファイルは読み込めません。
解決方法
4つ考えられます。
PySoundFile
やscipy.io.wavfile.read
などの非標準モジュールを使う- 自前でWavファイルのバイナリをパースするモジュールを実装する
- 読み込むファイルを別アプリでPCM形式に変換しておく
- wave モジュールをハックする
1 は、楽ですが非標準モジュールを使う必要があります。また、2 は実装・検証がめんどうですし、3をいちいちやるのはもっと面倒です。
そこで、この中で一番効率がよさそうな 4 をやってみました。
waveモジュールのWav_read
クラスをハック
wave_open()
を見てみましょう。
def open(f, mode=None): if mode is None: if hasattr(f, 'mode'): mode = f.mode else: mode = 'rb' if mode in ('r', 'rb'): return Wave_read(f) elif mode in ('w', 'wb'): return Wave_write(f) else: raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
WAVファイル読込の場合、wave.Wave_read
クラスを呼び出しているだけです。
また、先ほどの_read_fmt_chunk()
関数はwave.Wave_read
クラスのメソッドです。
従って、以下のようにwave.Wave_read._read_fmt_chunk()
をオーバーライドすることでハックできます。
具体的には、Wav_read
を継承したクラスにWAVE_FORMAT_IEEE_FLOAT
で'unknown format
のraise
を避けるようにしています。
また、後段でint
とfloat
を区別できるようにするために、getformatid
関数をWav_read
を継承したクラスに追加します。
import wave import numpy as np import struct import matplotlib.pyplot as plt WAVE_FORMAT_PCM = 0x0001 WAVE_FORMAT_IEEE_FLOAT = 0x0003 class HackedWaveRead(wave.Wave_read): def _read_fmt_chunk(self, chunk): try: wFormatTag, self._nchannels, self._framerate, dwAvgBytesPerSec, wBlockAlign = struct.unpack_from('<HHLLH', chunk.read(14)) except struct.error: raise EOFError from None if wFormatTag == WAVE_FORMAT_PCM or wFormatTag == WAVE_FORMAT_IEEE_FLOAT: self.wFormatTag = int(wFormatTag) try: sampwidth = struct.unpack_from('<H', chunk.read(2))[0] except struct.error: raise EOFError from None self._sampwidth = (sampwidth + 7) // 8 if not self._sampwidth: raise Error('bad sample width') else: raise Error('unknown format: %r' % (wFormatTag,)) if not self._nchannels: raise Error('bad # of channels') self._framesize = self._nchannels * self._sampwidth self._comptype = 'NONE' self._compname = 'not compressed' def getformatid(self): return self.wFormatTag def load_wav_float(wavfilepath): # wavファイルを読み込む wf = HackedWaveRead(wavfilepath) # wavファイルのパラメタを取得 sr = wf.getframerate() # サンプリング周波数 n_ch = wf.getnchannels() # チャンネル数 n_frames = wf.getnframes() # オーディオフレーム数 n_bytes = wf.getsampwidth() # 1サンプル数あたりのバイト数 format_id = wf.getformatid() # フォーマットID取得(HackedWaveRead()で定義 ) # 信号データを取得 audiobuffer = wf.readframes(n_frames) wf.close() # 1サンプルあたりのバイト数に応じて信号を読込 norm = 1.0 if n_bytes == 4 and format_id == WAVE_FORMAT_PCM : data = np.frombuffer(audiobuffer, dtype=np.int32) norm = 2**31 mode = "32-bit int" elif n_bytes == 4 and format_id == WAVE_FORMAT_IEEE_FLOAT: data = np.frombuffer(audiobuffer, dtype=np.float32) mode = "32-bit float" elif n_bytes == 8 and format_id == WAVE_FORMAT_IEEE_FLOAT: data = np.frombuffer(audiobuffer, dtype=np.float64) mode = "64-bit float" # 各チャネルの信号に分離 n_samples = data.shape[0] // n_ch x = np.zeros((n_ch, n_samples)) for k in range(n_ch): x[k] = data[k::n_ch] # [-1, 1) に正規化 x /= norm print(wavfilepath, mode, f"{n_ch}CH", np.min(x), np.max(x)) return x, sr if __name__ == "__main__": filepath_list = [ "sin_32bit_int_5ch.wav", "sin_32bit_float_5ch.wav", "sin_64bit_float_5ch.wav", ] for k, filepath in enumerate(filepath_list): x, sr = load_wav_float(filepath) plt.subplot(len(filepath_list), 1, k+1) for c in range(0, x.shape[0]): plt.plot(x[c], label=f"{c} ch.") plt.ylim(-1.0, 1.0) plt.title(filepath) plt.legend() plt.tight_layout() plt.show()
検証
5チャネルのwavファイルを読み込みました。このオーディオファイルは、チャネル順に振幅の幅が(-1.0,1.0)のサイン波を鳴らしています。
コマンドライン実行結果
>sin_32bit_int_5ch.wav 32-bit int 5CH -1.0 0.9921875 >sin_32bit_float_5ch.wav 32-bit float 5CH -1.0 0.9921894073486328 >sin_64bit_float_5ch.wav 64-bit float 5CH -1.0 0.992188572883606