親子IoTワークショップ

マイコンとクラウドを使ってIoTとプログラミングを学ぼう

[ トップ | 開催予定・概要 | 2024-12開催 | お知らせ | Facebook ]

ビープ音(純音)をグラフにして「見える化」する

音とは、空気の震え(振動)で、その振動は空気中を「波」として伝わります(「PyAudioを使って音を作り、スピーカーから流す」)。ここでは、その波(音波)をグラフ化して、目で見えるようにします。

まずは、準備として「scipy」という科学計算用のモジュールをインストールします。Terminalで以下コマンドを実行してください。

sudo apt install python3-scipy -y

ビープ音(純音)をグラフに表示する

以下では、PyAudioを使ってビープ音を作り、その音をグラフとして表示します。以下に進む前に、「PyAudioを使って音を作り、スピーカーから流す」を先に読んでおいてください。

最初に使うサンプルプログラムはpuretone-samples.pyです。このプログラムのように、PyAudioを使って音を作ったり、鳴らすには、先頭部分に以下の一行を入れてください。

import sound

また、PyAudioで作った音をグラフ化したり、さまざまな分析するには、先頭部分に以下の一行も入れてください。

import soundanalysis as sanalysis

puretone-samples.pyでは、以下のようにsound.playTone()という関数(function)を呼んでビープ音を鳴らしています(C4の音を1秒間)。ビープ音の鳴らし方、sound.playTone()の使い方については、「PyAudioを使って音を作り、スピーカーから流す」を参照のこと。

stream = sound.init(samplingRate=16000)
soundSamples = sound.playTone(stream, toneFrequency, duration)

ここで新しいのは、関数init()を呼ぶときにサンプリング・レートを指定していることです。デフォルトのサンプリング・レートは44,100(CD品質)ですが、それ以外の値を使いたいときには、このように指定します。

もうひとつ新しいのは、関数sound.playTone()が返すデータを使っていることです。「PyAudioを使って音を作り、スピーカーから流す」でこの関数を使った時には、以下のようにsound.playTone()を呼んで音を出しただけでした。

sound.playTone(stream, toneFrequency, duration)

実はsound.playTone()には、音を出す以外にもうひとつ機能があり、それが音の元になっているデータを返すことです。以下のように、sound.playTone()が返すデータを変数(variable)に格納(保存)して、後で使うことができます。ここではsoundSamplesという名前の変数を使っています。

soundSamples = sound.playTone(stream, toneFrequency, duration)

soundSamplesの中に入っているデータ、つまり「音の元になっているデータ」とは、音波の曲線上に等間隔にプロットされた点のことです。次のグラフを見てください。

これは周波数261の音(C4の音)の音波です。周波数が261ということは、この音波は1秒間に261回振動します(1秒間に261回波打つ)。つまり、上記のような波が261個横方向につながると時間が1秒たちます。上の図では、その261個のうち最初の1つ目の波だけを示しています。波が261回波打つと時間が1秒たつのですから、1回波打つのにかかる時間は1/261秒(1秒の261分の1)です。したがって上図では、波が1回波打ち終わると時間が1/261秒かかることを図示しています。

「音の元になっているデータ」の話に戻ります。これは、音波の上にプロットされた点のことで、個々の点の時間間隔は等しくなっています。上の図では、その最初の5つを赤い点として示しています。最初の点(一番左の点)は音波の先頭部分、つまりグラフの原点(0, 0)にプロットされています。ここから1/16,000秒(1秒の16,000分の1)経過した時点で、2番目の点がプロットされています。3番目の点はさらに1/16,000秒進んだ時点でプロットされています。このように、時間軸上の最初の点(time=0の点)から、1/16,000秒進むごとに、つぎつぎと音波の上に点がプロットされていきます。この点の集合が、「音の元になっているデータ」です。

上図の例では、1/16,000秒進むごとに点がプロットされていますが、それは関数sound.playTone()の使う(デフォルトの)サンプリング・レートが16,000だからです。サンプリング・レートとは、1秒間に何回音を採取、記録、利用するかを示す数値です。つまり、ここでは1秒間に16,000回細分化した音を作っています。この音データをスピーカーから出力すると、1秒間に16,000回細分化された音を耳にすることになります。サンプリング・レートについては、「マイクを使って音を録音する」を参照してください。

なお、「音の元になっているデータ」というのは、音波の曲線上から一定時間ごとに採取した「飛び飛びの点」のことなので、その点のことを「サンプル」と呼びます。飛び飛びの点を採取する作業のことを「サンプリングする」と言います(英語ではsampleという動詞を使います。日本語の数学的表現では「標本抽出する」といいます)。同じ長さの音でも、サンプリング・レートが高いほどサンプルの数は増え、サンプリング・レートが低いほどサンプルの数は減ります。サンプル数の多い方が、スピーカーで聞いたときに鮮明な(質の高い)音になります。一方、サンプル数が多いということは、データ量が多くなります(ファイルサイズが大きくなる)。

さて、puretone-samples.pyでは、以下のようにして「音の元になっているデータ」(音のサンプル)を変数soundSamplesに入れて、そのあと関数print()を使ってそのデータをプリント(ThonnyのShellに文字情報として出力)しています。

soundSamples = sound.playTone(stream, toneFrequency, duration)
print(soundSamples)

ThonnyのShellの部分を見てみてください。音のサンプルが以下のように出力されているはずです。

[ 0.0, 0.1023151, 0.20355631, ... -0.30266101,  -0.20355631,  -0.1023151 ]

これは、先ほどの図の中のサンプル(赤い点)の縦軸方向の値です。1番目の数値「0.0」は、1番目のサンプルの縦軸方向(Y軸方向)の値が0だという意味です。同様に、2番目、3番目のサンプルの縦軸方向の値は0.1023151、0.20355631となります。上図の中には、この数値を書き込んであります。サンプリング・レートに16,000を想定しているので、2番目のサンプルの座標は(1/16,000, 0.1023151)、3番目のサンプルの座標は(2/16,000, 0.20355631)となります。

変数soundSamplesには、音のサンプルが16,000個入っています(サンプリング・レートが16,000で、1秒間音を鳴らそうとしているので)。ただし、サンプル数が多いので、ThonnyのShellには全サンプルは表示されていません。最初の3つと最後の3つだけ表示され、その間の15,994個は省略されています(・・・になっている)。

なお、関数sound.playTone()は、音のサンプルを「リスト」として返しています。リストというのは、データが順番に並んでいるもののことで、[ と ]の間にデータを並べて表現します。

ここまでの説明をまとめると、関数sound.playTone()がやっていることは、

ということになります。

puretone-samples.pyは、音のサンプルを取得した後に、それをグラフとして表示します。以下のように、関数sanalysis.drawSoundSamples()を使います。

sanalysis.drawSoundSamples(soundSamples = soundSamples,
                           duration = duration,
                           samplingRate = 16000)

sanalysis.drawSoundSamples()には、

を渡します。プログラムを実行すると、以下のようなグラフ(?)が表示されます。

このグラフの左端(time=0ミリ秒)から右端(time=1,000ミリ秒)までは、1秒(1,000ミリ秒)の時間間隔があります。この1秒間に261回振動する波が描かれているのですが、サンプル(波の線)が何度も重なって描かれた結果、全体としては色のついた長方形のように見えてしまっています。

もう少し波の実態を見やすく表示するプログラムがpuretone-subset-samples.pyです。ひとつ前のプログラム(puretone-samples.py)とほぼ同じ内容ですが、最後の部分だけが少し違います。

subsetSamples = soundSamples[:400]
sanalysis.drawSoundSamples(soundSamples = subsetSamples,
                           duration = 0.025,
                           samplingRate = 16000)

ここでは、波を1秒間(time=0ミリ秒からtime=1,000ミリ秒まで)表示するのではなく、もっと短い時間間隔で表示しようとしています。そうすれば、サンプル(波の線)が重ならずにすみます。短くした時間間隔として、ここでは25ミリ秒を使っています。25ミリ秒は1,000ミリ秒(1秒)の40分の1です。したがって、グラフ中に表示するサンプルの数も16,000個ではなく、その40分の1(400個)で済みます。そこで、以下のようにして、

soundSamples[:400]

リストsoundSamplesに入っているサンプルのうち最初の400個だけを残して、それ以降のものは全て捨てる作業をしています。また、関数sanalysis.drawSoundSamples()を呼ぶときには、この最初の400個だけのサンプルを渡し、そのサンプルをグラフ表示する時間間隔として0.25秒を指定しています。

このプログラムを実行すると以下のようなグラフが表示されます。

グラフの左端(time=0ミリ秒)から右端(time=25ミリ秒)までは、25ミリ秒の時間間隔になっており、そこに周波数261の音波が表示されています。これが、C4をビープ音として流す音波です。

このように、単純な波が繰り返し続く音を純音(pure tone)といいます。ビープ音は純音の代表例です。純音の音波は規則正しくきれいで見やすいですが、耳で聞くと無味乾燥?な印象があります。一方、日常耳にする音(話し声、騒音・雑音、楽器の音、音楽など)は純音ではなく、もっと複雑な波が繰り返し続きます。それについては、「録音した音をグラフにして「見える化」する」を参照してください。

自習プロジェクトの目次に戻る