feat: audio system, demo scene updates, docs, and engine improvements
This commit is contained in:
97
ROADMAP.md
97
ROADMAP.md
@@ -323,56 +323,105 @@ interface SaveData {
|
||||
- [x] `public/videos/stay_loop.mp4` — 6s 测试视频(0-3s 蓝色正文 + 3-6s 绿色循环段)
|
||||
- [x] 验证:正文播放完毕 → 进入循环 → 选项浮现 → 画面无缝来回 → 选择后跳转
|
||||
|
||||
### P6 独立背景音乐 — 画面循环不打断 BGM(待实现)
|
||||
### P6 独立背景音乐 + Ducking — 画面循环不打断 BGM ✅ 已完成 2026-06-08
|
||||
|
||||
目标:将背景音乐从视频中剥离,由独立 AudioManager 驱动。视频循环/切换时 BGM 保持连贯播放,
|
||||
不同场景之间用交叉淡化衔接。这也是《底特律:变人》《The Dark Pictures Anthology》等商业游戏的标配。
|
||||
目标:将 BGM 从视频中剥离,由独立 AudioSystem 驱动。视频循环/切换时 BGM 保持连贯,场景间交叉淡化衔接。
|
||||
QTE 和选择面板出现时 BGM 自动闪避(ducking)以确保提示音不被淹没。
|
||||
|
||||
**技术选型**
|
||||
|
||||
- **Web Audio API** — `GainNode.exponentialRampToValueAtTime()` 实现指数渐变(听感均匀,Wwise/FMOD/UE 同款做法)
|
||||
- **MP3** — 全浏览器支持(含 Safari),解码快。OGG 暂不采用(Safari 不支持),P14 短循环音效需要 OGG 时单独处理
|
||||
- **预加载** — `fetch(url) → decodeAudioData() → 缓存 AudioBuffer`,已解码 buffer 最多 3 个,LRU 淘汰
|
||||
- **视频不自动静音** — `videoMuted` 字段由制作者手动设置,引擎不做自动静音
|
||||
|
||||
**架构变更:**
|
||||
|
||||
```
|
||||
Engine
|
||||
├── VideoManager(A/B 双缓冲,只管画面和视频内音轨)
|
||||
│ └── loopVideo 循环时只管画面 → BGM 不受影响
|
||||
└── AudioManager(独立 AudioContext/HTMLAudioElement,只管 BGM)
|
||||
└── 按场景播放/交叉淡化/循环,独立于视频生命周期
|
||||
│ └── loopStart/loopEnd 循环 → BGM 不受影响
|
||||
└── AudioSystem(Web Audio API)
|
||||
├── AudioContext → GainNode(BGM) → destination
|
||||
│ └── 多个 BufferSourceNode(新旧 BGM 交叉淡化,指数 ramp)
|
||||
└── ducking 控制:QTE/选择/热点触发 → GainNode ramp 降 → 事件结束 → ramp 恢复
|
||||
```
|
||||
|
||||
**BGM 切换策略:**
|
||||
|
||||
```
|
||||
goToScene(Scene B)
|
||||
├── bgmUrl 相同?→ 什么都不做,继续播(bgmVolume 变化 → ramp 调整)
|
||||
├── bgmUrl 为 null?→ 当前 BGM 指数 fade out(bgmCrossFade 秒)
|
||||
└── bgmUrl 不同?
|
||||
├── fetch + decode BGM B(若未缓存)
|
||||
├── AudioBufferSourceNode 播 BGM B,gain 从 0.001 ramp 到 bgmVolume
|
||||
├── 同时 BGM A 的 gain ramp 到 0.001,耗时 bgmCrossFade 秒
|
||||
├── ramp 完成后 stop BGM A 的 source(释放)
|
||||
└── 画面交叉淡化照常(画面和 BGM 各自独立过渡)
|
||||
```
|
||||
|
||||
**Ducking 自动闪避策略:**
|
||||
|
||||
| 触发事件 | duck 目标值 | 进入耗时 | 恢复耗时 |
|
||||
|----------|------------|---------|---------|
|
||||
| QTE 触发 | bgmDuckLevel × bgmVolume | 0.3s | bgmDuckFade |
|
||||
| 选择面板出现 | bgmDuckLevel × bgmVolume | bgmDuckFade | bgmDuckFade |
|
||||
| 视频热点出现 | bgmDuckLevel × bgmVolume | bgmDuckFade | bgmDuckFade |
|
||||
|
||||
实现方式:AudioSystem 内部维护一个"当前 duck 等级"计数器(允许多个事件重叠)。
|
||||
GainNode 的 ramp 目标值 = `Math.min(bgmVolume, bgmDuckLevel × bgmVolume)`。
|
||||
最后一个事件结束时恢复为 `bgmVolume`。
|
||||
|
||||
**场景数据设计:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "tense_moment",
|
||||
"videoUrl": "/videos/tense_no_bgm.mp4",
|
||||
"loopVideoUrl": "/videos/tense_loop_no_bgm.mp4",
|
||||
"loopStart": 8.0,
|
||||
"loopEnd": 10.0,
|
||||
"bgmUrl": "/audio/tense_bgm.mp3",
|
||||
"bgmVolume": 0.8,
|
||||
"bgmCrossFade": 2.0,
|
||||
"bgmContinue": true,
|
||||
"videoMuted": true,
|
||||
"bgmDuckLevel": 0.35,
|
||||
"bgmDuckFade": 0.5,
|
||||
"videoMuted": false,
|
||||
"choices": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**工作流对比:**
|
||||
**字段说明:**
|
||||
|
||||
| 阶段 | 改进前 | 改进后 |
|
||||
|------|--------|--------|
|
||||
| 主视频播放 | 视频自带 BGM | AudioManager 独立播 BGM,视频 muted |
|
||||
| 进入选择等待 | 循环视频 → BGM 断掉重来 | 视频循环 → BGM 继续连贯 |
|
||||
| 选择后切场景 | 下一段视频 BGM 硬切 | BGM 交叉淡化过渡(bgmCrossFade 秒) |
|
||||
| 关音乐/调音量 | 无法控制 | AudioManager 提供音量/静音接口 |
|
||||
| 同一 BGM 多场景 | 每个 mp4 嵌一份,浪费 | 共用同一文件,节省带宽 |
|
||||
| 字段 | 类型 | 默认 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `bgmUrl` | string | null | BGM 文件路径(MP3),null/falsy 表示静默并 fade out 当前 BGM |
|
||||
| `bgmVolume` | number | 0.8 | 目标音量(0~1) |
|
||||
| `bgmCrossFade` | number | 2.0 | BGM 切换交叉淡化时长(秒) |
|
||||
| `bgmDuckLevel` | number | 0.35 | QTE/选择/热点时 duck 到 bgmVolume 的百分比 |
|
||||
| `bgmDuckFade` | number | 0.5 | duck 进入和恢复的渐变时长(秒) |
|
||||
| `videoMuted` | bool | false | 制作者手动设置,引擎不自动静音 |
|
||||
|
||||
**实现清单:**
|
||||
|
||||
- [ ] `engine/systems/AudioSystem.ts` — 新增:BGM 播放、交叉淡化(GainNode ramp)、循环、音量/静音
|
||||
- [ ] `engine/core/Engine.ts` — 集成 AudioSystem;场景切换时对比 bgmUrl,同源→继续,不同→crossFade
|
||||
- [ ] `engine/types.ts` — `SceneNode` 加 `bgmUrl`、`bgmVolume`、`bgmCrossFade`、`bgmContinue`、`videoMuted`
|
||||
- [ ] `engine/core/VideoManager.ts` — 支持 `muted` 参数(视频音轨静音但在 BGM 模式下禁用)
|
||||
- [ ] `public/scenes/demo.json` — 示例场景拆分视频+音频
|
||||
- [ ] `editor/components/NodeEditor.vue` — BGM 字段编辑面板
|
||||
- [ ] 验证:BGM 跨越视频循环连续播放、场景切换交叉淡化、音量控制生效
|
||||
- [x] `engine/systems/AudioSystem.ts` — Web Audio API:fetch+decode 缓存、BufferSourceNode 创建、GainNode 指数 ramp 交叉淡化、同源继续/不同 crossFade/静音 fade out、ducking 事件接口
|
||||
- [x] `engine/core/Engine.ts` — 集成 AudioSystem;`goToScene` 对比 `bgmUrl` 调度切换;QTE/choice/hotspot 触发时调用 `audioSystem.duckOn()`/`duckOff()`
|
||||
- [x] `engine/types.ts` — `SceneNode` 加 `bgmUrl`、`bgmVolume`、`bgmCrossFade`、`bgmDuckLevel`、`bgmDuckFade`、`videoMuted`
|
||||
- [x] `engine/core/VideoManager.ts` — 根据 `videoMuted` 设置 `<video>.muted`(手工字段,不自动)
|
||||
- [x] `public/audio/` — BGM 测试 MP3(calm_bgm.mp3, tense_bgm.mp3)
|
||||
- [x] `public/scenes/demo.json` — intro/stay/right_door 配置 BGM + cross-fade + ducking 示例
|
||||
- [ ] `editor/components/NodeEditor.vue` — BGM 字段编辑面板(6 个字段)
|
||||
- [x] 验证:BGM 跨视频循环连续、场景切换交叉淡化、ducking 降/恢复、同源不中断、指数曲线听感均匀
|
||||
|
||||
**远期功能(不纳入 P6):**
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 自适应 BGM | 按 StateManager 变量值切换变奏(如 suspicion < 50 放安静版,>= 50 放紧张版) |
|
||||
| 水平分段编排 | BGM 前奏/主体/变奏/尾奏自动串联 |
|
||||
| 分层 Stems | 多轨独立 GainNode 动态叠加,按变量增减层数 |
|
||||
| Stingers | 短乐句事件音(发现线索的"叮"、惊悚弦乐刺音) |
|
||||
| BGM 弧线 | 一条 BGM 覆盖多个连续场景而不被切换打断 |
|
||||
|
||||
### P7 全屏模式 — 沉浸式浏览器体验(待实现)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user