SG
01Bio02Projects03Lab04Writing05Contact↓ scroll? nope. it’s one page.
INDEX 04

给网站设计一套音效系统:从一个音阶说起

一套网站音效系统是怎么设计出来的。

网页上的声音,大多数时候只有两种命运:要么没有,要么烦人。

静音是默认状态,绝大部分网站是哑的。烦人,是少数想做点什么的网站的下场:一个突兀的"叮",一段自动播放的背景音乐,一个你恨不得立刻静音的弹窗提示。久而久之大家得出一个结论:网页不该有声音。

真正的问题不是"有没有声音",而是声音是怎么来的。大部分人给界面配音的方式,和挑一个图标没区别,去素材库里找一个"听起来不错"的 click.mp3,塞进 onClick,完事。八个交互就是八个互不相干的文件,凑在一起也许觉察不出有什么异样。

但我希望即使是一组简单的网站音效,也要像作曲一样设计出来,不靠拼凑。 这篇讲的,就是我给这个网站做的音效系统:它最后长成了什么样,以及背后那些"乐理"。如果你现在就把声音打开、随手点几下,听到的正是下面要拆解的东西。

第一条约束:没有一个音频文件

整套 UI 音效里,没有任何一个 .mp3.wav。所有声音都是浏览器用 Web Audio 在你点下去的那一刻实时算出来的,振荡器产生波形,包络控制音量曲线,几个节点串起来,发声,结束。

这不是炫技,是一个有用的约束。文件意味着固定:固定的音高、固定的时长、固定的响度。而合成意味着这些全是参数,我可以让同一个"喵"每次叫得略有不同,可以让一个音随手势滑动,可以在不增加任何加载体积的前提下,把整套声音当成一个可以调音的乐器

一个音阶,解决"连点不刺耳"

先说最底层、也最关键的一条:所有有确定音高的交互音,都取自同一个音阶,C 大调五声音阶 C–D–E–G–A

为什么是五声?因为它是"不会弹错音"的音阶。五声音阶里抽掉了大调里最容易打架的两个音(第四级和第七级),剩下的五个音,任意两个叠在一起都不会出现刺耳的小二度或三全音

这件事对界面声音特别重要,因为界面声音天然是会重叠的:你快速划过一个列表,hover 在几十毫秒内被触发十几次;你连点一个按钮;你在一个音还没衰减完的时候触发了下一个。如果这些音是随便定的频率,叠起来就是噪音;而只要它们都落在五声音阶上,无论怎么叠、怎么抢,结果永远是和谐的。这是整套系统能"经得起乱点"的根本原因。

具体到每个音(音名按等程律,A4=440,取最接近的音):

音效频率音名波形音程 / 功能
hover880 HzA5正弦nav 之上一个纯四度,最高、最轻的反馈
nav660 HzE5三角主交互的中心音
click1320 HzE6方波 + 噪声正好是 nav 高八度,短促瞬态
boop(进入)520→780 HzC5→G5三角上行纯五度,"打开 / 前进"的手势
pop(关闭)1100→700 Hz下行滑音正弦方向与 boop 相反,"释放 / 收起"
chord(成功)392·494·587 HzG4·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):泛音最少、最纯净,像长笛或口哨。配给最高、最轻的 hoverpop
  • 三角(triangle):柔和的奇次泛音,接近八音盒、钢片琴。这是旋律声部,给 navboopchord 这些"主音"。
  • 方波(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,而偏低的 navboop 是 0.14。目的是让每个音的主观响度接近,而不是数值上相等。

一条共享的混响。 所有的 one-shot 都会往同一条很淡的合成混响里送一点点信号(一个约 0.5 秒的房间脉冲,send 量 0.12)。这相当于把所有声音"录在同一个房间里",它是整套声音听起来像一个整体的关键,却淡到你几乎意识不到它的存在。湿度由一个常量控制,想多一点空间感就调大它。

那只猫。 meow 是这套里唯一"奢侈"的合成:一条锯齿波做音高滑动(低→高→低),再穿过一个同样在扫动的带通滤波器,模拟"喵"这个元音从闭到开的过程;每次触发还带一点随机抖动,所以连点不会听起来像复读机。

边界

  • 默认开还是默认关? 我选了默认开(浏览器的自动播放策略保证了在你第一次交互之前它绝不会响),偏好会被记住。这是一个有争议的选择:从无障碍和"别打扰人"的角度,更稳妥的做法是默认静音、让用户主动打开。但本站的调性,它本来就是一个想让你玩一玩的地方。
  • 频率是手调的近似,不是精确音名。 代码里写的是 5207801100 这种整数,它们离标准音高有几个音分的偏差(520 比标准 C5 低约 11 音分)。听感上对,但严格说不是"精确的 C 大调"。
  • 克制是设计的一部分。 任何声音重复得够快都会变成噪音。所以 hover 有一个 45 毫秒的冷却,你飞快划过一个列表时,它不会变成一阵机关枪。声音设计里,决定"哪里不出声"和决定"出什么声"一样重要。

声音也是设计材料,和颜色、排版、动效放在一起考虑,不是最后才想起来的事。


← all writing