音效与触感
合成音、音频文件、震动、屏幕常亮。
完整清单见「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。
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 等同关音)。
按和弦就是连续起几个音、中间不关:
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 是单通道的(后一个音掐掉前一个)⇒ 一个事件永远只有一个音在响,结构上不可能削波。代价是没有和弦、没有音色 —— 对"打中 / 爆炸 / 胜负"这类 一次性提示音够用。
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(亮),比全用同一种波形清楚得多。
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)
立刻全停(不进淡出,声音当场断掉)。用于「用户按了强制停止」这种场合 —— 淡出才安静下来的那几十毫秒,在急停的语义下就是"没停"。
ui_tone_panic(); /* 一键静音 */
⚠ 急停只是把声部停掉,程序自己的记账要一并清 —— 否则"还按着的手指"抬手时会去减一本已经清空的账,那个键以后就起不来音了。Examples/c/app/piano.c 的「清音」键就是三件事一起做的:ui_tone_panic() + 清引用计数 + 清槽位记账。
ui_audio_play(char* path, int loop)
播放一个音频文件(相对路径按沙箱根解)。loop 非 0 = 循环(BGM 用)。返回 0 成功 / -1 失败(文件不存在 / 路径非法 / 平台不支持)。
ui_audio_play("bgm.mp3", 1); /* 循环播放 */
ui_audio_stop(void)
停掉正在播的音频。
ui_audio_stop();
ui_audio_set_volume(int volume)
整体音量 0–100(超出会被钳)。对之后播放的音生效。
ui_audio_set_volume(60);
ui_scr_set_keep_on(int on)
屏幕常亮开关(玩游戏的都该开)。
ui_scr_set_keep_on(1); /* 1 开 0 关 */
ui_vibrate_simple(int ms, int strength)
震动:ms 毫秒,strength 强度(0–255,0 = 用系统默认)。
ui_vibrate_simple(120, 0);
ui_vibrate_pattern(int* pattern, int count)
按节奏震动:pattern 是 int 数组(奇数下标 = 静、偶数下标 = 动,同 Android 语义), count 是段数。返回 0 成功 / -1 失败。
int pat[4] = { 100, 60, 100, 60 };
ui_vibrate_pattern(pat, 4); /* 震-停-震-停 */