iOS开发者图书馆

AudioKit 入门教程

2018-07-05  本文已影响167人  Andy矢倉

原文:AudioKit Tutorial: Getting Started
作者:Colin Eberhardt
同时感谢:kmyhy


近来手上有一些音频相关的开发工作,搜搜基Hub,目前最为强大,性能屌爆,编码炫酷的开源库也只有AudioKit了。Raywenderlich也能找到相关教程,介于作者是3.0+的教程,很多代码都不能跑了,特此整理一趴。
本文不仅是一篇iOS开发教程,更是一篇精彩的科普文。关于编程与艺术的结合,声学物理与音乐的碰撞,尽在此文。推荐所有程序员都好好读一读它,让我们的生活除了代码,还有艺术,还有音乐。感谢作者Colin Eberhardt

iOS 设备提供了丰富的多媒体体验,比如绚丽的视觉效果、声音和可触摸的交互界面。尽管能够使用各种各样的特性,但作为开发者,我们更多地关注了应用的视觉设计,而忽略了用户体验的声学效果。

AudioKit是一个高级音频框架,由声学设计师、程序员和音乐家为 iOS 专门打造。AudioKit底层混合了SwiftObjective-CC++C,负责和苹果音频已硬件的Api打交道。所有神奇(同时十分复杂的)技术都封装成为极其友好的 Swift Api ,你甚至可以直接在XcodePlayground中使用它。

本文无法全面覆盖 AudioKit 的知识点。相反,我们会通过介绍声音合成和计算机声频的历史,来带你进行一次有趣和时尚的 AudioKit 之旅。通过这种方式,你会学到基本的声学物理,了解早期的合成器比如电子琴是如何工作的。最终来到现代,一个大混音时代。

请给自己来一杯咖啡,拖过一张椅子,开始我们的旅程!

image

开始

原教程的原始内容使用的是 3.4.0 版本,用的是Playground做案例,本文使用最新的 4.3.0 ,用Xcode创建的Swift项目做案例。

港真,教程的第一步并不是特别鸡冻的。为了在 Playground 中使用 AudioKit,我们必须提前进行一些准备工作。

我们需要先到AudioKit下源码AudioKit-4.3,或者自行clone:

git clone https://github.com/AudioKit/AudioKit.git

解压好后用Terminal进入AudioKit-4.3文件夹执行编译命令:

$ cd Frameworks
$ ./build_frameworks.sh

编译时间较长(14的顶配编译的近十分钟吧),你阔以继续阅读,或者撸一把,或者鸡一把。

一定要编译完成,编译好后进入Playgrounds文件,打开下面的AudioKitPlaygrounds.xcodeproj,这里我们就阔以看官方为我们写好的示例代码,提前体验一把 AudioKit 的强大。体验完了别忘了纸巾,进入正题开始教程之旅。

image

新建一个名为JorneryPlayground。录入一下代码:

import AudioKitPlaygrounds
import AudioKit


let oscillator = AKOscillator()

AudioKit.output = oscillator
try? AudioKit.start()

oscillator.start()

sleep(10)

编译时,你会听到大约 10 秒钟的蜂鸣声。你可以点击 Playground 调试窗口左下角的 Play/Stop 按钮停止或运行 Playground。

注意:建议打开工程的时候就直接Command + B编译一遍,再跑想看的代码。如果 Playground 执行出错,并在 Debug 窗口中出现错误,你可以重启 Xcode。不幸的是,当 Playground 和框架一起使用时,总是容易出现一些小问题,并且无法预知。
另外建议把 Run 方式改为 Manually RunAutomatically Run太容易故障了。

振荡器和声学基础

人类通过物体制造音乐——通过击打、拖拉或者弹奏等形式——有数千年的历史。我们的许多民族乐器,比如鼓、吉他,已经发明几个世纪了。电子乐器的第一个次使用记录,或者是第一次通过电路发声,是 1874 年 Elisha Gray 创下的,他从事电信行业。Elisha 发明了振荡器,最原始的声音合成装置,你的探索将从这个东西开始。

右键点击 Playground,选择 New Playground Page,创建一个新的 Playground 文件 Oscillators

将 Xcode 产生的代码替换为:

import AudioKitPlaygrounds
import AudioKit


// 1. Create an oscillator
let oscillator = AKOscillator()

// 2. Start the AudioKit 'engine'
AudioKit.output = oscillator
try? AudioKit.start()

// 3. Start the oscillator
oscillator.start()


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

这个 Playground 将没完没了地发出哔哔声——呃,有意思。你可以按 Stop 来停止它。
这和前面创建的测试 Playground 差不多,但这次我们将深入讨论细节。
代码分成了几个步骤:

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

image

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

oscillator.frequency = 300
oscillator.amplitude = 0.5

倾听一会,你会发现现在的音量只是刚才的一半,而且音高也比刚才低了。频率单位为赫兹(即每秒周期数),决定了音符的音高。而振幅,范围从 0-1 ,决定了音量。
Elisha Gray 在专利官司中输给了Alexander Graham Bell,失去了成为电话机发明者的机会。但是,他的偶然发明振荡器却导致第一个电子乐器专利的产生。

image

许多年以后,Léon Theremin 发明了一个怪异的电子乐器,至今仍然被使用着。使用特雷门琴,你可以在这个乐器上方挥舞手臂来改变电子振荡器的频率。如果你不知道怎么形容这个乐器发出的声音,我建议你听一听 The Beach Boys 演唱的 Good Vibrations, 这首歌曲中特雷门琴所发出的独特声音令人记忆深刻。

你可以在 Playground 的最后加入以下代码模拟这种效果:

oscillator.rampDuration = 0.2
oscillator.frequency = 500


import AudioKitUI

AKPlaygroundLoop(every: 0.5) {
    oscillator.frequency = (oscillator.frequency == 500 ? 100 : 500)
}

rampDuration 属性允许振荡器在属性值之间平滑过渡(比如频率或振幅)。AKPlaygroundLoop 是一个很有用的实用函数,允许周期性地执行 Playground 中的代码。在这里,你简单滴每 0.5 秒就切换一次振荡器的频率,从 500Hz 到 100 Hz。

你制造了自己的特雷门琴!

简单的振荡器可以发出音符,但是并不能令耳朵愉悦。真正的乐器还受许多别的因素的影响,比如钢琴,它的声音很独特。在后面几节中,你会继续探索它们是如何形成的。
完整代码:

import AudioKitPlaygrounds
import AudioKit


let oscillator = AKOscillator()
oscillator.frequency = 300
oscillator.amplitude = 0.5

AudioKit.output = oscillator
try? AudioKit.start()

oscillator.start()

oscillator.rampDuration = 0.2
oscillator.frequency = 500


import AudioKitUI

AKPlaygroundLoop(every: 0.5) {
    oscillator.frequency = (oscillator.frequency == 500 ? 100 : 500)
}


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

声音封包

当乐器演奏出一个音符时,振幅(或音量)是会变化的,并且每个乐器都不相同。有一个能够模拟这个效果的模型,叫做 Attack-Decay-Sustain-Release (ADSR) 封包:

image

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

一台钢琴,当琴弦被木锤敲击,会发出一个非常短促的上行音然后迅速下降。一把小提琴则会发出比较长的上行、下行和维持,因为演奏时琴弓不会离开琴弦。
电子琴是第一批电子乐器中使用 ADSR 封包的乐器之一。这种乐器发明于 1939 年,由 163 个电子管和 1000 多个特制的电容器构成,重达 500 英磅(230 kg)。但不幸的是,只制造了 1000 台电子琴,它没有获得商业上的成功。

image
图片来自于 courtesy of Hollow Sun – 遵循 CC attribution 协议。

右键单击 Playground 中的顶层元素,Journey,选择 New Playground Page ,创建一个新的 Playground 叫做ADSR。编辑文件内容为:

import AudioKitPlaygrounds
import AudioKit


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
try? AudioKit.start()

oscillator.start()

AudioKit 引擎启动,这次将输出改成 ADSR 封包,然后打开振荡器。

image

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

import AudioKitUI

AKPlaygroundLoop(every: 0.5) {
    if (envelope.isStarted) {
        envelope.stop()
    } else {
        envelope.start()
    }
}


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

现在你会听到同一个音符被反复播放,但这次带上了声音封包效果,听起来有点钢琴的味道了。
每秒播放两次,每个循环都以 ADSR 开始和结束。当循环开始后,快速上行到最大音量,这个过程大约 0.01 秒,紧接着是 0.1 秒的下行,到达维持水平。这个过程约 0.5 秒,然后释放 0.3 秒。
修改 ADSR 值,尝试创建其他声音的效果。试试如何模拟小提琴?
从振荡器发出正弦波开始到现在,已经过去很长时间了。当你用振荡器演奏音符的同时,会使用 ADSR 去让声音更加柔和,但你仍然不能把它称之为真正的音乐!
下一节,你会学习如何创建更加丰富的声音。
完整代码:

import AudioKitPlaygrounds
import AudioKit


let oscillator = AKOscillator()

let envelope = AKAmplitudeEnvelope(oscillator)
envelope.attackDuration = 0.01
envelope.decayDuration = 0.1
envelope.sustainLevel = 0.1
envelope.releaseDuration = 0.3

AudioKit.output = envelope
try? AudioKit.start()

oscillator.start()


import AudioKitUI

AKPlaygroundLoop(every: 0.5) {
    if (envelope.isStarted) {
        envelope.stop()
    } else {
        envelope.start()
    }
}


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

声音叠加合成

每种乐器都有独一无二的音质,并以其音色而得名。这就是为什么钢琴的声音和小提琴的声音截然不同的原因,哪怕它们演奏同一个音符。音色的一个重要属性是乐器所产生的声谱。声谱表示乐器发出一个单音符时所组成的频率范围。你的 Playground 当前所用的振荡器只能发出单一的频率,所以听起来非常假。
通过将一系列振荡器合并在一起作为输出并演奏同一个音符,你能够真实地模拟出一个乐器。这就是“叠加合成”。这是你的下一个课题。

右键单击 Playground,选择 New Playground Page 创建新的页,叫做Additive Synthesis,编辑如下代码:

import AudioKitPlaygrounds
import AudioKit

func createAndStartOscillator(frequency: Double) -> AKOscillator {
    let oscillator = AKOscillator()
    oscillator.frequency = frequency
    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(input: $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
try? AudioKit.start()

oscillators.map { $0.start() }


import AudioKitUI

AKPlaygroundLoop(every: 0.5) {
    if (envelope.isStarted) {
        envelope.stop()
    } else {
        envelope.start()
    }
}

上述代码你已经很熟悉了;它用 mixer 创建了一个 ADSR 封包,将它提供给 AudioKit 引擎,然后不停地播放和停止它。
要真正能够听出合成的效果,你可以尝试一下将这些频率进行不同的组合。当你尝试这样做的时候,Playground 的 live-view 是一个不错的工具!
加入下列代码:

class LiveView: AKLiveViewController {

    override func viewDidLoad() {
        addTitle("Mixer")

        oscillators.forEach { oscillator in
            let slider = AKSlider(property: "\(oscillator.frequency) Hz", value: oscillator.amplitude) { amplitude in
                oscillator.amplitude = amplitude
            }
            addView(slider)
        }
    }
}


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = LiveView()

AudioKit 有许多类允许你轻松创建交互式的 Playground;我们在这里也使用了其中几个。
LiveView 类是 AKLiveViewController 的子类,它由一系列垂直排列的 subview 组成。在 viewDidLoad 方法中,你遍历每个振荡器,为每个振荡器创建一个 AKSlider。每个 Slider 用每个振荡器的频率和振幅进行初始化,当遍历到一个 slider 时,都可以为它设置一个回调块,这样当你拖动 slider 时回调块被执行。尾随闭包就是这个回调块,允许你修改每个振荡器的频率。通过这种简单的方式,你可以和 Playground 进行交互。

为了测试上述代码,你必须开启 live view。点击右上角的双环图标,打开助手窗口。同时将 live view 设置为正确的 playground 文件。

image

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

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

image

C3 电传簧风琴 – 图片来自 public domain image

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

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

复音

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

image

1983 年的 Yamaha DX7 – 图片来自public domain image

你所有的 Playground 都被死死限制在只能一次演奏一个音符。如果使用多个乐器,音乐家可以同时演奏多个音符。这种演奏方式就叫做“复音”,相反,如果一次只能演奏一个音符,就像你的 Playground 一样,则叫做“单音”。
为了制造复音,你需要创建多个振荡器,每个振荡器演奏不同的音符,并通过一个 mixer 节点播放出来。但是,我们还有一种更简单的 方法:使用 AudioKit 的振荡器 bank。

右键单击 Playground,选择 New Playground Page 创建一个新的 page 就叫做Polyphony。写入以下代码:

import AudioKitPlaygrounds
import AudioKit

let bank = AKOscillatorBank()
AudioKit.output = bank
try? AudioKit.start()

这里创建了一个振荡器 bank,并将它作为 AudioKit 的输出。如果你按下 Command 键点击 AKOscilatorBank,你将看到它的类定义,你会发现它其实继承了 AKPolyphonicNode。如果你继续深究下去,你会发现它又继承了 AKNode 并采用了AKPolyphonic 协议。

因此,振荡器 bank 和其他 AudioKit 一样,它的输出也能够被 mixer、封包和其它滤镜和效果所加工。AKPolyphonic 协议描述了你应该如何在这个复音节点上演奏音符,等下你就知道了。

为了测试这个振荡器,你需要设法和谐地播放多个音符。这听起来好复杂?
在 Playground 后面加入下列代码,同时打开 live view:

import AudioKitUI

class LiveView: AKLiveViewController, AKKeyboardDelegate {

    override func viewDidLoad() {
        addTitle("Keyboard")
        let keyboard = AKKeyboardView(width: 440, height: 100)
        keyboard.delegate = self
        keyboard.polyphonicMode = true
        addView(keyboard)
    }

    func noteOn(note: MIDINoteNumber) {
        bank.play(noteNumber: note, velocity: 80)
    }

    func noteOff(note: MIDINoteNumber) {
        bank.stop(noteNumber: note)
    }
}


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = LiveView()

当 Playground 编译成功,你会看到这个:

image

这么酷?一个 Playground 居然画出了一个音乐键盘?
AKKeyboardView 另外一个 AudioKit 提供的实用工具,它使这个框架真的容易使用和研究里面的功能。
当你按下一个键,键盘会调用 noteON 委托方法。方法的实现很简单,简单地播放了振荡器 bank。noteOff 方法则调用对应的 stop 方法。
点击并在键盘上滑动,你会发现它演奏出了优美的音阶。振荡器 bank 内置了 ADSR 支持。因此,一个音符的下行会和另一个音符的上升、松开和保持混在了一起,发出了令人愉悦的声音。

你可能注意到了,键盘提供的音符不再以频率的方式提供,而是以 MIDINoteNumber 类型提供。如果你按住 Command 键并点击左鼠键,查看它的定义,你会看到它只是一个整型:

public typealias MIDINoteNumber = Int

MIDI 标准全称是 Musical Instrument Digital Interface(乐器数字接口),它在乐器间进行通讯时广泛使用。 音符数字和标准键盘上的音符一一对应。play 方法的第二个参数 velocity 是另一个 MIDI 属性,用于描述一个音符的敲击力度。值越小表明敲击得越轻,会发出一个更小的声音。

最后一步是将键盘设置为复音模式。在 setup 方法代码最后中加入:

keyboard.polyphonicMode = true

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

image

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

抽样

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

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

image

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

你将用另一个 Playground 来学习这些属性。
在 Playground 上右键,选择 New Playground Page 并创建新的 page 名为Samples。编辑如下代码:

import AudioKitPlaygrounds
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
try? AudioKit.start()

player.play()


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

这会将你的声频播放器传递给 AudioKit 引擎并开始播放。调大音量,注意听。
这个简单的例子重复播放各种声音,这些声音很难用基本的振荡器来模拟。
正在使用的音频有一个比较高的位深和取样率,能够产生清脆和清晰的声音。为了试验这两个参数,在创建音频播放器之后,加入如下代码:

let bitcrusher = AKBitCrusher(player)
bitcrusher.bitDepth = 16
bitcrusher.sampleRate = 40000
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

这将播放同一个文件,但在左右扬声器之间有一个非常短的延迟。
完整代码:

import AudioKitPlaygrounds
import AudioKit

let file = try AKAudioFile(readFileName: "climax-disco-part2.wav", baseDir: .resources)
let player = try AKAudioPlayer(file: file)
player.looping = true

//let bitcrusher = AKBitCrusher(player)
//bitcrusher.bitDepth = 16
//bitcrusher.sampleRate = 40000
//AudioKit.output = bitcrusher

let delay = AKDelay(player)
delay.time = 0.1
delay.dryWetMix = 1

let leftPan = AKPanner(player, pan: -1)
let rightPan = AKPanner(delay, pan: 1)
let mix = AKMixer(leftPan, rightPan)

AudioKit.output = mix
try? AudioKit.start()

player.play()


import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true
image

结语

在本教程中,你对“使用 AudioKit 能干什么”有了一个大致的理解了。开始探险吧——尝试一下穆格过滤,升降调、混响,或者图像均衡器的效果怎么样?
只需要一小点创意,你就可以制造出自己的声音、电子乐器或者游戏音效。

你可以下载最终项目。当然,你仍然还需要像最开始那样编译动态库,并将最终代码拖入进去即可。

最后,感谢 AudioKit 项目的Lead,Aurelius Prochazka,审阅了本文。

上一篇下一篇

猜你喜欢

热点阅读