morikomorou’s blog

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

【python】sounddeviceで音声のリアルタイムプロット


はじめに

以前の記事で音声データの読み取り方法やFFT解析の方法についての説明を行いました。

今回はマイクからの入力信号をリアルタイムでプロットする方法についてやっていきたいと思います。




音声の入出力

使用するライブラリ

Pythonで音声信号の収音・再生・録音を行うためのライブラリはいろいろありますが、リアルタイム処理等においてpython-sounddeviceが使いやすいとのことなのでこれを使ってみたいと思います。
サンプルコードを参考に作ってみます。

こちらのブログも大変参考になりました。

デバイスの選択

まずは、入出力のデバイスの選択を行います。
下記コードで自分のPC上の入出力デバイス一覧を取得できます。

import sounddevice as sd

device_list = sd.query_devices()
print(device_list)
   0 Microsoft Sound Mapper - Input, MME (2 in, 0 out)
>  1 マイク (Realtek High Definition Au, MME (2 in, 0 out)
   2 マイク (JBL Pebbles), MME (2 in, 0 out)
   3 FaceRig Virtual Microphone (Fac, MME (2 in, 0 out)
   4 マイク (DroidCam Virtual Audio), MME (1 in, 0 out)
   5 Microsoft Sound Mapper - Output, MME (0 in, 2 out)
<  6 スピーカー (JBL Pebbles), MME (0 in, 2 out)
   7 スピーカー (FaceRig Virtual Audio Dr, MME (0 in, 2 out)
   8 Acer ET241Y (NVIDIA High Defini, MME (0 in, 2 out)
   9 マイク (Realtek HD Audio Mic input), Windows WDM-KS (2 in, 0 out)
  10 ライン入力 (Realtek HD Audio Line input), Windows WDM-KS (2 in, 0 out)
  11 ステレオ ミキサー (Realtek HD Audio Stereo input), Windows WDM-KS (2 in, 0 out)
  12 Speakers (Realtek HD Audio output), Windows WDM-KS (0 in, 8 out)
  13 MIDI (DroidCam Audio), Windows WDM-KS (1 in, 0 out)
  14 Output (DroidCam Audio), Windows WDM-KS (0 in, 1 out)
  15 Input (Larmkanal), Windows WDM-KS (2 in, 0 out)
  16 Output (Larmkanal), Windows WDM-KS (0 in, 2 out)
  17 Output (NVIDIA High Definition Audio), Windows WDM-KS (0 in, 2 out)
  18 スピーカー (JBL Pebbles), Windows WDM-KS (0 in, 2 out)
  19 マイク (JBL Pebbles), Windows WDM-KS (2 in, 0 out)

デフォルトの入出力デバイスが現状1番のマイクと、6番のスピーカとなっています
これの変更は下記コードでできます。
(今回は変更しませんが)

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

リアルタイム収音

さっそくリアルタイム収音をやってみましょう。
python-sounddeviceには入出力信号をリアルタイムでnumpy配列として処理できるStreamというクラスが実装されておりますので、これを使えば簡単にリアルタイム処理が書けます。
収音にはInputStreamというクラスを使用します。
下記は10秒間マイクからの入力信号をNumpy配列にしてprintするという処理を行うコードです。
callback関数を作成しておき、その中でリアルタイムに処理したい処理を書きます。

import sounddevice as sd
import numpy as np

sd.default.device = [1, 6] # Input, Outputデバイス指定
duration = 10  # 10秒間収音する

def callback(indata, frames, time, status):
    # indata.shape=(n_samples, n_channels)
    # print root mean square in the current frame
    print(indata)

with sd.InputStream(
        channels=1,
        dtype='float32',
        callback=callback
    ):
    sd.sleep(int(duration * 1000))

こんな感じの出力になります。

[[ 0.0000000e+00]
 [ 9.1552734e-05]
 [ 6.1035156e-05]
 ...
 [-9.1552734e-05]
 [-2.7465820e-04]
 [-1.2207031e-04]]
[[ 0.00012207]
 [ 0.00018311]
 [ 0.00045776]
 ...
 [-0.00033569]
 [-0.0005188 ]
 [-0.00048828]]

...

[[-0.00045776]
 [-0.00021362]
 [-0.00018311]
 ...
 [ 0.00097656]
 [ 0.00088501]
 [ 0.00073242]]
[[3.9672852e-04]
 [6.1035156e-05]
 [6.1035156e-04]
 ...
 [2.7465820e-04]
 [3.0517578e-04]
 [3.0517578e-04]]


indataの要素数を見てみます。

import sounddevice as sd
import numpy as np

sd.default.device = [1, 6] # Input, Outputデバイス指定
duration = 10  # 10秒間収音する

def callback(indata, frames, time, status):
    # indata.shape=(n_samples, n_channels)
    # print root mean square in the current frame
    print(indata.shape)

with sd.InputStream(
        channels=1,
        dtype='float32',
        callback=callback
    ):
    sd.sleep(int(duration * 1000))
(1136, 1)
(1136, 1)
(1136, 1)
...
(1136, 1)
(1136, 1)

(1136, 1)の配列となっています。
1136は1chunk分の信号データで、サンプリング周波数が44100Hzなので0.026sec分の信号データが毎回indataに渡されていることがわかります。
1次元なのは、channels=1としたためであり、マイクがモノラル入力なのでこうなってます。

あとはこのデータをグラフ化するだけです。

リアルタイム収音データのプロット

グラフには1秒間分のデータ数をプロットすることとします。
とりあえず1秒分のデータを0で初期化してプロットしてみます。

import sounddevice as sd
import numpy as np
import matplotlib.pyplot as plt

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.yaxis.grid(True)
fig.tight_layout()
plt.show()

結果はこちら。

このplotdataをcallback関数で更新していくことでリアルタイムプロットができます。
リアルタイム収音では0.026秒分のデータが毎回取得できるのでplotdataの前半0.026秒分は削除して、後ろに取得した0.026秒分のデータを追加する処理です。
作成したcallback関数は以下です。

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

np.rollでリアルタイム収音したデータ数分、plotdataを左にシフトさせたあと、右側のデータを収音したデータで置き換える作業をしています。

全コード



あとはアニメーションにするだけです。
matplotlibを用いたアニメーションの方法は、下記記事を参照ください。

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()

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

体感ですが、そこまでカクツキは気にならなかったです。
ラグもほぼなくプロットできてそうです。

おわりに

自分で作成したプログラムで自分の声がリアルタイムでプロットできるのは面白いですね。
sounddeviceではボイスチェンジャーみたいなのも作成できるようなのでぜひ試してみたいですね。