WWDC Quick Look 💓 By SwiftGGTeam
Swan's Quest, Chapter 2: A time for tones

Swan's Quest, Chapter 2: A time for tones

观看原视频

Highlight

第二章的挑战是:用代码生成特定频率的音调(tone),通过设备扬声器播放出来。和第一章的「检测声音」不同,这一章要求你「生成声音」。

核心内容

Swan’s Quest 第一章让玩家接触 accessible interfaces。第二章把场景带回 Lizard 和 Swan 的卷轴谜题:玩家要为 Swan 播放一串 musical notes,才能通过挑战。

这件事的难点不是播放一个声音文件。挑战要求你按频率生成 tone,再按固定节奏依次播放。演讲把入口压缩成两个 API:ToneOutput 负责发声,Timer 负责按时间推进音符序列。

单个 440 Hz 的 middle A 很容易响起来。真正要完成挑战,还要在 400 毫秒一拍的循环里切换音符,结束时停止 ToneOutput、取消 timer,并调用 endPerformance() 告诉 Swan 表演完成。

最后的 side quest 继续沿用同一套代码。Swan 给的是 C-major scale,练习题要求改成 F-major scale。演讲给出的推理很具体:从 F4 开始,复用已有频率,需要升八度时把频率乘以 2,例如把 A4 的 440 Hz 变成 A5 的 880 Hz。

详细内容

ToneOutput 负责生成连续声音

01:09ToneOutput 是这场挑战的发声入口。演讲说明它每秒生成 44,100 个 samples,让人耳听到连续声音。

//  ToneOutput.swift

public class ToneOutput : AURenderCallbackDelegate {
    let sampleRate = 44100.0

    public func play(tone: Tone) { /**/ }

    public func stopTones() { /**/ }

    // ...

}

关键点:

  • sampleRate = 44100.0 对应 transcript 中的 44,100 samples a second。
  • play(tone:) 创建 tone 对应的信号。
  • stopTones() 停止正在播放的 tone。
  • 这段类型来自 Sonic Workshop,并被放进 Sonic Create,供玩家在自己的项目里使用。

01:30)传给 ToneOutput 的值是 Tone。它只暴露两个核心参数:pitch 和 volume。

//  ToneOutput.swift

public struct Tone: Codable {
    public var pitch: Double
    public var volume: Double

    // ...
}

关键点:

  • pitch 是频率的 Double 表示。
  • volume 也是 Double
  • Codable 让 tone 作为简单数据结构保存和传递。
  • 这让挑战代码不用直接处理底层音频 sample。

先播放一个 middle A

01:45)第一段可运行代码只做一件事:创建 ToneOutput,构造 440 Hz、音量 0.3 的 tone,然后播放它。

// Play a middle A

import SPCAudio

let toneOutput = ToneOutput()
let middleA = Tone(pitch: 440.0, volume: 0.3)
toneOutput.play(tone: middleA)

关键点:

  • import SPCAudio 引入示例所需的音频类型。
  • ToneOutput() 是发声对象。
  • Tone(pitch: 440.0, volume: 0.3) 对应 transcript 中的 440 Hz middle A。
  • toneOutput.play(tone: middleA) 会让 Playground 发出持续音调。

02:21)演讲提醒:如果 Playground 代码不停止运行,这个 tone 会一直播放。示例用 DispatchQueue.main.asyncAfter 在 400 毫秒后调用 stopTones()

// Play a middle A

import SPCAudio

let toneOutput = ToneOutput()
let a4 = Tone(pitch: 440.0, volume: 0.3)
toneOutput.play(tone: a4)

DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.milliseconds(400)) {
    toneOutput.stopTones()
}

关键点:

  • a4 仍然是 440 Hz 的 middle A。
  • toneOutput.play(tone: a4) 先启动声音。
  • DispatchTimeInterval.milliseconds(400) 给声音一个短时长。
  • 闭包里的 toneOutput.stopTones() 负责结束持续播放。

用 Timer 串起多个音符

02:51)单个延迟适合停止一个音。要连续播放多个音符,演讲推荐 Timer,因为它能按固定间隔重复执行同一段逻辑。

// Play more than one tone

let toneOutput = ToneOutput()
let tones = [
    Tone(pitch: 440.00, volume: 0.3),
    Tone(pitch: 493.88, volume: 0.3),
    Tone(pitch: 523.25, volume: 0.3)
]

var toneIndex = 0
Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { timer in
    guard toneIndex < tones.count else {
        toneOutput.stopTones()
        timer.invalidate()
        owner.endPerformance()
        return
    }

    toneOutput.play(tone: tones[toneIndex])
    toneIndex += 1
}

关键点:

  • tones 保存 middle A、middle B、middle C 的频率。
  • toneIndex 记录下一次要播放数组里的哪一个 tone。
  • withTimeInterval: 0.4 对应 transcript 中每 400 毫秒播放下一个音符。
  • guard toneIndex < tones.count 到达数组末尾后进入收尾分支。
  • toneOutput.stopTones() 停止音频输出,timer.invalidate() 停止重复 timer。
  • owner.endPerformance() 告诉挑战系统表演已经完成,玩家获得进度。

核心启发

  1. 做一个音阶练习器 做什么:用按钮选择 C-major、F-major 等音阶,然后按顺序播放每个音。 为什么值得做:session 已经给出 tone 数组、0.4 秒 timer 和升八度时频率乘以 2 的方法。 怎么开始:先复用官方的 tones 数组和 Timer.scheduledTimer,再把不同音阶映射成不同的 Tone(pitch:volume:) 列表。

  2. 做一个听音记忆挑战 做什么:播放三到五个 tone,让用户按顺序复现。 为什么值得做:这正好延伸 Swan’s Quest 中“为 Swan 播放一串 musical notes”的任务。 怎么开始:用 ToneOutput.play(tone:) 播放题目,用 toneIndex 控制进度,用户完成后调用和挑战类似的结束逻辑。

  3. 做一个频率实验面板 做什么:提供 pitch 和 volume 输入,让用户听到不同频率、不同音量的结果。 为什么值得做Tone 的公开参数只有 pitchvolume,很适合用来理解频率和响度。 怎么开始:把 Tone(pitch: 440.0, volume: 0.3) 中的两个数字换成控件输入,播放前生成新的 Tone

  4. 做一个 side quest 辅助器 做什么:把 C-major scale 改写成 F-major scale,并标出需要新增的 B-flat。 为什么值得做:transcript 明确要求从 F4 开始、复用 C-major 代码、用频率翻倍得到高八度。 怎么开始:先保留原有播放循环,只替换 tones 数组里的频率;遇到高八度音时用原频率乘以 2。

关联 Session

评论

GitHub Issues · utterances