Traitons le son sur Go

Avertissement: je ne considère aucun algorithme ni API pour travailler avec la reconnaissance du son et de la parole. Cet article concerne les problèmes audio et comment les résoudre avec Go.

gopher


phono est un cadre d'application pour travailler avec le son. Sa fonction principale est de créer un convoyeur à partir de diverses technologies qui traitera le son pour toi de la manière dont vous avez besoin.


Qu'est-ce que le convoyeur a à voir avec cela, en plus de différentes technologies, et pourquoi un autre cadre? Voyons maintenant.


D'où vient le son?


En 2018, le son est devenu la manière standard dont les humains interagissent avec la technologie. La plupart des géants de l'informatique ont créé leur propre assistant vocal ou le font actuellement. Le contrôle vocal est déjà sur la plupart des systèmes d'exploitation, et la messagerie vocale est une caractéristique typique de tout messager. Dans le monde, environ un millier de startups travaillent sur le traitement du langage naturel et environ deux cents sur la reconnaissance vocale.


Avec la musique, une histoire similaire. Il joue à partir de n'importe quel appareil et l'enregistrement sonore est disponible pour tous ceux qui ont un ordinateur. Le logiciel musical est développé par des centaines d'entreprises et des milliers de passionnés à travers le monde.


Tâches courantes


Si vous deviez travailler avec du son, les conditions suivantes devraient vous être familières:


  • L'audio doit être obtenu à partir d'un fichier, d'un appareil, d'un réseau, etc.
  • L'audio doit être traité : ajouter des effets, transcoder, analyser, etc.
  • L'audio doit être transféré vers un fichier, un appareil, un réseau, etc.
  • Les données sont transmises dans de petits tampons.

Il en résulte un pipeline régulier - il existe un flux de données qui passe par plusieurs étapes de traitement.


Des solutions


Pour plus de clarté, prenons une tâche de la vie réelle. Par exemple, vous devez convertir une voix en texte:


  • Nous enregistrons l'audio de l'appareil
  • Supprimer le bruit
  • Égaliser
  • Passer le signal à l'API de reconnaissance vocale

Comme toute autre tâche, celle-ci a plusieurs solutions.


Le front


Hardcore uniquement cyclistes programmeurs. Nous enregistrons le son directement via le pilote de la carte son, écrivons une réduction intelligente du bruit et un égaliseur multibande. C'est très intéressant, mais vous pouvez oublier votre tâche d'origine pendant plusieurs mois.


Long et très difficile.


Normal


Une alternative consiste à utiliser les API existantes. Vous pouvez enregistrer de l'audio en utilisant ASIO, CoreAudio, PortAudio, ALSA et autres. Il existe également plusieurs types de plugins pour le traitement: AAX, VST2, VST3, AU.


Un large choix ne signifie pas que vous pouvez tout utiliser à la fois. En règle générale, les restrictions suivantes s'appliquent:


  1. Système d'exploitation Toutes les API ne sont pas disponibles sur tous les systèmes d'exploitation. Par exemple, AU est une technologie OS X native et n'est disponible que là-bas.
  2. Langage de programmation La plupart des bibliothèques audio sont écrites en C ou C ++. En 1996, Steinberg a publié la première version du SDK VST, toujours le standard de plugin le plus populaire. Après 20 ans, il n'est plus nécessaire d'écrire en C / C ++: pour VST il y a des wrappers en Java, Python, C #, Rust, et qui sait quoi d'autre. Bien que le langage reste une limitation, désormais même le son est traité en JavaScript.
  3. Fonctionnel. Si la tâche est simple et directe, il n'est pas nécessaire d'écrire une nouvelle application. Le même FFmpeg peut faire beaucoup.

Dans cette situation, la complexité dépend de votre choix. Dans le pire des cas, vous devez traiter avec plusieurs bibliothèques. Et si vous n'avez pas de chance, avec des abstractions complexes et des interfaces complètement différentes.


Quel est le résultat?


Vous devez choisir entre très complexe et complexe :


  • soit gérer plusieurs API de bas niveau pour écrire vos vélos
  • soit traiter avec plusieurs API et essayer de se faire des amis avec eux

Quelle que soit la méthode choisie, la tâche revient toujours au convoyeur. Les technologies utilisées peuvent varier, mais l'essence est la même. Le problème est qu'encore une fois, au lieu de résoudre un vrai problème, vous devez écrire le vélo bande transporteuse.


Mais il y a une issue.


phono


phono


phono créé pour résoudre des problèmes courants - « recevoir, traiter et transmettre » le son. Pour ce faire, il utilise le pipeline comme l'abstraction la plus naturelle. Il y a un article sur le blog officiel Go qui décrit le modèle de pipeline. L'idée principale du pipeline est qu'il existe plusieurs étapes de traitement des données qui fonctionnent indépendamment les unes des autres et échangent des données via des canaux. Ce dont vous avez besoin.


Pourquoi partir?


Premièrement, la plupart des programmes et bibliothèques audio sont écrits en C, et Go est souvent désigné comme son successeur. En outre, il existe des cgo et un bon nombre de classeurs pour les bibliothèques audio existantes. Vous pouvez prendre et utiliser.


Deuxièmement, à mon avis personnel, Go est une bonne langue. Je n'irai pas en profondeur, mais je noterai son multithreading . Les canaux et les gorutins simplifient considérablement la mise en œuvre du convoyeur.


Abstraction


Le cœur du phono est le type pipe.Pipe . C'est lui qui met en œuvre le pipeline. Comme dans l' exemple du blog , il existe trois types d'étapes:


  1. pipe.Pump (pompe anglaise) - réception du son, uniquement les canaux de sortie
  2. pipe.Processor (processeur anglais) - traitement du son, canaux d'entrée et de sortie
  3. pipe.Sink (English sink) - transmission du son, canaux d'entrée uniquement

À l'intérieur du pipe.Pipe données dans des tampons. Règles de construction d'un pipeline:


pipe_diagram


  1. Un pipe.Pump
  2. Plusieurs pipe.Processor placé séquentiellement les uns après les autres
  3. Un ou plusieurs pipe.Sink placé en parallèle
  4. Tous les composants des pipe.Pipe doivent avoir les mêmes:
    • Taille de la mémoire tampon (messages)
    • Taux d'échantillonnage
    • Nombre de canaux

La configuration minimale est une pompe et un évier, le reste est facultatif.


Regardons quelques exemples.


Simple


Tâche: lire le fichier wav.


Apportons-le sous la forme " recevoir, traiter, transférer ":


  1. Obtenez l' audio d'un fichier wav
  2. Transférer l' audio vers un appareil portaudio


L'audio est lu et immédiatement lu.


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

Tout d'abord, nous créons les éléments du futur pipeline: wav.Pump et portaudio.Sink et les passons au pipe.New constructeur. La fonction d' p.Do(pipe.actionFn) error démarre le pipeline et attend qu'il se termine.


Plus dur


Tâche: divisez le fichier wav en échantillons, composez-en une piste, enregistrez le résultat et jouez-le simultanément.


Une piste est une séquence d'échantillons et un échantillon est un petit segment d'audio. Pour couper l'audio, vous devez d'abord le charger en mémoire. Pour ce faire, utilisez le type asset.Asset du asset.Asset phono/asset . Nous divisons la tâche en étapes standard:


  1. Obtenez l' audio d'un fichier wav
  2. Transférer l' audio en mémoire

Maintenant, nous faisons des échantillons avec nos mains, les ajoutons à la piste et terminons la tâche:


  1. Obtenez l' audio d'une piste
  2. Transférer l' audio vers
    • fichier wav
    • appareil portaudio

exemple_normal


Encore une fois, sans étape de traitement, mais deux pipelines!


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

Par rapport à l'exemple précédent, il existe deux pipe.Pipe . Le premier transfère les données en mémoire afin que vous puissiez couper les échantillons. Le second a deux destinataires à la fin: wav.Sink et portaudio.Sink . Avec ce schéma, le son est simultanément enregistré dans un fichier wav et lu.


Plus dur


Tâche: lire deux fichiers wav, mélanger, traiter le plugin vst2 et enregistrer dans un nouveau fichier wav.


Il y a un mixer.Mixer simple. mixer.Mixer dans le mixer.Mixer phono/mixer . Il peut transmettre des signaux de plusieurs sources et en mélanger une. Pour ce faire, il implémente simultanément pipe.Pump et pipe.Sink .


Encore une fois, la tâche consiste en deux sous-tâches. Le premier ressemble à ceci:


  1. Obtenez le fichier audio wav
  2. Transférez l' audio vers la console de mixage

Deuxièmement:


  1. Obtenez l' audio de la console de mixage.
  2. Traitement du plugin audio
  3. Transférer l' audio vers un fichier wav

example_hard


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

Il y a déjà trois pipe.Pipe , tous reliés entre eux par un mélangeur. Pour commencer, utilisez la fonction p.Begin(pipe.actionFn) (pipe.State, error) . Contrairement à l' p.Do(pipe.actionFn) error , il ne bloque pas l'appel, mais renvoie simplement un état qui peut ensuite être attendu avec l' p.Wait(pipe.State) error .


Et ensuite?


Je veux que le phono devienne le cadre d'application le plus pratique. Si vous avez un problème avec le son, vous n'avez pas besoin de comprendre les API complexes et de passer du temps à étudier les normes. Il suffit de construire un convoyeur à partir d'éléments appropriés et de le faire fonctionner.


Pendant six mois, les packages suivants ont été filmés:


  • phono/wav - lire / écrire des fichiers wav
  • phono/vst2 - liaisons incomplètes du SDK VST2, alors que vous ne pouvez ouvrir que le plugin et appeler ses méthodes, mais pas toutes les structures
  • phono/mixer - table de mixage, ajoute N signaux, sans balance ni volume
  • phono/asset - échantillonnage tampon
  • phono/track - lecture séquentielle des échantillons (superposition cassée)
  • phono/portaudio - lecture du signal pendant les expériences

En plus de cette liste, il y a un arriéré sans cesse croissant de nouvelles idées et idées, notamment:


  • Compte à rebours
  • Variable à la volée
  • Pompe / évier HTTP
  • Automatisation des paramètres
  • Processeur de rééchantillonnage
  • Balance et volume du mixeur
  • Pompe en temps réel
  • Pompe synchronisée pour plusieurs pistes
  • Vst2 complet

Dans les articles suivants, j'analyserai:


  • pipe.Pipe vie du pipe.Pipe - en raison de la structure complexe, son état est contrôlé par l'atome final
  • comment écrire les étapes de votre pipeline

Ceci est mon premier projet open-source, donc je serai reconnaissant pour toute aide et recommandations. Vous êtes les bienvenus.


Les références


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


All Articles