AudioKit 教程:入门

原文:AudioKit Tutorial: Getting Started
作者:Colin Eberhardt
译者: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 教程:入门_第1张图片

下载 AudioKit 源代码 将文件加压到 playground 同一目录。

打开 Finder,找到 AudioKit-3.4.1/AudioKit/iOS/AudioKit For iOS.xcodeproj 文件,将它拖到 workspace 的根目录下。
AudioKit 教程:入门_第2张图片

你的导航窗口将是这个样子:
AudioKit 教程:入门_第3张图片

在编译目标中选择 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 差不多,但这次我们将深入讨论细节。
代码分成了几个步骤:

  1. 创建一个 AudioKit 振荡子,它是一个 AKNode 子类。节点是构成你的音频序列的主要元素。
  2. 将 AudioKit 引起和你最终输出的节点联系起来,在这里这个节点是你唯一的节点。音频引擎就像物理引擎或游戏引擎:你必须启动它并让它持续运转,这样你的音频序列才能被执行。
  3. 最后,打开振荡器,它会发出一条声波。

一个振荡子会创建一个重复的、或者周期性的无限延续的信号。在这个 Playground 中,AKOscillator 发出了一个正弦波。这个数字化的正弦波经过 AudioKit 处理,直接输出到你的扬声器或者耳麦,导致真正的振荡子以同样的正弦波进行振荡。声音通过压缩你耳朵周围的空气传播到你耳中,最终你就听到了这个烦人的啸叫声!

AudioKit 教程:入门_第4张图片

有两个参数决定了振荡器发出的声音是什么样子:振幅,它是正弦波的高度并决定声音的大小,以及频率,它决定了音高。
在你的 Playground 中,在创建振荡子之后加入这两句:

oscillator.frequency = 300
oscillator.amplitude = 0.5

倾听一会,你会发现现在的音量只是刚才的一半,而且音高也比刚才底了。频率单位为赫兹(即每秒周期数),决定了音符的音高。而振幅,范围从 0-1 ,决定了音量。

Elisha Gray 在专利官司中输给了Alexander Graham Bell,失去了成为电话机发明者的机会。但是,他的偶然发明振荡子却导致第一个电子乐器专利的产生。

AudioKit 教程:入门_第5张图片

许多年以后, 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) 封皮:
AudioKit 教程:入门_第6张图片

这个封皮由几个部分构成:

  • Attack 上升: 在这个阶段声音上行至最大音量。
  • Decay 下行: 这个时候声音下滑到 Sustain 水平。
  • Sustain 维持: 这个阶段声音会维持在退败终止时的音量,一直到开始松开。
  • Release 松开: 这个阶段音量开始下滑到 0。
    一台钢琴,当琴弦被木锤敲击,会发出一个非常短促的上升音然后迅速下降。一把小提琴则会发出比较长的上升、下行和维持,因为演奏时琴弓不会离开琴弦。

电子琴是第一批电子乐器中使用 ADSR 封皮的乐器之一。这种乐器发明于 1939 年,由 163 个电子管和 1000 多个特制的电容器构成,重达 500 英磅(230 kg)。但不幸的是,只制造了 1000 台电子琴,它没有获得商业上的成功。

AudioKit 教程:入门_第7张图片

图片来自于 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 封皮,然后打开振荡器。

AudioKit 教程:入门_第8张图片

为了听到封皮效果,你必须重复播放封皮,然后停止封皮:

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 文件。

AudioKit 教程:入门_第9张图片

你可以通过修改每个 Slider 的振幅来改变乐器的音色。为了获得更加自然的音质,我建议你参照上图来进行设置。

最早的一种采用加法合成的合成器是 200 吨重的电传簧风琴。如此巨大的体量,立即宣告了这种乐器的消亡。结局更好的电子管风琴使用了类似的转速脉冲轮技术,但体积更小,用同样的加法合成实现了独特的声音。电传簧风琴在 1935 年发明,在前卫摇滚时代仍然是一种广为人知的流行乐器。

AudioKit 教程:入门_第10张图片

C3 电传簧风琴 – 图片来自 [public domain image](public domain image)

转速脉冲轮上有旋转的轮盘,轮沿上有许多光滑的隆起,旋转轮盘附近有一个拾波器总成。电传簧风琴由许多这样的转速脉冲轮组成,它们以不同的速度旋转。音乐家通过拉杆来混合这些声音并产生一个音符。这种发声方式真的十分简陋,严格地讲,与其说是电子式的,不如说是机电式的。

要创建更真实的声谱有许多别的技术,比如 调频技术(FM)和脉宽调制技术(PWM),这两种在 AudioKit 中都可以通过 AKFMOscillator 和 AKPWMOscillator 类来实现。无疑,我将鼓励你去尝试这两者。为什么不在你的 Playground 中用这两者将 AKOscillator 替换掉呢?

复音

上个世纪 70 年代,出现了一种偏离模块化合成的理论,它使用单独的振荡器、封皮和过滤器,并使用了微处理器。替代模拟电路,它使用了数字合成的方式发声。它导致了价格极其低廉和便携式合成器的出现,比如著名的雅马哈电子合成器,被专业和业余音乐爱好者广泛使用。

AudioKit 教程:入门_第11张图片

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 编译成功,你会看到这个:

AudioKit 教程:入门_第12张图片

这么酷?一个 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

你会发现现在可以同时演奏多个音符了,只需要这样:

AudioKit 教程:入门_第13张图片

……太不可思议了,C-大调。这个项目使用了 Soundpipe,代码来自于 CSound,一个起始于 1985 年的 MIT 开源项目。令人不可思议的是它可以在 Playground 中运行并添加到你的 App 中,而它竟然拥有超过 30 年的历史了!

取样

你已经学习了半天的声音合成技术了,在这个过程中你尝试用非常原始的方式制造拟真的声音:振荡器、过滤器和混合器。早在上世纪 70 年代,随着计算机处理能力和存储的增长,一种完全不同的方法出现了——声音取样——目标是制造声音的数字复制品。

取样是相对简单的概念,它和数字影像技术中的原理相同。自然声音是光滑的波形,取样只是在固定的时间间隔内简单地记录声波的震动:

AudioKit 教程:入门_第14张图片

在取样过程中,有两个因素直接影响了记录的拟真度:

  • 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 教程:入门_第15张图片

结束

在本教程中,你对“使用 AudioKit 能干什么”有了一个大致的理解了。开始探险吧——尝试一下穆格过滤,升降调、混响,或者图像均衡器的效果怎么样?

只需要一小点创意,你就可以制造出自己的声音、电子乐器或者游戏音效。

你可以下载完成项目。当然,你仍然还需要用“开始”一节中描述的技术将 AudioKit 库添加到工作空间中。

最后,感谢 AudioKit 项目的 Lead,Aurelius Prochazka](https://github.com/aure),审阅了本文。

如果你有任何疑问或建议,请在下面留言。

你可能感兴趣的:(iPhone开发)