录音与音频可视化

需求背景

实现效果

现在很多AI智能体应用,会向用户开放自定义音色的功能,以便用户能使用自己的声音生成一个角色。实现自定义音色的前提就是采集用户自己的声音,以供后续大模型生成角色的自定义音色。本文将介绍,怎么在web中实现录音以及音频的可视化。

实现效果如下,类似iphone的语音备忘录:

它具有以下特点:

  • 波形随着音量实时变化
  • 从右向左滚动
  • 播放时可以复现录音波形

技术选型

怎么采集声音

浏览器中录音主要有三种方案:MediaRecorder、Web Audio API、Recorder-Core
1. MediaRecorder API:是浏览器提供的高层封装录音接口 用法如下:

js 复制代码
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

const recorder = new MediaRecorder(stream);

recorder.ondataavailable = (e) => {
  const blob = e.data; // 音频文件
};

recorder.start();

这是浏览器原生支持的API,简单高效实现录音,但是只能拿到编码后的blob,无法获取原始的振幅数据,也就无从实现实时可视化了。

2. Web Audio API 这是浏览器提供的音频处理引擎,只提供原始的振幅数据,可以实现音频可视化。可是,这套API不做任何封装,需要自行处理buffer 拼接、多通道处理、文件头封装等问题,实现过于复杂,因此不做考虑

3. Recorder-Core 这个库基于Web Audio API,实现了以下功能:

  • 实时回调输出振幅数据 (实现音频实时可视化的关键)
  • WAV编码
  • 生命周期封装 大大简化了音频采集的实现,并且具有良好的浏览器兼容性,在PC + 主流移动浏览器上都能使用,因此,最终选择recorder-core来实现音频采集

怎么实现音频的实时可视化

高频更新的动画,使用DOM实现会频繁触发回流、重绘,会造成显著的性能浪费,而这种场景使用canvas再合适不过了,用一张独立布局的画布,承接频繁更新的动画

因此,我们选择Recorder-Core + Canvas来实现本次需求。

核心流程

录音与采集音频数据

1️⃣ 初始化录音器

js 复制代码
function createRecorder() {
    return RecorderCore({
        type: "wav",
        // 采样率
        sampleRate: 44100,
        onProcess(buffers, powerLevel, duration, sampleRate, newBufferIdx) {
            ...
        },
    });
}

recorder = createRecorder();
  • type: "wav" → 输出 wav 文件
  • onProcess: 实时拿到 PCM(振幅) 数据,以供后续绘制图形

2️⃣ 音频数据处理

在这一步中,原始采集到的音频数据在-32768, 32767区间,去掉正负值,并转换成0, 1区间的值。

js 复制代码
const latest = buffers[newBufferIdx] || buffers[buffers.length - 1];

// 将原始 PCM 采样转换成 0 到 1 之间的能量值,供可视化使用。
function calcEnergyFromPCM(pcm: Int16Array): number {
    if (!pcm || pcm.length === 0) return 0;
    let sum = 0;
    for (let i = 0; i < pcm.length; i++) {
        sum += Math.abs(pcm[i]);
    }
    return Math.min(1, sum / pcm.length / 32768);
}

3️⃣ 实时数据采集

  • liveEnergyQueue,用来存实时展示所需的数据
  • energyTimeline,用来存全量数据,播放回放时使用
js 复制代码
const energy = calcEnergyFromPCM(latest);

// 实时显示
liveEnergyQueue.push(energy);
// 播放回放
energyTimeline.push(energy);

4️⃣ 录音控制

开始录音

js 复制代码
// 开始录音
await recorder.open(
    () => resolve(),
    (msg: string, _isUserNotAllow: boolean) => reject(new Error(msg)),
);

recorder.start();

结束录音

js 复制代码
recorder.stop((blob, duration) => {
    wavBlob.value = blob;
});

注意:开始录音会请求麦克风权限,只有https和localhost能获取,其他环境会报错

音频可视化

1️⃣ 核心数据准备

js 复制代码
const liveEnergyQueue: number[] = []; // 实时队列
const energyTimeline: number[] = [];  // 全量数据(播放用)
const visualBuffer: number[] = [];    // 当前屏幕显示

2️⃣ 动画驱动

录音、回放都统一使用startVisual实现绘制,内部通过requestAnimationFrame绘制每一帧动画

js 复制代码
function startVisual(mode) {
    function draw() {
        ...
        animationId = requestAnimationFrame(draw);
    }
    draw();
}

3️⃣ 滚动窗口

两种模式energy的取值方式不一样,最终都是加入visualBuffer,visualBuffer会维护最大窗口,超出窗口就不再展示

js 复制代码
if (mode === "record") {
    energy = liveEnergyQueue.shift();
} else {
    energy = energyTimeline[playIndex++];
}

visualBuffer.push(energy);

if (visualBuffer.length > MAX_COLUMNS) {
    visualBuffer.shift();
}

4️⃣ 绘制柱状图

从右往左绘制柱状图

js 复制代码
const startX = width - barWidth;

for (let i = visualBuffer.length - 1, col = 0; i >= 0; i--, col++) {
    const x = startX - col * step;
    if (x + barWidth < 0) break;

    const e = visualBuffer[i];
    const barHeight = Math.min(
        maxBarHeight,
        Math.max(minBarHeight, e * height * VISUAL_HEIGHT_GAIN),
    );
    const y = (height - barHeight) / 2;
    ctx.fillStyle = "#ff3b30";
    ctx.fillRect(x, y, barWidth, barHeight);
}

5️⃣ Canvas高清适配

现代高清显示屏的dpr通常会大于1,也就是说实际的物理像素会比css像素更大,以dpr=2,css像素200x200为例,浏览器实际的物理像素时400x400,也就是会拉伸canvas尺寸,把一个200x200尺寸画布上绘制的图形,渲染在400x400的画布上,图形就容易模糊,因此需要对canvas根据dpr进行整体缩放

js 复制代码
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
相关推荐
胡萝卜术7 小时前
从API调用到手写LRU:力扣146「LRU缓存」的哈希表+双向链表终极进化之路
前端·javascript·面试
科技道人8 小时前
记录 默认置灰/禁用 app ‘Search Engine Selector‘ 的disable按钮
开发语言·前端·javascript
天疆说8 小时前
Zotero Connector 在 Edge 里找不到桌面 Zotero(Linux / Zotero 9.0.6 / Edge 150)
linux·前端·edge
李伟_Li慢慢9 小时前
02-从硬件说起WebGL
前端·three.js
kyriewen10 小时前
看了微软几万人用AI编程的数据——效率涨24%的代价没人提
前端·ai编程·claude
2601_9637713710 小时前
How to Scale Your WordPress Store Traffic in 2026
前端·php·plugin
亿元程序员10 小时前
老板说我的3D箭头游戏用来做试玩太普通了,没人想玩,让我变点花样...
前端
李伟_Li慢慢11 小时前
01-threejs架构原理-课程简介
前端·three.js
_瑞12 小时前
深入理解 iOS 渲染原理
前端·ios
IT_陈寒12 小时前
SpringBoot自动配置失灵?你可能忘了这个关键注解
前端·人工智能·后端