灌音与音频可视化

[复制链接]
发表于 2026-7-9 17:00:36 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣
需求配景

实现效果
现在很多AI智能体应用,会向用户开放自界说音色的功能,以便用户能使用自己的声音天生一个脚色。实现自界说音色的条件就是收罗用户自己的声音,以供后续大模子天生脚色的自界说音色。本文将先容,怎么在web中实现灌音以及音频的可视化。
实现效果如下,类似iphone的语音备忘录:

它具有以下特点:

  • 波形随着音量实时变革
  • 从右向左滚动
  • 播放时可以复现灌音波形
技能选型

怎么收罗声音

欣赏器中灌音告急有三种方案:MediaRecorder、Web Audio API、Recorder-Core
1. MediaRecorder API:是欣赏器提供的高层封装灌音接口用法如下:
  1. const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  2. const recorder = new MediaRecorder(stream);
  3. recorder.ondataavailable = (e) => {
  4.   const blob = e.data; // 音频文件
  5. };
  6. 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️⃣ 初始化灌音器
  1. function createRecorder() {
  2.     return RecorderCore({
  3.         type: "wav",
  4.         // 采样率
  5.         sampleRate: 44100,
  6.         onProcess(buffers, powerLevel, duration, sampleRate, newBufferIdx) {
  7.             ...
  8.         },
  9.     });
  10. }
  11. recorder = createRecorder();
复制代码

  • type: "wav" → 输出 wav 文件
  • onProcess: 实时拿到 PCM(振幅) 数据,以供后续绘制图形
2️⃣ 音频数据处置处罚

在这一步中,原始收罗到的音频数据在[-32768, 32767]区间,去掉正负值,并转换成[0, 1]区间的值。
  1. const latest = buffers[newBufferIdx] || buffers[buffers.length - 1];
  2. // 将原始 PCM 采样转换成 0 到 1 之间的能量值,供可视化使用。
  3. function calcEnergyFromPCM(pcm: Int16Array): number {
  4.     if (!pcm || pcm.length === 0) return 0;
  5.     let sum = 0;
  6.     for (let i = 0; i < pcm.length; i++) {
  7.         sum += Math.abs(pcm[i]);
  8.     }
  9.     return Math.min(1, sum / pcm.length / 32768);
  10. }
复制代码
3️⃣ 实时数据收罗


  • liveEnergyQueue,用来存实时展示所需的数据
  • energyTimeline,用来存全量数据,播放回放时使用
 
  1. const energy = calcEnergyFromPCM(latest);
  2. // 实时显示
  3. liveEnergyQueue.push(energy);
  4. // 播放回放
  5. energyTimeline.push(energy);
复制代码
4️⃣ 灌音控制

开始灌音
  1. // 开始录音
  2. await recorder.open(
  3.     () => resolve(),
  4.     (msg: string, _isUserNotAllow: boolean) => reject(new Error(msg)),
  5. );
  6. recorder.start();
复制代码
竣事灌音
 
  1. recorder.stop((blob, duration) => {
  2.     wavBlob.value = blob;
  3. });
复制代码
注意:开始灌音会哀求麦克风权限,只有https和localhost能获取,其他情况会报错
音频可视化

1️⃣ 核心数据准备
  1. const liveEnergyQueue: number[] = []; // 实时队列
  2. const energyTimeline: number[] = [];  // 全量数据(播放用)
  3. const visualBuffer: number[] = [];    // 当前屏幕显示
复制代码
2️⃣ 动画驱动

灌音、回放都同一使用startVisual实现绘制,内部通过requestAnimationFrame绘制每一帧动画
  1. function startVisual(mode) {
  2.     function draw() {
  3.         ...
  4.         animationId = requestAnimationFrame(draw);
  5.     }
  6.     draw();
  7. }
复制代码
3️⃣ 滚动窗口

两种模式energy的取值方式不一样,终极都是到场visualBuffer,visualBuffer会维护最大窗口,超出窗口就不再展示
  1. if (mode === "record") {
  2.     energy = liveEnergyQueue.shift();
  3. } else {
  4.     energy = energyTimeline[playIndex++];
  5. }
  6. visualBuffer.push(energy);
  7. if (visualBuffer.length > MAX_COLUMNS) {
  8.     visualBuffer.shift();
  9. }
复制代码
4️⃣ 绘制柱状图

从右往左绘制柱状图
  1. const startX = width - barWidth;
  2. for (let i = visualBuffer.length - 1, col = 0; i >= 0; i--, col++) {
  3.     const x = startX - col * step;
  4.     if (x + barWidth < 0) break;
  5.     const e = visualBuffer[i];
  6.     const barHeight = Math.min(
  7.         maxBarHeight,
  8.         Math.max(minBarHeight, e * height * VISUAL_HEIGHT_GAIN),
  9.     );
  10.     const y = (height - barHeight) / 2;
  11.     ctx.fillStyle = "#ff3b30";
  12.     ctx.fillRect(x, y, barWidth, barHeight);
  13. }
复制代码
5️⃣ Canvas高清适配

当代高清体现屏的dpr通常会大于1,也就是说现实的物理像素会比css像素更大,以dpr=2,css像素200x200为例,欣赏器现实的物理像素时400x400,也就是会拉伸canvas尺寸,把一个200x200尺寸画布上绘制的图形,渲染在400x400的画布上,图形就轻易暗昧,因此须要对canvas根据dpr举行团体缩放
  1. const dpr = window.devicePixelRatio || 1;
  2. const rect = canvas.getBoundingClientRect();
  3. canvas.width = Math.max(1, Math.floor(rect.width * dpr));
  4. canvas.height = Math.max(1, Math.floor(rect.height * dpr));
  5. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
复制代码
  
假如对您有所资助,接待您点个关注,我会定时更新技能文档,各人一起讨论学习,一起进步。


回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表