morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】sounddeviceでリアルタイムスペクトラムアナライザを作る


はじめに

前回の記事にて、python-sounddeviceというライブラリを使用して、マイク入力の信号をリアルタイムにプロットすることを試しました。

今回はこれを応用して、リアルタイムなスペクトラムアナライザを作成してみたいと思います。
スペクトラムアナライザ、通称スペアナは信号データを周波数成分別に表示するものです。
よくオーディオ機器の画面に表示されていたりするやつです。
今回は高速フーリエ変換(FFT)を用いてリアルタイムに振幅スペクトルを表示できるようにしてみたいと思います。




sounddeviceによる音声のリアルタイムプロット

前回使ったコードを少し変えるだけでスペアナの実装はできそうです。
↓詳細はこちら

参考までに前回のコードを少し修正したものを載せておきます。

import sounddevice as sd
import numpy as np
from matplotlib.animation import FuncAnimation
import matplotlib.pyplot as plt

device_list = sd.query_devices()
print(device_list)

sd.default.device = [1, 6] # Input, Outputデバイス指定

def callback(indata, frames, time, status):
    # indata.shape=(n_samples, n_channels)
    global plotdata
    data = indata[::downsample, 0]
    shift = len(data)
    plotdata = np.roll(plotdata, -shift, axis=0)
    plotdata[-shift:] = data


def update_plot(frame):
    """This is called by matplotlib for each plot update.
    """
    global plotdata
    line.set_ydata(plotdata)
    return line,

downsample = 10
length = int(1000 * 44100 / (1000 * downsample))
plotdata = np.zeros((length))

fig, ax = plt.subplots()
line, = ax.plot(plotdata)
ax.set_ylim([-1.0, 1.0])
ax.set_xlim([0, length])
ax.yaxis.grid(True)
fig.tight_layout()

stream = sd.InputStream(
        channels=1,
        dtype='float32',
        callback=callback
    )
ani = FuncAnimation(fig, update_plot, interval=30, blit=True)
with stream:
    plt.show()

FFT解析

スペクトラムアナライザは前回作成したコードのグラフを更新する関数にて、ただ信号をプロットするのではなく、FFTを行えばいいだけです。
FFT解析の実装方法については過去記事にて詳しくまとめてますので参照ください。
ほぼ以下の記事のコードのコピペで何とかなりそうです。

リアルタイムで収音された信号データはplotdataという配列の後ろからどんどん更新されていきますのでFFT解析で使用するデータはplotdataの後ろから2048個のデータとします。
高速フーリエ変換なので2の累乗のデータ数としています。




実装

それではさっそく実装していきましょう。
前回のコードでいじるのは、グラフの初期化の部分と、グラフ更新関数の部分です。
グラフはx軸に周波数、y軸に振幅スペクトルをプロットします。
全コードは以下です。

import sounddevice as sd
import numpy as np
from matplotlib.animation import FuncAnimation
import matplotlib.pyplot as plt
from scipy import signal

device_list = sd.query_devices()
print(device_list)

sd.default.device = [1, 6] # Input, Outputデバイス指定

def callback(indata, frames, time, status):
    # indata.shape=(n_samples, n_channels)
    global plotdata
    data = indata[::downsample, 0]
    shift = len(data)
    plotdata = np.roll(plotdata, -shift, axis=0)
    plotdata[-shift:] = data


def update_plot(frame):
    """This is called by matplotlib for each plot update.
    """
    global plotdata, window
    x = plotdata[-N:] * window
    F = np.fft.fft(x) # フーリエ変換
    F = F / (N / 2) # フーリエ変換の結果を正規化
    F = F * (N / sum(window)) # 窓関数による補正
    Amp = np.abs(F) # 振幅スペクトル
    line.set_ydata(Amp[:N // 2])
    return line,

downsample = 1  # FFTするのでダウンサンプリングはしない
length = int(1000 * 44100 / (1000 * downsample))
plotdata = np.zeros((length))
N =2048            # FFT用のサンプル数
fs = 44100            # 音声データのサンプリング周波数
window = signal.hann(N) # 窓関数
freq = np.fft.fftfreq(N, d=1 / fs) # 周波数スケール

fig, ax = plt.subplots()
line, = ax.plot(freq[:N // 2], np.zeros(N // 2))
ax.set_ylim([0, 1])
ax.set_xlim([0, 3000])
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Amplitude spectrum')
fig.tight_layout()

stream = sd.InputStream(
        channels=1,
        dtype='float32',
        callback=callback
    )
ani = FuncAnimation(fig, update_plot, interval=30, blit=True)
with stream:
    plt.show()

結果はこちら。
適当にマイクでしゃべった結果です。

動画ではカクついてますが、実際はカクつきは感じないです。
matplotlibはプロットが遅いとよく言われますが、30msecごとのプロットであれば違和感なくプロットできているように思います。

おわりに

matplotlibでスペクトラムアナライザを作成するのは難しいかと思ってましたが、案外ちゃんと機能するものができました。
もっとオーディオ機器の画面みたいにビニングして表示してみるのも面白そうです。