7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

まえがき

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

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

01.png

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

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

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

#目標

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

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

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

#環境

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

■Windows10
■Python3.7

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


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

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

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

#ソースコード

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の数値から指定します.
デバイスインデックスの値がわからない場合は以下のコードで調べることができます.

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もそれぞれ指定していただいて構いません.

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に入力し,1秒ごとにおける音声波形の正の値の最大値を抽出しframesに追加しております.

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

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

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くらいが丁度良い値のようです.
この値も自身の環境にあわせて変更してください.

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の中に適当に入れておきました.

try:
    while True:
        surveillance()

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

stream.close()
audio.terminate()

あとはプログラムをループさせて監視します.

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

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

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

まとめ

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

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?