# 音效与触感

合成音、音频文件、震动、屏幕常亮。

> 完整清单见「[UI 开发](help:vml/ui)」。下面每个接口都带一句用法和一个例子；
> 签名取自 `Lib/c/vml_ui.h`（权威来源）。
>
> **想一次听完这一整组接口**：命令行页跑 `vml run examples/c/demo/audio_all.c` ——
> 它自己一项一项往下走（12 项、约 25 秒），每项先写明"在验什么、该听见什么"。

### `ui_audio_beep(int freq, int ms)`
**现场合成**一个音（不用带音频文件）：`freq` 赫兹、`ms` 毫秒。
⚠ **单通道** —— 一次只响一个，连着发的只有最后一个听得见。所以用**音高**表达好坏：消一行 880Hz、四行 1568Hz，赢了盖过一切。
⚠ 这是它的**老语义，没有变**。要**同时**响几个音（和弦 / 旋律）用下面的 `ui_tone_on/off`。
```c
ui_audio_beep(880, 80);      /* 消一行 */
ui_audio_beep(1568, 160);    /* 消四行，音更高 */
```
### `ui_tone_on(int ch, int note, int vel)` / `ui_tone_off(int ch, int note)`
**复音** —— 多个音**同时**响（最多 32 个声部）。
音符号是**真 MIDI 语义**：A4 = 69 = 440Hz、中央 C = 60。通道 0–15、力度 0–127（**0 等同关音**）。

按和弦就是连续起几个音、**中间不关**：
```c
ui_tone_on(0, 60, 100);   /* do  */
ui_tone_on(1, 64, 100);   /* mi  */
ui_tone_on(2, 67, 100);   /* sol —— 三个音同时在响 */
...
ui_tone_off(2, 67); ui_tone_off(1, 64); ui_tone_off(0, 60);
```
⚠ 参数**一律钳、不拒**：越界的音符号会被钳到 0–127 继续响，而不是默默不发声。
⚠ `ui_audio_beep` 与它**互不干扰** —— 蜂鸣走自己的专用声道，按着和弦时照常打音效也没事。

现成例子：`Examples/c/test/audio_test.c`（音阶/和弦/旋律）与 `Examples/c/app/piano.c`（钢琴）。

#### 游戏音效用哪个？—— **`ui_audio_beep` 单音**（⚠ 别用音序器）

**结论先行**：游戏事件提示音一律用 `ui_audio_beep(freq, ms)`。
共享库那套**音序器**（`ui_sfx_add` / `ui_sfx_tick`）**真机上会破音**，
本仓 2026-09-26 已把**全部 28 个游戏的音效**从它退回了 `ui_audio_beep`。

**为什么会破音**（两条都是**配置**问题，不是机制问题）：

| 原因 | 现场 |
|---|---|
| **多个声部同时响** | 命中配成三声部和弦、撞楼配成三个低音一起轰 ⇒ 多声部混音一叠加就顶到**削波** |
| **长音拖尾** | 胜负音末了一个音拖 12–14 拍（一拍 33ms ≈ 400ms）⇒ 外放"滋滋" |

`ui_audio_beep` 是**单通道**的（后一个音掐掉前一个）⇒ **一个事件永远只有一个音在响**，
结构上不可能削波。代价是没有和弦、没有音色 —— 对"打中 / 爆炸 / 胜负"这类
**一次性提示音**够用。

```c
void sfx_hit(void)  { ui_audio_beep(523, 264); }   /* 命中：清亮 */
void sfx_boom(void) { ui_audio_beep(131, 198); }   /* 撞楼：一声闷响（最低） */
void sfx_win(void)  { ui_audio_beep(1047, 320); }  /* 赢：最高音、最长 */
void sfx_lose(void) { ui_audio_beep(131, 320); }   /* 输：最低音、最长 */
```

**频率怎么定**（机械规则，别随手改）：取原音型的**首音**（MIDI → Hz）；
**结局取两端的极值**（赢取最高音、输取最低音 —— 「不看屏幕也分得出输赢」就靠这个，
五子棋/象棋两版也是这么配的：赢 1320 / 输 240）；时长取整块时长、**封顶 320ms**。

⚠ 另外两条：**低音别写太低** —— 手机外放在 200Hz 以下衰减很快，C2(65Hz) 出来是
"噗"的一声闷响，玩家听着像**没响**（别低于 C3 = 131Hz）；**胜负一定要有声音**，
玩家那一刻的视线在棋盘上、不在你写的横幅上。
可抄的例子：`examples/c/app/gomoku.c`、`chess.c`（最干净）、`examples/cpp/app/gorilla.cpp`（八种最全）。
完整版见开发者文档 `docs/VML游戏开发指南.md` 的 §4.5。

#### `ui_sfx_*` 音序器还能用吗？

**游戏里别再用了**。机制本身没问题（它会替你处理"同通道旧槽要先关掉""按真实流逝时间补拍"
这些容易忘的事），但只要配出**多声部叠加**或**长音拖尾**，就会回到上面那两条破音原因上，
而且**桌面验不出来**（桌面 `vmlcli` 不经过手机那条混音路径）。真要用就先按
"单声部 + 每音 ≤ 320ms"配，并且**上真机听**。

### `ui_tone_off_all(void)` / `ui_tone_get_voices(void)`
全关（走**淡出**，不是硬切）；查"此刻在响的声部数" —— 调和弦时有用。
### `ui_tone_set_wave(int ch, int wave)` / `ui_tone_set_max_voices(int n)`
设**某个通道**的默认波形（`VML_WAVE_SINE`(0) / `SQUARE`(1) / `SAW`(2) / `TRIANGLE`(3)）、
设**同时允许的声部上限**（1–32，默认 `VML_TONE_MAX_VOICES` = 32）。
音色不同是"和弦听起来不糊"的一半：几个音同时响时，**给低音声部换 `TRIANGLE`**（谐波少、
不抢）、旋律声部留 `SQUARE`（亮），比全用同一种波形清楚得多。
```c
ui_tone_set_wave(0, VML_WAVE_TRIANGLE);   /* 通道 0 当低音铺底 */
ui_tone_set_wave(1, VML_WAVE_SQUARE);     /* 通道 1 走旋律 */
ui_tone_set_max_voices(16);               /* 声部够用就行，太多会互相掩盖 */
```
### `ui_tone_panic(void)`
**立刻**全停（**不进淡出**，声音当场断掉）。用于「用户按了强制停止」这种场合 ——
淡出才安静下来的那几十毫秒，在急停的语义下就是"没停"。
```c
ui_tone_panic();   /* 一键静音 */
```
⚠ 急停只是**把声部停掉**，程序自己的记账要一并清 —— 否则"还按着的手指"抬手时
会去减一本已经清空的账，那个键以后就起不来音了。`Examples/c/app/piano.c` 的「清音」键
就是三件事一起做的：`ui_tone_panic()` + 清引用计数 + 清槽位记账。
### `ui_audio_play(char* path, int loop)`
播放一个**音频文件**（相对路径按沙箱根解）。`loop` 非 0 = 循环（BGM 用）。
返回 0 成功 / -1 失败（文件不存在 / 路径非法 / 平台不支持）。
```c
ui_audio_play("bgm.mp3", 1);   /* 循环播放 */
```
### `ui_audio_stop(void)`
停掉正在播的音频。
```c
ui_audio_stop();
```
### `ui_audio_set_volume(int volume)`
整体音量 0–100（超出会被钳）。对**之后**播放的音生效。
```c
ui_audio_set_volume(60);
```
### `ui_scr_set_keep_on(int on)`
屏幕常亮开关（玩游戏的都该开）。
```c
ui_scr_set_keep_on(1);   /* 1 开 0 关 */
```
### `ui_vibrate_simple(int ms, int strength)`
震动：`ms` 毫秒，`strength` 强度（0–255，0 = 用系统默认）。
```c
ui_vibrate_simple(120, 0);
```
### `ui_vibrate_pattern(int* pattern, int count)`
按**节奏**震动：`pattern` 是 int 数组（奇数下标 = 静、偶数下标 = 动，同 Android 语义），
`count` 是段数。返回 0 成功 / -1 失败。
```c
int pat[4] = { 100, 60, 100, 60 };
ui_vibrate_pattern(pat, 4);   /* 震-停-震-停 */
```
