九个旋钮、零采样:folia-major 的 Web Audio 效果链如何把干净信号调出复古听感
【免费下载链接】folia-major专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major
folia-major 是播放本地与多平台在线音乐的播放器,它的复古听感不靠任何采样文件:本文拆解这条浏览器原生 Web Audio 效果链——接线顺序、九个旋钮的节点映射与零素材生成,读完能画出整条音频图。
信号主链:一行写死的接线顺序与九个旋钮的落点
decks → mix → equaliser → effects → volume → fade → analyser → out这是播放图里信号走的完整顺序,写死在 playbackGraph.ts 的一个函数里。注释说顺序重要到值得单独拿出来——它从 React hook 搬进可独立测试的接线函数,让顺序本身可以被断言。automix 的两个 deck 先落在同一个混音点上,EQ 与 analyser 因此各只存在一份、看到的是混合后的总和:crossfade 期间音色不打嗝,歌词可视化画的是真正被听见的波形。
主路上九个旋钮依次接线:highpass → lowpass → drive → crush → wow 延迟线 → M/S 拓宽 → 压缩器 → makeup 增益;noise(黑胶噪声)与 space(混响)是两条旁支,直接混进链的输出。主路上游是十段 EQ:31、62、125、250、500 Hz 与 1k、2k、4k、8k、16k,每段 ±12 dB、Q 1.4,频段定义在 audioEqualizer.ts。audioEqualizerGraph.ts 在播放源生命周期内只建一次这串十滤波器链,之后 UI 更新只调 apply 函数,用setTargetAtTime把增益写进已有节点,时间常数 15 ms——React 状态不进音频处理路径。
为什么从这条链切入:效果链的所有取舍都发生在顺序上。节点摆在什么位置,决定旋钮控制的是什么;节点如何更新,决定有没有咔哒声。
音量推子挪到效果链之后:三组 dBFS 实测支撑的取舍
结论先行:音量节点必须放在效果链之后,而不是它最初的混音点位置。
九个效果里有三种是“绝对值”行为——按满量程或固定阈值计算,而不是相对输入电平。如果推子留在它们上游,会怎样?用 OfflineAudioContext 把同一串链在搬动前后各测一遍,playbackGraph.ts 的注释块记录了全部数据:
- 黑胶噪声:噪声分支混在链的输出端,上游推子根本碰不到。音量 1.00、0.50、0.25 三档读出的爆裂声都是 -51.9 dBFS;音乐每调小一半,噪声相对抬升 6 dB,没有任何档位能压住它。挪到下游后三档变成 -51.8 / -57.9 / -64.0 dBFS,随音量正常衰减。
- 比特碎化:量化阶梯按满量程划分,输入被调小后阶梯占比反而变大。一个 -12 dBFS 测试音,音量 1.00 时信噪比 38.2 dB,0.25 时跌到 25.8 dB——等效位深凭空掉两位,动的只是音量推子。挪到下游后所有档位稳定在 38.2 dB。
- punch 压缩器:阈值固定,输入变安静后直接够不到阈值,效果整个消失。
对照组干净:全部效果关闭时两种接线顺序测得一致(信噪比都是 82.6 dB),不开效果的用户听不出差别。反事实也清楚:推子留在混音点,改音量就等于改效果的“行为”——爆裂声压不住、位深掉两位、压缩器在安静段落失效。该文件还在启动时把接线顺序打印出来,注释明说顺序就是修复:如果哪天“音量”又排在“效果”前面,噪声就不再听音量旋钮的话了。
🎛️ 五处反直觉实现:4x 过采样、开方映射与延迟释放
- drive 段开 4x 过采样:effectNodes.ts 给饱和节点设
oversample = '4x',这是 WaveShaperNode 可用的最高档。注释给出原因:软削波产生的谐波会越过奈奎斯特频率,混叠谐波正是波形整形听起来“发毛”而不是“温暖”的来源。代价是这个节点按 4 倍采样成本运算。 - 响度匹配的饱和曲线:推 drive 只加谐波,不加音量。effectSources.ts 的
measureSaturationCompensation先测 0.5 幅度参考正弦过 tanh 曲线后 RMS 抬升了多少,再反向乘回去;量程顶端峰值电平信号约 20% THD,典型预设值停在 5% 附近。 - crush 按开方映射位深:位深 =
16 − √amount × 12,即 4~16 bit。不开方时,听得出来的 8~10 bit 段会被挤到滑杆最末端;开方后落在中段,最“脆”的段落留给末端(同在 effectSources.ts)。 - wow 调的是延迟线,不是采样率:wowBranch.ts 用 0.7 Hz 慢 wow 与 6.8 Hz 快 flutter 两个振荡器,同时调制一条基准 12 ms 延迟线的 delayTime。磁带的音高漂移不用变速实现,而用延迟调制,和磁带机的 wow 同源。
- 800 ms 延迟释放:ConvolverNode、噪声源、LFO 都是昂贵节点,关闭后先淡出、800 ms 后才拆。effectBranchRelease.ts 只排一个定时器,窗口内重新打开则节点原封不动,避免反复创建带来的爆音;reverb、noise、wow 三个分支共用这套协议。
代价与边界:这套效果链在哪里不成立
- 单条 per-listener 链,没有 per-track 效果:混音点下游全部属于听者,automix 两个 deck 共用同一组九个旋钮。做不到给 A 轨挂 lofi、B 轨保持干燥,crossfade 也穿过同一条链。
- 噪声底是 3 秒循环:黑胶噪声缓冲只生成一次且
source.loop = true,咔嗒模式每 3 秒重复一遍;增益按amount^1.4 × 0.55映射,滑杆低端变化很平。 - hall 是一个固定房间:脉冲响应 2.2 s 长、(1−t)^2.8 衰减,分支创建时生成一次;space 旋钮只改 wet/dry 比例(wet =
amount×0.75,dry =1 − amount×0.2),不改房间大小。 - 六个预设 id 是对外契约:audioPresets.ts 的注释写明 flat/lofi/radio/hall/vocal/bass 承诺给 mod 系统,Folium 1.x 的
folium.equalizer.setMode必须继续接受;退役预设只能把旧 id 指到最近的继任者,预设参数因此被兼容性锁住。
源码阅读地图:从接线到分支的半小时路径
先读 playbackGraph.ts,接线加实测注释块约 10 分钟,把“音量在效果之后”的顺序立住;再读 effectChain.ts 的 apply 函数,约 10 分钟看完九个旋钮如何一次写进全部节点参数;最后并行读 reverbBranch.ts、noiseBranch.ts、wowBranch.ts 与 effectBranchRelease.ts,再 10 分钟。主链是一个函数,分支共用一个协议,整条阅读路径半小时足够走完。
【免费下载链接】folia-major专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考