同一个节奏动效要支持 SVG 和 Canvas,没必要让一个大类同时管理音源、频谱、颜色和绘图句柄。先固定交换的数据,再分别接入分析器、映射器与渲染器。
三层之间传小对象
分析器输出 time、rms、bands,映射器把指标变成有界视觉强度,渲染器只接收样式状态并 draw。插件不接受任意模型生成脚本,也不自行拥有应用级 AudioContext。
Web Audio 节点图、Web Animations 和 WebGL 各有生命周期约束。分析器可以被多个渲染器共享,但共享不意味着每个订阅者都能关闭音源。下面的 Hub 只组织订阅和映射,不隐藏真实浏览器对象;destroy 只调用渲染器自己的 dispose。
function mapFrame(f,reduced=false){ if(!f||!Number.isFinite(f.time)||!Number.isFinite(f.rms)||!Array.isArray(f.bands)||f.bands.length!==3||!f.bands.every(Number.isFinite))throw Error('frame'); const clamp=x=>Math.max(0,Math.min(1,x)); return {time:f.time,strength:reduced?0:clamp(f.rms),bands:reduced?[0,0,0]:f.bands.map(clamp)}; } class VisualHub{ constructor(){this.subscribers=new Set();this.disposed=false;this.reduced=false;} subscribe(renderer){ if(this.disposed||typeof renderer.draw!=='function'||typeof renderer.dispose!=='function')throw Error('renderer'); if(this.subscribers.has(renderer))throw Error('duplicate renderer'); this.subscribers.add(renderer);let active=true; return ()=>{if(active){active=false;if(this.subscribers.delete(renderer))renderer.dispose();}}; } push(frame){ if(this.disposed)return;const state=mapFrame(frame,this.reduced);const errors=[]; for(const r of this.subscribers)try{r.draw({...state,bands:[...state.bands]});}catch(e){errors.push(e);} if(errors.length)throw new AggregateError(errors,'draw'); } destroy(){ if(this.disposed)return;this.disposed=true;const errors=[]; for(const r of this.subscribers)try{r.dispose();}catch(e){errors.push(e);} this.subscribers.clear();if(errors.length)throw new AggregateError(errors,'dispose'); } }图中的共享 AudioContext 不归某个 renderer 关闭。上面的 Hub 是教学核心,尚未包含 RAF、实际分析器和 renderer;这些适配器必须由应用显式创建,不能把这段代码称为完整音频插件。
初始化失败也要有释放路径
获取音源失败时不创建渲染循环;shader 编译失败时释放已创建的 buffer/program;订阅解除与 destroy 都应幂等。媒体元素跨域还受服务器 CORS 与资源政策约束,不能用设置 crossOrigin 属性就承诺分析成功。
一个渲染器报错不应让其他渲染器静默停住,本例继续分发后汇总错误。同一 renderer 重复订阅会被拒绝,避免两个释放函数争用同一实例;这不是引用计数接口。destroy 后再调用解除函数也不会二次 dispose。
本轮测试了强度上限、静态模式、解除订阅、错误隔离与销毁,没有启动真实音频或绘图。路由切换、音源拒绝、共享订阅竞争与跨域资源仍需浏览器联调。
复现记录
本轮在 Node.js v26.3.0 中运行随文核心逻辑的构造样例,进程退出码为 0。测试输出:
PASS: bounded mapping, reduced motion, unsubscribe idempotence, destroy, invalid contract将上方完整代码保存为 example.mjs,以 Node.js 26 执行 node example.mjs 可加载核心与教学输入;函数默认不访问网络、模型或硬件。浏览器适配入口需要明确传入对应对象,本轮未执行这些入口。上面的通过项来自独立构造样例,不能替代集成测试。
资料与时效
- Web Audio API 1.1:2026-09-22。
- Web Animations:2023-06-05。
- WebGL Specification:首次发布日期未核实;更新 2026-06-30。
- Media Queries Level 5:2026-02-19。
本稿对应原预排时段;实际发布前须人工复核来源更正、SDK/API 版本及可用性,不据此预言 10 月 24–31 日的新事件。