流行りのWeb飲み会,Pythonで無音状態を検知して音声を流す．

# まえがき

世が世なので流行っているWeb飲み会．

しかしある程度会を重ねていくと，必ず**だれも発言しないタイミング**っていうのが生じますよね．
そもそもみんなずっと家にいるんだから話題そのものもあんまりなくって．
そんな飲み会参加すんなよって思う方もいらっしゃると思いますが，これが断る理由がなくって難しいんですよ．

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629378/2063cd19-2ea2-469c-6c40-8c08b7d58b18.png)


どんなに仲が良いやつでも，しゃべる内容がなくなると飲み会は無音状態となります．

飲み会自体はだらだらと続いてしまうもんですから，気まずいったらありゃしない．
そんな状態はいかなる理由であれ最悪です．

そこで今回はWeb飲み会における無音状態**(による気まずい状態)**を回避するべく，無音状態を検知して音声を流すプログラムをPythonで作成したいと思います．(気休めですけどね)

#目標

私は普段からWeb会議等打ち合わせにはZoomを使用しているので，**Zoom**で利用することを目的としています．

しかし実際はシステム全体の音声を監視することになるのでおそらくどのソフトでも対応可能のはずです．

目標はZoomの音声入力を10秒間監視して，入力が無い＝無音状態と判断した場合，指定したフォルダから音楽ファイルをランダムに再生します．

#環境

思い付きで始めたのでプログラム言語には**Python**を使用しておりますが特に意味はありません．
動作環境は以下の通りです．

■Windows10
■Python3.7

使用したライブラリは以下の通りです．

```python

import pyaudio
import numpy as np
import wave
import math

from mutagen.mp3 import MP3 as mp3
import pygame
import time

import glob
import random
import sys
```

私は個人的になるべく良い音質でZoomを使用したかったので，オーディオインターフェイスとマイクを別に用意しております(恐らくZoom側で音声はかなりカットされているのであまり意味はありません，自己満です)．

![03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629378/27005b4f-142d-cf7b-d0e2-875a986befb1.jpeg)

■marantz / AUDIO SCOPE SG-5BC
■CREATIVE / SB X-Fi Surround 5.1

それではプログラムを書いていきます．

#ソースコード

```python
audio = pyaudio.PyAudio()

def system(FORMAT, CHANNELS, RATE, CHUNK):

    stream = audio.open(format=FORMAT,
                        channels=CHANNELS,
                        rate=RATE,
                        input=True,
                        output=True,
                        input_device_index=1,#←適したインデックスに変更してください.
                        output_device_index=7,#←適したインデックスに変更してください.
                        frames_per_buffer=CHUNK)

    return stream
```

はじめにマイクで入力された音声を監視するために`pyaudio.PyAudio()`をインスタンス化して使用します．

監視する入力音声は` input_device_index`の数値から指定します．
デバイスインデックスの値がわからない場合は以下のコードで調べることができます．

```python
for index in range(0, p.get_device_count()):
    print(p. get_device_info_by_index(index))
```

私の環境を例にして出力すると以下のようになります．

```
{'index': 0, 'structVersion': 2, 'name': 'Microsoft サウンド マッパー - Input', 'hostApi': 0, 'maxInputChannels': 2, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 1, 'structVersion': 2, 'name': '再生リダイレクト (SB X-Fi Surround 5.1)', 'hostApi': 0, 'maxInputChannels': 2, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 2, 'structVersion': 2, 'name': 'ライン (USB2.0 High-Speed True HD ', 'hostApi': 0, 'maxInputChannels': 2, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 3, 'structVersion': 2, 'name': 'ライン/マイク入力 (SB X-Fi Surround 5.1', 'hostApi': 0, 'maxInputChannels': 2, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 4, 'structVersion': 2, 'name': 'SPDIF In (USB2.0 High-Speed Tru', 'hostApi': 0, 'maxInputChannels': 2, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 5, 'structVersion': 2, 'name': 'マイク (USB2.0 High-Speed True HD ', 'hostApi': 0, 'maxInputChannels': 2, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 6, 'structVersion': 2, 'name': 'Microsoft サウンド マッパー - Output', 'hostApi': 0, 'maxInputChannels': 0, 'maxOutputChannels': 2, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 7, 'structVersion': 2, 'name': 'スピーカー (SB X-Fi Surround 5.1)', 'hostApi': 0, 'maxInputChannels': 0, 'maxOutputChannels': 6, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 8, 'structVersion': 2, 'name': 'SPDIF出力 (SB X-Fi Surround 5.1)', 'hostApi': 0, 'maxInputChannels': 0, 'maxOutputChannels': 6, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 9, 'structVersion': 2, 'name': 'SPDIF Out (USB2.0 High-Speed Tr', 'hostApi': 0, 'maxInputChannels': 0, 'maxOutputChannels': 2, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
{'index': 10, 'structVersion': 2, 'name': 'スピーカー (USB2.0 High-Speed True H', 'hostApi': 0, 'maxInputChannels': 0, 'maxOutputChannels': 8, 'defaultLowInputLatency': 0.09, 'defaultLowOutputLatency': 0.09, 'defaultHighInputLatency': 0.18, 'defaultHighOutputLatency': 0.18, 'defaultSampleRate': 44100.0}
```

私の環境だといくつかオーディオインターフェイスがつながっているのでインデックスはこのように山ほど出てきます．

今回は入力される音声だけでなく，Zoomで相手方が話している状態や共有している画面の状態も読み取る必要があります．

したがってこの場合はシステム全体を監視するために，使用するインデックスは再生リダイレクト，1ということになります．
この値は各自調べて適した値を代入してください．

`output_device_index`を指定している項目がありますが，これは`wav`形式の音声ファイルを再生したいがために指定しております．
とくに`wav`ファイルを再生する予定がない方はこの項目は不要です．
そもそもそれがためにわざわざ関数化しているため，予定がない人は関数化せず`FORMAT, CHANNELS, RATE, CHUNK`もそれぞれ指定していただいて構いません．

```python
frames = []

def surveillance():

    print("Under surveillance...")

    FORMAT = pyaudio.paInt16
    CHANNELS = 1  # モノラル
    RATE = 44100  # サンプルレート
    CHUNK = 2 ** 11  # データ点数
    RECORD_SECONDS = 10  # 録音する時間の長さ

    stream = system(FORMAT, CHANNELS, RATE, CHUNK)

    for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
        buf = stream.read(CHUNK)
        data = np.frombuffer(buf, dtype="int16")
        frames.append(max(data))

    stream.stop_stream()

    calculation()
```

関数名は"監視"です．

ここでは10秒間音声をpythonに入力し，１秒ごとにおける音声波形の**正の値の最大値**を抽出し`frames`に追加しております．

少し解説をいたしますと，この場合`RATE=44100`となっておりますのでサンプリング周波数は44.1khzということになります．
これはつまり１秒間に44100個の音量レベルを取得していることになります．
音とは波であり，波である以上は負の値も当然含まれます．
もし1/44.1秒ごとの正確なレベルを調べたい場合は絶対値を取得する必要がありますが，今回は10秒の間に音がしているか判断できれば良いので最大値のみを保存しています．

取得した値は`np.frombuffer`でダイナミックレンジ16bitの2**16の段階に変換されています．
しかし先にも述べたように正負の値を有しているので最大値は32767となります．

```python
def calculation():

    print("Calculation")

    rms = (max(frames))

    db = 20 * math.log10(rms) if rms > 0.0 else -math.inf
    print(f"RMS：{format(db, '3.1f')}[dB]")

    if (db<=65):#←環境にあわせて数字は調整してください
        random_music()
        #disc_jockey()

    else:
        pass

    frames.clear()
```

次に無音状態を判断する関数です．

取得した値を対数化してレベルの変化をわかりやすくします．
スレッショルドを決定して`if`で分岐します．私の環境では大体65dBくらいが丁度良い値のようです．
この値も自身の環境にあわせて変更してください．

```python
def random_music():

    print("Random music")

    files = [r.split('/')[-1] for r in glob.glob('./data/*.mp3')]
    filename = random.choice(files)  # 再生したいmp3ファイル
    print(filename)

    pygame.mixer.init()
    pygame.mixer.music.load(filename)  # 音源を読み込み
    mp3_length = mp3(filename).info.length  # 音源の長さ取得
    pygame.mixer.music.play(1)  # 再生開始。1の部分を変えるとn回再生(その場合は次の行の秒数も×nすること)
    time.sleep(mp3_length + 0.25)  # 再生開始後、音源の長さだけ待つ(0.25待つのは誤差解消)
    pygame.mixer.music.stop()  # 音源の長さ待ったら再生停止
```
最後に任意のフォルダからランダムに`mp3`ファイルを抽出して再生するプログラムとなります．
私の場合はソースコードのディレクトリ直下の`data`の中に適当に入れておきました．

```python
try:
    while True:
        surveillance()

except KeyboardInterrupt:
    print("Emergency stop")
    sys.exit(0)

stream.close()
audio.terminate()
```
あとはプログラムをループさせて監視します．

緊急で無言が破られたときは`ctrl+c`で終了します．

```python
def disc_jockey():

    print("Play...")

    filename = "./disc_jockey.wav"
    wf = wave.open(filename, "rb")

    FORMAT = audio.get_format_from_width(wf.getsampwidth())
    CHANNELS = wf.getnchannels()
    RATE = wf.getframerate()
    CHUNK = wf.getnframes()

    stream = system(FORMAT, CHANNELS, RATE, CHUNK)

    data = wf.readframes(CHUNK)
    stream.write(data)

    stream.start_stream()
    stream.stop_stream()
    stream.close()

    random_music()
```

ところで最初のほうに`wav`も再生させたいと述べていたのでその方法も記載しておきます．

基本的には録音方法と同じですが，こちらの場合はファイルの状態に合わせて値を決定する必要があるので，値はそれぞれ`wave`で取得して代入します．

関数名がDJとなっているのは，**いきなり音楽が流れても相手方は意味不明なので**ここでは曲紹介音声を入れようと思って作成したのです．

そして手元にあったデータが**クリス〇プラーさん**の「ここで曲の方いきましょう」と、**物真似をしている伊〇院光さん**の音声`wav`ファイルだったのでこうなりました．

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629378/3385b2dd-03ee-c669-1e9a-959099fd8a48.png)


…何故あったかは不明です．

# まとめ

某ウイルスのせいで外にも出れず，無言のWeb飲み会はまだまだ続きますが，みなさんがんばりましょう．