では、練習問題です。問題の考え方について解説した動画も用意してあります。ぜひご視聴ください。
ベクトルaとベクトルbの内積a⋅bは以下の式で求めることができます。
ただし、aもbも零(ゼロ)ベクトルではないものとします。
この式を使って、aとbのなす角度θに対するcosθの値を求めてみてください。例えば、a=(1,3,4)とb=(2,−1,3)の場合は、cosθ=0.576..となります。
440Hz(ヘルツ)の正弦波で表される「ラ」の音を作成する関数makesoundを作成し、作成したサウンドデータをWAVファイルとして保存してみましょう。
440Hzの正弦波とは、図9の赤い色で示したように、0〜2πまでの波が1秒間に440回繰り返されるようなsin関数の波のことです。振幅(波の高さ)は1とし、32ビットの浮動小数点数として表すものとします。
図9 440Hzの正弦波波の高さを数値として表すことを量子化と呼びます。また、短い時間間隔で区切って量子化された値を順に取り出すことをサンプリング(標本化)と呼びます。サウンドデータはこのようにして作られるというわけです。ここでは、1秒当たり44100回のサンプリングを行うことにします。図9の青い色で示したようなイメージです*3。
*3 1秒当たりのサンプリングの回数のことをサンプリング周波数と呼びます(同じHzという単位を使いますが、正弦波そのものの周波数とは異なります)。ちなみに、CDでは、量子化ビット数が16、サンプリング周波数が44100Hzで、左右の音が別々に(ステレオで)記録されています。
サンプリングしたデータを保存すればWAVファイル(拡張子:.wav)が作成できます。以下のリスト7の関数makesoundを完成させてください。関数makesoundの引数には、作成したい正弦波の周波数、持続時間、サンプリング周波数を指定するものとします。
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile
def makesound(freq, duration, rate):
# この内容を書く
# 作成したデータはsampleという名前の配列に記録するものとする
return sample.astype(np.float32)
freq = 440
duration = 1
rate = 44100
sample = makesound(freq, duration, rate)
# サウンドを保存する
wavfile.write('sample.wav', rate, sample)
print(max(sample), min(sample)) # 振幅が適切かどうかを確認しておく
# 一部だけグラフを表示
plt.plot(sample)
plt.axis([0, 400, -1.0, 1.0])
plt.show()
サウンドデータの作成方法は、目標3で取り扱った例と考え方は全く同じです。目標3の最後の例では、周波数が3の正弦波を作成し、1秒間に100回のサンプリングを行っていたものと考えられます。また、持続時間をdurationで指定していることにも注意してください。
コードを完成させて実行すると、sample.wavという名前のファイルが作成されます。図10に示した方法でダウンロードし、再生してみましょう。ただし、間違ったコードを書くと、耳障りな音になったり、大音量で再生されたりしてスピーカーにダメージを与える可能性があるので、十分に注意してください。sampleの値が全て-1.0〜1.0の範囲に収まっていれば、音量に関しては問題ありませんが、最初はできるだけボリュームを絞って再生してください*4。
*4 ここでは、32ビット浮動小数点数のサウンドデータを作成しましたが、wavfile.write関数では、8ビット符号付き整数、16ビット符号付き整数、32ビット符号付き整数のいずれでもサウンドデータの作成ができます。例えば、上記の*3で触れたCDのサウンドデータは16ビット符号付き整数なので、astype(np.int16)を指定し、-32768〜32767の範囲の整数として保存する必要があります。しかし、それらのデータを間違って浮動小数点数として保存すると、-1.0〜1.0を大きく逸脱した値になるので、とてつもなく大きな音量で再生されてしまいます。
図10 作成されたサウンドとダウンロードの方法ダウンロードしたsample.wavファイルは、WindowsでもmacOSでも、通常[ダウンロード]フォルダーに保存されます。エクスプローラーやFinderを開いてsample.wavファイルをダブルクリックすると再生ができます。
作成した「ラ」の音は、以下のような平板な音になるはずです(音が大きめなので、音量は小さくして再生してください)。
これは単純なsin関数の波形(純音)だからです。実際の楽器では、倍音(基本となる周波数の何倍かの周波数の音)が含まれていたり、それらの減衰速度が異なったりすることにより、楽器独特の音色が生み出されます。次の練習問題では、実際の楽器の波形を描いてみます。
実際のサウンドを読み込んで、その情報や波形を描画してみましょう。例えば、以下の音は電子ピアノ(Korg C1 Air)で弾いたラ(A4)の音で、最初に無音部分があり、徐々に減衰していきます。このサウンドの波形を描くことにしましょう。
以下のコード(リスト8とリスト9)は、上記のサウンドファイル(.wavファイル)を取得し、サウンドデータを読み込むためのものです。このコードに続けて、サンプリング周波数、データのタイプ(量子化ビット数)、チャンネル数(モノラルかステレオか)、持続時間を表示するとともに、波形を描画してみてください。ただし、データ量が多いので、振幅が最大である位置から400個のサンプルを抽出してプロットすることにしましょう。横軸と縦軸の目盛は特に指定しなくても構いません。
# WAVファイルをGoogle Colabにダウンロードする(1回実行しておけばよい)
!wget "https://raw.githubusercontent.com/Gessys/math/main/data/la.wav"
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile
# WAVファイルを読み込む
rate, data = wavfile.read('la.wav')
# サンプリング周波数、データのタイプ、チャンネル数、持続時間を表示するための
# コードをここに書く
# 波形のグラフを描くコードをここに書く
実行例は図11の通りです。
図11 電子ピアノのラ音の波形図11を見ただけではどのような周波数の波が含まれているかは分かりませんが、離散フーリエ変換と呼ばれる手法を利用すれば、波に含まれる周波数の成分を求めることができます。これについては、練習問題(3)の作成例の後で、発展的な話題として(簡単にですが)紹介することにします。
以下、解答とプログラムの作成例です。もちろん、異なるやり方もあるので、これらが唯一の答えというわけではありません。
これは、以下の式を変形すれば簡単です。aもbも零ベクトルではないので、|a|も|b|も正の値となります。従って、両辺を|a||b|で割れば、
は、
となります。
なので、(1)式をそのままコードとして表すだけでできます(リスト10)。
import numpy as np
a = np.array([1, 3, 4])
b = np.array([2, -1, 3])
print(a@b / np.sqrt(a@a * b@b))
# 出力例:
# 0.5765566601970551
(1)式の分母は、
ですが、全体に√をかけて、
と表すこともできます。リスト10の作成例では(2)式で分母を求めています(細かいことですが、sqrt関数の呼び出しが1回で済みます)。
さまざまなデータの並びはn次元のベクトルとして表すことができます。例えば、気温ならx={12, 15, 24, 38, 27, 32}のように表せます。気温のそれぞれの値に対応するビールの出荷数(ケース単位)ならy={5, 8, 14, 57, 33, 48}といった具合です。これらは一般的に、x={x1, x2, ... xn}や、y={y1, y2, ... yn}と表すことができます。ここで、各要素から平均値
を引いて作成したベクトル
を考えてみましょう。これらは、原点を
に移動した場合のxとyと考えられます。このとき、XとYのなす角度θに対するcosθの値は、
となります。
この式をどこかで見たことがないでしょうか。この式は相関係数を表す式にほかなりません。相関係数の正体はcosθだったのです。
このことから、相関係数はベクトルがどれぐらい同じ向きを向いているかということだという図形的なイメージがつかめると思います。また、相関係数が-1〜1の値を取ることも納得できると思います。
以下のパターンに、サウンドの周波数freq、持続時間duration、サンプリング周波数rateを当てはめるだけで、サウンドデータが作成できます。以下の手順通りにコードを書けばどのような正弦波も必ず作成できますが、意味を理解することが重要なので、コードの後に図解も掲載しておきます。
以下の例では、サウンドデータは-1.0〜1.0の範囲の浮動小数点数として表されます。
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile
def makesound(freq, duration, rate):
n = rate * duration # データ数
t = np.linspace(0, duration, n) # 持続時間をデータ数で区切る
sample = np.sin(2*np.pi * freq * t)
return sample.astype(np.float32)
freq = 440
duration = 1
rate = 44100
sample = makesound(freq, duration, rate)
# サウンドを保存する
wavfile.write('sample.wav', rate, sample)
print(max(sample), min(sample)) # 振幅が適切かどうかを確認しておく
# 一部だけグラフを表示
plt.plot(sample)
plt.axis([0, 400, -1.0, 1.0])
plt.show()
図12で考え方をいま一度確認しておきましょう。
図12 サウンドを作成する上での考え方リスト11のt = np.linspace(0, duration, n)というコードに注目してください。durationをn=rate × duration個に分けるということなので、刻み幅はduration/(rate × duration)=1/rateとなります。つまり、tを1/rate刻みでdurationまで増やしていくということです。sample = np.sin(2*np.pi * freq * t)いうコードで正弦波が作れますが、これは目標3のコードそのままです。
ここでは、サウンドを32ビット浮動小数点数として表しているので振幅は1.0ですが、16ビット符号付き整数で表すのであれば、値の範囲が-32768〜32367なので、sin関数の値に32767を掛けて、振幅を32767までとします。
def makesound16(freq, duration, rate):
n = rate * duration # データ数
t = np.linspace(0, duration, n) # 持続時間をデータ数で区切る
amp = 32767 # 16ビット符号付き整数の最大値
sample = np.sin(2*np.pi * freq * t) * amp
return sample.astype(np.int16) # 必ずデータ型を合わせておくこと
WAVファイルを読み込むためのコードはすでに書かれており、rateでサンプリング周波数が、dataでサウンドデータが参照できるようになっているので、以下の値を求めれば必要な情報が表示できます。
また、振幅の最大値がどの位置にあるかは、numpyモジュールのargmax関数を使って、配列の中の最大値のインデックスを求めれば分かります。その位置から400個分のデータを取り出して波形を描画すればコードの完成です。matplotlib.pyplotモジュールのplot関数を使って折れ線グラフを描くだけです。
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile
# WAVファイルを読み込む
rate, data = wavfile.read('la.wav')
print('サンプリング周波数:', rate)
print('データのタイプ:', data.dtype)
print('チャンネル数:', data.ndim)
print('持続時間:', len(data) / rate)
# グラフを描画する
start = np.argmax(data) # 振幅が最大となる位置
end = start + 400
sample = data[start:end]
plt.plot(sample)
plt.show()
サウンドデータを読み込んで波形を表示するだけであれば簡単ですね。ただ、サウンドを解析したり、サウンドを合成したりするためには、サウンドに含まれる周波数成分を調べるなど、さまざまな処理の方法を知る必要があります。ここでは、ほんの入り口だけしか紹介できませんが、以下、発展的な話題として、ハニング窓によって分析する範囲を取り出す方法と、離散フーリエ変換によって周波数成分を取り出す方法を紹介します。サウンドの解析やサウンドプログラミングについては『Excelで学ぶフーリエ変換』(小川智哉監修、渋谷道夫・渡邊八一著、オーム社)や『Pythonではじめる音のプログラミング』(青木直史著、オーム社)などが詳しいので、興味のある方はご参照ください。
サウンドデータにどの周波数の波が含まれているかを調べるには離散フーリエ変換と呼ばれる方法を使います。この連載の第4回の練習問題でフーリエ級数を利用して矩形波を描画する例を紹介しましたが、離散フーリエ変換は、その逆の計算を行うものと考えられます。
まず、離散フーリエ変換を行うための準備として、サウンドデータのうち、分析の対象とする部分を取り出す関数を作っておきましょう。そのような関数は、全てのデータの一部分を「窓」から覗(のぞ)くようなイメージなので、窓関数と呼ばれます。
上の練習問題では、一定の範囲をそのまま取り出しましたが、それが最もシンプルな「窓」です。その場合、四角い窓から見える部分だけを取り出しているようなイメージなので「方形窓」と呼ばれます(図13の左)。
しかし、方形窓では、開始位置と終了位置の波の高さが異なるので、取り出した波が繰り返すものとすると、その位置にギャップができてしまい、分析の精度が下がってしまいます。そのため、開始位置と終了位置がスムーズにつながるように、両端の振幅を小さくするような窓関数を使って分析のためのデータを取り出すのが一般的です。ここでは、さまざまな窓関数のうち、よく使われているハニング窓(ハン窓)関数を作成してみます。図13の右がハニング窓を適用した例です。
図13 方形窓とハニング窓ハニング窓関数は、以下の式で表されます。nは取り出すデータの個数です。
取り出したデータの値にこの式の値をそれぞれ掛ければ、分析に使うデータが作成できます。が、その前にハニング窓関数の作成と動作確認をしておきます(リスト14)。
def hanning(n, sym=True):
x = np.linspace(0, 1, n, endpoint=sym)
return 0.5 - 0.5 * np.cos(2*np.pi * x)
# 動作確認のためのコード
print(hanning(6, True))
print(hanning(6, False))
# 出力例:
# [0. 0.3454915 0.9045085 0.9045085 0.3454915 0. ] # 左右対称の値になっている
# [0. 0.25 0.75 1. 0.75 0.25] # 左右対称になっていない
実はハニング窓を作るための関数としては、numpyモジュールのhanningという関数が利用できます。numpyモジュールのhanning関数では引数にデータ数のみを指定します。その場合、リスト14で作成したhanning関数にsym=Trueを指定した結果と同じになります。ちなみに、scipy.signal.windowsモジュールにもhann関数があり、そちらはリスト14で作成したhanning関数と同じ働きになります(sym=Trueが既定値となっていますが、sym=Falseの指定もできます)。
分析対象の部分に窓関数を適用したデータを作成し、numpy.fftモジュールのfft関数により離散フーリエ変換を行えば、周波数成分が取り出せます。つまり、波形を表すデータ(縦軸が振幅、横軸が時間)を、周波数を表すデータ(縦軸が振幅、横軸が周波数)に変換できるというわけです。このとき、得られる値(振幅)は複素数の形になっているので、その周波数成分の大きさを得るには、複素数の絶対値を求める必要があります*5。ただし、そのままでは値が大きくなるので、n/2で割った値の絶対値を求めて振幅の値とします。横軸となる周波数の配列は自分で作成することもできますが、numpy.fftモジュールのfffreq関数を使えば簡単に作れます。これらの値をプロットすれば、周波数成分が可視化できるというわけです。
*5 離散フーリエ変換によって求められた値は、各周波数成分に対する複素数平面上の円の半径、つまり振幅を複素数で表したものとなっています。図8で見た複素数平面上の円の半径に当たります(図8では半径=1でした)。
練習問題(3)で見た、電子ピアノの「ラ」の音を例として、コード全体と実行結果を示しておきます(リスト15〜16、図14)。分析対象の範囲は振幅が最大の位置から2048個のデータとします。
# WAVファイルをGoogle Colabにダウンロードする(1回実行しておけばよい)
!wget "https://raw.githubusercontent.com/Gessys/math/main/data/la.wav"
import numpy as np
from scipy.io import wavfile
import matplotlib.pyplot as plt
# ハニング窓関数
def hanning(n, sym=True):
x = np.linspace(0, 1, n, endpoint=sym)
return 0.5 - 0.5 * np.cos(2*np.pi * x)
# WAVファイルを読み込む
rate, data = wavfile.read('la.wav')
# 分析対象の範囲
start = np.argmax(data)
end = start + 2048
sample = data[start:end]
# ハニング窓を適用
n = len(sample)
w = hanning(n)
sample = (sample * w).astype(np.int16)
# 離散フーリエ変換により、周波数成分を表示する
sp = np.fft.fft(sample)
amp = np.abs(sp/(n/2)) # 振幅の配列(縦軸の値)
f = np.fft.fftfreq(n, d=1# 周波数の配列(横軸の値)
plt.plot(f[1: n//2], amp[1: n//2])
plt.xlabel("Freqency")
plt.ylabel("Amplitude")
plt.axis([0, 5000, 0, 2500]) # 主要な部分だけグラフ化
plt.show()
図14 リスト16の実行結果(左:ハニング窓、右:方形窓)実行結果を見ると、ラ音を表す440Hzの周波数のほか、倍音の880Hzや3倍音の1320Hz、4倍音の1760Hzなどの周波数成分が含まれていることが分かります。このような周波数の特性や各成分の減衰時間などを基に、サウンドを合成することもできるというわけです。
プロットする周波数の上限をサンプリング周波数の半分までとしているのは、サウンドを再現するためにはサウンドに含まれる周波数の倍以上の周波数でサンプリングする必要があるからです。それよりもサンプリング周波数が小さいと、うまく元の音が再現できません。サンプリング周波数の半分の周波数をナイキスト周波数と呼びますが、ナイキスト周波数よりも高い周波数のサウンドが含まれていると、正しくサンプリングができないということです。逆に言うと、22050Hzまでの周波数が含まれているサウンドであれば、44100Hzの周波数でサンプリングすれば元の音が再現できることになります。ここでは、元のサウンドに22050Hz以上の周波数が含まれていないという前提でサンプリングを行っているので、44100Hzの半分までをプロットしているわけです(ちなみに、CDなどでは22050Hz以上の周波数はカットされて収録されています。人間の可聴域が約20000Hzまでと言われているので、聞こえる音は22050Hzまでで十分に再現できるというのがその理由です)。
……というわけで、今回は、三角関数の基本の基本から、サウンドデータの作成や分析について紹介しました。AI/機械学習においても、サウンドデータを取り扱うことがあります。そのための初めの一歩といった感じです。サウンドデータの取り扱いを理解するにはグラフによる可視化が大いに役立ちます。
次回は、機械学習に必要不可欠な大量データの取り扱いと記述統計について一通り整理し、応用例として回帰や分類の例も紹介したいと思います。
Copyright© Digital Advantage Corp. All Rights Reserved.