web-dev-qa-db-ja.com

マイクからスペクトログラムを生成する

以下は、マイクから入力を取得するコードです。オーディオブロックの平均が特定のしきい値を超えると、オーディオブロックのスペクトログラム(長さ30 ms)が生成されます。通常の会話の途中で生成されたスペクトログラムは次のようになります。

enter image description here

私が見たものから見ると、これは、オーディオとその環境を考えると、スペクトログラムがどのように見えると期待しているかには似ていません。私は次のようなものを期待していました(スペースを節約するために転置):

enter image description here

私が録音しているマイクは私のMacbookのデフォルトですが、何が問題なのかについての提案はありますか?


record.py:

import pyaudio
import struct
import math
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt


THRESHOLD = 40 # dB
RATE = 44100
INPUT_BLOCK_TIME = 0.03 # 30 ms
INPUT_FRAMES_PER_BLOCK = int(RATE * INPUT_BLOCK_TIME)

def get_rms(block):
    return np.sqrt(np.mean(np.square(block)))

class AudioHandler(object):
    def __init__(self):
        self.pa = pyaudio.PyAudio()
        self.stream = self.open_mic_stream()
        self.threshold = THRESHOLD
        self.plot_counter = 0

    def stop(self):
        self.stream.close()

    def find_input_device(self):
        device_index = None
        for i in range( self.pa.get_device_count() ):
            devinfo = self.pa.get_device_info_by_index(i)
            print('Device %{}: %{}'.format(i, devinfo['name']))

            for keyword in ['mic','input']:
                if keyword in devinfo['name'].lower():
                    print('Found an input: device {} - {}'.format(i, devinfo['name']))
                    device_index = i
                    return device_index

        if device_index == None:
            print('No preferred input found; using default input device.')

        return device_index

    def open_mic_stream( self ):
        device_index = self.find_input_device()

        stream = self.pa.open(  format = pyaudio.Paint16,
                                channels = 1,
                                rate = RATE,
                                input = True,
                                input_device_index = device_index,
                                frames_per_buffer = INPUT_FRAMES_PER_BLOCK)

        return stream

    def processBlock(self, snd_block):
        f, t, Sxx = signal.spectrogram(snd_block, RATE)
        plt.pcolormesh(t, f, Sxx)
        plt.ylabel('Frequency [Hz]')
        plt.xlabel('Time [sec]')
        plt.savefig('data/spec{}.png'.format(self.plot_counter), bbox_inches='tight')
        self.plot_counter += 1

    def listen(self):
        try:
            raw_block = self.stream.read(INPUT_FRAMES_PER_BLOCK, exception_on_overflow = False)
            count = len(raw_block) / 2
            format = '%dh' % (count)
            snd_block = np.array(struct.unpack(format, raw_block))
        except Exception as e:
            print('Error recording: {}'.format(e))
            return

        amplitude = get_rms(snd_block)
        if amplitude > self.threshold:
            self.processBlock(snd_block)
        else:
            pass

if __name__ == '__main__':
    audio = AudioHandler()
    for i in range(0,100):
        audio.listen()

コメントに基づく編集:

レートを16000 Hzに制限し、カラーマップに対数スケールを使用する場合、これはマイクの近くをタップするための出力です。

enter image description here

これはまだ少し奇妙に見えますが、正しい方向への一歩のようにも見えます。

Soxを使用して、私のプログラムから生成されたスペクトログラムと比較します。

15
syb0rg

まず、コードが最大100のスペクトログラム(processBlockが複数回呼び出された場合)を重ねてプロットし、最後のスペクトログラムのみが表示されることを確認します。あなたはそれを修正したいかもしれません。さらに、30msのオーディオ録音を使用する理由を知っていると思います。個人的には、ラップトップマイクで30ミリ秒録音して興味深い洞察を得ることができる実用的なアプリケーションは考えられません。何を録音しているのか、どのように録音をトリガーしているのかに依存しますが、この問題は実際の質問に接しています。

それ以外の場合、コードは完全に機能します。 processBlock関数にいくつかの小さな変更を加え、背景知識を適用するだけで、有益で美的なスペクトログラムを取得できます。

それでは、実際のスペクトログラムについて話しましょう。 SoXの出力を参照として使用します。カラーバー注釈は、それがdBFSであることを示しています1、これは対数測定です(dBは Decibel の略です)。それでは、まずスペクトログラムをdBに変換しましょう。

    f, t, Sxx = signal.spectrogram(snd_block, RATE)   
    dBS = 10 * np.log10(Sxx)  # convert to dB
    plt.pcolormesh(t, f, dBS)

enter image description here

これにより、カラースケールが改善されました。これで、以前は隠されていた高い周波数帯域にノイズが表示されます。次に、時間解決に取り組みます。スペクトログラムは信号をセグメントに分割し(デフォルトの長さは256)、それぞれのスペクトルを計算します。これは、周波数分解能は優れていますが、時間分解能が非常に低いことを意味します。これは、そのようなセグメントが信号ウィンドウ(約1300サンプル長)に収まるためです。時間と周波数分解能の間には常にトレードオフがあります。これは 不確実性の原則 に関連しています。それでは、信号をより短いセグメントに分割することにより、いくつかの周波数分解能を時間分解能と交換してみましょう。

f, t, Sxx = signal.spectrogram(snd_block, RATE, nperseg=64)

enter image description here

すごい!これで、両方の軸で比較的バランスの取れた解像度が得られましたが、お待ちください!なぜ結果がそれほどピクセル化されているのですか?実際、これは30msの短い時間枠にあるすべての情報です。 1300のサンプルを2次元で分布させる方法は非常に多くあります。ただし、少しごまかして、より高いFFT解像度とオーバーラップセグメントを使用できます。これにより、追加情報は提供されませんが、結果はよりスムーズになります。

f, t, Sxx = signal.spectrogram(snd_block, RATE, nperseg=64, nfft=256, noverlap=60)

enter image description here

かなりのスペクトル干渉パターンを見よ。 (これらのパターンは、使用されるウィンドウ関数によって異なりますが、ここでは詳しく説明しません。これらを操作するには、スペクトログラム関数のwindow引数を参照してください。)結果は見栄えが良いですが、実際には何も含まれていません。前の画像よりも多くの情報。

結果をより多くのSoX-lixeにするために、SoXスペクトログラムが時間軸でかなり不鮮明になっていることを確認します。元の低時間解像度(長いセグメント)を使用してこの効果を得ますが、スムーズにするためにそれらを重ね合わせます。

f, t, Sxx = signal.spectrogram(snd_block, RATE, noverlap=250)

enter image description here

私は個人的に3番目のソリューションを好みますが、自分の好みの時間/周波数のトレードオフを見つける必要があります。

最後に、SoXに似たカラーマップを使用してみましょう。

plt.pcolormesh(t, f, dBS, cmap='inferno')

enter image description here

次の行の短いコメント:

THRESHOLD = 40 # dB

しきい値は、入力信号のRMS=と比較されます。これは、dBで測定されますが生の振幅単位ではではありません


1 どうやらFSはフルスケールの略です。dBFSは、dB測定値が最大範囲に関連していることを意味します。0dBは現在の表現で可能な最大の信号なので、実際の値は<= 0でなければなりませんdB。

15
kazemakase

[〜#〜] update [〜#〜]私の答えをより明確にし、うまくいけば@kazemakaseによる優れた説明を補完し、私は3つのことを見つけました役立つと思います:

  1. LogNormを使用します。

    _plt.pcolormesh(t, f, Sxx, cmap='RdBu', norm=LogNorm(vmin=Sxx.min(), vmax=Sxx.max()))
    _
  2. numpyのfromstringメソッドを使用する

RMSデータは制約された長さのデータ型であり、オーバーフローが負になるため、この方法では計算が機能しません。つまり、507 * 507 = -5095です。

  1. スケールを確認できると、eveythingが簡単になるので、colorbar()を使用します。

    _plt.colorbar()
    _

元の回答:

わずかな変更を加えただけで、コードに10kHzの周波数を再生するのに適切な結果が得られました。

  • logNormをインポートする

    _from matplotlib.colors import LogNorm
    _
  • メッシュでLogNormを使用する

    _plt.pcolormesh(t, f, Sxx, cmap='RdBu', norm=LogNorm(vmin=Sxx.min(), vmax=Sxx.max()))
    _

これは私に与えました: LogNorm Scale pcolormesh

また、savefigの後にplt.close()を呼び出す必要がある場合があります。後のイメージでサウンドの最初の4分の1がドロップされるため、ストリームの読み取りにはいくつかの作業が必要だと思います。

また、plt.colorbar()をお勧めします。これにより、最終的に使用されるスケールを確認できます

更新:誰かが投票に時間をかけたように見える

スペクトログラムの作業バージョンの私のコードを示します。 5秒間の音声をキャプチャし、スペックファイルと音声ファイルに書き出して比較できるようにします。まだ改善すべき点が多く、ほとんど最適化されていません。オーディオファイルとスペックファイルを書き込む時間があるので、チャンクが欠落していることを確認してください。より良いアプローチは、ノンブロッキングコールバックを使用することです。これは後で行うかもしれません。

元のコードとの主な違いは、numpyの正しい形式でデータを取得するための変更でした。

_np.fromstring(raw_block,dtype=np.int16)
_

の代わりに

_struct.unpack(format, raw_block)
_

これを使用してオーディオをファイルに書き込もうとするとすぐに、これは大きな問題として明らかになりました。

_scipy.io.wavfile.write('data/audio{}.wav'.format(self.plot_counter),RATE,snd_block)
_

いい音楽のドラムです。ドラムは明白です。

some music

コード:

_import pyaudio
import struct
import math
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
import time
from scipy.io.wavfile import write

THRESHOLD = 0 # dB
RATE = 44100
INPUT_BLOCK_TIME = 1 # 30 ms
INPUT_FRAMES_PER_BLOCK = int(RATE * INPUT_BLOCK_TIME)
INPUT_FRAMES_PER_BLOCK_BUFFER = int(RATE * INPUT_BLOCK_TIME)

def get_rms(block):
    return np.sqrt(np.mean(np.square(block)))

class AudioHandler(object):
    def __init__(self):
        self.pa = pyaudio.PyAudio()
        self.stream = self.open_mic_stream()
        self.threshold = THRESHOLD
        self.plot_counter = 0

    def stop(self):
        self.stream.close()

    def find_input_device(self):
        device_index = None
        for i in range( self.pa.get_device_count() ):
            devinfo = self.pa.get_device_info_by_index(i)
            print('Device %{}: %{}'.format(i, devinfo['name']))

            for keyword in ['mic','input']:
                if keyword in devinfo['name'].lower():
                    print('Found an input: device {} - {}'.format(i, devinfo['name']))
                    device_index = i
                    return device_index

        if device_index == None:
            print('No preferred input found; using default input device.')

        return device_index

    def open_mic_stream( self ):
        device_index = self.find_input_device()

        stream = self.pa.open(  format = self.pa.get_format_from_width(2,False),
                                channels = 1,
                                rate = RATE,
                                input = True,
                                input_device_index = device_index)

        stream.start_stream()
        return stream

    def processBlock(self, snd_block):
        f, t, Sxx = signal.spectrogram(snd_block, RATE)
        zmin = Sxx.min()
        zmax = Sxx.max()
        plt.pcolormesh(t, f, Sxx, cmap='RdBu', norm=LogNorm(vmin=zmin, vmax=zmax))
        plt.ylabel('Frequency [Hz]')
        plt.xlabel('Time [sec]')
        plt.axis([t.min(), t.max(), f.min(), f.max()])
        plt.colorbar()
        plt.savefig('data/spec{}.png'.format(self.plot_counter), bbox_inches='tight')
        plt.close()
        write('data/audio{}.wav'.format(self.plot_counter),RATE,snd_block)
        self.plot_counter += 1

    def listen(self):
        try:
            print "start", self.stream.is_active(), self.stream.is_stopped()
            #raw_block = self.stream.read(INPUT_FRAMES_PER_BLOCK, exception_on_overflow = False)

            total = 0
            t_snd_block = []
            while total < INPUT_FRAMES_PER_BLOCK:
                while self.stream.get_read_available() <= 0:
                  print 'waiting'
                  time.sleep(0.01)
                while self.stream.get_read_available() > 0 and total < INPUT_FRAMES_PER_BLOCK:
                    raw_block = self.stream.read(self.stream.get_read_available(), exception_on_overflow = False)
                    count = len(raw_block) / 2
                    total = total + count
                    print "done", total,count
                    format = '%dh' % (count)
                    t_snd_block.append(np.fromstring(raw_block,dtype=np.int16))
            snd_block = np.hstack(t_snd_block)
        except Exception as e:
            print('Error recording: {}'.format(e))
            return

        self.processBlock(snd_block)

if __name__ == '__main__':
    audio = AudioHandler()
    for i in range(0,5):
        audio.listen()
_
7
spacepickle

問題は、30msオーディオブロックのスペクトログラムを実行しようとしていることです。これは、信号が定常的であると見なすことができるほど短いためです。
スペクトログラムは実際にはSTFTであり、これは Scipyのドキュメント でも確認できます。

scipy.signal.spectrogram(x、fs = 1.0、window =( 'tukey'、0.25)、nperseg = None、noverlap = None、nfft =なし、detrend = 'constant'、return_onesided = True、scaleing = 'density'、axis = -1、mode = 'psd')

連続するフーリエ変換でスペクトログラムを計算します。

スペクトログラムは、非定常信号の周波数コンテンツの経時変化を視覚化する方法として使用できます。

最初の図では、信号ブロックで4つの連続したfftの結果である4つのスライスがあり、ウィンドウ処理とオーバーラップが行われています。 2番目の図には固有のスライスがありますが、使用したスペクトログラムパラメーターによって異なります。
要点は、その信号で何をしたいかです。アルゴリズムの目的は何ですか?

5
Simone Cifani