# 第9次课 讲稿

> 课程：小程序开发
> 主题：音频与媒体API
> 教材：《微信小程序开发实战（第2版）》第4章
> 时长：3学时（理论 1 + 实践 2）
> 重点：音频播放、背景音频、图片操作、录音
> 难点：音频状态管理、后台播放

---

## 一、开场（0:00）

第3章结束，从今天进入第4章——常用 API。

前面三章我们学的是"怎么把页面做出来"，从现在开始学的是"怎么调用手机的能力"。音频、相机、文件、画布、定位……这些是小程序区别于普通网页的地方。

今天讲音频。看起来是个很窄的主题，但它有个特别好的教学价值：**音频播放是一个典型的"有状态"功能**。播放中、暂停、加载中、播放完毕——你必须时刻知道它处在哪个状态，界面才不会出错。这个"状态管理"的思路，比音频 API 本身更值钱。

---

## 二、明确本课目标（约 5:00）

把目标写在白板上或直接翻到课件第 2 页，逐条念一遍：

- 掌握InnerAudioContext音频播放
- 理解背景音频管理器的使用
- 掌握图片选择与预览API
- 了解录音API的使用
- 案例：案例4-1 音乐播放器、案例4-2 录音机

本次课 3 学时的安排是：**理论讲解 1 学时，上机实践 2 学时**——音频 API 讲解 1 学时，案例 4-1/4-2 实操 2 学时。

---

## 三、知识点讲解

### 幻灯片 3：InnerAudioContext 音频播放

**【8:00 讲解】**

- wx.createInnerAudioContext() 创建实例
**src**

音频资源地址

- autoplay / loop / volume 属性
- play() / pause() / stop() 方法
- onPlay / onPause / onEnded / onError 事件

### 幻灯片 4：BackgroundAudioManager 背景音频

**【13:00 讲解】**

- wx.getBackgroundAudioManager() 获取实例
- 支持后台播放(小程序切到后台继续播放)
- 必须设置title属性才能播放
- singer / coverImgUrl / epname 元数据
- onPrev / onNext 事件(锁屏控制)

### 幻灯片 5：图片API

**【18:00 讲解】**

**wx.chooseMedia**

选择图片/视频(新版)

**wx.previewImage**

全屏预览图片

**wx.getImageInfo**

获取图片信息

**wx.saveImageToPhotosAlbum**

保存到相册

- count/sizeType/sourceType 参数配置

### 幻灯片 6：案例4-1 音乐播放器 / 案例4-2 录音机

**【23:00 讲解】**

**案例4-1 音乐播放器**

InnerAudioContext 实现播放、暂停、进度跳转

**案例4-2 录音机**

wx.getRecorderManager() 录音，再用 InnerAudioContext 回放

- 录音管理器方法：start / pause / resume / stop，onStop 回调中拿到 tempFilePath
- 录音授权：scope.record 权限申请，被拒后用 wx.openSetting 引导
- 综合运用音频 API 完成播放与录制两个教材案例

---

## 四、难点突破：用类比讲清楚

下面这几个点是学生最容易卡住的地方，抽象讲不通就换成类比。

### InnerAudioContext 与 BackgroundAudioManager

前者像插在手机上的耳机，你退出小程序声音就停；后者像车载音响，接管了系统的播放控制，锁屏了还能放、还能在通知栏切歌。代价是必须填 title，而且同一时刻全系统只有一个。

### 音频状态机

把播放器想成一台自动售货机：投币（play）、退币（pause）、缺货（onError）、出货完成（onEnded）。每个按钮在不同状态下该不该亮，是由当前状态决定的。写代码前先在纸上画出这几个状态和转移关系，逻辑会清楚很多。

### 录音授权

像进别人家要先敲门。第一次调用会弹授权框，用户点了拒绝，之后再调就直接失败且**不再弹框**——必须引导他去设置页手动打开。这是所有需要授权的 API 的通用套路。

---

## 五、代码演示环节

以下每一段都建议**现场敲**，不要直接贴完整代码。边敲边问"下一行该写什么"，让学生跟上节奏。

### 演示 1：InnerAudioContext

**文件：** `inner-audio.js`　**预期效果：** InnerAudio 播放器

```js
// 创建内部音频上下文
const innerAudio = wx.createInnerAudioContext()

// 设置音频源
innerAudio.src = 'https://example.com/music.mp3'

// 播放控制
innerAudio.play()
innerAudio.pause()
innerAudio.stop()
innerAudio.seek(30)

// 常用属性
innerAudio.autoplay = true
innerAudio.loop = true
innerAudio.volume = 0.8
innerAudio.startTime = 0

// 监听事件
innerAudio.onPlay(() => {
console.log('开始播放')
})
innerAudio.onPause(() => {
console.log('暂停播放')
})
innerAudio.onStop(() => {
console.log('停止播放')
})
innerAudio.onEnded(() => {
console.log('自然播放结束')
})
innerAudio.onTimeUpdate(() => {
console.log(innerAudio.currentTime, innerAudio.duration)
})

// ⚠️ 销毁实例释放资源
innerAudio.destroy()
```

**讲解口径：**

**wx.createInnerAudioContext** 创建内部音频实例，适用于短音频播放。设置 **src** 指定音频地址，调用 **play/pause/stop** 控制播放。常用属性有 autoplay 自动播放、loop 循环、volume 音量。通过 **onPlay/onPause/onEnded/onTimeUpdate** 监听播放状态，onTimeUpdate 可获取当前时间和总时长实现进度条。注意 InnerAudio **不支持后台播放**，页面隐藏即暂停，用完需调用 **destroy** 释放资源。

### 演示 2：BackgroundAudioManager

**文件：** `bg-audio.js`　**预期效果：** 后台音频播放

```js
// 获取全局唯一的后台音频管理器
const bgAudio = wx.getBackgroundAudioManager()

// 必须设置 title 和 src 才能播放
bgAudio.title = '夜曲'
bgAudio.singer = '周杰伦'
bgAudio.epname = '十一月的萧邦'
bgAudio.coverImgUrl = 'https://example.com/cover.jpg'
bgAudio.src = 'https://example.com/yequ.mp3'

// 播放控制（与 InnerAudio 类似）
bgAudio.play()
bgAudio.pause()
bgAudio.stop()
bgAudio.seek(60)

// 监听事件
bgAudio.onPlay(() => console.log('播放中'))
bgAudio.onPause(() => console.log('已暂停'))
bgAudio.onStop(() => console.log('已停止'))
bgAudio.onEnded(() => console.log('播放结束'))
bgAudio.onTimeUpdate(() => {
console.log(bgAudio.currentTime, bgAudio.duration)
})

// ✅ 支持后台播放
// 退出小程序后仍可在系统通知栏控制
// 全局唯一，无需 destroy

// ⚠️ 与 InnerAudio 的区别：
// 1. 必须设置 title 才能播放
// 2. 支持后台播放，系统通知栏可控
// 3. 全局唯一实例，无需创建和销毁
// 4. iOS 可被电话等中断后自动恢复
```

**讲解口径：**

**wx.getBackgroundAudioManager** 获取后台音频管理器，适用于音乐类应用。与 InnerAudio 的关键区别：**必须设置 title** 才能播放；**支持后台播放**，退出小程序后可在系统通知栏控制；**全局唯一实例**，不需要创建和销毁；iOS 上被电话中断后可自动恢复。还需设置 singer、coverImgUrl 等元数据显示在通知栏和锁屏界面。

### 演示 3：图片API

**文件：** `image-api.js`　**预期效果：** 图片API流程

```js
// 选择图片/视频（推荐使用 chooseMedia）
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success(res) {
const tempFiles = res.tempFiles
console.log(tempFiles[0].tempFilePath)
console.log(tempFiles[0].size)
}
})

// 预览图片
wx.previewImage({
current: 'https://example.com/img1.jpg',
urls: [
'https://example.com/img1.jpg',
'https://example.com/img2.jpg',
'https://example.com/img3.jpg'
]
})

// 保存图片到相册
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
success() {
wx.showToast({ title: '保存成功' })
},
fail() {
// 用户拒绝授权，引导开启
wx.showModal({
title: '提示',
content: '需要您授权保存图片到相册'
})
}
})

// ⚠️ 保存到相册需要用户授权 scope.writePhotosAlbum
```

**讲解口径：**

图片三大 API：**wx.chooseMedia** 选择图片或视频，替代了旧版 chooseImage，支持 mediaType 指定类型、sourceType 指定来源（相册/相机）、count 限制数量，返回临时文件路径；**wx.previewImage** 全屏预览图片，current 指定当前显示，urls 数组支持左右滑动浏览；**wx.saveImageToPhotosAlbum** 保存图片到相册，**需要用户授权** scope.writePhotosAlbum，拒绝时应引导用户开启。

### 演示 4：案例4-1音乐播放器

**文件：** `index.js`　**预期效果：** 音乐播放器界面

```js
// 案例4-1：音乐播放器 index.js
const audioBgm = wx.getBackgroundAudioManager()

Page({
data: {
musicList: [
{ id: 1, title: '夜曲', singer: '周杰伦',
src: 'https://example.com/yequ.mp3',
cover: 'https://example.com/cover1.jpg' },
{ id: 2, title: '晴天', singer: '周杰伦',
src: 'https://example.com/qingtian.mp3',
cover: 'https://example.com/cover2.jpg' }
],
currentIndex: 0,
isPlaying: false,
currentTime: '00:00',
duration: '00:00',
progress: 0
},

onLoad() {
this.setupAudioEvents()
},

setupAudioEvents() {
audioBgm.onPlay(() => {
this.setData({ isPlaying: true })
})
audioBgm.onPause(() => {
this.setData({ isPlaying: false })
})
audioBgm.onTimeUpdate(() => {
const { currentTime, duration } = audioBgm
this.setData({
currentTime: this.formatTime(currentTime),
duration: this.formatTime(duration),
progress: currentTime / duration * 100
})
})
audioBgm.onEnded(() => {
this.playNext()
})
},

playMusic() {
const music = this.data.musicList[this.data.currentIndex]
audioBgm.title = music.title
audioBgm.singer = music.singer
audioBgm.coverImgUrl = music.cover
audioBgm.src = music.src
},

togglePlay() {
if (audioBgm.src) {
this.data.isPlaying ? audioBgm.pause() : audioBgm.play()
} else {
this.playMusic()
}
},

playNext() {
const idx = (this.data.currentIndex + 1) % this.data.musicList.length
this.setData({ currentIndex: idx })
this.playMusic()
}
})
```

**讲解口径：**

案例4-1音乐播放器使用 **getBackgroundAudioManager** 实现后台播放。核心逻辑：在 **setupAudioEvents** 中注册所有事件监听，**onTimeUpdate** 实时更新播放进度和时间显示；**onEnded** 自动播放下一首；**playMusic** 设置 title、singer、coverImgUrl、src 后自动播放；**togglePlay** 切换播放/暂停状态。时间格式化用 formatTime 将秒转为 mm:ss。

---

## 六、常见错误与排查

上机环节学生一定会遇到下面这些问题，提前讲一遍能省掉大量重复答疑。

| # | 现象 | 原因与解决 |
|:-:|------|------------|
| 1 | 创建了多个 InnerAudioContext 实例 | 每次点击播放都 createInnerAudioContext，结果几首歌一起响。实例应该在 onLoad 里创建一次，页面卸载时 destroy。 |
| 2 | 忘了 destroy 造成内存泄漏 | onUnload 里一定要调 destroy()，否则离开页面音频还在后台跑。 |
| 3 | 背景音频不播放 | BackgroundAudioManager 必须设置 title，不设直接静默失败，且控制台不一定报错。 |
| 4 | 进度条拖动后跳回去 | 拖动时要先暂停 onTimeUpdate 对进度的回写，松手后再恢复，否则会和用户"抢方向盘"。 |
| 5 | 用户拒绝录音授权后无提示 | fail 回调里应该用 wx.showModal 引导，确认后调 wx.openSetting()。 |

---

## 七、上机实践指引

本次课对应教材案例 2 个，源码已放在站点 `code/session09/` 下：

- **【案例4-1】音乐播放器** — 含 知识储备、案例实现、服务器端 共 3 个工程
- **【案例4-2】录音机** — 含 知识储备、案例实现 共 2 个工程

建议顺序：先带学生跑一遍「知识储备」里的最小示例，确认每个知识点都能单独跑通，再进「案例实现」做完整案例。直接上完整案例，出错了不知道错在哪。

---

## 八、本课小结

回到课件小结页，逐条回顾：

- InnerAudioContext
- BackgroundAudioManager
- 图片API
- 案例4-1音乐播放器

今天的两个案例：音乐播放器练状态管理，录音机练授权流程。这两件事在后面的定位、相册、支付里会反复出现，套路是一样的。

回去做作业时，重点体会一下：**界面按钮的显示状态，永远是从数据推导出来的，不是手动去改的**。

---

## 九、布置作业

三档任务，基础必做，进阶建议做，挑战选做。

### 【基础】完成音乐播放器

按照案例4-1，实现音乐播放器，包含播放/暂停、进度显示、上下曲切换

### 【进阶】完成录音机

按照案例4-2，实现录音机功能，支持录音、播放录音、录音时长显示

### 【挑战】语音备忘录

结合录音与播放功能，实现语音备忘录，支持多条录音记录保存与回放

---

> 配套资源：课件 `slides-v2/session09.pptx`　课堂视频 `videos-output/session09.mp4`　扩展阅读 `docs/第9次课_扩展阅读.md`
