Saltar al contenido

Produciendo espectrograma desde micrófono

Posterior a de una prolongada búsqueda de información pudimos resolver este dilema que presentan algunos de nuestros lectores. Te brindamos la solución y esperamos que te resulte de mucha ayuda.

Solución:

Primero, observe que su código traza hasta 100 espectrogramas (si processBlock se llama varias veces) una encima de la otra y solo se ve la última. Es posible que desee arreglar eso. Además, supongo que sabe por qué quiere trabajar con grabaciones de audio de 30 ms. Personalmente, no puedo pensar en una aplicación práctica en la que 30 ms grabados con un micrófono de computadora portátil puedan brindar información interesante. Depende de lo que esté grabando y cómo active la grabación, pero este problema es tangencial a la pregunta real.

De lo contrario, el código funciona perfectamente. Con solo unos pequeños cambios en el processBlock función, aplicando algunos conocimientos previos, puede obtener espectrogramas informativos y estéticos.

Así que hablemos de espectrogramas reales. Tomaré la salida de SoX como referencia. La anotación de la barra de colores dice que es dBFS1, que es una medida logarítmica (dB es la abreviatura de Decibel). Entonces, primero convierta el espectrograma a dB:

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

ingrese la descripción de la imagen aquí

Esto mejoró la escala de colores. Ahora vemos ruido en las bandas de frecuencia más alta que estaba oculto antes. A continuación, abordemos la resolución del tiempo. El espectrograma divide la señal en segmentos (la longitud predeterminada es 256) y calcula el espectro para cada uno. Esto significa que tenemos una resolución de frecuencia excelente pero una resolución de tiempo muy pobre porque solo unos pocos segmentos de este tipo encajan en la ventana de señal (que tiene una longitud de aproximadamente 1300 muestras). Siempre existe una compensación entre la resolución de tiempo y frecuencia. Esto está relacionado con el principio de incertidumbre. Así que cambiemos algo de resolución de frecuencia por resolución de tiempo dividiendo la señal en segmentos más cortos:

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

ingrese la descripción de la imagen aquí

¡Excelente! Ahora tenemos una resolución relativamente equilibrada en ambos ejes, ¡pero espere! ¡¿Por qué el resultado está tan pixelado ?! En realidad, esta es toda la información que hay en la breve ventana de tiempo de 30 ms. Hay tantas formas en que se pueden distribuir 1300 muestras en dos dimensiones. Sin embargo, podemos hacer un poco de trampa y usar una resolución FFT más alta y segmentos superpuestos. Esto hace que el resultado sea más suave aunque no proporciona información adicional:

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

ingrese la descripción de la imagen aquí

Contempla patrones de interferencia espectrales bonitos. (Estos patrones dependen de la función de ventana utilizada, pero no nos detengamos en los detalles aquí. Consulte la window argumento de la función espectrograma para jugar con estos.) El resultado se ve bien, pero en realidad no contiene más información que la imagen anterior.

Para hacer que el resultado sea más SoX-lixe, observe que el espectrograma SoX está más bien manchado en el eje del tiempo. Obtiene este efecto usando la resolución de tiempo baja original (segmentos largos) pero déjelos superponerse para suavidad:

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

ingrese la descripción de la imagen aquí

Personalmente prefiero la tercera solución, pero necesitará encontrar su propia compensación de tiempo / frecuencia preferida.

Finalmente, usemos un mapa de colores que se parezca más al de SoX:

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

ingrese la descripción de la imagen aquí

Un breve comentario sobre la siguiente línea:

THRESHOLD = 40 # dB

El umbral se compara con el valor eficaz de la señal de entrada, que es no medido en dB pero unidades de amplitud brutas.


1 Aparentemente, FS es la abreviatura de escala completa. dBFS significa que la medida de dB es relativa al rango máximo. 0 dB es la señal más fuerte posible en la representación actual, por lo que los valores reales deben ser <= 0 dB.

ACTUALIZAR para aclarar mi respuesta y, con suerte, complementar la excelente explicación de @kazemakase, encontré tres cosas que espero ayuden:

  1. Utilice LogNorm:

    plt.pcolormesh(t, f, Sxx, cmap='RdBu', norm=LogNorm(vmin=Sxx.min(), vmax=Sxx.max()))
    
  2. usar el método fromstring de numpy

Resulta que el cálculo RMS no funcionará con este método ya que los datos tienen un tipo de datos de longitud restringida y los desbordamientos se vuelven negativos: es decir, 507 * 507 = -5095.

  1. use la barra de color () ya que todo se vuelve más fácil cuando puede ver la escala

    plt.colorbar()
    

Respuesta original:

Obtuve un resultado decente al reproducir una frecuencia de 10 kHz en su código con solo un par de alteraciones:

  • importar el LogNorm

    from matplotlib.colors import LogNorm
    
  • Utilice LogNorm en la malla

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

Esto me dio:
LogNorm Scale pcolormesh

Es posible que también deba llamar a plt.close () después de savefig, y creo que la lectura de la transmisión necesita algo de trabajo, ya que las imágenes posteriores dejaron caer el primer cuarto del sonido.

También recomendaría plt.colorbar() para que puedas ver la escala que termina usando

ACTUALIZACIÓN: viendo que alguien se tomó el tiempo de votar en contra

Aquí está mi código para una versión funcional del espectrograma. Captura cinco segundos de audio y los escribe en un archivo de especificaciones y un archivo de audio para que pueda comparar. Todavía hay mucho que mejorar y está poco optimizado: estoy seguro de que se están cayendo trozos debido al tiempo para escribir archivos de audio y especificaciones. Un mejor enfoque sería usar la devolución de llamada sin bloqueo y podría hacerlo más tarde

La principal diferencia con el código original fue el cambio para obtener los datos en el formato correcto para numpy:

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

en lugar de

struct.unpack(format, raw_block)

Esto se volvió obvio como un problema importante tan pronto como intenté escribir el audio en un archivo usando:

scipy.io.wavfile.write('data/audio.wav'.format(self.plot_counter),RATE,snd_block)

Aquí hay una buena música, la batería es obvia:

alguna música

El código:

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()

Creo que el problema es que estás intentando hacer el espectrograma de un bloque de audio de 30 ms, que es tan corto que puedes considerar la señal como estática.
El espectrograma es de hecho el STFT, y puede encontrarlo también en la documentación de Scipy:

scipy.signal.spectrogram(x, fs = 1.0, window = ('tukey', 0.25), nperseg = None, noverlap = None, nfft = None, detrend = 'constante', return_onesided = True, escala = 'densidad', eje = -1, modo = 'psd')

Calcule un espectrograma con transformadas de Fourier consecutivas.

Los espectrogramas se pueden utilizar como una forma de visualizar el cambio del contenido de frecuencia de una señal no estacionaria a lo largo del tiempo.

En la primera figura, tiene cuatro cortes que son el resultado de cuatro fft consecutivos en su bloque de señal, con algunas ventanas y superposición. La segunda figura tiene un corte único, pero depende de los parámetros del espectrograma que haya utilizado.
La cuestión es qué quieres hacer con esa señal. ¿Cuál es el propósito del algoritmo?

Reseñas y calificaciones

Si para ti ha resultado provechoso nuestro artículo, nos gustaría que lo compartas con más desarrolladores de esta manera nos ayudas a dar difusión a nuestro contenido.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *