タイトル画像
琴葉茜

さて、次は「sounddevice」を使って、マイクからの入力を試してみよう!

琴葉葵

おおー!

琴葉茜

...とその前に、ざっくりと「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のプログラムと今回のプログラムを合体させようか!