让我们在Go上处理声音

免责声明:我不考虑用于声音和语音识别的任何算法和API。 本文介绍音频问题以及如何使用Go解决问题。

地鼠


phono是用于处理声音的应用程序框架。 它的主要功能是利用各种技术来制造输送机,以处理声音 为你 以您需要的方式。


除了采用不同的技术外,输送机还需要做什么?为什么还要使用其他框架? 现在让我们弄清楚。


声音从哪里来?


到2018年,声音已成为人类与技术互动的标准方式。 大多数IT巨头已经创建了自己的语音助手,或者现在正在这样做。 语音控制已经在大多数操作系统上,并且语音消息传递是任何Messenger的典型功能。 在世界上, 大约一千家初创公司正在从事自然语言处理, 约有200家正在从事语音识别。


与音乐类似的故事。 它可以在任何设备上播放,并且录音对所有拥有计算机的人都可用。 音乐软件是由全球数百家公司 和数千名爱好者开发的。


常见任务


如果您必须处理声音,那么以下条件听起来应该很熟悉:


  • 必须从文件,设备,网络等获得音频。
  • 必须处理音频:添加效果,转码,分析等。
  • 音频必须传输到文件,设备,网络等。
  • 数据在小缓冲区中传输。

事实证明,这是一条常规的管道-数据流经过多个处理阶段。


解决方案


为了清楚起见,让我们从现实生活中完成一项任务。 例如,您需要将语音转换为文本:


  • 我们从设备录制音频
  • 消除噪音
  • 均衡
  • 将信号传递到语音识别API

像其他任何任务一样,此任务有多种解决方案。


额头


仅铁杆 骑单车的人 程序员。 我们直接通过声卡驱动程序录制声音,编写智能降噪和多频段均衡器。 这非常有趣,但是您可能会忘记几个月的原始任务。


很长很困难。


正常的


一种替代方法是使用现有的API。 您可以使用ASIO,CoreAudio,PortAudio,ALSA等录制音频。 还有多种类型的插件需要处理:AAX,VST2,VST3,AU。


多种选择并不意味着您可以一次使用所有内容。 通常,以下限制适用:


  1. 作业系统 并非所有API在所有操作系统上都可用。 例如,AU是本机OS X技术,仅在此可用。
  2. 程式语言 大多数音频库都是用C或C ++编写的。 1996年,Steinberg发行了第一个版本的VST SDK,它仍然是最受欢迎的插件标准。 20年后,不再需要用C / C ++编写:对于VST,Java,Python,C#,Rust中都有包装器,还有谁知道。 尽管语言仍然有局限性,但现在甚至可以在JavaScript中处理声音。
  3. 功能性。 如果任务简单明了,则无需编写新的应用程序。 相同的FFmpeg可以做很多事情。

在这种情况下,复杂程度取决于您的选择。 在最坏的情况下,您必须处理多个库。 如果您根本不走运,那么您可以使用复杂的抽象和完全不同的界面。


结果如何?


您必须在非常复杂复杂之间选择:


  • 要么处理几个底层API来编写自行车
  • 要么处理多个API,然后尝试与他们成为朋友

无论选择哪种方法,任务始终落在传送带上。 所使用的技术可能有所不同,但本质是相同的。 问题是,除了解决实际问题,您还必须编写 自行车 传送带。


但是有一个出路。


唱机


唱机


phono创建是为了解决常见问题-“ 接收,处理和传输 ”声音。 为此,他使用管道作为最自然的抽象。 Go 官方博客上一篇文章描述了管道模式。 管道的主要思想是,数据处理有多个阶段,这些阶段彼此独立工作并通过通道交换数据。 您需要什么。


为什么去


首先,大多数音频程序和库都是用C编写的,而Go通常被称为其后继程序。 此外,还有cgo许多现有音频库的活页夹 。 您可以使用。


其次,以我个人的观点,Go是一门好语言。 我不会深入,但是我会注意到它的多线程 。 通道和gorutins大大简化了输送机的实施。


抽象化


phono的心脏是pipe.Pipe类型。 实现管道的是他。 就像博客示例中一样 ,共有三种类型的阶段:


  1. pipe.Pump (英式泵)- 接收声音,仅输出通道
  2. pipe.Processor (英语处理器)-声音处理 ,输入和输出通道
  3. pipe.Sink (英语水槽)-声音传输 ,仅输入通道

pipe.Pipe内部pipe.Pipe数据在缓冲区中传递。 建立管道的规则:


pipe_diagram


  1. 一管pipe.Pump
  2. pipe.Processor一个接一个地放置
  3. 一个或多个pipe.Sink平行放置
  4. 所有pipe.Pipe组件必须具有相同的内容:
    • 缓冲区大小(消息)
    • 采样率
    • 通道数

最低配置为泵和一个水槽,其余为可选。


让我们看几个例子。


简单的


任务:播放wav文件。


让我们其转换为“ 接收,处理,转移 ”的形式:


  1. 从WAV文件获取音频
  2. 音频传输到端口音频设备


音频被读取并立即播放。


代号
 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.Pumpportaudio.Sink并将它们传递给pipe.New构造函数。 p.Do(pipe.actionFn) error函数启动管道并等待其完成。


更难


任务:将wav文件拆分为样本,从样本中组成曲目,保存结果并同时播放。


音轨是样本序列,样本是一小段音频。 要剪切音频,必须首先将其加载到内存中。 为此,请使用phono/asset包中的asset.Asset类型。 我们将任务分为标准步骤:


  1. 从WAV文件获取音频
  2. 音频传输到内存

现在,我们用手制作样本,将其添加到轨道中并完成任务:


  1. 从轨道获取音频
  2. 音频传输
    • WAV文件
    • 端口音频设备

example_normal


同样,没有处理阶段,但是有两个管道!


代号
 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.Sinkportaudio.Sink 。 使用此方案,声音可以同时记录在wav文件中并进行播放。


更难


任务:读取两个wav文件,混合,处理vst2插件并保存到新的wav文件。


phono/mixer mixer.Mixer有一个简单的mixer.Mixer 。 它可以从多个源传输信号并混合一个。 为此,它同时实现了pipe.Pumppipe.Sink


同样,该任务包含两个子任务。 第一个看起来像这样:


  1. 获取音频WAV文件
  2. 音频传输到调音台

第二:


  1. 从调音台获取音频。
  2. 处理音频插件
  3. 音频传输到WAV文件

example_hard


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

已经有三个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实验期间的信号播放

除了此列表以外,新想法和想法的积压也不断增加,其中包括:


  • 倒数计时
  • 动态变化的管道
  • HTTP泵/水槽
  • 参数自动化
  • 重采样处理器
  • 搅拌机平衡和体积
  • 实时泵
  • 多轨同步泵
  • 完整的vst2

在以下文章中,我将分析:


  • pipe.Pipe生命周期-由于结构复杂,其状态由最终原子控制
  • 如何编写管道阶段

这是我的第一个开源项目,因此,我将感谢您的帮助和建议。 不客气


参考文献


Source: https://habr.com/ru/post/zh-CN424623/


All Articles