feat: audio system, demo scene updates, docs, and engine improvements

This commit is contained in:
2026-06-08 23:18:33 +08:00
parent 514c8f5207
commit 4bfdfbc27d
8 changed files with 316 additions and 24 deletions

View File

@@ -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
├── VideoManagerA/B 双缓冲,只管画面和视频内音轨)
│ └── loopVideo 循环时只管画面 → BGM 不受影响
└── AudioManager独立 AudioContext/HTMLAudioElement只管 BGM
── 按场景播放/交叉淡化/循环,独立于视频生命周期
│ └── loopStart/loopEnd 循环 → BGM 不受影响
└── AudioSystemWeb 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 outbgmCrossFade 秒)
└── bgmUrl 不同?
├── fetch + decode BGM B若未缓存
├── AudioBufferSourceNode 播 BGM Bgain 从 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 文件路径MP3null/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 APIfetch+decode 缓存BufferSourceNode 创建GainNode 指数 ramp 交叉淡化同源继续/不同 crossFade/静音 fade outducking 事件接口
- [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 测试 MP3calm_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 全屏模式 — 沉浸式浏览器体验(待实现)