原文:AudioKit Tutorial: Getting Started
译者:kmyhy
译者注:很久没有看到如此高质量的技术文章了,因为它不仅仅是一篇优秀的 iOS 开发教程,更是一篇精彩的科普文。关于编程与艺术的结合,声学物理与音乐的碰撞,尽在此文。推荐所有程序员都好好读一读它,让我们的生活除了代码,还有艺术,还有音乐。感谢作者 Colin Eberhardt。
iOS 设备提供了丰富的多媒体体验,比如鲜艳的视觉、声音和可以触控的界面。尽管能够使用各种各样的特性,但作为开发者,我们更多地关注了 App 的视觉设计,而忽略了用户体验的声学效果。
AudioKit 是一个高级音频框架,由声学设计师、程序员和音乐家为 iOS 专门打造。在底层,AudioKit 混合了 Swift、O-C、C++ 和 C,负责和苹果的声频部件 API 打交道。所有神奇的(同时十分复杂的)技术都封装成为极其友好的 Swift API,你甚至可以直接在 Xcode 的 Playground 中使用它。
本文无法全面覆盖 AudioKit 的知识点。相反,我们会通过介绍声音合成和计算机声频的历史,来带你进行一次有趣和时尚的 AudioKit 之旅。通过这种方式,你会学到基本的声学物理,了解早期的合成器比如电子琴是如何工作的。最终来到现代,一个由抽样和混声统治的时代。
请给自己来一杯咖啡,拖过一张椅子,开始我们的旅程!
开始
坦诚讲,旅程的第一步并不十分激动人心。为了在 Playground 中使用 AudioKit,我们必须进行一些“管道工作”。
打开 Xcode,用 File\New\Workspace 菜单新建一个 workspace 叫做 Journey Through Sound, 保存 workspace。
这里我们创建了一个空的 workspace。点击导航窗口左下角的 + 按钮,选择 New Playground… 选项,命名为 Journey, 保存到和 workspace 相同的地方。
新的 Playground 会被编译和执行,看起来是这个样子:
下载 AudioKit 源代码 将文件加压到 playground 同一目录。
打开 Finder,找到 AudioKit-3.4.1/AudioKit/iOS/AudioKit For iOS.xcodeproj 文件,将它拖到 workspace 的根目录下。
你的导航窗口将是这个样子:
在编译目标中选择 iPhone 7 Plus:
打开菜单 Product\Build ,编译 AudioKit 框架。这个框架大概有 700 多个文件,编译可能需要一点时间。
注意:如果在 App 中使用 AudioKit,而不是在 Playground 中使用,你可以下载和使用已经编译好的框架,或者使用 CocoaPods 或 Carthage。我们刚才那样做的原因是因为 Playground 还不支持框架。
编译完成后,点击 Jornery 打开你的 Playground。将 Xcode 的代码替换为:
import AudioKit
let oscillator = AKOscillator()
AudioKit.output = oscillator
AudioKit.start()
oscillator.start()
sleep(10)
编译时,你会听到大约 10 秒钟的蜂鸣声。你可以点击 Playground 调试窗口左下角的 Play/Stop 按钮停止或运行 Playground。
注意:如果 Playground 执行出错,并在 Debug 窗口中出现错误,你可以重启 Xcode。不幸的是,当 Playground 和框架一起使用时,总是容易出现一些小问题,并且无法预知。
振荡子和声音物理学
人类通过物体制造音乐——通过击打、拖拉或者弹奏等形式——有数千年的历史。我们的许多民族乐器,比如鼓、吉他,已经发明几个世纪了。电子乐器的第一个次使用记录,或者是第一次通过电路发声,是 1874 年 Elisha Gray 创下的,他从事电信行业。Elisha 发明了振荡子,最原始的声音合成装置,你的探索将从这个东西开始。
右键点击 Playground,选择 New Playground Page,创建一个新的 Playground 文件 Oscillators。
将 Xcode 产生的代码替换为:
import AudioKit
import PlaygroundSupport
// 1. Create an oscillator
let oscillator = AKOscillator()
// 2. Start the AudioKit ‘engine‘
AudioKit.output = oscillator
AudioKit.start()
// 3. Start the oscillator
oscillator.start()
PlaygroundPage.current.needsIndefiniteExecution = true
这个 Playground 将没完没了地发出哔哔声——呃,有意思。你可以按 Stop 来停止它。
这和前面创建的测试 Playground 差不多,但这次我们将深入讨论细节。
代码分成了几个步骤:
- 创建一个 AudioKit 振荡子,它是一个 AKNode 子类。节点是构成你的音频序列的主要元素。
- 将 AudioKit 引起和你最终输出的节点联系起来,在这里这个节点是你唯一的节点。音频引擎就像物理引擎或游戏引擎:你必须启动它并让它持续运转,这样你的音频序列才能被执行。
- 最后,打开振荡器,它会发出一条声波。
一个振荡子会创建一个重复的、或者周期性的无限延续的信号。在这个 Playground 中,AKOscillator 发出了一个正弦波。这个数字化的正弦波经过 AudioKit 处理,直接输出到你的扬声器或者耳麦,导致真正的振荡子以同样的正弦波进行振荡。声音通过压缩你耳朵周围的空气传播到你耳中,最终你就听到了这个烦人的啸叫声!
有两个参数决定了振荡器发出的声音是什么样子:振幅,它是正弦波的高度并决定声音的大小,以及频率,它决定了音高。
在你的 Playground 中,在创建振荡子之后加入这两句:
oscillator.frequency = 300
oscillator.amplitude = 0.5
倾听一会,你会发现现在的音量只是刚才的一半,而且音高也比刚才底了。频率单位为赫兹(即每秒周期数),决定了音符的音高。而振幅,范围从 0-1 ,决定了音量。
Elisha Gray 在专利官司中输给了Alexander Graham Bell,失去了成为电话机发明者的机会。但是,他的偶然发明振荡子却导致第一个电子乐器专利的产生。
许多年以后, Léon Theremin 发明了一个怪异的电子乐器,至今仍然被使用着。使用特雷门琴,你可以在这个乐器上方挥舞手臂来改变电子振荡器的频率。如果你不知道怎么形容这个乐器发出的声音,我建议你听一听 The Beach Boys 演唱的 Good Vibrations, 这首歌曲中特雷门琴所发出的独特声音令人记忆深刻。
你可以在 Playground 的最后加入以下代码模拟这种效果:
oscillator.rampTime = 0.2
oscillator.frequency = 500
AKPlaygroundLoop(every: 0.5) {
oscillator.frequency =
oscillator.frequency == 500 ? 100 : 500
}
rampTime 属性允许振荡器在属性值之间平滑过渡(比如频率或振幅)。AKPlaygroundLoop 是一个很有用的实用函数,允许周期性地执行 Playground 中的代码。在这里,你简单滴每 0.5 秒就切换一次振荡器的频率,从 500Hz 到 100 Hz。
你制造了自己的特雷门琴!
简单的振荡子可以发出音符,但是并不能令耳朵愉悦。真正的乐器还受许多别的因素的影响,比如钢琴,它的声音很独特。在后面几节中,你会继续探索它们是如何形成的。
声音封皮
当乐器演奏出一个音符时,振幅(或音量)是会变化的,并且每个乐器都不相同。有一个能够模拟这个效果的模型,叫做 Attack-Decay-Sustain-Release (ADSR) 封皮:
这个封皮由几个部分构成:
- Attack 上升: 在这个阶段声音上行至最大音量。
- Decay 下行: 这个时候声音下滑到 Sustain 水平。
- Sustain 维持: 这个阶段声音会维持在退败终止时的音量,一直到开始松开。
- Release 松开: 这个阶段音量开始下滑到 0。
一台钢琴,当琴弦被木锤敲击,会发出一个非常短促的上升音然后迅速下降。一把小提琴则会发出比较长的上升、下行和维持,因为演奏时琴弓不会离开琴弦。
电子琴是第一批电子乐器中使用 ADSR 封皮的乐器之一。这种乐器发明于 1939 年,由 163 个电子管和 1000 多个特制的电容器构成,重达 500 英磅(230 kg)。但不幸的是,只制造了 1000 台电子琴,它没有获得商业上的成功。
图片来自于 courtesy of Hollow Sun – 遵循 CC attribution 协议。
右键单击 Playground 中的顶层元素,Journey,选择 New Playground Page ,创建一个新的 Playground 叫做 ADSR。编辑文件内容为:
import AudioKit
import PlaygroundSupport
let oscillator = AKOscillator()
创建了一个振荡器,这个你已经很熟悉了。然后继续加入代码:
let envelope = AKAmplitudeEnvelope(oscillator)
envelope.attackDuration = 0.01
envelope.decayDuration = 0.1
envelope.sustainLevel = 0.1
envelope.releaseDuration = 0.3
这次创建了一个 AKAmplitudeEnvelope 并定义了一个 ADSR 封皮。durantion参数用秒为单位指定,level 参数指定的是音量,取值访问 0-1 之间。
AKAmplitudeEnvelope 是 AKNode 子类,同 AKOscillator 一样。在上面的代码中,你可以看到,振荡器作为参数被传递给了封皮的构造函数,两个节点连在了一起。
接着:
AudioKit.output = envelope
AudioKit.start()
oscillator.start()
AudioKit 引擎启动,这次将输出改成 ADSR 封皮,然后打开振荡器。
为了听到封皮效果,你必须重复播放封皮,然后停止封皮:
AKPlaygroundLoop(every: 0.5) {
if (envelope.isStarted) {
envelope.stop()
} else {
envelope.start()
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
现在你会听到同一个音符被反复播放,但这次带上了声音封皮效果,听起来有点钢琴的味道了。
每秒播放两次,每个循环都以 ADSR 开始和结束。当循环开始后,快速上行到最大音量,这个过程大约 0.01 秒,紧接着是 0.1 秒的下行,到达维持水平。这个过程约 0.5 秒,然后释放 0.3 秒。
修改 ADSR 值,尝试创建其他声音的效果。试试如何模拟小提琴?
从振荡器发出正弦波开始到现在,已经过去很长时间了。当你用振荡器演奏音符的同时,会使用 ADSR 去让声音更加柔和,但你仍然不能把它称之为真正的音乐!
下一节,你会学习如何创建更加丰富的声音。
加法合成
每种乐器都有独一无二的音质,并以其音色而得名。这就是为什么钢琴的声音和小提琴的声音截然不同的原因,哪怕它们演奏同一个音符。音色的一个重要属性是乐器所产生的声谱。声谱表示乐器发出一个单音符时所组成的频率范围。你的 Playground 当前所用的振荡器只能发出单一的频率,所以听起来非常假。
通过将一系列振荡器合并在一起作为输出并演奏同一个音符,你能够真实地模拟出一个乐器。这就是“加法合成”。这是你的下一个课题。
右键单击 Playground,选择 New Playground Page 创建新的页,叫做 Additive Synthesis,编辑如下代码:
import AudioKit
import PlaygroundSupport
func createAndStartOscillator(frequency: Double) -> AKOscillator {
let oscillator = AKOscillator()
oscillator.frequency = frequency
oscillator.start()
return oscillator
}
对于加法合成,你需要使用多个振荡器。createAndStartOscillator 方法用于创建它们。
然后写入:
let frequencies = (1...5).map { $0 * 261.63 }
这里用了一个 Range 操作来创建一个从 1 到 5 的序列。然后对这个序列进行 map 操作,将每个数字乘以 261.63。这个数字是标注键盘上的中音 C 的音频。将其他数字乘以这个值,这就是“和声”。
然后继续加入:
let oscillators = frequencies.map {
createAndStartOscillator(frequency: $0)
}
再次进行一个 map 操作,以创建多个振荡器。
然后将它们合成在一起。加入:
let mixer = AKMixer()
oscillators.forEach { mixer.connect($0) }
AKMixer 类也是 AudioKit 中的节点;
它将 1 个或多个节点作为输出并将它们合成在一起。
然后:
let envelope = AKAmplitudeEnvelope(mixer)
envelope.attackDuration = 0.01
envelope.decayDuration = 0.1
envelope.sustainLevel = 0.1
envelope.releaseDuration = 0.3
AudioKit.output = envelope
AudioKit.start()
AKPlaygroundLoop(every: 0.5) {
if (envelope.isStarted) {
envelope.stop()
} else {
envelope.start()
}
}
上述代码你已经很熟悉了;它用 mixer 创建了一个 ADSR 封皮,将它提供给 AudioKit 引擎,然后不停地播放和停止它。
要真正能够听出加法合成的效果,你可以尝试一下将这些频率进行不同的组合。当你尝试这样做的时候,Playground 的 live-view 是一个不错的工具!
加入下列代码:
class PlaygroundView: AKPlaygroundView {
override func setup() {
addTitle("Harmonics")
oscillators.forEach {
oscillator in
let harmonicSlider = AKPropertySlider(
property: "\(oscillator.frequency) Hz",
value: oscillator.amplitude
) { amplitude in
oscillator.amplitude = amplitude
}
addSubview(harmonicSlider)
}
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = PlaygroundView()
AudioKit 有许多类允许你轻松创建交互式的 Playground;我们在这里也使用了其中几个。
PlagroundView 类是 AKPlaygroundView 的子类,它由一系列垂直排列的 subview 组成。在 setup 方法中,你遍历每个振荡器,为每个振荡器创建一个 AKPropertySlider。每个 Slider 用每个振荡器的频率和振幅进行初始化批,当遍历到一个 slider 时,都可以为它设置一个回调块,这样当你拖动 slider 时回调块被执行。尾随闭包就是这个回调块,允许你修改每个振荡器的频率。通过这种简单的方式,你可以和 Playground 进行交互。
为了测试上述代码,你必须开启 live view。点击右上角的双环图标,打开助手窗口。同时将 live view 设置为正确的 playground 文件。
你可以通过修改每个 Slider 的振幅来改变乐器的音色。为了获得更加自然的音质,我建议你参照上图来进行设置。
最早的一种采用加法合成的合成器是 200 吨重的电传簧风琴。如此巨大的体量,立即宣告了这种乐器的消亡。结局更好的电子管风琴使用了类似的转速脉冲轮技术,但体积更小,用同样的加法合成实现了独特的声音。电传簧风琴在 1935 年发明,在前卫摇滚时代仍然是一种广为人知的流行乐器。
C3 电传簧风琴 – 图片来自 [public domain image](public domain image)
转速脉冲轮上有旋转的轮盘,轮沿上有许多光滑的隆起,旋转轮盘附近有一个拾波器总成。电传簧风琴由许多这样的转速脉冲轮组成,它们以不同的速度旋转。音乐家通过拉杆来混合这些声音并产生一个音符。这种发声方式真的十分简陋,严格地讲,与其说是电子式的,不如说是机电式的。
要创建更真实的声谱有许多别的技术,比如 调频技术(FM)和脉宽调制技术(PWM),这两种在 AudioKit 中都可以通过 AKFMOscillator 和 AKPWMOscillator 类来实现。无疑,我将鼓励你去尝试这两者。为什么不在你的 Playground 中用这两者将 AKOscillator 替换掉呢?
复音
上个世纪 70 年代,出现了一种偏离模块化合成的理论,它使用单独的振荡器、封皮和过滤器,并使用了微处理器。替代模拟电路,它使用了数字合成的方式发声。它导致了价格极其低廉和便携式合成器的出现,比如著名的雅马哈电子合成器,被专业和业余音乐爱好者广泛使用。
1983 年的 Yamaha DX7 – 图片来自public domain image
你所有的 Playground 都被死死限制在只能一次演奏一个音符。如果使用多个乐器,音乐家可以同时演奏多个音符。这种演奏方式就叫做“复音”,相反,如果一次只能演奏一个音符,就像你的 Playground 一样,则叫做“单音”。
为了制造复音,你需要创建多个振荡器,每个振荡器演奏不同的音符,并通过一个 mixer 节点播放出来。但是,我们还有一种更简单的 方法:使用 AudioKit 的振荡器 bank。
右键单击 Playground,选择 New Playground Page 创建一个新的 page 就叫做 Polyphony。写入以下代码:
import PlaygroundSupport
import AudioKit
let bank = AKOscillatorBank()
AudioKit.output = bank
AudioKit.start()
这里创建了一个振荡器 bank,并将它作为 AudioKit 的输出。如果你按下 Command 键点击 AKOscilatorBank,你将看到它的类定义,你会发现它其实继承了 AKPolyphonicNode。如果你继续深究下去,你会发现它又继承了 AKNode 并采用了AKPolyphonic 协议。
因此,振荡器 bank 和其他 AudioKit 一样,它的输出也能够被 mixer、封皮和其它滤镜和效果所加工。AKPolyphonic 协议描述了你应该如何在这个复音节点上演奏音符,等下你就知道了。
为了测试这个振荡器,你需要设法和谐地播放多个音符。这听起来好复杂?
在 Playground 后面加入下列代码,同时打开 live view:
class PlaygroundView: AKPlaygroundView {
override func setup() {
let keyboard = AKKeyboardView(width: 440, height: 100)
addSubview(keyboard)
}
}
PlaygroundPage.current.liveView = PlaygroundView()
PlaygroundPage.current.needsIndefiniteExecution = true
当 Playground 编译成功,你会看到这个:
这么酷?一个 Playground 居然画出了一个音乐键盘?
AKKeyboardView 另外一个 AudioKit 提供的实用工具,它使这个框架真的容易使用和研究里面的功能。点击键盘上的键,你会发现并没有声音发出。
还需要做一些工作。
修改你的 PlayroundView 的 setup 方法为:
let keyboard = AKKeyboardView(width: 440, height: 100)
keyboard.delegate = self
addSubview(keyboard)
这样就将 keyboard view 的 delegate 属性绑定到 PlaygroundView 类。通过这个委托,你可以对按键进行处理。
修改类的定义:
class PlaygroundView: AKPlaygroundView, AKKeyboardDelegate
声明采用 AKKeyboardDelegate 协议。然后在 setup 方法后添加如下方法:
func noteOn(note: MIDINoteNumber) {
bank.play(noteNumber: note, velocity: 80)
}
func noteOff(note: MIDINoteNumber) {
bank.stop(noteNumber: note)
}
当你按下一个键,键盘会调用 noteON 委托方法。方法的实现很简单,简单地播放了振荡器 bank。noteOff 方法则调用对应的 stop 方法。
点击并在键盘上滑动,你会发现它演奏出了优美的音阶。振荡器 bank 内置了 ADSR 支持。因此,一个音符的下行会和另一个音符的上升、松开和保持混在了一起,发出了令人愉悦的声音。
你可能注意到了,键盘提供的音符不再以频率的方式提供,而是以 MIDINoteNumber 类型提供。如果你按住 Command 键并点击左鼠键,查看它的定义,你会看到它只是一个整型:
public typealias MIDINoteNumber = Int
MIDI 标准全称是 Musical Instrument Digital Interface(乐器数字接口),它在乐器间进行通讯时广泛使用。 音符数字和标准键盘上的音符一一对应。play 方法的第二个参数 velocity 是另一个 MIDI 属性,用于描述一个音符的敲击力度。值越小表明敲击得越轻,会发出一个更小的声音。
最后一步是将键盘设置为复音模式。在 setup 方法代码最后中加入:
keyboard.polyphonicMode = true
你会发现现在可以同时演奏多个音符了,只需要这样:
……太不可思议了,C-大调。这个项目使用了 Soundpipe,代码来自于 CSound,一个起始于 1985 年的 MIT 开源项目。令人不可思议的是它可以在 Playground 中运行并添加到你的 App 中,而它竟然拥有超过 30 年的历史了!
取样
你已经学习了半天的声音合成技术了,在这个过程中你尝试用非常原始的方式制造拟真的声音:振荡器、过滤器和混合器。早在上世纪 70 年代,随着计算机处理能力和存储的增长,一种完全不同的方法出现了——声音取样——目标是制造声音的数字复制品。
取样是相对简单的概念,它和数字影像技术中的原理相同。自然声音是光滑的波形,取样只是在固定的时间间隔内简单地记录声波的震动:
在取样过程中,有两个因素直接影响了记录的拟真度:
- Bit depth 位深: 表示一个取样器能够复制的离散振幅数。
- Sample rate 取样率: 表示多久进行一次振幅测量,单位是 Hz。
你将用另一个 Playground 来学习这些属性。
在 Playground 上右键,选择 New Playground Page 并创建新的 page 名为 Samples。编辑如下代码:
import PlaygroundSupport
import AudioKit
let file = try AKAudioFile(readFileName: "climax-disco-part2.wav", baseDir: .resources)
let player = try AKAudioPlayer(file: file)
player.looping = true
这段代码载入了一个示例音频,创建了一个声音播放器,并设置它的循环播放这个声音。
这个波表文件放在这个zip文件中。解压缩这个 zip 文件,将 WAV 文件拖到 Playground 的 resources 文件夹中。
然后,在 Playground 文件最后继续加入:
AudioKit.output = player
AudioKit.start()
player.play()
PlaygroundPage.current.needsIndefiniteExecution = true
这会将你的声频播放器传递给 AudioKit 引擎并开始播放。调大音量,注意听。
这个简单的例子重复播放各种声音,这些声音很难用基本的振荡器来模拟。
正在使用的 MP3 有一个比较高的位深和取样率,能够产生清脆和清晰的声音。为了试验这两个参数,在创建音频播放器之后,加入如下代码:
let bitcrusher = AKBitCrusher(player)
bitcrusher.bitDepth = 16
bitcrusher.sampleRate = 40000
And update the AudioKit output:
AudioKit.output = bitcrusher
现在播放的声音就截然不同了:仍然是同一个抽样文件,但声音变得非常尖锐。
AKBitCrusher 是一种 AudioKit 音效,用于模拟低位深低取样率的效果。使用它,你可以制造出这种效果,就像是早期用 ZX Spectrum 或 BBC Micro 进行抽样的声音,这些电脑仅有几 Kb 的内存和处理器,比起如今的电脑来说要慢上几百万倍!
最后的实验,是将许多节点组合在一起,制造出立体声延迟效果。删除代码中用于创建和配置 bitcrusher 的三行代码。然后添加:
let delay = AKDelay(player)
delay.time = 0.1
delay.dryWetMix = 1
这会用你的抽样文件创建出大约 0.1 秒的延迟效果。干/湿混合值让你将延迟声音和未延迟的声音进行混合,设置为 1 表示只有经过延迟的声音被节点输出。
然后,加入代码:
let leftPan = AKPanner(player, pan: -1)
let rightPan = AKPanner(delay, pan: 1)
AKPanner 节点允许你将音频进行移动,左移、右移或者之间的某个地方。上述代码将延迟过的音频左移,为延迟的声音右移。
最后一个步骤是将两者混合在一起,并设置 AudioKit 的输出,用下面的代码替换掉原来设置 AudioKit 的输出为 bitcrusher 的代码:
let mix = AKMixer(leftPan, rightPan)
AudioKit.output = mix
这将播放同一个取样文件,但在左右扬声器之间有一个非常短的延迟。
结束
在本教程中,你对“使用 AudioKit 能干什么”有了一个大致的理解了。开始探险吧——尝试一下穆格过滤,升降调、混响,或者图像均衡器的效果怎么样?
只需要一小点创意,你就可以制造出自己的声音、电子乐器或者游戏音效。
你可以下载完成项目。当然,你仍然还需要用“开始”一节中描述的技术将 AudioKit 库添加到工作空间中。
最后,感谢 AudioKit 项目的 Lead,Aurelius Prochazka](https://github.com/aure),审阅了本文。
如果你有任何疑问或建议,请在下面留言。