Изготовление спектрограммы из микрофона

Ниже у меня есть код, который будет принимать входные данные от микрофона, и если среднее значение звукового блока пройдет определенное пороговое значение, оно произведет спектрограмму аудиоблока (длиной 30 мс). Вот как выглядит сгенерированный спектрограмм в середине обычного разговора:

введите описание изображения здесь

Из того, что я видел, это не похоже на то, что я ожидаю, что спектрограмма будет выглядеть так, как звук и окружающая среда. Я ожидал чего-то большего следующего (транспонированного для сохранения пространства):

введите описание изображения здесь

Микрофон, который я записываю с по умолчанию на моем 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 Гц и используем логарифмическую шкалу для цветовой карты, это выход для нажатия рядом с микрофоном:

введите описание изображения здесь

Которая все еще выглядит немного странной для меня, но также кажется шагом в правильном направлении.

Использование Sox и сравнение со спектрограммой, созданной в моей программе:

2ZeFv.pngv3gPF.png

Ответы

Ответ 1

Во-первых, processBlock внимание, что ваш код отображает до 100 спектрограмм (если processBlock вызывается несколько раз) друг над другом, и вы видите только последнюю. Вы можете исправить это. Кроме того, я предполагаю, что вы знаете, почему вы хотите работать с 30 мс аудио-записями. Лично я не могу придумать практического применения, где 30 мс, записанные микрофоном ноутбука, могли бы дать интересные идеи. Это зависит от того, что вы записываете и как вы запускаете запись, но эта проблема имеет отношение к реальному вопросу.

В противном случае код работает отлично. С помощью нескольких небольших изменений в функции processBlock, применяя некоторые processBlock знания, вы можете получить информативные и эстетические спектрограммы.

Итак, давайте поговорим о реальных спектрограммах. Я возьму вывод SoX в качестве ссылки. В примечании к цветовой полосе указано, что это dBFS 1 что является логарифмической мерой (дБ - сокращение от децибела). Итак, сначала преобразуем спектрограмму в дБ:

    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

Большой! Теперь мы получили относительно сбалансированное разрешение по обеим осям - но подождите! Почему результат такой пиксельный? На самом деле, это вся информация, которая есть в коротком временном окне 30 мс. Есть только так много способов, которыми 1300 образцов могут быть распределены в двух измерениях. Однако мы можем немного обмануть и использовать более высокое разрешение 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

Порог сравнивается со среднеквадратичным значением входного сигнала, которое измеряется не в дБ, а в единицах необработанной амплитуды.


1 Видимо, FS - это сокращение от полной шкалы. dBFS означает, что показатель дБ относится к максимальному диапазону. 0 дБ - самый громкий сигнал, возможный в текущем представлении, поэтому фактические значения должны быть <= 0 дБ.

Ответ 2

ОБНОВЛЕНИЕ, чтобы сделать мой ответ более ясным и, надеюсь, дополню отличное объяснение от @kazemakase, я нашел три вещи, которые, я надеюсь, помогут:

  • Использовать LogNorm:

    plt.pcolormesh(t, f, Sxx, cmap='RdBu', norm=LogNorm(vmin=Sxx.min(), vmax=Sxx.max()))
    
  • использовать метод numpy fromstring

Выключает вычисление RMS не будет работать с этим методом, поскольку данные ограничены тип данных длины, а переполнения становятся отрицательными: 507 * 507 = -5095.

  1. используйте colorbar(), поскольку все становится легче, когда вы можете увидеть масштаб

    plt.colorbar()
    

Исходный ответ:

У меня получился приличный результат, который воспроизводит частоту 10 кГц в вашем коде только с несколькими изменениями:

  • импортировать LogNorm

    from matplotlib.colors import LogNorm
    
  • Используйте LogNorm в сетке

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

Это дало мне: Шкала LogNorm pcolormesh

Вам также может потребоваться вызвать plt.close() после savefig, и я думаю, что чтение потока требует некоторой работы, поскольку более поздние изображения отбрасывали первую четверть звука.

Id также рекомендует plt.colorbar(), чтобы вы могли увидеть масштаб, который он закончил, используя

ОБНОВЛЕНИЕ: видя, как кто-то потратил время на downvote

Вот мой код для рабочей версии спектрограммы. Он захватывает пять секунд аудио и записывает их в файл спецификации и аудиофайл, чтобы вы могли сравнивать. Theres все еще много, чтобы улучшить, и его вряд ли оптимизировали: я уверен, что его падающие куски из-за времени, чтобы написать аудио и спецификации файлов. Лучшим подходом было бы использовать неблокирующий обратный вызов, и я мог бы сделать это позже

Основное отличие от исходного кода заключалось в изменении для получения данных в правильном формате для 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()

Ответ 3

Я думаю, проблема в том, что вы пытаетесь создать спектрограмму 30-миллисекундного аудиоблока, который настолько короткий, что вы можете считать сигнал стационарным.
Спектрограмма фактически является STFT, и вы можете найти это также в документации Scipy:

scipy.signal.spectrogram (x, fs = 1.0, window = ('tukey', 0.25), nperseg = нет, noverlap = нет, nfft = нет, detrend = "константа", return_onesided = True, масштабирование = "плотность", ось = -1, mode = 'psd')

Вычислить спектрограмму с последовательными преобразованиями Фурье.

Спектрограммы можно использовать как способ визуализации изменения частотного содержания нестационарных сигналов во времени.

На первом рисунке у вас есть четыре среза, которые являются результатом четырех последовательных fft на вашем сигнальном блоке, с некоторыми окнами и перекрытиями. Второй рисунок имеет уникальный срез, но он зависит от параметров спектрограммы, которые вы использовали.
Дело в том, что вы хотите сделать с этим сигналом. Какова цель алгоритма?

Ответ 4

Я не уверен, что работа непосредственно в Python - лучший способ для обработки звука и наиболее точно с FFT... [на мой взгляд, используя cython отображаются как обязательство в обработке звука с помощью python]

Вы оцениваете возможность привязки любого внешнего метода FFT (используя fftw, например) и продолжайте использовать python только для отправки задания к внешнему методу и обновить результат изображения?

Вы можете найти некоторую информацию относительно оптимизации FFT в python здесь, а также можете посмотреть scipy Реализация FFT.