今年夏天,我写了一篇简短的文章,向亲爱的读者介绍声音和Core Audio的基本知识。 但是现在该更深入了。 这次,我们将探索一些新的音频节点,并将使用我们的第一个音频回调函数。
但是今天这篇文章的真正触发点是我想为钢琴购买一些效果。 我真的很喜欢自然钢琴的声音,但是通过一些效果器处理它的想法确实让我兴奋。 我最喜欢的吉他盒一直是延迟+弯针,所以我开始四处张望。 我听到了20条不同的建议,而且比想像的要多-嘿,我可以延迟一下! (而且也许比说服自己我实际上并不需要一个,也许多做些练习会更好)。 我们已经知道上一篇文章有什么延迟。 Looper允许您录制音轨,循环播放它们以及在它们上播放。 这使您可以自己创建真实的声音墙。
那我们该怎么办? iOS或MacOS? 我决定使用后者–在音频输入方面要更加灵活(我仍然有带输入的Mac Pro!),我们不需要处理音频会话。 接下来要去哪里? 最好从UI开始,而我觉得上面的图片太复杂了。 因此,让我们做这样的事情。
我们将能够录制2首曲目并进行播放! 右侧的按钮将开始和停止这些曲目,所有内容都应通过延迟效果播放。 不要告诉任何人,但是延迟播放会让您听起来像一个真正有成就的音乐家。
现在,我们现在看到界面的外观,是时候考虑实际的实现了。 在上一篇文章中,我们已经讨论过AVAudioEngine-相对较新的类,可以解决大量与音频有关的问题。 AVAudioEngine是AVAudioNode的集合。 仅查看我们精心绘制的界面,我们就可以假定在某个时候我们需要播放2个不同的音频循环并仍然记录我们的输入(无论它来自何处)。 我们可以将音频样本保留在内存或磁盘中,而后者将更加安全,因为我们还没有真正讨论过循环的持续时间。
好的,我们将记录来自音频输入的音频,将其保存到文件中,并且还需要支持同时播放。 对于某种混音器来说,这听起来不错。 而且,除非我们不想为每个输入调整延迟参数,否则我们最好只保留一个延迟。 听起来有点复杂,但是我刚才描述的想法可以这样表达:
从上一篇文章中,我们已经知道每个AVAudioEngine的输入和输出已经存在。 通过使用inputNode和mainMixerNode属性可以轻松访问它们。
上次我们已经使用了延迟。 AVAudioUnitDelay在这里可以提供帮助。 为了启用这种优美的声学技巧,我们只需要调用一些方法即可。 60年前,我们需要这样的东西:
如果您已经很了解Core Audio,那么我建议您考虑一下如何使用音频缓冲区实现数字延迟。 我保证您会玩得开心–但可能不及Mike Battle和他的团队弄清楚Echoplex的磁带机制那么多。 实际上听起来也很棒。
回到我们的实现–我们尚未使用的一个节点是AVAudioPlayerNode。 名称不言而喻-我们可以播放音频文件,这些文件的片段甚至单个音频缓冲区。 音频引擎的主要输出实际上是AVAudioMixerNode的一个实例-因此我们也可以将此类用于我们自己的目的。
让numberOfInputs = 2
让引擎= AVAudioEngine()
让延迟= AVAudioUnitDelay()
让混音器= AVAudioMixerNode()
var播放器:[AVAudioPlayerNode] = []
让format = engine.inputNode.outputFormat(forBus:0)
//在连接节点之前,我们需要将其连接到引擎
engine.attach(延迟)
engine.attach(混音器)
对于总线在0 .. <numberOfInputs {
让播放器= AVAudioPlayerNode()
players.append(玩家)
engine.attach(玩家)
engine.connect(播放器,to:混音器,fromBus:0,toBus:bus,format:format)
}
engine.connect(engine.inputNode,至:混合器,fromBus:0,toBus:numberOfInputs,format:format)
engine.connect(混音器,至:延迟,格式:format())
engine.connect(延迟,至:engine.mainMixerNode,format:format())
这可能是整个设置中最有趣的部分。 我们创建所需的所有节点,将它们连接到引擎(在下面将其连接到AUGraph ),然后根据上述方案将它们连接起来。 在大多数情况下,您只需要使用AVAudioEngine.connect(node1 :, to :, format :)方法,但是由于在我们的图片中,我们的混合器节点有多个输入,因此我们需要使用总线。 在逻辑上可以将其视为连接点。 通常,节点的名称和常用用法表明了它可能拥有的总线数量。 我们的调音台显然有多个输入和一个输出,效果只有一个输入和输出。 每个总线都有一种格式,可以建议我们要使用的一组音频参数(例如采样率等,但我们在上一篇文章中已进行了介绍)。
然后,我们只需要使用这些节点来记录和读取音频。 让我们从录音开始。 要将音频记录到文件中,我们需要创建一个AVAudioFile实例,并设置引擎的默认inputNode进行写入。 我们走吧。
让url = URL(fileURLWithPath:“ \(NSTemporaryDirectory())input.caf”)
//之后检查文件实际位置很方便
打印(URL)
让文件=尝试AVAudioFile(forWriting:url,settings:format.settings)
//注意,我们正在“监听”引擎的输入而不是引擎的输出上的音频。 我们希望立即记录“干净”的音轨,否则运行两次延迟会破坏整个体验
engine.inputNode.installTap(onBus:0,bufferSize:4096,format:file.processingFormat){
(buffer:AVAudioPCMBuffer !, when:AVAudioTime!)在
做{
尝试file.write(from:buffer)
} {
打印(“书写问题”)
}
}
尝试engine.start()
安装分接头(或在此API的先前版本中-提供回调函数)可能是Core Audio最重要的概念。 在我们的例子中,我们要求输入节点每次收到新的音频时都通知我们-在这个版本中,它被巧妙地包装在AVAudioPCMBuffer中。 实际上,我们可以在任何节点的任何总线上安装(然后移除)tap,然后由我们来决定如何处理这些音频缓冲区。 我们可以将它们转换为其他格式,通过网络进行流传输,使用快速傅立叶变换来构建该信号的可视表示,仅出于调试目的而观察此数据等等。 无论我们做什么,我们都需要快速做到。 因为将经常调用此方法(取决于缓冲区大小和采样率)。 即使添加简单的日志记录,有时也会导致音频故障。 我还需要提及的是,此API有一个版本,您可以自己提供缓冲区-当您要构建自己的合成器甚至是简单的振荡器时,这很有用。 因此,您确实需要知道您对这些缓冲区的处理方式,以便顺利运行音频软件。 在我们的案例中,我们正在调用专门为此目的设计的方法-因此我们在这里必须感到安全。
重要代码的最后一位将与播放有关。 而且由于我们要循环播放曲目,因此我们需要弄清楚一旦记录文件,如何组织文件的连续播放。 实际上, AVAudioPlayerNode播放方法的一种变体具有使用AVAudioPlayerNodeBufferOptions准备音频缓冲区的版本,其中此OptionSet的元素之一是“ 循环 ”。 听起来像我们需要的。
尝试engine.start()
let buffer = AVAudioPCMBuffer(pcmFormat:file.processingFormat,frameCapacity:UInt32(file.length))
尝试file.read(入:缓冲区)
player.scheduleBuffer(buffer,at:AVAudioTime(hostTime:0),options:.loops,completionHandler:nil)
player.play()
因此,这里我们要使用文件的内容创建一个音频缓冲区,使用.loops选项将其安排为从文件开头开始为播放器播放,而不是要求播放器播放(我们的引擎必须处于运行状态,以便听到任何声音-由于某些原因,我们将播放器连接到了输出,对吗?)。
此设置的唯一棘手的问题是拥有一系列这些播放器,并确保在我们要开始播放时拥有文件。
“这非常有趣,但是我们需要一个演示!”。 好吧,我在这里完全和你在一起。 妳去 很抱歉心情低落–伦敦现在的天气就是这样。
再一次,我在这里使用笔记本电脑的内置麦克风对其进行录制,并且这两个音轨都记录在同一会话中。
我鼓励您在此处检查该应用程序的代码:github。 您可以轻松地使用AVAudioUnitDelay参数-列表非常详尽。
这将我们带到了该职位的标题– 别忘了戴上耳机 ! 您的麦克风和扬声器可能在同一设备上,并且延迟运行。 这将100%引起一些可怕的回声! 而且从性能角度来看,最好事先录制不会从当前输入中流失的声音。
谈论与Core Audio或任何其他音频框架一起使用-您总是有错误计算增益,音量,频率和其他重要参数的危险。 我惊呆了很多次,至少在你身上戴着耳机可以保护周围的人。 另一方面,您的错误可能导致计算机生成漂亮的音乐,尤其是在处理MIDI时。 我会在这里留下您的辛酸甜蜜的回忆,并会花更多时间在此应用程序上。