さて、次は「sounddevice」を使って、マイクからの入力を試してみよう!
おおー!
...とその前に、ざっくりと「sounddevice」について説明するね
「sounddevice」は、マイクからの音声入力やスピーカーからの音声出力を簡単にするライブラリで、音声データはNumPyの配列で数値として管理されてるのがポイントだね
これって、具体的にはどんなことができるの?
基本的には「マイクで収音」→「音声を処理」→「音声を再生」っていう流れでプログラムを作れるんだけど、それ以外にもリアルタイムで収音と処理を繰り返す「ストリーミング機能」も簡単に利用できるのが特徴なんだ
なるほど!
このストリーミング機能が、前回のMatplotlibのアニメーション機能と組み合わせられるのが良いんだね!
そういうこと!
ということで、実際にどんな感じでストリーミング機能が使えるのかを紹介するよ!
まずはいつも通り、pipでインストールしよう
pip install sounddevice
準備おっけー!
じゃあまずは、マイクからの音の大きさに応じて「#」の数が変化するシンプルなプログラムを動かしてみよう!
import numpy as np
import sounddevice as sd
SAMPLE_RATE = 44100
BLOCK_SIZE = 1024
class MicVolumeMeter:
"""マイクの音量を測定し、ターミナルに表示するクラス"""
def __init__(self):
self.current_volume = 0.0
def audio_callback(self, indata, frames, time, status):
"""マイクから新しい音声データが届くたびに自動で呼ばれる"""
# RMS(二乗平均平方根)で波形データを「音量」という1つの数字にする
self.current_volume = float(np.sqrt(np.mean(indata ** 2)))
# 音量を見やすくするため、簡易的な棒グラフ風に表示する
bar = "#" * int(self.current_volume * 100)
print(f"{self.current_volume:.4f} {bar}")
meter = MicVolumeMeter()
print("マイクに向かって話しかけてみてください。(Enterキーで終了)")
with sd.InputStream(
channels=1,
samplerate=SAMPLE_RATE,
blocksize=BLOCK_SIZE,
callback=meter.audio_callback,
):
input() # Enterキーが押されるまでプログラムを終了させずに待つ
おおー!声の大きさに合わせて「#」が増減してて面白い!
ここでポイントになるのは28行目の「sd.InputStream()」
「channels」は、マイクの入力チャンネル数(モノラル:1、ステレオ:2)を指定
「samplerate」は、1秒間に何回データを取得するか(Hz)を指定(今回は44100Hz)
「blocksize」は、「コールバック関数」が1回呼ばれる度に渡すデータの数を指定
「callback」は、データの更新ごとに動かす「コールバック関数」の指定
になるよ
with sd.InputStream(
channels=1,
samplerate=SAMPLE_RATE,
blocksize=BLOCK_SIZE,
callback=meter.audio_callback,
):
input() # Enterキーが押されるまでプログラムを終了させずに待つ
なんだかちょっと難しそうな設定項目があるね...
まぁこれに関しては「音」についての専門設定だからね(汗)
基本的には
・「channels」:1
・「samplerate」:44100 か 48000
・「blocksize」:1024 などの2の累乗の値
に設定しておけば問題なく使えると思うよ!
じゃあ説明中に出てきた「コールバック関数」ってなんなの?
これは、Matplotlibの時と同じく、新しい音声データが届く度に自動で呼ばれる関数って思っておいてくれて良いよ
ただ、「sd.InputStream()」で扱うコールバック関数の場合はルールがあって、関数を定義する場合は「def 関数名(indata, frames, time, status):」(14行目)という形で作る必要があるんだ
def audio_callback(self, indata, frames, time, status):
"""マイクから新しい音声データが届くたびに自動で呼ばれる"""
# RMS(二乗平均平方根)で波形データを「音量」という1つの数字にする
self.current_volume = float(np.sqrt(np.mean(indata ** 2)))
# 音量を見やすくするため、簡易的な棒グラフ風に表示する
bar = "#" * int(self.current_volume * 100)
print(f"{self.current_volume:.4f} {bar}")
関数の定義方法にルールがあるんだね!
この時の引数はそれぞれ、
・「indata」:マイクから取得した音声データ(NumPy配列)
・「frames」:さっきの「blocksize」に設定されたデータの数
・「time」:音声を取得した時間やコールバックが呼ばれた時間情報
・「status」:処理が正常に行われているかの状態情報
というデータが自動的に渡されるようになるよ
「sd.InputStream()」から呼び出された時に、それぞれの引数に情報が入る仕組みなんだね!
そういうことだね
あと、「sd.InputStream()」にはファイルの「open()」の時のように、最初に「マイクを開く準備」、最後に「使用したマイクを解放する処理」が含まれているから、「with」ブロック(28行目)を使って、Enterキーが押されるまで動くようにしてるよ
with sd.InputStream(
channels=1,
samplerate=SAMPLE_RATE,
blocksize=BLOCK_SIZE,
callback=meter.audio_callback,
):
input() # Enterキーが押されるまで待つ(withを抜けるとマイクが解放される)
なるほど!
これのおかげで、自分で無限ループを書かなくても動き続けてくれるんだね!
次は、コールバック関数「audio_callback()」の中身について
この関数では、生の波形データを「二乗平均平方根(RMS)」(17行目)で「音量」っていう1つの数値に変換しているよ
# RMS(二乗平均平方根)で波形データを「音量」という1つの数字にする
self.current_volume = float(np.sqrt(np.mean(indata ** 2)))
なんか難しそうな計算をやってる...!
要は、波形データは「blocksize」(1024個)の数だけあるから、その平均値を出してるって感じだね
ただ、「波形」って基本的にはプラスとマイナスの値を交互に繰り返すから、普通に平均値を計算してしまうとプラスマイナスが打ち消し合ってほぼ「0」になっちゃうんだよね
それじゃ音の大きさが測れなくて困るね
だから、全部の値を2乗してマイナスを消してから平均を取り、その平方根(root)を計算することで、音の大きさに応じたプラスの数値を取り出しているのが今回の方法(RMS)だよ
なるほど!
扱いやすい音量データに変換してるんだね!
まぁそれくらいの認識で良いよ!
あとは、その計算結果を100倍して整数の分だけ「#」の文字列を作って表示(20行目)することで、声の大きさを表現してるって感じだよ
bar = "#" * int(self.current_volume * 100)
print(f"{self.current_volume:.4f} {bar}")
ほうほう!
こう見ると、メインの処理は「with」の部分だけだし、あとはコールバック関数の中身を作るだけで動いてくれるシンプルな構造なんだね!
そうでしょ?
使用するマイクについては、指定がなければPCのOSで現在標準設定されているマイクが自動的に使われるから、基本的にはこれでバッチリ動いてくれるよ!
もし外付けマイクとか、別のマイクを選びたい時はどうするの?
そう言われると思いまして!
最後にちょっとおまけで、使用するマイクを選択できるバージョンのプログラムも紹介しておくね!
用意周到...!!
import numpy as np
import sounddevice as sd
SAMPLE_RATE = 44100
BLOCK_SIZE = 1024
def choose_device():
"""使用できるマイク一覧をターミナルに表示し、選ばれたデバイス番号を返す"""
devices = sd.query_devices()
input_devices = [(i, d["name"]) for i, d in enumerate(devices) if d["max_input_channels"] > 0]
print("使用できるマイク一覧:")
for i, name in input_devices:
print(f" {i}: {name}")
valid_indices = [i for i, _ in input_devices]
while True:
text = input("使用するマイクの番号を入力してください: ")
if text.isdigit() and int(text) in valid_indices:
return int(text)
print("番号が正しくありません。もう一度入力してください。")
class MicVolumeMeter:
"""マイクの音量を測定し、ターミナルに表示するクラス"""
def __init__(self):
self.current_volume = 0.0
def audio_callback(self, indata, frames, time, status):
"""マイクから新しい音声データが届くたびに自動で呼ばれる"""
self.current_volume = float(np.sqrt(np.mean(indata ** 2)))
bar = "#" * int(self.current_volume * 100)
print(f"{self.current_volume:.4f} {bar}")
device_index = choose_device()
meter = MicVolumeMeter()
print("マイクに向かって話しかけてみてください。(Enterキーで終了)")
with sd.InputStream(
device=device_index, # 選択したマイク番号を指定
channels=1,
samplerate=SAMPLE_RATE,
blocksize=BLOCK_SIZE,
callback=meter.audio_callback,
):
input()
マイクを選択したい場合は、「choose_device()」(8行目)という関数を用意して、「sd.query_devices()」で使用可能なマイク一覧を取得しているよ
そこで選ばれた番号を「sd.InputStream(device=device_index)」(45行目)のように「device」引数に渡してあげれば、好きなマイクを指定して録音できるようになるんだ!
def choose_device():
"""使用できるマイク一覧をターミナルに表示し、選ばれたデバイス番号を返す"""
devices = sd.query_devices()
input_devices = [(i, d["name"]) for i, d in enumerate(devices) if d["max_input_channels"] > 0]
print("使用できるマイク一覧:")
for i, name in input_devices:
print(f" {i}: {name}")
# ...番号を入力して return int(text)
device_index = choose_device()
with sd.InputStream(
device=device_index, # 選択したマイク番号を指定
channels=1,
samplerate=SAMPLE_RATE,
blocksize=BLOCK_SIZE,
callback=meter.audio_callback,
):
input()
へぇー!マイクを個別に指定したい時も、関数をひとつ追加して「device」引数を渡すだけで簡単にカスタマイズできるんだね!
そういうこと!
じゃあ次回は、Matplotlibのプログラムと今回のプログラムを合体させようか!
