Lassen Sie uns den Sound auf Go bearbeiten

Haftungsausschluss: Ich berücksichtige keine Algorithmen und APIs für die Arbeit mit Ton- und Spracherkennung. Dieser Artikel befasst sich mit Audio-Problemen und wie Sie sie mit Go lösen können.

gopher


phono- Anwendungsrahmen für die Arbeit mit Sound. Seine Hauptfunktion besteht darin, eine Pipeline verschiedener Technologien zu erstellen, die den Klang verarbeiten.für dich wie du willst.


Was macht der Förderer neben anderen Technologien und warum ein anderes Gerüst? Jetzt werden wir verstehen.


Woher kommt der Ton?


Bis 2018 hat sich der Ton als Standard für die menschliche Interaktion mit der Technologie etabliert. Die meisten IT-Giganten haben ihren Sprachassistenten entwickelt oder tun dies gerade. Die Sprachsteuerung ist in den meisten Betriebssystemen bereits vorhanden, und Sprachnachrichten sind ein typisches Merkmal eines Messenger. In der Welt arbeiten ungefähr tausend Startups an der Verarbeitung natürlicher Sprache und ungefähr zweihundert an Spracherkennung.


Mit Musik ist eine ähnliche Geschichte. Es kann von jedem Gerät abgespielt werden und Tonaufnahmen stehen jedem zur Verfügung, der einen Computer besitzt. Musiksoftware wird von Hunderten von Unternehmen und Tausenden von Enthusiasten auf der ganzen Welt entwickelt.


Allgemeine Aufgaben


Wenn Sie mit Sound arbeiten mussten, sollten die folgenden Bedingungen bekannt sein:


  • Audio muss von einer Datei, einem Gerät, einem Netzwerk usw. abgerufen werden .
  • Audio muss verarbeitet werden : Effekte hinzufügen, umkodieren, analysieren usw.
  • Audio muss in eine Datei, ein Gerät, ein Netzwerk usw. übertragen werden.
  • Die Daten werden in kleinen Puffern übertragen.

Es stellt sich die übliche Pipeline heraus - es gibt einen Datenstrom, der mehrere Verarbeitungsstufen durchläuft.


Lösungen


Zur Klarheit nehmen wir das Problem aus dem wirklichen Leben. Zum Beispiel müssen Sie eine Stimme in Text konvertieren:


  • Wir nehmen Audio vom Gerät auf
  • Geräusche entfernen
  • Ausgleichen
  • Signal an die Spracherkennungs-API übergeben

Wie bei jedem anderen Problem gibt es mehrere Lösungen.


Auf der Stirn


Nur für Hardcore RadfahrerProgrammierer. Wir nehmen den Sound direkt über den Soundkartentreiber auf, wir schreiben einen intelligenten Geräusch- und Multiband-Equalizer. Das ist sehr interessant, aber Sie können Ihre ursprüngliche Aufgabe für einige Monate vergessen.


Lang und sehr schwer.


Auf normale Weise


Die Alternative besteht darin, vorhandene APIs zu verwenden. Sie können Audio mit ASIO, CoreAudio, PortAudio, ALSA und anderen aufnehmen. Es gibt auch verschiedene Arten von Plug-Ins für die Verarbeitung: AAX, VST2, VST3, AU.


Eine reichhaltige Auswahl bedeutet nicht, dass Sie alles auf einmal verwenden können. In der Regel gelten folgende Einschränkungen:


  1. Operationssystem. Nicht alle APIs sind auf allen Betriebssystemen verfügbar. AU ist beispielsweise eine native OS X-Technologie und ist nur dort verfügbar.
  2. Programmiersprache Die meisten Audiobibliotheken sind in C oder C ++ geschrieben. 1996 brachte Steinberg die erste Version des VST SDK heraus, das nach wie vor der beliebteste Plug-In-Standard ist. Nach 20 Jahren ist es nicht mehr notwendig, in C / C ++ zu schreiben: Für VST gibt es Wrapper für Java, Python, C #, Rust und wer weiß noch was. Die Sprache bleibt zwar eine Einschränkung, aber jetzt wird der Sound auch in JavaScript verarbeitet.
  3. Funktional Wenn die Aufgabe einfach und klar ist, muss keine neue Anwendung geschrieben werden. Dasselbe FFmpeg kann viel tun.

In dieser Situation hängt die Komplexität von Ihrer Wahl ab. Im schlimmsten Fall müssen Sie sich mit mehreren Bibliotheken beschäftigen. Und wenn Sie überhaupt kein Glück haben, mit komplexen Abstraktionen und völlig unterschiedlichen Schnittstellen.


Was ist das ergebnis


Sie müssen zwischen sehr komplex und komplex wählen :


  • Entweder verwenden Sie mehrere Low-Level-APIs, um Ihre Fahrräder zu schreiben
  • Entweder behandeln Sie mehrere APIs und versuchen Sie, sich mit ihnen anzufreunden

Unabhängig von der gewählten Methode kommt es immer auf die Pipeline an. Die verwendeten Technologien können variieren, die Essenz bleibt jedoch unverändert. Das Problem ist, dass Sie, anstatt ein echtes Problem zu lösen, wieder schreiben müssenFahrrad Förderband


Aber es gibt einen Ausweg.


Phono


Phono


phonoerstellt, um häufige Probleme zu lösen - " empfangen, verarbeiten und übertragen ". Dafür benutzt er die Pipeline als die natürlichste Abstraktion. Im offiziellen Blog Go gibt es einen Artikel , der die Muster-Pipeline (englische Pipeline) beschreibt. Die Hauptidee der Pipeline ist, dass es mehrere Stufen der Datenverarbeitung gibt, die unabhängig voneinander arbeiten und Daten über Kanäle austauschen. Das ist notwendig


Warum gehen


Erstens sind die meisten Audioprogramme und -bibliotheken in C geschrieben, und Go wird oft als Nachfolger genannt. Darüber hinaus gibt es cgo und einige Bindungen für vorhandene Audiobibliotheken . Sie können nehmen und verwenden.


Zweitens ist Go meiner Meinung nach eine gute Sprache. Ich werde nicht tief gehen, aber ich werde das Multithreading bemerken . Kanäle und Kanäle vereinfachen die Implementierung der Pipeline erheblich.


Abstraktionen


Das Herz phonoist der Typ pipe.Pipe(englische Pfeife). Dass es die Pipeline implementiert. Wie in dem Beispiel aus dem Blog gibt es drei Arten von Stufen:


  1. pipe.Pump(Englische Pumpe) - Ton empfangen , nur Ausgangskanäle
  2. pipe.Processor(Englischer Handler) - Tonverarbeitung , Eingabe- und Ausgabekanäle
  3. pipe.Sink- (English sink.) Übertragung nur von Schalleingangskanäle

Im Inneren werden die pipe.PipeDaten von Puffern übertragen. Die Regeln, nach denen Sie die Pipeline erstellen können:


Rohrdiagramm


  1. Eins pipe.Pump
  2. Mehrere pipe.Processornacheinander platziert
  3. Eine oder mehrere pipe.Sinkparallel platziert
  4. Alle Komponenten pipe.Pipemüssen gleich sein:
    • Puffergröße (Nachrichten)
    • Abtastrate
    • Anzahl der Kanäle

Die minimale Konfiguration ist Pump and one Sink, der Rest ist optional.


Schauen wir uns einige Beispiele an.


Einfach


Aufgabe: WAV-Datei abspielen.


Bringen wir es auf das Formular " Empfangen, Verarbeiten, Übertragen ":


  1. Holen Sie sich Audio aus der WAV-Datei
  2. Wir übertragen Audio auf ein Portaudio-Gerät


Audio wird gelesen und sofort abgespielt.


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 portaudiofunceasy() {
    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)
}

Erstens haben wir die Elemente der zukünftigen Pipeline erstellen: wav.Pumpund portaudio.Sinkund an den Konstruktor übergeben pipe.New. Die Funktion p.Do(pipe.actionFn) errorstartet die Pipeline und wartet auf das Ende der Arbeit.


Härter


Die Aufgabe: Die Wav-Datei in Samples aufteilen, daraus einen Track zusammenstellen, das Ergebnis speichern und gleichzeitig abspielen.


Ein Track ist eine Sequenz von Samples und ein Sample ist ein kleines Stück Audio. Um Audio zu schneiden, müssen Sie es zuerst in den Speicher laden. Dazu verwenden wir den Typ asset.Assetaus dem Paket phono/asset. Wir teilen die Aufgabe in Standardschritte auf:


  1. Holen Sie sich Audio aus der WAV-Datei
  2. Wir übertragen Audio in den Speicher

Jetzt machen wir Proben mit unseren Händen, fügen sie der Spur hinzu und erledigen die Aufgabe:


  1. Wir bekommen Audio von der Spur
  2. Wir übertragen Audio auf
    • WAV-Datei
    • Portaudio-Gerät

example_normal


Wieder ohne Verarbeitungsphase, aber bis zu zwei 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 portaudiofuncnormal() {
    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)
}

Im Vergleich zum vorherigen Beispiel gibt es zwei pipe.Pipe. Der erste überträgt Daten in den Speicher, so dass Proben geschnitten werden können. Der zweite hat am Ende nur zwei Empfänger: wav.Sinkund portaudio.Sink. Bei diesem Schema wird der Ton gleichzeitig in einer WAV-Datei aufgenommen und abgespielt.


Noch schwieriger


Aufgabe: Lesen Sie zwei WAV-Dateien, mischen Sie das vst2-Plugin und speichern Sie es in einer neuen WAV-Datei.


Im Paket phono/mixerist ein einfacher Mixer enthalten mixer.Mixer. Sie können Signale von mehreren Quellen an ihn senden und eine davon erhalten . Dazu implementiert er gleichzeitig pipe.Pumpund pipe.Sink.


Wieder besteht die Aufgabe aus zwei Unteraufgaben. Der erste sieht so aus:


  1. Erhalten Sie eine Audio - WAV - Datei
  2. Wir übertragen Audio in den Mixer

Zweitens:


  1. Holen Sie sich Audio aus dem Mixer
  2. Wir verarbeiten Audio-Plugins
  3. Wir übertragen Audio in die WAV-Datei

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 i.e: sample rate, bit depth and number of channels.// In real life implicit conversion will be needed.funchard() {
    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)
}

Es gibt bereits drei pipe.Pipe, die alle durch einen Mischer miteinander verbunden sind. Zum Starten wird die Funktion verwendet p.Begin(pipe.actionFn) (pipe.State, error). Im Gegensatz p.Do(pipe.actionFn) errordazu wird der Anruf nicht blockiert, sondern es wird einfach ein Status zurückgegeben, mit dem Sie warten können p.Wait(pipe.State) error.


Was weiter?


Ich möchte phonodas praktischste Anwendungsframework werden. Wenn ein Soundproblem vorliegt, müssen Sie keine komplexen APIs verstehen und Zeit mit dem Studium von Standards verbringen. Sie müssen lediglich eine Pipeline mit geeigneten Elementen erstellen und diese starten.


Für ein halbes Jahr wurden die folgenden Pakete heruntergespült:


  • phono/wav - WAV-Dateien lesen / schreiben
  • phono/vst2 - unvollständige VST2-SDK-Bindung, während Sie das Plugin nur öffnen und seine Methoden aufrufen können, jedoch nicht alle Strukturen
  • phono/mixer - Mischer, fügt N Signale hinzu, ohne Balance und Lautstärke
  • phono/asset - Probenahmepuffer
  • phono/track - sequentielles Lesen von Proben (geschichtete Schichtung)
  • phono/portaudio - Wiedergabe eines Signals während des Experiments

Neben dieser Liste gibt es einen ständig wachsenden Rückstand an neuen Ideen und Ideen, darunter:


  • Countdown
  • Veränderbar im laufenden Betrieb
  • HTTP-Pumpe / -Senke
  • Automatisierung von Parametern
  • Prozessor nachdenken
  • Balance und Lautstärke im Mixer
  • Echtzeitpumpe
  • Synchronisierte Pumpe für mehrere Spuren
  • Volles vst2

In den folgenden Artikeln werde ich analysieren:


  • Lebenszyklus pipe.Pipe- aufgrund der komplexen Struktur wird der Zustand vom Endatom gesteuert
  • wie Sie Ihre Pipelinestufen schreiben

Dies ist mein erstes Open-Source-Projekt, daher bin ich für Ihre Hilfe und Empfehlungen dankbar. Willkommen zurück.


Links



Jetzt auch beliebt: