You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Web Audio API实现实时变声器遇到问题求助

问题描述

我正尝试基于Web Audio API实现实时变声器,试过多种方法但均未得到预期效果。我参照MDN教程编写代码,没有使用bufferSource,而是通过createMediaStreamSource从流创建音频源,并将其传入demonBeastTransform方法。以下是我的最终代码:

JavaScript代码

const video = document.querySelector("video");
const range = document.querySelector("#gain");


navigator.mediaDevices
  .getUserMedia({
    audio: true,
    video: true
  })
  .then((stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
      video.muted = true;
    };

    // 创建MediaStreamAudioSourceNode
    // 将HTMLMediaElement传入其中
    const audioCtx = new AudioContext();
    const source = audioCtx.createMediaStreamSource(stream);

    demonBeastTransform(source, audioCtx)

  })
  .catch((err) => {
    console.error(`发生以下错误: ${err}`);
  });

async function demonBeastTransform(source, ctx, distortionAmount = 100) {

  /*  let ctx = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate); */

  // 源
  /*       let source = ctx.createBufferSource();
        source.buffer = audioBuffer; */

  // 混响
  let convolver = ctx.createConvolver();
  convolver.buffer = await ctx.decodeAudioData(await (await fetch("https://voicechanger.io/audio/impulse-responses/voxengo/Large Wide Echo Hall.wav")).arrayBuffer());


  // 火焰音效
  let fire = ctx.createBufferSource();
  fire.buffer = await ctx.decodeAudioData(await (await fetch("https://voicechanger.io/audio/backgrounds/brush_fire-Stephan_Schutze-55390065.mp3")).arrayBuffer());
  fire.loop = true;

  // 压缩器
  let compressor = ctx.createDynamicsCompressor();
  compressor.threshold.value = -50;
  compressor.ratio.value = 16;

  // 颤音效果
  let oscillator = ctx.createOscillator();
  oscillator.frequency.value = 50;
  oscillator.type = 'sawtooth';
  // ---
  let oscillatorGain = ctx.createGain();
  oscillatorGain.gain.value = 0.004;
  // ---
  let delay = ctx.createDelay();
  delay.delayTime.value = 0.01;
  // ---
  let fireGain = ctx.createGain();
  fireGain.gain.value = 0.2;
  // ---
  let convolverGain = ctx.createGain();
  convolverGain.gain.value = 2;

  // 滤波器
  let filter = ctx.createBiquadFilter();
  filter.type = "highshelf";
  filter.frequency.value = 1000;
  filter.gain.value = 10;

  // 创建音频连接图
  oscillator.connect(oscillatorGain);
  oscillatorGain.connect(delay.delayTime);
  // ---
  source.connect(delay)
  delay.connect(convolver);
  //waveShaper.connect(convolver);

  fire.connect(fireGain);
  convolver.connect(convolverGain);

  convolverGain.connect(filter);
  filter.connect(compressor);

  fireGain.connect(ctx.destination);

  compressor.connect(ctx.destination);

  let filter2 = ctx.createBiquadFilter();
  filter2.type = "lowpass";
  filter2.frequency.value = 2000;
  let noConvGain = ctx.createGain();
  noConvGain.gain.value = 0.9;
  delay.connect(filter2);
  filter2.connect(filter);
  filter.connect(noConvGain);
  noConvGain.connect(compressor);

  // 启动节点
  oscillator.start(0);
  source.start(0);
  fire.start(0);
  /*   let outputAudioBuffer = await ctx.startRendering();
    return outputAudioBuffer; */

}

HTML代码

<h1>Web Audio API 示例: MediaStreamAudioSourceNode</h1>
<video controls></video>
<br />

问题分析与修复方案

你的代码存在几个关键问题,导致变声效果不符合预期:

  1. MediaStreamAudioSourceNode无需调用start()
    来自媒体流的音频源节点是实时流,不需要手动调用source.start(0),这行代码会抛出错误,直接删除即可。

  2. 音频连接路径混乱,导致信号冲突
    当前代码中delay同时连接到convolver和filter2,后续又多次连接到同一个filter和compressor,会造成音频信号叠加混乱。需要梳理连接逻辑,确保信号路径清晰。

  3. 缺少核心变声模块
    现有代码只有混响、延迟、滤波和背景音,没有实现真正的变声核心逻辑(比如音调偏移、波形失真)。可以添加WaveShaperNode实现失真,或者使用AudioWorklet进行更复杂的音调处理。

  4. 异步资源加载时机问题
    音频资源(混响脉冲响应、火焰音效)的加载是异步的,但没有等待加载完成就启动节点,可能导致部分效果不生效。需要确保所有资源加载完成后再建立连接并启动节点。

修复后的代码示例:

const video = document.querySelector("video");

navigator.mediaDevices
  .getUserMedia({ audio: true, video: true })
  .then(async (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
      video.muted = true;
    };

    const audioCtx = new AudioContext();
    const source = audioCtx.createMediaStreamSource(stream);

    await demonBeastTransform(source, audioCtx);
  })
  .catch((err) => {
    console.error(`发生以下错误: ${err}`);
  });

async function demonBeastTransform(source, ctx) {
  // 加载所有音频资源
  const [reverbBuffer, fireBuffer] = await Promise.all([
    fetch("https://voicechanger.io/audio/impulse-responses/voxengo/Large Wide Echo Hall.wav")
      .then(res => res.arrayBuffer())
      .then(buf => ctx.decodeAudioData(buf)),
    fetch("https://voicechanger.io/audio/backgrounds/brush_fire-Stephan_Schutze-55390065.mp3")
      .then(res => res.arrayBuffer())
      .then(buf => ctx.decodeAudioData(buf))
  ]);

  // 混响节点
  const convolver = ctx.createConvolver();
  convolver.buffer = reverbBuffer;
  const convolverGain = ctx.createGain();
  convolverGain.gain.value = 1.5;

  // 火焰背景音
  const fire = ctx.createBufferSource();
  fire.buffer = fireBuffer;
  fire.loop = true;
  const fireGain = ctx.createGain();
  fireGain.gain.value = 0.15;

  // 压缩器
  const compressor = ctx.createDynamicsCompressor();
  compressor.threshold.value = -45;
  compressor.ratio.value = 12;

  // 颤音延迟效果
  const oscillator = ctx.createOscillator();
  oscillator.frequency.value = 40;
  oscillator.type = 'sawtooth';
  const oscillatorGain = ctx.createGain();
  oscillatorGain.gain.value = 0.003;
  const delay = ctx.createDelay();
  delay.delayTime.value = 0.01;

  // 滤波器组
  const highShelfFilter = ctx.createBiquadFilter();
  highShelfFilter.type = "highshelf";
  highShelfFilter.frequency.value = 1200;
  highShelfFilter.gain.value = 8;

  const lowPassFilter = ctx.createBiquadFilter();
  lowPassFilter.type = "lowpass";
  lowPassFilter.frequency.value = 1800;
  const lowPassGain = ctx.createGain();
  lowPassGain.gain.value = 0.8;

  // 核心变声:波形失真
  const distortion = ctx.createWaveShaper();
  distortion.curve = makeDistortionCurve(400); // 调整失真程度
  distortion.oversample = '4x';
  const distortionGain = ctx.createGain();
  distortionGain.gain.value = 0.7;

  // 构建清晰的音频路径
  // 颤音控制延迟时间
  oscillator.connect(oscillatorGain);
  oscillatorGain.connect(delay.delayTime);

  // 人声路径1:延迟+失真+混响+滤波
  source.connect(delay);
  delay.connect(distortion);
  distortion.connect(distortionGain);
  distortionGain.connect(convolver);
  convolver.connect(convolverGain);
  convolverGain.connect(highShelfFilter);

  // 人声路径2:延迟+低通
  delay.connect(lowPassFilter);
  lowPassFilter.connect(lowPassGain);
  lowPassGain.connect(highShelfFilter);

  // 最终输出
  highShelfFilter.connect(compressor);
  compressor.connect(ctx.destination);

  // 背景音输出
  fire.connect(fireGain);
  fireGain.connect(ctx.destination);

  // 启动节点
  oscillator.start();
  fire.start();
}

// 生成失真曲线函数
function makeDistortionCurve(amount) {
  const samples = 44100;
  const curve = new Float32Array(samples);
  const deg = Math.PI / 180;
  for (let i = 0; i < samples; i++) {
    const x = (i * 2) / samples - 1;
    curve[i] = ((3 + amount) * x * 20 * deg) / (Math.PI + amount * Math.abs(x));
  }
  return curve;
}

关键改进点说明

  • 移除了错误的source.start()调用
  • 使用Promise.all()确保所有音频资源加载完成后再构建音频图
  • 添加了WaveShaperNode实现核心失真变声效果
  • 梳理了音频连接路径,避免信号冲突
  • 调整了各节点参数,优化变声效果和背景音平衡

内容的提问来源于stack exchange,提问作者Baloylu_unzi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 18:18:27