Developer Tools 入门 8m
天鹅任务第二章:音调时刻
Swan's Quest, Chapter 2: A time for tones
2020年6月23日
一句话判断
Swan’s Quest 第二章把方向反转——你需要用代码让设备「发出」特定频率的声音来解锁机关,在第一章「听」的基础上加上「说」,让你完整理解 AVFoundation 的音频输入输出管道。
这场 Session 讲了什么
第二章的挑战是:用代码生成特定频率的音调(tone),通过设备扬声器播放出来。和第一章的「检测声音」不同,这一章要求你「生成声音」。
你需要用 AVAudioEngine 构建一个音频输出管道,用 AVAudioPlayerNode 或自定义的 AVAudioSourceNode 生成正弦波信号,然后播放指定频率和时长的音调。
挑战的趣味在于:你需要按照特定的频率序列播放音调(像密码锁一样),播放正确序列后才能解锁下一关。这涉及到定时控制(每个音调的起止时间)和频率精度。
值得深挖的点
AVAudioSourceNode 的使用
AVAudioSourceNode 是 iOS 13 引入的,允许你用闭包实时生成音频数据。你不需要预先录制音频文件,而是用数学公式(正弦函数)实时计算每一帧的采样值。这个能力在游戏音效生成、音乐创作工具等场景中非常有用。
代码片段
import AVFoundation
// 生成指定频率的正弦波音调
class ToneGenerator {
let engine = AVAudioEngine()
let sampleRate: Double
var phase: Double = 0
init() {
sampleRate = engine.outputNode.outputFormat(forBus: 0).sampleRate
}
func playTone(frequency: Double, duration: TimeInterval) {
let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)!
// 创建音频源节点,实时生成正弦波
let sourceNode = AVAudioSourceNode(format: format) { [weak self] _, _, frameCount, audioBufferList in
guard let self = self else { return noErr }
let bufferList = UnsafeMutableAudioBufferListPointer(audioBufferList)
let buffer = bufferList[0]
let frames = UnsafeMutableBufferPointer<Float>(buffer)
// 生成正弦波采样数据
for i in 0..<Int(frameCount) {
let sample = sin(2.0 * .pi * self.phase)
frames[i] = Float(sample) * 0.5 // 音量 50%
self.phase += frequency / self.sampleRate
// 保持 phase 在 0-1 范围内,避免精度问题
if self.phase > 1.0 { self.phase -= 1.0 }
}
return noErr
}
// 连接音频图
engine.attach(sourceNode)
engine.connect(sourceNode, to: engine.mainMixerNode, format: format)
try? engine.start()
// 播放指定时长后停止
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in
self?.engine.stop()
}
}
// 按序列播放多个音调
func playSequence(frequencies: [Double], noteDuration: TimeInterval) {
for (index, freq) in frequencies.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * noteDuration * 1.5) {
self.playTone(frequency: freq, duration: noteDuration)
}
}
}
}
最佳实践
- 控制 phase 变量的精度。长时间运行时 phase 的累积误差会导致频率偏移。每次超过 1.0 时要取模重置。
- 音量不要设太大。
AVAudioSourceNode生成的音频是原始浮点数据,直接全量输出可能爆音。乘以 0.3-0.5 的系数更安全。
还有什么值得关注
- 完成第二章需要和第一章配合——你发出的音调可能需要被另一台设备检测到,这也暗示了设备间通信的可能性。
AVAudioSourceNode的闭包在实时音频线程中执行,不能做内存分配或调用 Objective-C/Swift 的运行时方法。- 第三章会转向完全不同的技术领域,不需要完成前两章也能独立挑战。
WWDC 2020