基于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 />
问题分析与修复方案
你的代码存在几个关键问题,导致变声效果不符合预期:
MediaStreamAudioSourceNode无需调用start()
来自媒体流的音频源节点是实时流,不需要手动调用source.start(0),这行代码会抛出错误,直接删除即可。音频连接路径混乱,导致信号冲突
当前代码中delay同时连接到convolver和filter2,后续又多次连接到同一个filter和compressor,会造成音频信号叠加混乱。需要梳理连接逻辑,确保信号路径清晰。缺少核心变声模块
现有代码只有混响、延迟、滤波和背景音,没有实现真正的变声核心逻辑(比如音调偏移、波形失真)。可以添加WaveShaperNode实现失真,或者使用AudioWorklet进行更复杂的音调处理。异步资源加载时机问题
音频资源(混响脉冲响应、火焰音效)的加载是异步的,但没有等待加载完成就启动节点,可能导致部分效果不生效。需要确保所有资源加载完成后再建立连接并启动节点。
修复后的代码示例:
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
相关产品推荐
相关产品推荐

