- PVSM.RU - https://www.pvsm.ru -
Дисклеймер: Я не рассматриваю какие-либо алгоритмы и API для работы со звуком и распознаванием речи. Эта статья о проблемах при работе с аудио и об их решении с помощью Go.

phono — прикладной фреймворк для работы со звуком. Его основная функция — создать конвейер из разных технологий, который обработает звук за вас нужным вам образом.
При чём тут конвейер, к тому же из разных технологий и зачем ещё один фреймворк? Сейчас разберёмся.
К 2018 году звук стал стандартным способом взаимодействия человека с технологиями. Большинство IT-гигантов создали [1] своего голосового помощника или делают это прямо сейчас. Голосовое управление уже есть в большинстве операционных систем, а голосовые сообщения — типичная функция любого мессенджера. В мире около тысячи [2] стартапов работают над обработкой естественного языка и около двух сотен [3] над распознаванием речи.
С музыкой похожая история. Она играет из любого устройства, а звукозапись доступна каждому, у кого есть компьютер. Музыкальный софт разрабатывают сотни компаний [4] и тысячи энтузиастов [5] по всему миру.
Если вам приходилось работать со звуком, то следующие условия должны звучать знакомо:
Получается обычный конвейер — есть поток данных, который проходит несколько стадий обработки.
Для наглядности, возьмём задачу из реальной жизни. Например, нужно преобразовать голос в текст:
Как и любая другая задача, эта имеет несколько решений.
Только для хардкорных велосипедистов программистов. Записываем звук непосредственно через драйвер звуковой карты, пишем умный шумодав и многополосный эквалайзер. Это очень интересно, но можно на несколько месяцев забыть о своей изначальной задаче.
Долго и очень сложно.
Альтернатива — использовать существующие API. Записать аудио можно с помощью ASIO, CoreAudio, PortAudio, ALSA и прочих. Для обработки тоже есть несколько видов плагинов: AAX, VST2, VST3, AU.
Богатый выбор не означает, что можно использовать всё сразу. Обычно действуют следующие ограничения:
В этой ситуации сложность зависит от вашего выбора. В худшем случае придётся иметь дело с несколькими библиотеками. И если совсем не повезёт, со сложными абстракциями и абсолютно разными интерфейсами.
Нужно выбирать между очень сложным и сложным:
Неважно какой способ выбран, задача всегда сводится к конвейеру. Используемые технологии могут отличаться, но суть неизменна. Проблема в том, что опять вместо решения реальной задачи, приходиться писать велосипед конвейер.
Но есть выход.

phono создан, чтобы решить общие задачи — "получить, обработать и передать" звук. Для этого он использует конвейер, как самую естественную абстракцию. В официальном блоге [6] Go есть статья [7], которая описывает паттерн pipeline (англ. конвейер). Главная идея pipeline в том, что есть несколько стадий обработки данных, которые работают независимо друг от друга и обмениваются данными через каналы. То, что надо.
А почему Go?
Во-первых, большинство аудио программ и библиотек написаны на C, а Go часто упоминается в качестве его преемника. К тому же, есть cgo [8] и довольно много биндингов [9] для существующих аудио библиотек. Можно брать и пользоваться.
Во-вторых, по моему личному мнению, Go — хороший язык. Не буду углубляться, но отмечу его многопоточность [10]. Каналы и горутины сильно упрощают реализацию конвейера.
Сердцем phono является тип pipe.Pipe (англ. труба). Именно он реализует pipeline. Как и в образце из блога [7], предусмотрено три вида стадий:
pipe.Pump (англ. насос) — получение звука, только выходные каналыpipe.Processor (англ. обработчик) — обработка звука, входные и выходные каналыpipe.Sink (англ. раковина) — передача звука, только входные каналыВнутри pipe.Pipe данные передаются буферами. Правила, по которым можно строить pipeline:

pipe.Pumppipe.Processor, размещённых последовательно друг за другомpipe.Sink, размещённых параллельноpipe.Pipe должны иметь одинаковые:
Минимальная конфигурация — Pump и один Sink, остальное опционально.
Разберём несколько примеров.
Задача: воспроизвести wav файл.
Приведём её к виду "получить, обработать, передать":

Аудио считывается и сразу воспроизводится.
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)
}
Сначала мы создаём элементы будущего конвейера: wav.Pump и portaudio.Sink и передаём их в конструктор pipe.New. Функция p.Do(pipe.actionFn) error запускает конвейер и ждёт окончания работы.
Задача: разбить wav файл на семплы, составить из них трек, результат сохранить и одновременно воспроизвести.
Трек — это последовательность семплов, а семпл — небольшой отрезок аудио. Чтобы аудио можно было резать, нужно сначала загрузить его в память. Для этого используем тип asset.Asset из пакета phono/asset. Разбиваем задачу на стандартные шаги:
Теперь руками делаем семплы, добавляем их в трек и добиваем задачу:

Снова, без стадии обработки, зато целых два pipeline!
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)
}
По сравнению с прошлым примером, есть два pipe.Pipe. Первый передаёт данные в память, чтобы можно было нарезать семплы. Второй имеет сразу два получателя в конце: wav.Sink и portaudio.Sink. При такой схеме звук одновременно записывается в wav файл и воспроизводится.
Задача: прочитать два wav файла, смешать, обработать vst2 плагином и сохранить в новый wav файл.
В пакете phono/mixer есть простой миксер mixer.Mixer. В него можно передать сигналы из нескольких источников и получить один смиксованный. Для этого он одновременно реализует pipe.Pump и pipe.Sink.
Опять задача состоит из двух подзадач. Первая выглядит так:
Вторая:

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.
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)
}
Здесь уже три pipe.Pipe, все связанные между собой через миксер. Для запуска используется функция p.Begin(pipe.actionFn) (pipe.State, error). В отличии от p.Do(pipe.actionFn) error, она не блокирует вызов, а просто возвращает состояние, которое потом можно дождаться с помощью p.Wait(pipe.State) error.
Я хочу, чтобы phono стал максимально удобным прикладным фреймворком. Если есть задача со звуком, не нужно разбираться в сложных API и тратить время на изучение стандартов. Всё, что надо — построить конвейер из подходящих элементов и запустить его.
За пол года запилены следующие пакеты:
phono/wav — читать/писать wav файлыphono/vst2 — неполные биндинги VST2 SDK, пока можно только открывать плагин и вызывать его методы, но нет всех структурphono/mixer — миксер, складывает N сигналов, без баланса и громкостиphono/asset — семплирование буферовphono/track — последовательное считывание семплов (разрулены наслоения)phono/portaudio — воспроизведение сигнала, пока экспериментыКроме этого списка, есть постоянно толстеющий бэклог из новых идей и задумок, среди которых:
В следующих статьях я разберу:
pipe.Pipe — из-за сложной структуры его состояние управляется конечным атоматомЭто мой первый open-source проект, так что я буду благодарен любой помощи и рекомендациям. Добро пожаловать.
Автор: dudk
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/294154
Ссылки в тексте:
[1] создали: https://www.predictiveanalyticstoday.com/top-intelligent-personal-assistants-automated-personal-assistants/
[2] около тысячи: https://angel.co/natural-language-processing
[3] около двух сотен: https://angel.co/speech-recognition
[4] сотни компаний: https://www.pluginboutique.com/manufacturers
[5] тысячи энтузиастов: https://www.kvraudio.com/marketplace/brands
[6] официальном блоге: https://blog.golang.org/
[7] статья: https://blog.golang.org/pipelines
[8] cgo: https://golang.org/cmd/cgo/
[9] довольно много биндингов: https://github.com/avelino/awesome-go#audio-and-music
[10] многопоточность: https://www.golang-book.com/books/intro/10
[11] phono: https://github.com/dudk/phono
[12] Источник: https://habr.com/post/424623/?utm_campaign=424623
Нажмите здесь для печати.