Wizard Notes

音楽信号解析の技術録、作曲活動に関する雑記

Python: waveモジュールを使ったWAVファイル読み込みとNumPy化 (8-bit, 16-bit, 24-bit, 32-bit int)

前置き

f:id:Kurene:20210413212010p:plain

音楽プレイヤーのような音楽ソフトウェアの開発では、まずオーディオファイルから時間信号を読み込む処理が第一歩となります。

オーディオファイルは数多くの種類のフォーマットがあり、音楽ソフトウェアにおけるユーザの利便性を考えると、なるべく多くのフォーマットに対応することが望ましいです。しかしながら、それら全てのフォーマットのデコード処理をプログラミングするのは困難です。そのため、なるべく誰かが開発した外部ライブラリを利用することになります。

Python においては、例えば ``Pysoundfile```モジュールなどを使うことで、数行でオーディオファイルを読み込むことができます

www.wizard-notes.com

pysoundfile.readthedocs.io

自分だけが使用するソフトウェアや、スクリプトとしての利用・配布であればこのようなモジュールを単純に採用すればよいのですが、実行ファイルで配布するようなソフトウェア開発の場合はライセンスに注意が必要です。

Pysoundfile自体はMITライセンスですが、そのコアであるlibsndfileというライブラリはLGPLライセンスです。

従って、PyInstallerなどで一つの実行ファイルにまとめるのはライセンス的には厳しいと考えられます。

また、PyInstallerなどを使った時に、非標準のライブラリを組み込むため実行ファイル(群)の総ファイルサイズが大きくなってしまうことも懸念されます。

Python wave モジュール

ところでPythonには waveモジュールという標準モジュールが用意されています。

docs.python.org

標準モジュールであるため、ライセンスの問題を回避でき、また、実行ファイルサイズを増やさないソフトウェア設計が可能です。

そこで、今回は waveモジュールを使って様々な形式のwavファイルを読み込んでみました

実装/テスト

unsign 8-bit, signed 16-bit, signed 32-bitについては、numpy.frombuffer()で素直に読み込んでいます。

signed 24-bit については、3バイトなので読み込み方法に悩みますが、以下の記事のように下位ビットを8ビット分ゼロ詰めして、4バイト (signed 32-bit) として読み込むことができます。

memotut.com

上位8ビットでは符号に影響が出るため下位8ビットであることがポイントです。

最初に掲載してある図が検証結果です。L/Rの信号が意図通り分離されており、また、[-1.0, 1.0) に正規化されていることを検証しました。

import wave
import numpy as np
from struct import unpack
import matplotlib.pyplot as plt


def load_wav(wavfilepath):
    # wavファイルを読み込む
    wf   = wave.open(wavfilepath, "r")
    # wavファイルのパラメタを取得
    sr        = wf.getframerate() # サンプリング周波数
    n_ch      = wf.getnchannels() # チャンネル数
    n_frames  = wf.getnframes()   # オーディオフレーム数
    n_bytes   = wf.getsampwidth() # 1サンプル数あたりのバイト数
    
    # 信号データを取得
    audiobuffer = wf.readframes(n_frames)
    wf.close()

    # 1サンプルあたりのバイト数に応じて信号を読込
    if   n_bytes == 1: # 8-bit uint
        norm = 2**7
        data = np.frombuffer(audiobuffer, dtype=np.uint8).astype(np.float32)
        data = data.copy() - norm
    elif n_bytes == 2: # 16-bit int
        data = np.frombuffer(audiobuffer, dtype=np.int16).astype(np.float32)
        norm = 2**15
    elif n_bytes == 3: # 24-bit int
        data = [unpack("<i",
                bytearray([0]) + audiobuffer[n_bytes * k:n_bytes * (k+1)])[0]
                for k in range(n_frames)]
        data = np.array(data, dtype='int32') 
        norm = 2**31
    elif n_bytes == 4: # 32-bit int
        data = np.frombuffer(audiobuffer, dtype=np.int32)
        norm = 2**31

    # 各チャネルの信号に分離
    n_samples = data.shape[0] // n_ch
    x = np.zeros((n_ch, n_samples), dtype=np.float32)
    for k in range(n_ch):
        x[k] = data[k::n_ch]

    # [-1, 1) に正規化
    x /= norm
    print(np.min(x), np.max(x))
    return x, sr
 

if __name__ == "__main__":    
    filepath_list = ["sinu8int.wav", "sin16int.wav", "sin24int.wav", "sin32int.wav"]
    for k, filepath in enumerate(filepath_list):
        x, sr = load_wav(filepath)
        x = x[:,0:1000]
        
        plt.subplot(len(filepath_list), 1, k+1)
        plt.plot(x[0], c="c", label="Right")
        plt.plot(x[1], c="m", label="Left")
        plt.ylim(-1.0, 1.0)
        plt.title(filepath)
        plt.legend()
        
    plt.tight_layout()
    plt.show()

最後に

Pythonにおいてwaveモジュールを使うことで、16-bitだけでなく、より高/低解像度なwavファイルの読み込みもできることを示しました。

wavioというライブラリもあるのですが、実行ファイル化を目指すPythonソフトウェア開発の場合は、これくらいは自分で書いてしまったほうが使い勝手が良いと思います。

github.com

なお、wav以外のオーディオファイルフォーマットの対応に関しては、ffmpegなどの開発中のソフトウェアとは切り離された外部アプリケーション/ライブラリを利用してwavファイルを作成し、waveモジュールで読み込むような設計が考えられます。

Float形式のWAVファイルについて

www.wizard-notes.com