pythonユーザの方で「MATLABだったらaudioreadとかaudiowriteなんて関数があって楽なんだけどpythonは無いからどうしたら良いんだろう…」なんて事思ったことありませんか?

実はpythonでも音響信号処理ができます!!

というか個人的にはMATLABよりも使いやすいと思っています(慣れがあるかもしれませんが)。

pythonで音響信号処理するのに一番の障壁は”ファイルの読み込み・書き出し”でしょう。

これさえクリアすれば結構便利に使えますし,一度読み込み・書き出しの関数を自分で作っちゃえば後は使い回せばOKです。

それでは本題です。

wavファイルの仕組み

wavファイルは(どれもそうですが)バイナリファイルといって簡単に言うと1と0だけで記述された計算機だけに読めるファイルです。

僕らが読むためには変換(翻訳)する必要があります。詳しくは以下の記事などが参考になります。

例えばC言語などでwaveファイルを読もうと思うとこれをきちんと理解しなければなりません。(僕も一度やったことがあります…)

しかしここで朗報です。

pythonでwavファイルを読み込む(書き込む)のはもっと簡単です!

実はpythonにはwaveというパッケージがあって,そちらの形式に従えばもっと簡単に読み込む(書き込む)ことができます。詳しくはこちらを参照して下さい。

このパッケージを扱うには以下のパラメータを意識する必要があります。(下2つは気にしなくていいです)

name説明
nchannelsチャンネル数(モノラル,ステレオなど)int1 (モノラル)
sampwidthサンプルバイト数(量子化ビット数/8)int2 (16bit)
framerateサンプリング周波数[Hz]int44100
nframesフレーム数(音データの数)int5*framerate (5秒の音源の場合)
comptype圧縮形式(現在は”NONE”しか選べません)str“NONE”
compname圧縮形式を人が読めるようにしたもの(“NONE”しかないのでそれに対応する”not compressed”しかありません)str“not compressed”

勿論読み込みの場合はファイルに既にパラメータが設定されているのでそこまで気にする必要はありませんが,書き込みでは設定する必要がありますので覚えておきましょう。

wavファイルの読み込み・書き込み

では早速waveパッケージを用いたwavファイルの読み込み・書き込みのコードについて説明します。

「プログラムコード→解説」という流れで書きますので実装だけしたい方はコードだけコピーして貰ったらいいかと思います。

また,今回は簡単のため1ch(モノラル音源)のみ対応の関数をご紹介します。
※binary2float等の方変換関数は次の章でご紹介します

読み込み

まずはプログラムコードです。

これはプログラム的に言うとfile_nameという名前のwavファイルのstartタップ目からendタップ目までを配列として返す関数です。

def read_wave(file_name, start=0, end=0):
    file = wave.open(file_name, "rb") # open file
    sampwidth = file.getsampwidth()
    nframes = file.getnframes()
    file.setpos(start)
    if end == 0:
        length = nframes-start
    else:
        length = end-start+1
    frames = file.readframes(length)
    file.close() # close file
    return binary2float(frames, length, sampwidth) # binary to float

では細かく見ていきましょう。

file = wave.open(file_name, 'r') # open file

ここではfile_nameという音楽ファイルを開いて,fileという変数にWave_readオブジェクトという物を入れています。

まあ簡単に音楽ファイルがfileに入ったと思って貰っていいです。

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

ここは上で説明したパラメータのうち2つを取ってきているだけですね。

file.setpos(start)

指定された範囲を読み込むためにポインタ(次ここから読み込みますよっていう目印みたいなもの)をstartタップ目にセットします。

if end == 0:
    length = nframes-start
else:
    length = end-start+1

読みだす音源の長さ(length)を決定する部分です。

endが0ならfileにセットしている音源の最後の要素まで読み込みます。そうでなければstartからendまでのタップ数になります。

frames = file.readframes(length)

ここが実際に音データを読み込んでいる命令です。設定したポインタからlengthの長さだけ読み込みます。

file.close() # close file

ここで開いたファイルを閉じています。開きっぱなしにすると常に編集が出来るような状態になって宜しくありませんので必ずcloseしましょう。

return binary2float(frames, length, sampwidth) # binary to float

ここまででデータは取って来れたのですが,上で説明した通りwavファイルはバイナリ(1と0だけ)で書かれていますのでこのままでは使用できません。

そこでバイナリ→float(浮動小数点数)への変換を行ってそのデータを最終的な結果として返します。

これによってデータが-1から1までの範囲で出てきますので扱いやすい形になります。

書き込み

def write_wave(file_name, data, sampwidth=3, fs=48000):
    file = wave.open(file_name, "wb") # open file
    # setting parameters
    file.setnchannels(1)
    file.setsampwidth(sampwidth)
    file.setframerate(fs)
    frames = float2binary(data, sampwidth) # float to binary
    file.writeframes(frames)
    file.close() # close file

ではこちらも細かく見ていきます。

file = wave.open(file_name, "wb") # open file # open file

ここではfile_nameというファイルを開いて,fileという変数にWave_writeオブジェクトという物を入れています。

読み込みとの違いは”wb”の部分ですね。

file.setnchannels(1)
file.setsampwidth(sampwidth)
file.setframerate(fs)

ここでは音源の書き込みをするためにパラメータ3つをセットしています。

勿論これ以外のパラメータもセットしていいのですが,必要ないのでここではしていません。

file.setnchannels(1)
file.setsampwidth(sampwidth)
file.setframerate(fs)

上でも述べたように音楽ファイルの段階ではバイナリファイルになっています。

これに形式を合わせるためにfloat(浮動小数点数)→バイナリの変換を行っています。

この関数で注意するところはfloat型の段階でデータを-1から1の間におさめておくことです。

これが絶対値が1を超えていると俗に言う「サチる」という状態になります。※音で言うと「プチッ」という音が鳴ります

file.writeframes(frames)

ここでfileにバイナリデータを書き込んでいます。後は以下の命令でファイルを閉じるという流れです。

file.close() # close file

float(浮動小数点数)型とバイナリ型の変換

前章で出てきたように音楽ファイルを扱うためにはfloat(浮動小数点数)型とバイナリ型の変換が必須です。

一般的なCDなんかは量子化ビット数16bitを使っているので大概はこれで対応できるのですが,たまに8bit音源だったりハイレゾ音源(24bitや32bit)を扱う場面もあります。

そのためここでは量子化ビット数に関わらず読み込めるような関数をざっくりご紹介します。

ここでもプログラム→解説のように進めて行きます。

バイナリ→float(浮動小数点数)

def binary2float(frames, length, sampwidth):
    if sampwidth==1:
        data = np.frombuffer(frames, dtype=np.uint8)
        data = data - 128
    elif sampwidth==2:
        data = np.frombuffer(frames, dtype=np.int16)
    elif sampwidth==3:
        a8 = np.frombuffer(frames, dtype=np.uint8)
        tmp = np.empty([length, 4], dtype=np.uint8)
        tmp[:, :sampwidth] = a8.reshape(-1, sampwidth)
        tmp[:, sampwidth:] = (tmp[:, sampwidth-1:sampwidth] >> 7) * 255
        data = tmp.view("int32")[:, 0]
    elif sampwidth==4:
        data = np.frombuffer(frames, dtype=np.int32)
    data = data.astype(float)/(2**(8*sampwidth-1)) # Normalize (int to float)
    return data

sampwidth(量子化ビット数)で場合分けをしていますが,やっていることは全て同じで,
バイナリからint型へ一度変換して,それをfloat型に変換しています。

「np.frombuffer()」という関数がバイナリからintへの変換の関数です。なので基本的にやりたいことは

data = np.frombuffer(frames, dtype=np.int16)

こんな事なのです。ただし,dtypeにint8やint24が存在しないので少しややこしいことになっています。

8bitについてはuint8(unsigned int 8bit)があるので2の7乗である128を引くことで調整しています。

24bitについては一度uint8型にしてreshapeして…など結構面倒なことをやってますが,やりたいことは同じです!

data = data.astype(float)/(2*(8sampwidth-1))

最後にここはデータを-1から1までにおさめるように正規化している部分です。

float(浮動小数点数)→バイナリ

def float2binary(data, sampwidth):
    data = (data*(2**(8*sampwidth-1)-1)).reshape(data.size, 1) # Normalize (float to int)
    if sampwidth==1:
        data = data+128
        frames = data.astype(np.uint8).tobytes()
    elif sampwidth==2:
        frames = data.astype(np.int16).tobytes()
    elif sampwidth==3:
        a32 = np.asarray(data, dtype = np.int32)
        a8 = (a32.reshape(a32.shape + (1,)) >> np.array([0, 8, 16])) & 255
        frames = a8.astype(np.uint8).tobytes()
    elif sampwidth==4:
        frames = data.astype(np.int32).tobytes()
    return frames

こちらはbinary2floatと逆のことをしているだけですね。

先ほど正規化していた割り算の辻褄を合わせるために頭に同じ値の掛け算が入っています。

パッケージ化と使い方

今までの部分をパッケージとして纏めて実際に使ってみましょう。

例えば「wave_func.py」という名前を付けて以下を保存してみましょう。
※後の記事のために幾つか関数を追加していますが気にしないでください

#-- coding: utf-8 --
import numpy as np
import wave

def binary2float(frames, length, sampwidth):
    if sampwidth==1:
        data = np.frombuffer(frames, dtype=np.uint8)
        data = data - 128
    elif sampwidth==2:
        data = np.frombuffer(frames, dtype=np.int16)
    elif sampwidth==3:
        a8 = np.frombuffer(frames, dtype=np.uint8)
        tmp = np.empty([length, 4], dtype=np.uint8)
        tmp[:, :sampwidth] = a8.reshape(-1, sampwidth)
        tmp[:, sampwidth:] = (tmp[:, sampwidth-1:sampwidth] >> 7) * 255
        data = tmp.view("int32")[:, 0]
    elif sampwidth==4:
        data = np.frombuffer(frames, dtype=np.int32)
    data = data.astype(float)/(2**(8*sampwidth-1)) # Normalize (int to float)
    return data

def float2binary(data, sampwidth):
    data = (data*(2**(8*sampwidth-1)-1)).reshape(data.size, 1) # Normalize (float to int)
    if sampwidth==1:
        data = data+128
        frames = data.astype(np.uint8).tobytes()
    elif sampwidth==2:
        frames = data.astype(np.int16).tobytes()
    elif sampwidth==3:
        a32 = np.asarray(data, dtype = np.int32)
        a8 = (a32.reshape(a32.shape + (1,)) >> np.array([0, 8, 16])) & 255
        frames = a8.astype(np.uint8).tobytes()
    elif sampwidth==4:
        frames = data.astype(np.int32).tobytes()
    return frames

def read_wave(file_name, start=0, end=0):
    file = wave.open(file_name, "rb") # open file
    sampwidth = file.getsampwidth()
    nframes = file.getnframes()
    file.setpos(start)
    if end == 0:
        length = nframes-start
    else:
        length = end-start+1
    frames = file.readframes(length)
    file.close() # close file
    return binary2float(frames, length, sampwidth) # binary to float

def write_wave(file_name, data, sampwidth=3, fs=48000):
    file = wave.open(file_name, "wb") # open file
    # setting parameters
    file.setnchannels(1)
    file.setsampwidth(sampwidth)
    file.setframerate(fs)
    frames = float2binary(data, sampwidth) # float to binary
    file.writeframes(frames)
    file.close() # close file

def getParams(file_name):
    file = wave.open(file_name) # open file
    params = file.getparams()
    file.close() # close file
    return params

では実際に使ってみましょう。まずパッケージを読み込んでみます。

カレントディレクトリに作ったパッケージを置いて

import wave_func as wf

これで作ったパッケージをwfという名前で読み込めました。

次は読み込みです。モノラル音源しか読み込めませんので注意してください。
※もし丁度いい音源が無ければ後でモノラル音源を一つ作ってみますのでそちらで試してみてください

では例えばカレントディレクトリにあるsample.wavというwavファイルを読み込むことにしましょう。

ここではdataという変数に格納することにします。実行は以下の命令を打ちます。

data = wf.read_wave("./sample.wav")

以上です。簡単ですね。

では書き込みをしてみましょう。

ここでは純音(440[Hz]5秒間)を自分で作って音楽ファイルにしましょう(sample.wav)。

import numpy as np

# create sine wave(440[Hz])
fs = 48000
freq = 440
sec = 5
t = np.linspace(0, sec, secfs) sound = np.sin(2np.pifreqt)

# write data
wf.write_wave("./smple.wav", sound)

こんな感じです。聴くこともできるかと思います。

ちなみにこのファイルを読み込むことも可能ですのでやってみてください。

まとめ

今回はpythonで音楽ファイルを扱う足掛かりとしてwavファイルの簡単な構造の説明と
読み込み・書き込みの方法をプログラムコードを載せて解説しました。

今後ニーズがあれば少し高度なこともやっていきたいと思っています。

お疲れ様でした。

※C言語ですが,もし自分でリバーブなどのエフェクトを本格的に作ってみたいという方は以下の本がおすすめです。いい勉強になると思います。