Procesemos el sonido en Go

Descargo de responsabilidad: no considero ningún algoritmo ni API para trabajar con el reconocimiento de sonido y voz. Este artículo trata sobre problemas de audio y cómo resolverlos con Go.

Gopher


phono es un marco de aplicación para trabajar con sonido. Su función principal es crear un transportador a partir de diversas tecnologías que procesará el sonido. para ti en la forma que necesitas.


¿Qué tiene que ver el transportador con él, además de las diferentes tecnologías, y por qué otro marco? Ahora vamos a resolverlo.


¿De dónde viene el sonido?


Para 2018, el sonido se ha convertido en la forma estándar en que los humanos interactúan con la tecnología. La mayoría de los gigantes de TI han creado su propio asistente de voz o lo están haciendo ahora mismo. El control por voz ya está en la mayoría de los sistemas operativos, y la mensajería de voz es una característica típica de cualquier mensajero. En el mundo, alrededor de mil nuevas empresas están trabajando en el procesamiento del lenguaje natural y alrededor de doscientas en el reconocimiento de voz.


Con música, una historia similar. Se reproduce desde cualquier dispositivo y la grabación de sonido está disponible para todos los que tengan una computadora. El software musical es desarrollado por cientos de empresas y miles de entusiastas de todo el mundo.


Tareas comunes


Si tuvo que trabajar con sonido, entonces las siguientes condiciones deberían sonarle familiares:


  • El audio debe obtenerse de un archivo, dispositivo, red, etc.
  • El audio debe ser procesado : agregar efectos, transcodificar, analizar, etc.
  • El audio debe transferirse a un archivo, dispositivo, red, etc.
  • Los datos se transmiten en pequeños buffers.

Resulta una tubería regular: hay un flujo de datos que pasa por varias etapas de procesamiento.


Soluciones


Para mayor claridad, tomemos una tarea de la vida real. Por ejemplo, necesita convertir una voz a texto:


  • Grabamos audio del dispositivo
  • Eliminar el ruido
  • Igualar
  • Pase la señal a la API de reconocimiento de voz

Como cualquier otra tarea, esta tiene varias soluciones.


Frente


Solo hardcore ciclistas programadores Grabamos el sonido directamente a través del controlador de la tarjeta de sonido, escribimos reducción de ruido inteligente y ecualizador multibanda. Esto es muy interesante, pero puede olvidarse de su tarea original durante varios meses.


Largo y muy difícil.


Normal


Una alternativa es utilizar las API existentes. Puede grabar audio usando ASIO, CoreAudio, PortAudio, ALSA y otros. También hay varios tipos de complementos para el procesamiento: AAX, VST2, VST3, AU.


Una amplia elección no significa que puede usar todo a la vez. Por lo general, se aplican las siguientes restricciones:


  1. Sistema operativo No todas las API están disponibles en todos los sistemas operativos. Por ejemplo, AU es tecnología nativa de OS X y solo está disponible allí.
  2. Lenguaje de programación La mayoría de las bibliotecas de audio están escritas en C o C ++. En 1996, Steinberg lanzó la primera versión del VST SDK, que sigue siendo el estándar de plugin más popular. Después de 20 años, ya no es necesario escribir en C / C ++: para VST hay contenedores en Java, Python, C #, Rust y quién sabe qué más. Aunque el idioma sigue siendo una limitación, ahora incluso el sonido se procesa en JavaScript.
  3. Funcional Si la tarea es simple y directa, no es necesario escribir una nueva aplicación. El mismo FFmpeg puede hacer mucho.

En esta situación, la complejidad depende de su elección. En el peor de los casos, debe lidiar con varias bibliotecas. Y si no tienes suerte, con abstracciones complejas e interfaces completamente diferentes.


Cual es el resultado?


Tienes que elegir entre muy complejo y complejo :


  • ya sea con varias API de bajo nivel para escribir sus bicicletas
  • lidiar con múltiples API e intentar hacer amigos con ellas

No importa qué método se seleccione, la tarea siempre se reduce al transportador. Las tecnologías utilizadas pueden variar, pero la esencia es la misma. El problema es que nuevamente, en lugar de resolver un problema real, tienes que escribir la bici cinta transportadora


Pero hay una salida.


phono


phono


phono creado para resolver problemas comunes: para " recibir, procesar y transmitir " sonido. Para hacer esto, usa la tubería como la abstracción más natural. Hay un artículo en el blog oficial de Go que describe el patrón de canalización. La idea principal de la tubería es que hay varias etapas de procesamiento de datos que funcionan independientemente entre sí e intercambian datos a través de canales. Lo que necesitas


Por qué ir


Primero, la mayoría de los programas de audio y bibliotecas están escritos en C, y Go a menudo se conoce como su sucesor. Además, hay cgo y bastantes carpetas para las bibliotecas de audio existentes. Puedes tomar y usar.


En segundo lugar, en mi opinión personal, Go es un buen idioma. No profundizaré, pero notaré su multihilo . Los canales y las gorutinas simplifican enormemente la implementación del transportador.


Abstracción


El corazón de phono es el tipo de pipe.Pipe . Es él quien implementa la tubería. Como en la muestra del blog , hay tres tipos de etapas:


  1. pipe.Pump (bomba inglesa): recepción de sonido, solo canales de salida
  2. pipe.Processor (procesador en inglés) - procesamiento de sonido, canales de entrada y salida
  3. pipe.Sink (sumidero inglés) - transmisión de sonido, solo canales de entrada

Inside pipe.Pipe datos se pasan en buffers. Reglas por las cuales construir una tubería:


pipe_diagram


  1. Una pipe.Pump
  2. Varios pipe.Processor colocado secuencialmente uno tras otro
  3. Uno o más pipe.Sink colocado en paralelo
  4. Todas las pipe.Pipe componentes de la pipe.Pipe deben tener lo mismo:
    • Tamaño del búfer (mensajes)
    • Tasa de muestreo
    • Numero de canales

La configuración mínima es Bomba y un fregadero, el resto es opcional.


Veamos algunos ejemplos.


Simple


Tarea: reproducir el archivo wav.


Llevémoslo al formulario " recibir, procesar, transferir ":


  1. Obtener audio de un archivo wav
  2. Transfiere audio a un dispositivo portaudio


El audio se lee y se reproduce de inmediato.


Código
 package example import ( "github.com/dudk/phono" "github.com/dudk/phono/pipe" "github.com/dudk/phono/portaudio" "github.com/dudk/phono/wav" ) // Example: // Read .wav file // Play it with portaudio func easy() { wavPath := "_testdata/sample1.wav" bufferSize := phono.BufferSize(512) // wav pump wavPump, err := wav.NewPump( wavPath, bufferSize, ) check(err) // portaudio sink paSink := portaudio.NewSink( bufferSize, wavPump.WavSampleRate(), wavPump.WavNumChannels(), ) // build pipe p := pipe.New( pipe.WithPump(wavPump), pipe.WithSinks(paSink), ) defer p.Close() // run pipe err = p.Do(pipe.Run) check(err) } 

Primero, creamos los elementos de la tubería futura: wav.Pump y portaudio.Sink y los pasamos a la pipe.New constructor. La función de p.Do(pipe.actionFn) error inicia la canalización y espera a que termine.


Mas duro


Tarea: dividir el archivo wav en muestras, componer una pista a partir de ellos, guardar el resultado y reproducirlo simultáneamente.


Una pista es una secuencia de muestras, y una muestra es un pequeño segmento de audio. Para cortar el audio, primero debe cargarlo en la memoria. Para hacer esto, use el asset.Asset Tipo de phono/asset paquete phono/asset . Dividimos la tarea en pasos estándar:


  1. Obtener audio de un archivo wav
  2. Transfiere audio a la memoria

Ahora hacemos muestras con nuestras manos, las agregamos a la pista y finalizamos la tarea:


  1. Obtén audio de una pista
  2. Transferir audio a
    • archivo wav
    • dispositivo portaudio

ejemplo_normal


De nuevo, sin una etapa de procesamiento, ¡pero dos tuberías!


Código
 package example import ( "github.com/dudk/phono" "github.com/dudk/phono/asset" "github.com/dudk/phono/pipe" "github.com/dudk/phono/portaudio" "github.com/dudk/phono/track" "github.com/dudk/phono/wav" ) // Example: // Read .wav file // Split it to samples // Put samples to track // Save track into .wav and play it with portaudio func normal() { bufferSize := phono.BufferSize(512) inPath := "_testdata/sample1.wav" outPath := "_testdata/example4_out.wav" // wav pump wavPump, err := wav.NewPump(inPath, bufferSize) check(err) // asset sink asset := &asset.Asset{ SampleRate: wavPump.WavSampleRate(), } // import pipe importAsset := pipe.New( pipe.WithPump(wavPump), pipe.WithSinks(asset), ) defer importAsset.Close() err = importAsset.Do(pipe.Run) check(err) // track pump track := track.New(bufferSize, asset.NumChannels()) // add samples to track track.AddFrame(198450, asset.Frame(0, 44100)) track.AddFrame(66150, asset.Frame(44100, 44100)) track.AddFrame(132300, asset.Frame(0, 44100)) // wav sink wavSink, err := wav.NewSink( outPath, wavPump.WavSampleRate(), wavPump.WavNumChannels(), wavPump.WavBitDepth(), wavPump.WavAudioFormat(), ) // portaudio sink paSink := portaudio.NewSink( bufferSize, wavPump.WavSampleRate(), wavPump.WavNumChannels(), ) // final pipe p := pipe.New( pipe.WithPump(track), pipe.WithSinks(wavSink, paSink), ) err = p.Do(pipe.Run) } 

En comparación con el ejemplo anterior, hay dos pipe.Pipe . El primero transfiere datos a la memoria para que pueda cortar las muestras. El segundo tiene dos destinatarios al final: wav.Sink y portaudio.Sink . Con este esquema, el sonido se graba simultáneamente en un archivo wav y se reproduce.


Mas duro


Tarea: lea dos archivos wav, mezcle, procese el complemento vst2 y guárdelo en un nuevo archivo wav.


Hay un mixer.Mixer simple. mixer.Mixer en el mixer.Mixer phono/mixer . Puede transmitir señales de varias fuentes y obtener una mezcla. Para hacer esto, implementa simultáneamente pipe.Pump y pipe.Sink .


Nuevamente, la tarea consta de dos subtareas. El primero se ve así:


  1. Obtenga el archivo de audio wav
  2. Transfiere audio al mezclador

Segundo:


  1. Obtenga audio del mezclador.
  2. Procesando complemento de audio
  3. Transferir audio a archivo wav

ejemplo_duro


Código
 package example import ( "github.com/dudk/phono" "github.com/dudk/phono/mixer" "github.com/dudk/phono/pipe" "github.com/dudk/phono/vst2" "github.com/dudk/phono/wav" vst2sdk "github.com/dudk/vst2" ) // Example: // Read two .wav files // Mix them // Process with vst2 // Save result into new .wav file // // NOTE: For example both wav files have same characteristics ie: sample rate, bit depth and number of channels. // In real life implicit conversion will be needed. func hard() { bs := phono.BufferSize(512) inPath1 := "../_testdata/sample1.wav" inPath2 := "../_testdata/sample2.wav" outPath := "../_testdata/out/example5.wav" // wav pump 1 wavPump1, err := wav.NewPump(inPath1, bs) check(err) // wav pump 2 wavPump2, err := wav.NewPump(inPath2, bs) check(err) // mixer mixer := mixer.New(bs, wavPump1.WavNumChannels()) // track 1 track1 := pipe.New( pipe.WithPump(wavPump1), pipe.WithSinks(mixer), ) defer track1.Close() // track 2 track2 := pipe.New( pipe.WithPump(wavPump2), pipe.WithSinks(mixer), ) defer track2.Close() // vst2 processor vst2path := "../_testdata/Krush.vst" vst2lib, err := vst2sdk.Open(vst2path) check(err) defer vst2lib.Close() vst2plugin, err := vst2lib.Open() check(err) defer vst2plugin.Close() vst2processor := vst2.NewProcessor( vst2plugin, bs, wavPump1.WavSampleRate(), wavPump1.WavNumChannels(), ) // wav sink wavSink, err := wav.NewSink( outPath, wavPump1.WavSampleRate(), wavPump1.WavNumChannels(), wavPump1.WavBitDepth(), wavPump1.WavAudioFormat(), ) check(err) // out pipe out := pipe.New( pipe.WithPump(mixer), pipe.WithProcessors(vst2processor), pipe.WithSinks(wavSink), ) defer out.Close() // run all track1Done, err := track1.Begin(pipe.Run) check(err) track2Done, err := track2.Begin(pipe.Run) check(err) outDone, err := out.Begin(pipe.Run) check(err) // wait results err = track1.Wait(track1Done) check(err) err = track2.Wait(track2Done) check(err) err = out.Wait(outDone) check(err) } 

Ya hay tres pipe.Pipe . pipe.Pipe , todos interconectados a través de un mezclador. Para comenzar, use la función p.Begin(pipe.actionFn) (pipe.State, error) . A diferencia del p.Do(pipe.actionFn) error , no bloquea la llamada, sino que simplemente devuelve un estado que luego se puede esperar con el p.Wait(pipe.State) error .


Que sigue


Quiero que phono convierta en el marco de aplicación más conveniente. Si tiene un problema con el sonido, no necesita comprender API complejas y pasar tiempo estudiando estándares. Todo lo que se necesita es construir un transportador a partir de elementos adecuados y ejecutarlo.


Durante medio año, se filmaron los siguientes paquetes:


  • phono/wav - lee / escribe archivos wav
  • phono/vst2 : enlaces incompletos del SDK de VST2, mientras que solo puede abrir el complemento y llamar a sus métodos, pero no todas las estructuras
  • phono/mixer - mezclador, agrega señales N, sin balance y volumen
  • phono/asset - muestreo de búfer
  • phono/track - lectura secuencial de muestras (estratificación rota)
  • phono/portaudio - reproducción de señal durante experimentos

Además de esta lista, hay una acumulación constante de nuevas ideas e ideas, que incluyen:


  • Cuenta regresiva
  • Variable en la tubería de vuelo
  • Bomba HTTP / sumidero
  • Automatización de parámetros
  • Procesador de remuestreo
  • Mezclador balance y volumen
  • Bomba en tiempo real
  • Bomba sincronizada para múltiples pistas.
  • Vst2 completo

En los siguientes artículos analizaré:


  • pipe.Pipe vida de la pipe.Pipe : debido a la estructura compleja, su estado está controlado por el átomo final
  • cómo escribir las etapas de tu tubería

Este es mi primer proyecto de código abierto, por lo que agradeceré cualquier ayuda y recomendaciones. De nada.


Referencias


Source: https://habr.com/ru/post/es424623/


All Articles