跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScriptNode.jsAI大前端

在浏览器里跑 FRCRN:WebAssembly 轻量化部署实录

把 FRCRN 语音降噪模型放进浏览器并不只是换个运行环境,关键是先把 PyTorch 模型转成 ONNX,再用 ONNX Runtime Web 和 WebAssembly 接住实时音频流。实现里要处理 STFT、逆变换、麦克风采集、内存复用和推理延迟这些细节。测试结果显示,这种方案在会议降噪和语音识别前置场景里够用,延迟和资源占用也比较克制,但模型量化、兼容性和音频同步仍是后续要继续打磨的点。

灰度发布发布于 2026/6/300 浏览

在浏览器里跑 FRCRN:WebAssembly 轻量化部署实录

在线会议里最烦的不是回声,而是背景噪声一直顶在语音后面。装一个完整客户端当然能解决问题,但对很多临时场景来说,这一步太重了。更顺手的做法,是把降噪放到浏览器里做,打开网页就能用。

这次用的是阿里巴巴达摩院开源的 FRCRN 语音降噪模型,目标很直接:通过 WebAssembly 把推理搬到浏览器端,尽量不改用户习惯,也不把音频上传到云端。

为什么选 FRCRN 和 WebAssembly

FRCRN(Frequency-Recurrent Convolutional Recurrent Network)走的是频域路线,卷积层负责局部特征,循环结构处理时间依赖,适合单通道降噪。它对持续噪声、突发噪声和人声干扰都能处理,保真度也还算稳,不是那种一刀切把声音磨平的模型。

WebAssembly 的价值也很实际:浏览器里能跑,性能比纯 JavaScript 好得多,而且有沙箱隔离。对这类音频推理来说,它的优势不是'炫',而是省掉安装和分发这两件麻烦事。用户只要授权麦克风,就能直接进页面。

环境准备和模型转换

先把运行环境准备好。浏览器、Node.js、Python 这些是基本盘。

npm install -g onnxruntime-web esbuild
mkdir frcrn-wasm-demo
cd frcrn-wasm-demo

原始模型是 PyTorch 格式,浏览器端一般还是得落到 ONNX,再交给 ONNX Runtime Web 去跑。

import torch
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks

ans_pipeline = pipeline(
    task=Tasks.acoustic_noise_suppression,
    model='damo/speech_frcrn_ans_cirm_16k'
)
print("模型下载完成!")

转换时要注意输入张量形状和动态轴,不然后面一接实时音频就容易卡住。

import torch
import onnx
from modelscope.models import Model

model_dir = '~/.cache/modelscope/hub/damo/speech_frcrn_ans_cirm_16k'
model = Model.from_pretrained(model_dir)
model.eval()

dummy_input = torch.randn(1, 257, 100, 2)
torch.onnx.export(
    model, dummy_input, "frcrn_model.onnx",
    input_names=["input"], output_names=["output"],
    dynamic_axes={'input': {2: 'time'}, 'output': {2: 'time'}},
    opset_version=13
)
print("ONNX 模型导出完成!")

导出后再用 ONNX Runtime 工具做一轮优化,重点不是追求极限压缩,而是先把算子兼容性和浏览器可跑性解决掉。

浏览器端怎么接起来

项目结构不复杂,核心就是页面、推理脚本、音频处理模块和模型文件。

frcrn-wasm-demo/
├── index.html
├── style.css
├── app.js
├── wasm/
│   ├── frcrn.onnx
│   ├── ort-wasm.wasm
│   └── ort-wasm.js
├── audio-processor.js
└── package.json

音频处理这块最容易踩坑。麦克风拿到的是连续 PCM 流,模型吃的却是固定格式的频域特征,中间得做 STFT 和逆变换,还要盯住采样率、帧长和重叠关系。

// audio-processor.js
class AudioProcessor {
    constructor() {
        this.audioContext = null;
        this.processor = null;
        this.model = null;
        this.isProcessing = false;
        this.SAMPLE_RATE = 16000;
        this.FRAME_SIZE = 512;
        this.HOP_SIZE = 256;
    }

    async init() {
        try {
            this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: this.SAMPLE_RATE });
            await this.audioContext.resume();
            return true;
        } catch (error) {
            console.error('初始化音频上下文失败:', error);
            return false;
        }
    }

    async startProcessing() {
        if (this.isProcessing) return;
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
                audio: {
                    sampleRate: this.SAMPLE_RATE,
                    channelCount: 1,
                    echoCancellation: false,
                    noiseSuppression: false,
                    autoGainControl: false
                }
            });
            const source = this.audioContext.createMediaStreamSource(stream);
            this.processor = this.audioContext.createScriptProcessor(this.FRAME_SIZE, 1, 1);
            source.connect(this.processor);
            this.processor.connect(this.audioContext.destination);
            this.processor.onaudioprocess = (event) => {
                if (!this.isProcessing || !this.model) return;
                const inputData = event.inputBuffer.getChannelData(0);
                this.processAudioFrame(inputData);
            };
            this.isProcessing = true;
        } catch (error) {
            console.error('启动音频处理失败:', error);
        }
    }

    processAudioFrame(audioData) {
        const stftData = this.computeSTFT(audioData);
        const output = await this.model.run(stftData);
        const processedAudio = this.inverseSTFT(output);
        return processedAudio;
    }

    computeSTFT(audioData) { /* 实现 STFT */ }
    inverseSTFT(spectrum) { /* 实现逆 STFT */ }
}

这里有个很现实的问题:processAudioFrame 里用了 await,但函数本身不是 async,实际落地时得补上,不然代码根本跑不起来。很多示例代码都喜欢把重点写出来,边界条件留给读者自己补,这里刚好就属于这种。

ONNX Runtime Web 的接入也不复杂,模型加载后直接创建推理会话就行。

// app.js
class FRCRNApp {
    constructor() {
        this.audioProcessor = new AudioProcessor();
        this.session = null;
        this.isModelLoaded = false;
    }

    async loadModel() {
        const loadingElement = document.getElementById('loading');
        loadingElement.textContent = '正在加载模型...';
        try {
            const ort = await import('https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js');
            this.session = await ort.InferenceSession.create('./wasm/frcrn.onnx', {
                executionProviders: ['wasm'],
                graphOptimizationLevel: 'all'
            });
            this.isModelLoaded = true;
            loadingElement.textContent = '模型加载完成!';
        } catch (error) {
            console.error('加载模型失败:', error);
        }
    }

    async processWithModel(inputTensor) {
        if (!this.session || !this.isModelLoaded) throw new Error('模型未加载');
        const feeds = { input: new ort.Tensor('float32', inputTensor, [1, 257, 100, 2]) };
        const results = await this.session.run(feeds);
        return results.output.data;
    }
}

性能上主要看什么

浏览器端做实时降噪,瓶颈通常不在模型本身,而在音频流转和内存分配。前端一旦频繁 new 缓冲区,延迟会很快抖起来。

所以比较实用的做法是重用内存池,把频繁申请的数组收起来;计算部分能放到 Web Worker 就放,主线程别被 STFT 和推理一起压住;帧长和重叠率也别写死,设备性能一般时适当放宽一点,体验会更稳。

class AudioBufferPool {
    constructor() { this.buffers = new Map(); }
    getBuffer(size) {
        if (!this.buffers.has(size)) this.buffers.set(size, new Float32Array(size));
        return this.buffers.get(size);
    }
}

实际体验

测试环境里用了 MacBook Air M1、iPhone 13 和一台 Windows PC,浏览器覆盖 Chrome、Safari 和 Firefox。对比对象包括原始音频、浏览器内置降噪和常见桌面软件。

结果上,FRCRN 在持续背景噪声上压得比较干净,突发噪声也能明显收住;语音本身没有被弄得太薄,听感还算自然。M1 机型上的端到端延迟大概在 45 到 60ms,做实时通话够用,内存和 CPU 占用也没有夸张到难以接受。

不同场景里,在线会议是最直接的用法,WebRTC 配合起来比较顺;录音监听也能用,但要注意回放链路;拿来给语音识别做前置,嘈杂环境下的识别率提升会比较明显。这里真正值钱的不是'降噪很强'这句话,而是它能把门槛压到浏览器这一层。

结尾

这套方案的核心不复杂:FRCRN 负责降噪,ONNX Runtime Web 负责推理,WebAssembly 负责把它塞进浏览器里。真正要处理的,是模型格式、音频流、缓存和延迟这些细节。

如果后面继续做,我会优先看三件事:模型量化、音频同步,以及更小的前端包体。多模型组合也有空间,比如把 VAD、AEC 一起接上,但这会把调参复杂度再抬高一截,得看实际需求值不值得。

目录

  1. 在浏览器里跑 FRCRN:WebAssembly 轻量化部署实录
  2. 为什么选 FRCRN 和 WebAssembly
  3. 环境准备和模型转换
  4. 浏览器端怎么接起来
  5. 性能上主要看什么
  6. 实际体验
  7. 结尾
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Vivado AXI4-Stream Data FIFO 配置与仿真记录
  • Web 开发里的 5 种加密算法:原理与代码
  • Python 协程与异步编程实战笔记
  • Spring 国际化原理与实战:MessageSource、LocaleResolver、LocaleContextHolder、MessageFormat
  • C++ list 容器的用法与简化实现
  • Java 中 Excel 转 PDF 的几种方案与取舍
  • OpenClaw 飞书机器人部署记录
  • StyleSelectorXL:在 SDXL 里管理 77 种绘画风格
  • Python 结合 Stable Diffusion 生成企业新春营销素材实战
  • 用 Go 做一个命令行 AI 对话客户端
  • Linux 系统下 Vim 编辑器基础使用指南
  • One API 镜像部署与多模型接入实战
  • 渗透测试入门书单与章节脉络
  • 安卓本地跑 Stable Diffusion 的开源工具
  • 鸿蒙金融理财全栈:风控、合规与产品实现
  • 鸿蒙 PC 的 Linux 融合开发引擎体验
  • OpenClaw 本地 Memory 配置:Ubuntu、CUDA、llama.cpp
  • CPU 也能跑的人脸识别部署实录
  • OpenClaw 的安装、启动和联网配置
  • Flutter 应用架构从入门到可扩展的演进实践

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online