paint-brush
Conceptos básicos de manejo de audio: Procesar archivos de audio en la línea de comandos o Pythonby@tyiannak
35,749
35,749

Conceptos básicos de manejo de audio: Procesar archivos de audio en la línea de comandos o Python

tldt arrow
ES

Conceptos básicos de manejo de audio: Procese archivos de audio en la línea de comandos o Python. Este artículo muestra los conceptos básicos del manejo de datos de audio mediante herramientas de línea de comandos. También proporciona una inmersión no tan profunda en el manejo de sonidos en Python. Los dos atributos básicos del sonido son la amplitud (lo que también llamamos volumen) y la frecuencia (una medida de las vibraciones de la onda por unidad de tiempo). Usamos la frecuencia de muestreo (fs = 1/Ts) como el atributo que describe el proceso de muestreo.

Company Mentioned

Mention Thumbnail
featured image - Conceptos básicos de manejo de audio: Procesar archivos de audio en la línea de comandos o Python
Theodoros Giannakopoulos HackerNoon profile picture

Like my articles? Feel free to vote for me
 as ML Writer of the year
 .

El manejo de datos de audio es una tarea esencial para los ingenieros de aprendizaje automático que trabajan en los campos del análisis de voz, la recuperación de información musical y el análisis de datos multimodales, pero también para los desarrolladores que simplemente desean editar, grabar y transcodificar sonidos. Este artículo muestra los conceptos básicos del manejo de datos de audio mediante herramientas de línea de comandos y también proporciona una inmersión no tan profunda en el manejo de sonidos en Python.

Entonces, ¿qué es el sonido y cuáles son sus atributos básicos?

Según la física, el sonido es una vibración viajera, es decir, una onda que se desplaza a través de un medio como el aire. La onda de sonido transfiere energía de partícula a partícula hasta que finalmente es "recibida" por nuestros oídos y percibida por nuestro cerebro. Los dos atributos básicos del sonido son la amplitud (lo que también llamamos volumen) y la frecuencia (una medida de las vibraciones de la onda por unidad de tiempo).

Foto de Kai Dahms en Unsplash

Al igual que las imágenes y los videos, el sonido es una señal analógica que debe transformarse en una señal digital para almacenarse en computadoras y analizarse mediante software. Esta conversión de analógico a digital incluye dos procesos: muestreo y cuantificación .

El muestreo se utiliza para convertir la señal continua variable en el tiempo x(t) en una secuencia discreta de números reales x(n). El intervalo entre dos muestras discretas sucesivas es el período de muestreo (Ts). Usamos la frecuencia de muestreo (fs = 1/Ts) como el atributo que describe el proceso de muestreo.

Las frecuencias de muestreo típicas son 8 KHz, 16 KHz y 44,1 KHz. 1 Hz significa una muestra por segundo, por lo que, obviamente, las frecuencias de muestreo más altas significan más muestras por segundo y, por lo tanto, una mejor calidad de la señal.

(En realidad, esto significa que la señal discreta puede capturar un rango más alto de frecuencias, es decir, de 0 a fs/2 Hz según la regla de Nyquist)

La cuantificación es el proceso de reemplazar cada número real, x(n), de la secuencia de muestras con una aproximación de un conjunto finito de valores discretos. En otras palabras, la cuantificación es el proceso de reducir la precisión del número infinito de una muestra de audio a una precisión finita definida por un número particular de bits.

En la mayoría de los casos, se utilizan 16 bits por muestra para representar cada muestra cuantificada, lo que significa que hay 2¹⁶ niveles para la señal cuantificada. Por esa razón, los valores de audio sin procesar generalmente varían de -2¹⁵ a 2¹⁵ (se usa 1 bit para el signo), sin embargo, como veremos más adelante, esto generalmente se normaliza en el rango (-1, 1) por simplicidad.

Normalmente llamamos a esta propiedad de resolución de bits del procedimiento de cuantificación "resolución de muestra" y se mide en bits por muestra .

Herramientas y bibliotecas utilizadas en este artículo

He seleccionado las siguientes herramientas de línea de comandos, programas y bibliotecas para usar en el manejo básico de datos de audio:

  • ffmpeg /libav. FFmpeg ( https://ffmpeg.org ) es un proyecto gratuito de código abierto para manejar archivos y transmisiones multimedia. Algunos piensan que ffmpeg y libav son lo mismo, pero en realidad libav es un proyecto bifurcado de ffmpeg
  • sox ( http://sox.sourceforge.net ), también conocida como “la navaja suiza de los programas de procesamiento de sonido”, es una utilidad de línea de comandos multiplataforma gratuita para el procesamiento básico de audio. A pesar de que no se ha actualizado desde 2015, sigue siendo una buena solución. En este artículo demostramos principalmente ffmpeg y un par de ejemplos en sox
  • audacity ( https://www.audacityteam.org ) es un programa gratuito, de código abierto y multiplataforma para editar sonidos

programación : usaremos pydub ( https://github.com/jiaaro/pydub ) y scipy ( https://scipy-cookbook.readthedocs.io ) para leer datos de audio y librosa ( https://librosa.github.io /librosa/ ).

También podríamos usar pyAudioAnalysis ( https://github.com/tyiannak/pyAudioAnalysis ) para IO o para extracción de funciones y análisis de señales más avanzados.

Finalmente, también usaremos plotly ( https://plotly.com ) para la visualización básica de señales.

Este artículo está dividido en dos partes:

  • 1ra parte: cómo usar ffmpeg y sox para manejar archivos de audio
  • 2da parte: cómo manejar archivos de audio mediante programación y realizar un procesamiento básico

Parte I: Manejo de datos de audio: la forma de la línea de comandos

A continuación, se muestran algunos ejemplos del manejo de audio más básico, como la conversión entre formatos, el recorte temporal, la fusión y la segmentación, utilizando principalmente ffmpeg y sox.

Para convertir video (mkv) a audio (mp3)

 ffmpeg -i video.mkv audio.mp3

Para reducir la resolución a 16 KHz, convertir estéreo (2 canales) a mono (1 canal) y convertir MP3 a WAV ( muestras de audio sin comprimir), es necesario usar las propiedades -ar (velocidad de audio) -ac (canal de audio):

 ffmpeg -i audio.wav -ar 16000 -ac 1 audio_16K_mono.wav

Tenga en cuenta que, en ese caso, la conversión de estéreo a mono significa que los dos canales se promedian en uno. Además, la reducción de muestreo de un archivo de audio y la conversión de estéreo a mono se pueden lograr usando sox de la siguiente manera: sox <source_file_ -r <new_sampling_rate> -c 1 <output_file>)

Ahora veamos los atributos del nuevo archivo usando ffmpeg:

 ffmpeg -i audio_16K_mono.wav

regresará:

 Input # 0 , wav, from 'audio_16K_mono.wav': Metadata: encoder : Lavf57 .71 .100 Duration: 00 : 03 : 10.29 , bitrate : 256 kb/s Stream # 0 : 0 : Audio: pcm_s16le ([ 1 ][ 0 ][ 0 ][ 0 ] / 0x0001 ), 16000 Hz, mono, s16, 256 kb/s

Para recortar un archivo de audio, por ejemplo, del segundo 60 al 80 (20 segundos de nueva duración):

 ffmpeg -i audio.wav -ss 60 -t 20 audio_small.wav

(Esto se puede lograr con el argumento -to, que se usa para definir el final del segmento recortado, en el ejemplo anterior sería 80)

Para concatenar dos o más archivos de audio, se puede usar el comando "ffmpeg -f concat". Suponga que desea concatenar todos los archivos f1.wav, f2.wav y f3.wav en un archivo grande llamado output.wav. Lo que debe hacer es crear un archivo de texto con el siguiente formato (digamos llamado 'list_of_files_to_concat'):

 file 'file1.wav' file 'file2.wav' file 'file3.wav'

y luego corre

 ffmpeg -f concat -i list_of_files_to_concat -c copy output.wav

Por otro lado, para dividir un archivo de audio en fragmentos ( segmentos ) sucesivos de la (misma) duración especificada se puede hacer con la opción "ffmpeg -f segmento". Por ejemplo, el siguiente comando dividirá output.wav en segmentos de 1 segundo que no se superponen denominados out00000.wav, out00001.wav, etc.:

 ffmpeg -i output.wav -f segment -segment_time 1 -c copy out% 05 d.wav

Con respecto al manejo de canales, además de la simple conversión de mono a estéreo (o de estéreo a mono) a través de la propiedad -ac, es posible que desee cambiar los canales estéreo (de derecha a izquierda). La forma de lograr esto es a través de la propiedad ffmpeg map_channel:

 ffmpeg -i stereo.wav -map_channel 0.0 .1 -map_channel 0.0 .0 stereo_inverted.wav

Para crear un archivo estéreo a partir de dos archivos mono , diga left.wav y right.wav:

 ffmpeg -i left.wav -i right.wav -filter_complex "[0:a][1:a]join=inputs=2:channel_layout=stereo[a]" -map "[a]" mix_channels.wav

En la dirección opuesta, para dividir un archivo estéreo en dos mono (uno para cada canal):

 ffmpeg -i stereo.wav -map_channel 0.0 .0 left.wav -map_channel 0.0 .1 right.wav

Map_channel también se puede usar para silenciar un canal de una señal estéreo, por ejemplo (abajo, el canal izquierdo está silenciado):

 ffmpeg -i stereo.wav -map_channel -1 -map_channel 0.0 .1 muted.wav

La adaptación del volumen también se puede lograr a través de ffmpeg, por ejemplo

 ffmpeg -i data/music_44100.wav -filter:a “volume= 0.5 ” data/music_44100_volume_50.wav ffmpeg -i data/music_44100.wav -filter:a “volume= 2.0 ” data/music_44100_volume_200.wav

La siguiente figura presenta una captura de pantalla de la visualización (con Audacity) del original, la adaptación de volumen al 50 % y las señales de adaptación de volumen x2 (200 %). La señal de volumen realzado x2 está claramente recortada (es decir, algunas muestras no se pueden representar y se les asigna el valor máximo permitido: 2¹⁵ para señales de 16 bits):

El cambio de volumen también se puede lograr con sox de la siguiente manera:

 sox -v 0.5 data/music_44100.wav data/music_44100_volume_50_sox.wav sox -v 2.0 data/music_44100.wav data/music_44100_volume_200_sox.wav

Parte II: Manejo de datos de audio: la forma de programación

Cargue archivos WAV y MP3 en la matriz

Primero carguemos nuestros datos de audio muestreados en una matriz numpy (usamos matrices numpy ya que se consideran la forma más ampliamente adoptada para procesar secuencias/vectores numéricos). La forma más común de cargar datos WAV en matrices numpy es scipy.io.wavfile, mientras que para datos MP3 se puede usar pydub ( https://github.com/jiaaro/pydub ) que usa ffmpeg para codificar/decodificar datos de audio.

En el siguiente ejemplo, la misma señal almacenada en archivos WAV y MP3 se carga en matrices numpy.

 # Read WAV and MP3 files to array from pydub import AudioSegment import numpy as np from scipy.io import wavfile from plotly.offline import init_notebook_mode import plotly.graph_objs as go import plotly # read WAV file using scipy.io.wavfile fs_wav, data_wav = wavfile.read( "data/music_8k.wav" ) # read MP3 file using pudub audiofile = AudioSegment.from_file( "data/music_8k.mp3" ) data_mp3 = np.array(audiofile.get_array_of_samples()) fs_mp3 = audiofile.frame_rate print( 'Sq Error Between mp3 and wav data = {}' . format(((data_mp3 - data_wav)** 2 ).sum())) print( 'Signal Duration = {} seconds' . format(data_wav.shape[ 0 ] / fs_wav))

resultado:

 Sq Error Between mp3 and wav data = 0 Signal Duration = 5.256 seconds

Nota: la duración total de la señal cargada (en segundos) se calcula dividiendo el número de muestras por la frecuencia de muestreo (Hz = muestras por segundo). Además, en el ejemplo anterior, calculamos el error cuadrático de la suma para asegurarnos de que las dos señales sean idénticas a pesar de su conversión de mp3 a wav.

Señales estéreo

Las señales estéreo se manejan a través de arreglos 2D. En el siguiente ejemplo, la matriz data_wav tiene dos columnas, una para cada canal. Por convención, el canal izquierdo es siempre el primero y el segundo el canal derecho.

 # Handling stereo signals fs_wav, data_wav = wavfile.read( "data/stereo_example_small_8k.wav" ) time_wav = np.arange( 0 , len(data_wav)) / fs_wav plotly.offline.iplot({ "data" : [go.Scatter(x=time_wav, y=data_wav[:, 0 ], name= 'left channel' ), go.Scatter(x=time_wav, y=data_wav[:, 1 ], name= 'right channel' )]})

Normalización

La normalización es necesaria para realizar cálculos en los valores de la señal de audio, ya que hace que los valores de la señal sean independientes de la resolución de la muestra (es decir, las señales con 24 bits por muestra tienen un rango de valores mucho más alto que las señales con 16 bits por muestra). El siguiente ejemplo demuestra cómo normalizar una señal de audio en el rango (-1, 1), simplemente dividiendo por 2¹⁵.

Esto se debe a que sabemos que la resolución de la muestra es de 16 bits por muestra. En el raro caso de 24 bits por muestra, esta normalización obviamente debería cambiar respectivamente.

 # Normalization fs_wav, data_wav = wavfile.read( "data/lost_highway_small.wav" ) data_wav_norm = data_wav / ( 2 ** 15 ) time_wav = np.arange( 0 , len(data_wav)) / fs_wav plotly.offline.iplot({ "data" : [go.Scatter(x=time_wav, y=data_wav_norm, name= 'normalized audio signal' )]})

Recortar / Segmentar

Los siguientes ejemplos muestran cómo obtener los segundos 2 a 4 de la señal previamente cargada y normalizada. Esto se hace simplemente refiriéndose a los índices respectivos en la matriz numpy. Obviamente, los índices deben estar en muestras de audio, por lo que los segundos deben multiplicarse por la frecuencia de muestreo.

 # Trim (segment) audio signal (2 seconds) data_wav_norm_crop = data_wav_norm[ 2 * fs_wav: 4 * fs_wav] time_wav_crop = np.arange( 0 , len(data_wav)) / fs_wav plotly.offline.iplot({ "data" : [go.Scatter(x=time_wav_crop, y=data_wav_norm_crop, name= 'cropped audio signal' )]})

Segmentación de tamaño fijo

En la primera parte, mostramos cómo podemos segmentar una grabación larga en segmentos que no se superponen usando ffmpeg. El siguiente ejemplo de código muestra cómo hacer lo mismo con Python. La línea 8 realiza la segmentación real en un comando de una sola línea. En general, el siguiente script carga y normaliza una señal de audio, y luego la divide en segmentos de 1 segundo y escribe cada uno de ellos en un archivo .

(Preste atención a la nota en el último comentario: deberá convertir a 16 bits antes de guardar en el archivo porque la conversión numpy ha llevado a resoluciones de muestra más altas).

 # Fix-sized segmentation (breaks a signal into non-overlapping segments) fs, signal = wavfile.read( "data/obama.wav" ) signal = signal / ( 2 ** 15 ) signal_len = len(signal) segment_size_t = 1 # segment size in seconds segment_size = segment_size_t * fs # segment size in samples # Break signal into list of segments in a single-line Python code segments = np.array([signal[x:x + segment_size] for x in np.arange( 0 , signal_len, segment_size)]) # Save each segment in a seperate filename for iS, s in enumerate(segments): wavfile.write( "data/obama_segment_{0:d}_{1:d}.wav" .format(segment_size_t * iS, segment_size_t * (iS + 1 )), fs, (s))

Un algoritmo simple para eliminar segmentos silenciosos de una grabación

El guión anterior ha dividido una grabación en una lista de segmentos de 1 segundo. El siguiente código implementa un método de eliminación de silencio muy simple. Con este fin, calcula la energía como la suma de los cuadrados de las muestras, luego calcula un umbral como el 50 % del valor de la energía mediana y, finalmente, mantiene los segmentos cuya energía está por encima de ese umbral:

 import IPython # Remove pauses using an energy threshold = 50% of the median energy: energies = [(s** 2 ).sum() / len(s) for s in segments] # (attention: integer overflow would occure without normalization here!) thres = 0.5 * np.median(energies) index_of_segments_to_keep = (np.where(energies > thres)[ 0 ]) # get segments that have energies higher than a the threshold: segments2 = segments[index_of_segments_to_keep] # concatenate segments to signal: new_signal = np.concatenate(segments2) # and write to file: wavfile.write( "data/obama_processed.wav" , fs, new_signal) plotly.offline.iplot({ "data" : [go.Scatter(y=energies, name= "energy" ), go.Scatter(y=np.ones(len(energies)) * thres, name= "thres" )]}) # play the initial and the generated files in notebook: IPython.display.display(IPython.display.Audio( "data/obama.wav" )) IPython.display.display(IPython.display.Audio( "data/obama_processed.wav" ))

El gráfico de energía/umbral se muestra en la siguiente figura (todos los segmentos cuyas energías están por debajo de la línea roja se eliminan del registro procesado). Además, tenga en cuenta las dos últimas líneas de código (usando la función IPython.display.display()) que se usan para agregar un clip de audio en el que se puede hacer clic directamente en el cuaderno para los archivos de audio iniciales y procesados, como muestra la siguiente captura de pantalla:

Puede escuchar las grabaciones originales y procesadas (después de la eliminación del silencio) a continuación:

Análisis musical: un ejemplo de juguete sobre estimación de bpm (pulsaciones por minuto)

El análisis de música es un dominio de aplicación de procesamiento de señales y aprendizaje automático, que se enfoca en analizar señales musicales, principalmente para la recuperación y recomendación basadas en contenido. Una de las principales tareas en el análisis musical es extraer atributos de alto nivel que describen una canción, como su género musical y el estado de ánimo subyacente.

El tempo es uno de los atributos más importantes de una canción. El seguimiento del tempo es la tarea de estimar automáticamente el tempo de una canción (en bpm) directamente desde la señal. Una de las implementaciones básicas del seguimiento de tempo está incluida en la biblioteca librosa .

El siguiente ejemplo de juguete toma como entrada un archivo de audio mono donde se almacena una canción y produce un archivo estéreo donde en el canal izquierdo está la canción inicial, mientras que en el canal derecho hay un sonido de "bip" periódico generado artificialmente que "sigue" el tempo principal de la canción:

 import numpy as np import scipy.io.wavfile as wavfile import librosa import IPython # load file and extract tempo and beats: [Fs, s] = wavfile.read( 'data/music_44100.wav' ) tempo, beats = librosa.beat.beat_track(y=s.astype( 'float' ), sr=Fs, units= "time" ) beats -= 0.05 # add small 220Hz sounds on the 2nd channel of the song ON EACH BEAT s = s.reshape( -1 , 1 ) s = np.array(np.concatenate((s, np.zeros(s.shape)), axis= 1 )) for ib, b in enumerate(beats): t = np.arange( 0 , 0.2 , 1.0 / Fs) amp_mod = 0.2 / (np.sqrt(t)+ 0.2 ) - 0.2 amp_mod[amp_mod < 0 ] = 0 x = s.max() * np.cos( 2 * np.pi * t * 220 ) * amp_mod s[int(Fs * b): int(Fs * b) + int(x.shape[ 0 ]), 1 ] = x.astype( 'int16' ) # write a wav file where the 2nd channel has the estimated tempo: wavfile.write( "data/music_44100_with_tempo.wav" , Fs, np.int16(s)) # play the generated file in notebook: IPython.display.display(IPython.display.Audio( "data/music_44100_with_tempo.wav" ))

El resultado del guión anterior es un archivo WAV donde el canal izquierdo es la canción inicial y el canal derecho es la secuencia de pitidos en los inicios de tempo estimados. A continuación se muestran dos ejemplos de sonidos generados para dos canciones iniciales diferentes:

Grabación en tiempo real y análisis de frecuencia

Todos los ejemplos de código presentados anteriormente se han centrado principalmente en leer datos de audio de archivos y realizar un procesamiento muy básico en los datos de audio, como recortar o segmentar para ventanas de tamaño fijo, y luego trazar o guardar los sonidos procesados en archivos.

El siguiente código va un paso más allá de dos maneras: (a) mostrando cómo un micrófono puede capturar el sonido de una manera que permita el procesamiento en línea y en tiempo real (b) introduciendo la representación del dominio de frecuencia de un sonido. Nuestro objetivo aquí es crear una secuencia de comandos de Python simple que capture el sonido en base a un segmento, y para cada segmento grafica en la terminal la distribución de frecuencia del segmento.

La captura de audio en tiempo real se logra a través de la biblioteca pyaudio . Las muestras de audio se capturan en pequeños segmentos (por ejemplo, de 200 msegundos de duración). Luego, para cada segmento, el código que se presenta a continuación realiza una representación de frecuencia básica ejecutando los siguientes pasos:

  1. calcular la magnitud X de la Transformada Rápida de Fourier (FFT) del segmento registrado. Además, mantenga los valores de frecuencia (en Hz) en una matriz separada, digamos freqs . Entonces, en pocas palabras, de acuerdo con la definición DFT, X(i) es la energía de la señal de audio que se concentra en frecuencia freqs(i) Hz
  2. reduce la muestra de X y frecuencias, de modo que mantenemos muchos menos coeficientes de frecuencia para visualizar
  3. el script también calcula la energía del segmento total (no solo la energía en intervalos de frecuencia particulares como se describe en 1). Esto se hace solo para normalizar contra el ancho máximo de la visualización de frecuencia.
  4. trace las energías de frecuencia reducidas X para todas las frecuencias (también reducidas) utilizando un gráfico de barras simple.

Estos cuatro pasos se implementan en el siguiente script. El código también está disponible aquí como parte de la biblioteca paura . Ver comentarios en línea para una explicación más detallada:

 # paura_lite: # An ultra-simple command-line audio recorder with real-time # spectrogram visualization import numpy as np import pyaudio import struct import scipy.fftpack as scp import termplotlib as tpl import os # get window's dimensions rows, columns = os.popen( 'stty size' , 'r' ).read().split() buff_size = 0.2 # window size in seconds wanted_num_of_bins = 40 # number of frequency bins to display # initialize soundcard for recording: fs = 8000 pa = pyaudio.PyAudio() stream = pa.open(format=pyaudio.paInt16, channels= 1 , rate=fs, input= True , frames_per_buffer=int(fs * buff_size)) while 1 : # for each recorded window (until ctr+c) is pressed # get current block and convert to list of short ints, block = stream.read(int(fs * buff_size)) format = "%dh" % (len(block) / 2 ) shorts = struct.unpack(format, block) # then normalize and convert to numpy array: x = np.double(list(shorts)) / ( 2 ** 15 ) seg_len = len(x) # get total energy of the current window and compute a normalization # factor (to be used for visualizing the maximum spectrogram value) energy = np.mean(x ** 2 ) max_energy = 0.02 # energy for which the bars are set to max max_width_from_energy = int((energy / max_energy) * int(columns)) + 1 if max_width_from_energy > int(columns) - 10 : max_width_from_energy = int(columns) - 10 # get the magnitude of the FFT and the corresponding frequencies X = np.abs(scp.fft(x))[ 0 :int(seg_len/ 2 )] freqs = (np.arange( 0 , 1 + 1.0 /len(X), 1.0 / len(X)) * fs / 2 ) # ... and resample to a fix number of frequency bins (to visualize) wanted_step = (int(freqs.shape[ 0 ] / wanted_num_of_bins)) freqs2 = freqs[ 0 ::wanted_step].astype( 'int' ) X2 = np.mean(X.reshape( -1 , wanted_step), axis= 1 ) # plot (freqs, fft) as horizontal histogram: fig = tpl.figure() fig.barh(X2, labels=[str(int(f)) + " Hz" for f in freqs2[ 0 : -1 ]], show_vals= False , max_width=max_width_from_energy) fig.show() # add exactly as many new lines as they are needed to # fill clear the screen in the next iteration: print( "\n" * (int(rows) - freqs2.shape[ 0 ] - 1 ))

Y este es un ejemplo de ejecución del script:

Todos los ejemplos de código presentados en la parte B están disponibles en este repositorio de github: https://github.com/tyiannak/basic_audio_handling como un cuaderno jupyter.

El último ejemplo (el analizador de espectro de línea de comandos en tiempo real) está disponible en https://github.com/tyiannak/paura/blob/master/paura_lite.py

Sobre el autor ( tyiannak.github.io )

Thodoris es actualmente el Director de ML en BehavioralSignals.com , donde su trabajo se centra en la construcción de algoritmos que reconocen emociones y comportamientos basados en información de audio. También enseña procesamiento de información multimodal en un programa de maestría de ciencia de datos e inteligencia artificial en Atenas, Grecia.