タイトル画像
琴葉茜

今回は「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()」関数を追加する方法を使えば良いよ!

琴葉葵

なるほど!

琴葉葵

基本のコードをシンプルにしておけば、マイク選択が必要な時だけ後から追加できるから便利だね!

琴葉茜

じゃあ次回は、この音量グラフに「一定の音量を超えたらカウントする」機能を追加していくよ!

琴葉葵

楽しみ!