今回は「Matplotlib」と「sounddevice」の連携だよ!
待ってました!これでマイクの音がそのままグラフになるんだね!
作成の流れとしては、「Matplotlib」でアニメーショングラフの土台を作って、「sounddevice」でマイクから入力されたデータを加工して、グラフを表示するための値を更新していくって感じになるよ
ほぅほぅ
さっそく完成形のプログラムを動かしてみよう!
了解!コピー&ペーストで試してみるね!
import numpy as np
import sounddevice as sd
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import japanize_matplotlib
HISTORY_LEN = 100 # グラフに表示する過去データの点数
class MicVolumeAnimator:
"""マイクの音量を取得しつつ、グラフに描画するクラス"""
def __init__(self, ax):
self.volume_history = [0.0] * HISTORY_LEN
self.current_volume = 0.0
self.line, = ax.plot(self.volume_history)
def audio_callback(self, indata, frames, time, status):
"""マイクから音声データが届くたびに自動で呼ばれる(Step3と同じ役割)"""
self.current_volume = float(np.sqrt(np.mean(indata ** 2)))
def update(self, frame):
"""一定間隔ごとに自動で呼ばれ、グラフを描き直す(Step2と同じ役割)"""
self.volume_history.append(self.current_volume)
self.volume_history.pop(0)
self.line.set_ydata(self.volume_history)
return self.line,
fig, ax = plt.subplots()
ax.set_ylim(0, 1.0)
ax.set_title("マイクの音量が動くグラフになる")
ax.set_xlabel("時間 (フレーム)")
ax.set_ylabel("音量")
animator = MicVolumeAnimator(ax)
stream = sd.InputStream(
channels=1,
samplerate=44100,
blocksize=1024,
callback=animator.audio_callback,
)
with stream:
ani = animation.FuncAnimation(fig, animator.update, interval=50)
plt.show()
おおー!声を出すとリアルタイムでグラフが波打って動いてる!
でもおねーちゃん、今回のプログラムってクラス(MicVolumeAnimator)を使ってるよね?
なんでわざわざクラスを使うの?global変数とかじゃダメなの?
良い質問だね!
実は、ここが「global変数ではなくクラスを使う一番のメリット」が分かるポイントなんだ
ほぅ
マイクから音声を取得する「audio_callback()」(19行目)と、グラフを描き直す「update()」(23行目)は、それぞれ別々のタイミングで勝手に呼ばれる関数なんだよね
def audio_callback(self, indata, frames, time, status):
"""マイクから音声データが届くたびに自動で呼ばれる"""
self.current_volume = float(np.sqrt(np.mean(indata ** 2)))
def update(self, frame):
"""一定間隔ごとに自動で呼ばれ、グラフを描き直す"""
self.volume_history.append(self.current_volume)
self.volume_history.pop(0)
self.line.set_ydata(self.volume_history)
return self.line,
あ、マイク側とグラフ側で動きのサイクルが違うんだ!
そう!
この2つの間で「今の音量」を共有したい時に、global変数を使うこともできるんだけど...
クラスを使えば、「self.current_volume」(15行目)っていう同じクラス内の変数を共有するだけで、間接的に安全にデータのやり取りができるんだよ
class MicVolumeAnimator:
def __init__(self, ax):
self.volume_history = [0.0] * HISTORY_LEN
self.current_volume = 0.0 # 2つのコールバックで共有する音量
self.line, = ax.plot(self.volume_history)
def audio_callback(self, indata, frames, time, status):
self.current_volume = float(np.sqrt(np.mean(indata ** 2))) # 書き込み
def update(self, frame):
self.volume_history.append(self.current_volume) # 読み取り
self.volume_history.pop(0)
self.line.set_ydata(self.volume_history)
return self.line,
なるほど!
audio_callback() で self.current_volume に音量を書き込んで、update() でその self.current_volume を読み取ってグラフに反映させてるんだね!
そういうこと!
グローバル変数と違って、この値は「MicVolumeAnimator」クラスの中だけで共有されるから、プログラムの他の場所から誤って書き換えられる心配がないのもメリットだね
クラスを使えば安全でスッキリ書けるんだね!
あともう一つのポイントは、グラフの履歴データ「self.volume_history」(14行目)の更新方法
25行目〜26行目で「.append(self.current_volume)」で最新の音量を配列の一番後ろに追加して、「.pop(0)」で配列の一番最初のデータ(一番古いデータ)を削除してるよ
self.volume_history.append(self.current_volume)
self.volume_history.pop(0)
self.line.set_ydata(self.volume_history)
要素を追加して古いものを捨てることで、データを常に100個でキープして、グラフが左に流れていくようなアニメーションになるんだね!
そういうこと
あとは、その配列を「self.line.set_ydata()」(27行目)に渡せば、グラフの描画が更新されるよ
流れがすごくわかりやすい!
Matplotlibとsounddeviceを繋げるのは
1. クラスの初期化でグラフの線(self.line)を用意(17行目)
2. sounddeviceのcallbackにクラスの「audio_callback」を指定(42行目)
3. FuncAnimationの更新関数にクラスの「update」を指定(46行目)
ってすれば、綺麗に連動させることができるよ!
animator = MicVolumeAnimator(ax)
stream = sd.InputStream(
channels=1,
samplerate=44100,
blocksize=1024,
callback=animator.audio_callback, # マイク側
)
with stream:
ani = animation.FuncAnimation(fig, animator.update, interval=50) # グラフ側
plt.show()
sounddevice が音を取得してクラス変数を更新して、FuncAnimation がそのクラス変数を読み取ってグラフを描き直す...凄い連携だね!
そうでしょ?
ちなみに、もし特定のマイクを指定したい場合は、Step3の最後に紹介した「choose_device()」関数を追加する方法を使えば良いよ!
なるほど!
基本のコードをシンプルにしておけば、マイク選択が必要な時だけ後から追加できるから便利だね!
じゃあ次回は、この音量グラフに「一定の音量を超えたらカウントする」機能を追加していくよ!
楽しみ!
