网页上的声音,大多数时候只有两种命运:要么没有,要么烦人。
静音是默认状态,绝大部分网站是哑的。烦人,是少数想做点什么的网站的下场:一个突兀的"叮",一段自动播放的背景音乐,一个你恨不得立刻静音的弹窗提示。久而久之大家得出一个结论:网页不该有声音。
真正的问题不是"有没有声音",而是声音是怎么来的。大部分人给界面配音的方式,和挑一个图标没区别,去素材库里找一个"听起来不错"的 click.mp3,塞进 onClick,完事。八个交互就是八个互不相干的文件,凑在一起也许觉察不出有什么异样。
但我希望即使是一组简单的网站音效,也要像作曲一样设计出来,不靠拼凑。 这篇讲的,就是我给这个网站做的音效系统:它最后长成了什么样,以及背后那些"乐理"。如果你现在就把声音打开、随手点几下,听到的正是下面要拆解的东西。
第一条约束:没有一个音频文件
整套 UI 音效里,没有任何一个 .mp3 或 .wav。所有声音都是浏览器用 Web Audio 在你点下去的那一刻实时算出来的,振荡器产生波形,包络控制音量曲线,几个节点串起来,发声,结束。
这不是炫技,是一个有用的约束。文件意味着固定:固定的音高、固定的时长、固定的响度。而合成意味着这些全是参数,我可以让同一个"喵"每次叫得略有不同,可以让一个音随手势滑动,可以在不增加任何加载体积的前提下,把整套声音当成一个可以调音的乐器。
一个音阶,解决"连点不刺耳"
先说最底层、也最关键的一条:所有有确定音高的交互音,都取自同一个音阶,C 大调五声音阶 C–D–E–G–A。
为什么是五声?因为它是"不会弹错音"的音阶。五声音阶里抽掉了大调里最容易打架的两个音(第四级和第七级),剩下的五个音,任意两个叠在一起都不会出现刺耳的小二度或三全音。
这件事对界面声音特别重要,因为界面声音天然是会重叠的:你快速划过一个列表,hover 在几十毫秒内被触发十几次;你连点一个按钮;你在一个音还没衰减完的时候触发了下一个。如果这些音是随便定的频率,叠起来就是噪音;而只要它们都落在五声音阶上,无论怎么叠、怎么抢,结果永远是和谐的。这是整套系统能"经得起乱点"的根本原因。
具体到每个音(音名按等程律,A4=440,取最接近的音):
| 音效 | 频率 | 音名 | 波形 | 音程 / 功能 |
|---|---|---|---|---|
hover | 880 Hz | A5 | 正弦 | 在 nav 之上一个纯四度,最高、最轻的反馈 |
nav | 660 Hz | E5 | 三角 | 主交互的中心音 |
click | 1320 Hz | E6 | 方波 + 噪声 | 正好是 nav 高八度,短促瞬态 |
boop(进入) | 520→780 Hz | C5→G5 | 三角 | 上行纯五度,"打开 / 前进"的手势 |
pop(关闭) | 1100→700 Hz | 下行滑音 | 正弦 | 方向与 boop 相反,"释放 / 收起" |
chord(成功) | 392·494·587 Hz | G4·B4·D5 | 三角 | G 大调三和弦 = C 大调的属和弦 |
meow(彩蛋) | 520→880→460 Hz | 滑音 | 锯齿 | 经带通滤波模拟"喵"的元音 |
音程是有意思的:它能携带含义
定好音阶只是让声音"不难听"。更进一步,音程本身可以承载交互的语义:
- 上行纯五度 = 进入 / 肯定。
boop从 C5 跳到 G5,是一个上行的纯五度。纯五度是除八度之外最协和的音程,上行又天然带"开启、向上、积极"的感觉,所以它配给"点进一个详情页"这种动作。 - 下行 = 关闭 / 消解。
pop是一个向下的滑音,方向和boop正好相反。一升一降,构成一对,对应"打开 / 收起"。 - 八度叠置 = 强调。
click的 E6 正好是nav那个 E5 的高八度,同一个音、不同八度。它强化了"E 是这套声音的中心",又不引入任何新的音。 - 属和弦 = 确认。 成功音
chord是一个 G 大调三和弦,而 G 大调是 C 大调的属(第五级,V)。属和弦在听感上天然带一种"悬而待定、即将落定"的终止式色彩,这正是一个"成功 / 完成"反馈想要的情绪。
没有人会在点击的时候真的去分析"哦这是个上行五度"。但这些选择是有累积效应的,它让整套声音听起来像是一个人设计的。
波形即配器:把它当一支小乐队
挑完音高,下一个问题是音色。Web Audio 的基础波形不多,但每一种泛音结构不同,听感差别很大。我把它们当成乐器声部来分配:
- 正弦(sine):泛音最少、最纯净,像长笛或口哨。配给最高、最轻的
hover和pop。 - 三角(triangle):柔和的奇次泛音,接近八音盒、钢片琴。这是旋律声部,给
nav、boop、chord这些"主音"。 - 方波(square):空心、复古,是 8-bit 游戏机那个味道。只留给
click的打击瞬态,当一个点缀。 - 锯齿(saw):泛音最全、最亮、最接近人声。整套里只有一个地方用它:彩蛋
meow。 - 滤波噪声:没有音高,纯质感。它是这套声音的打击与织体声部:
click里那层"咔哒"、打字音type、还有倾斜主图时那条沙沙的 drone。
换句话说:旋律声部 + 打击声部 + 一个特殊的"主唱"。这就是一支极简的乐队编制。
几个让它"专业"起来的细节
骨架立住之后,剩下的是手艺。这几条单独看都很小,合起来决定了它听上去是"业余调的"还是"有人认真做过的":
统一的包络。 所有音共用同一种音量曲线:极快的起音(约 4 毫秒)+ 指数衰减、没有延音。这是一种"拨奏 / 敲击"型包络,像八音盒或马林巴,短促(40–100 毫秒)、清脆、绝不拖泥带水。正是"无延音"让它们叠在一起也不会糊成一团。
// 一个最小的 blip:起音 4ms,然后指数衰减到静音
g.gain.setValueAtTime(0, t0);
g.gain.linearRampToValueAtTime(vol, t0 + 0.004);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);等响度补偿。 人耳对不同频率的敏感度不一样,同样的能量,高频听起来更响(这就是 Fletcher–Munson 曲线讲的事)。所以越高的音我给的音量越小:hover(A5)只有 0.10,click 那层高频副音只有 0.06,而偏低的 nav、boop 是 0.14。目的是让每个音的主观响度接近,而不是数值上相等。
一条共享的混响。 所有的 one-shot 都会往同一条很淡的合成混响里送一点点信号(一个约 0.5 秒的房间脉冲,send 量 0.12)。这相当于把所有声音"录在同一个房间里",它是整套声音听起来像一个整体的关键,却淡到你几乎意识不到它的存在。湿度由一个常量控制,想多一点空间感就调大它。
那只猫。 meow 是这套里唯一"奢侈"的合成:一条锯齿波做音高滑动(低→高→低),再穿过一个同样在扫动的带通滤波器,模拟"喵"这个元音从闭到开的过程;每次触发还带一点随机抖动,所以连点不会听起来像复读机。
边界
- 默认开还是默认关? 我选了默认开(浏览器的自动播放策略保证了在你第一次交互之前它绝不会响),偏好会被记住。这是一个有争议的选择:从无障碍和"别打扰人"的角度,更稳妥的做法是默认静音、让用户主动打开。但本站的调性,它本来就是一个想让你玩一玩的地方。
- 频率是手调的近似,不是精确音名。 代码里写的是
520、780、1100这种整数,它们离标准音高有几个音分的偏差(520比标准 C5 低约 11 音分)。听感上对,但严格说不是"精确的 C 大调"。 - 克制是设计的一部分。 任何声音重复得够快都会变成噪音。所以
hover有一个 45 毫秒的冷却,你飞快划过一个列表时,它不会变成一阵机关枪。声音设计里,决定"哪里不出声"和决定"出什么声"一样重要。
声音也是设计材料,和颜色、排版、动效放在一起考虑,不是最后才想起来的事。
← all writing