Vamos processar o som no Go

Isenção de responsabilidade: não considero algoritmos e APIs para trabalhar com reconhecimento de som e fala. Este artigo é sobre problemas de áudio e como resolvê-los com o Go.

Gopher


phono é uma estrutura de aplicação para trabalhar com som. Sua principal função é criar um transportador a partir de várias tecnologias que processam o som para você da maneira que você precisa.


O que o transportador tem a ver com isso, além de diferentes tecnologias, e por que outra estrutura? Agora vamos descobrir.


De onde vem o som?


Em 2018, o som se tornou a maneira padrão como os humanos interagem com a tecnologia. A maioria dos gigantes de TI criou seu próprio assistente de voz ou está fazendo isso agora. O controle de voz já está na maioria dos sistemas operacionais e as mensagens de voz são um recurso típico de qualquer mensageiro. No mundo, cerca de mil startups estão trabalhando no processamento de linguagem natural e cerca de duzentas no reconhecimento de fala.


Com música, uma história semelhante. É reproduzido a partir de qualquer dispositivo e a gravação de som está disponível para todos que possuem um computador. O software musical é desenvolvido por centenas de empresas e milhares de entusiastas em todo o mundo.


Tarefas comuns


Se você tivesse que trabalhar com som, as seguintes condições pareceriam familiares:


  • O áudio deve ser obtido de um arquivo, dispositivo, rede etc.
  • O áudio deve ser processado : adicione efeitos, transcodifique, analise etc.
  • O áudio deve ser transferido para um arquivo, dispositivo, rede etc.
  • Os dados são transmitidos em pequenos buffers.

Acontece um pipeline regular - há um fluxo de dados que passa por vários estágios de processamento.


Soluções


Para maior clareza, vamos dar uma tarefa da vida real. Por exemplo, você precisa converter uma voz em texto:


  • Gravamos áudio do dispositivo
  • Remover ruídos
  • Igualar
  • Passe o sinal para a API de reconhecimento de fala

Como qualquer outra tarefa, esta possui várias soluções.


Testa


Apenas hardcore ciclistas programadores. Gravamos som diretamente através do driver da placa de som, escrevemos redução inteligente de ruído e equalizador de várias bandas. Isso é muito interessante, mas você pode esquecer sua tarefa original por vários meses.


Longo e muito difícil.


Normal


Uma alternativa é usar APIs existentes. Você pode gravar áudio usando ASIO, CoreAudio, PortAudio, ALSA e outros. Existem também vários tipos de plugins para processamento: AAX, VST2, VST3, AU.


Uma ampla escolha não significa que você pode usar tudo de uma vez. Normalmente, as seguintes restrições se aplicam:


  1. Sistema operacional Nem todas as APIs estão disponíveis em todos os sistemas operacionais. Por exemplo, AU é tecnologia nativa do OS X e só está disponível lá.
  2. Linguagem de programação A maioria das bibliotecas de áudio é escrita em C ou C ++. Em 1996, Steinberg lançou a primeira versão do VST SDK, ainda o padrão de plug-in mais popular. Após 20 anos, não é mais necessário escrever em C / C ++: para o VST, existem wrappers em Java, Python, C #, Rust e quem sabe o que mais. Embora o idioma continue sendo uma limitação, agora o som é processado em JavaScript.
  3. Funcional. Se a tarefa for simples e direta, não será necessário escrever um novo aplicativo. O mesmo FFmpeg pode fazer muito.

Nesta situação, a complexidade depende da sua escolha. Na pior das hipóteses, você precisa lidar com várias bibliotecas. E se você não tiver sorte, com abstrações complexas e interfaces completamente diferentes.


Qual é o resultado?


Você precisa escolher entre muito complexo e complexo :


  • ou lide com várias APIs de baixo nível para escrever suas bicicletas
  • ou lide com várias APIs e tente fazer amizade com elas

Independentemente do método selecionado, a tarefa sempre se resume ao transportador. As tecnologias utilizadas podem variar, mas a essência é a mesma. O problema é que, novamente, em vez de resolver um problema real, você deve escrever a bicicleta correia transportadora.


Mas há uma saída.


phono


phono


phono criado para solucionar problemas comuns - para " receber, processar e transmitir " sons. Para fazer isso, ele usa o pipeline como a abstração mais natural. Há um artigo no blog oficial do Go que descreve o padrão de pipeline. A idéia principal do pipeline é que existem vários estágios do processamento de dados que funcionam independentemente um do outro e trocam dados por canais. O que você precisa


Porque ir


Primeiro, a maioria dos programas e bibliotecas de áudio são escritos em C, e o Go é frequentemente chamado de sucessor. Além disso, existem cgo e muitos fichários para bibliotecas de áudio existentes. Você pode pegar e usar.


Em segundo lugar, na minha opinião pessoal, Go é uma boa linguagem. Não irei fundo, mas observarei seu multithreading . Canais e gorutinas simplificam bastante a implementação do transportador.


Abstração


O coração do phono é o tipo de pipe.Pipe . É ele quem implementa o pipeline. Como na amostra do blog , existem três tipos de estágios:


  1. pipe.Pump (bomba inglesa) - recebendo som, apenas canais de saída
  2. pipe.Processor (processador inglês) - processamento de som, canais de entrada e saída
  3. pipe.Sink (coletor inglês) - transmissão de som, apenas canais de entrada

Dentro do pipe.Pipe dados são passados ​​em buffers. Regras pelas quais construir um pipeline:


pipe_diagram


  1. Um pipe.Pump
  2. pipe.Processor colocado sequencialmente um após o outro
  3. Um ou mais pipe.Sink colocada em paralelo
  4. Todos os componentes pipe.Pipe devem ter o mesmo:
    • Tamanho do buffer (mensagens)
    • Taxa de amostragem
    • Número de canais

A configuração mínima é Pump e um dissipador, o restante é opcional.


Vejamos alguns exemplos.


Simples


Tarefa: reproduz o arquivo wav.


Vamos trazê- lo para o formulário " receber, processar, transferir ":


  1. Obter áudio de um arquivo wav
  2. Transferir áudio para um dispositivo portaudio


O áudio é lido e reproduzido imediatamente.


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

Primeiro, criamos os elementos do pipeline futuro: wav.Pump e portaudio.Sink e os passamos para o pipe.New construtor. A função de p.Do(pipe.actionFn) error inicia o pipeline e aguarda o término.


Mais difícil


Tarefa: divida o arquivo wav em amostras, componha uma faixa, salve o resultado e toque-o simultaneamente.


Uma faixa é uma sequência de amostras e uma amostra é um pequeno segmento de áudio. Para cortar o áudio, você deve primeiro carregá-lo na memória. Para fazer isso, use o tipo asset.Asset do pacote phono/asset . Dividimos a tarefa em etapas padrão:


  1. Obter áudio de um arquivo wav
  2. Transferir áudio para a memória

Agora fazemos amostras com nossas mãos, adicionamos à faixa e concluímos a tarefa:


  1. Obter áudio de uma faixa
  2. Transfira o áudio para
    • arquivo wav
    • dispositivo portaudio

exemplo_normal


Novamente, sem uma etapa de processamento, mas dois pipelines!


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

Comparado ao exemplo anterior, existem dois pipe.Pipe . O primeiro transfere dados para a memória para que você possa cortar as amostras. O segundo tem dois destinatários no final: wav.Sink e portaudio.Sink . Com esse esquema, o som é gravado simultaneamente em um arquivo wav e reproduzido.


Mais difícil


Tarefa: leia dois arquivos wav, misture, processe o plug-in vst2 e salve em um novo arquivo wav.


Existe um simples mixer.Mixer no mixer.Mixer phono/mixer . Pode transmitir sinais de várias fontes e misturar um. Para fazer isso, ele implementa simultaneamente pipe.Pump e pipe.Sink .


Novamente, a tarefa consiste em duas subtarefas. O primeiro é assim:


  1. Obtenha o arquivo wav de áudio
  2. Transferir áudio para o mixer

Segundo:


  1. Obtenha áudio do mixer.
  2. Processando plugin de áudio
  3. Transferir áudio para arquivo wav

example_hard


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

Já existem três pipe.Pipe , todos interconectados através de um mixer. Para iniciar, use a função p.Begin(pipe.actionFn) (pipe.State, error) . Diferentemente do p.Do(pipe.actionFn) error , ele não bloqueia a chamada, mas simplesmente retorna um estado que pode ser esperado com o p.Wait(pipe.State) error .


O que vem a seguir?


Quero que o phono torne a estrutura de aplicativo mais conveniente. Se você tem algum problema com o som, não precisa entender APIs complexas e gastar tempo estudando padrões. Tudo o que é necessário é construir um transportador a partir de elementos adequados e executá-lo.


Durante meio ano, os seguintes pacotes foram filmados:


  • phono/wav - lê / grava arquivos wav
  • phono/vst2 - ligações incompletas do VST2 SDK, enquanto você só pode abrir o plug-in e chamar seus métodos, mas nem todas as estruturas
  • phono/mixer - mixer, adiciona sinais N, sem equilíbrio e volume
  • phono/asset - amostragem de buffer
  • phono/track - leitura seqüencial de amostras (estratificação quebrada)
  • phono/portaudio - reprodução de sinal durante experimentos

Além desta lista, há uma lista crescente de novas idéias e idéias, incluindo:


  • Countdown
  • Variável em tempo real
  • Bomba / pia HTTP
  • Automação de parâmetros
  • Processador de reamostragem
  • Balanço e volume do misturador
  • Bomba em tempo real
  • Bomba sincronizada para várias faixas
  • Vst2 completo

Nos seguintes artigos, analisarei:


  • ciclo de vida do pipe.Pipe - devido à estrutura complexa, seu estado é controlado pelo átomo final
  • como escrever seus estágios de pipeline

Este é o meu primeiro projeto de código aberto, por isso serei grato por qualquer ajuda e recomendações. De nada.


Referências


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


All Articles