# Emulator 101：从零构建一个 NES 模拟器

用 Python 逐帧绘制 2D 画面、合成中文旁白，生成一支介绍「模拟器是怎么工作的」教学视频。全片 23 个场景，1920 × 1080、30 fps，画面由 Pillow 绘制，旁白由 edge-tts 合成，最终由 FFmpeg 编码合成。

制作流程说明页：[pipeline.html](pipeline.html)

## 仓库结构

| 路径 | 作用 |
| --- | --- |
| `engine.py` | 绘制原语：字体、缓动、文字/面板/箭头/代码块等 |
| `scene.py` | `Scene` 定义与逐帧合成入口 `compose` |
| `widgets.py` | 手柄等可复用控件 |
| `codeview.py` | 代码面板（语法高亮样式的代码展示） |
| `scenes_a.py` ～ `scenes_j.py` | 23 个场景的画面与旁白文案（`S0` ～ `S22`） |
| `scenes.py` | 场景汇总为 `SCENES` 列表 |
| `build.py` | 构建入口：TTS → 时间轴 → 逐帧渲染 → FFmpeg 合成 |
| `requirements.txt` | Python 依赖清单（pillow / numpy / edge-tts） |
| `pipeline.html` | 视频生成流程说明页 |

## 环境要求

- macOS。`engine.py` 写死了 PingFang 与 Menlo 的系统字体路径（`/System/Library/Fonts/`），其他平台需自行改字体。
- Python 3，依赖 `pillow`、`numpy`、`edge-tts`（见 `requirements.txt`）。
- `ffmpeg` 可从命令行直接调用（配音 mp3 → WAV 转码、视频编码都靠它）。
- edge-tts 首次合成旁白需要联网；合成结果按内容哈希缓存在 `out/tts/`，重复构建不再请求网络。旁白不再依赖系统 `say`，因此也**不需要音频设备**。

从零安装依赖：

```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```

## 构建

```bash
.venv/bin/python build.py              # 完整渲染 → out/nes_emulator.mp4
.venv/bin/python build.py stills       # 每个场景在 50% / 92% 处各截一张 PNG → out/stills/
.venv/bin/python build.py stills 7 9   # 只截第 7、9 个场景
```

产物：

```
out/
├── nes_emulator.mp4    # 成片（H.264 + AAC，约 14 分钟）
├── nes_emulator.srt    # 与语音逐句对齐的字幕
├── audio.wav           # 全片旁白（22.05 kHz 单声道）
├── parts/00-22.mp4     # 分场景中间片段
├── stills/             # 各场景预览图
└── tts/*.wav           # 语音缓存（改文案后重跑只会重新合成改过的句子）
```

构建顺序是先有声音、再定画面节奏：`build.py` 为每条旁白生成语音并读取 WAV 时长，据此计算每个场景的开始标记、结束时间和总时长；随后用多进程逐帧绘制画面，FFmpeg 将帧编码为 H.264 并与旁白合成。中间结果与语音缓存都写入 `out/`（不提交 Git）。

## 旁白音色

音色与语速在 `build.py` 顶部一处配置:

```python
VOICE, RATE, SR = 'zh-CN-XiaoxiaoNeural', '+15%', 22050
```

换音色改 `VOICE`（如 `zh-CN-YunxiNeural` 男声），语速改 `RATE`（如 `+0%`、`-10%`）。缓存键包含这两项，改完重跑会自动重新合成全部旁白，旧缓存不受影响。
