Web Audio API:调整音频样本(如C5.mp3)时长且不改变音高的方法
嘿,这个问题问得很到位!我来一步步给你拆解怎么用Web Audio API实现你要的功能,完全不用依赖MIDI.js这类工具,还能保持SoundFont样本的自然音质。
用Web Audio API实现音频样本的时长调整(含音高保持)
首先先区分两种场景:一种是允许音高随时长变化的简单调整,另一种是你真正需要的保持音高不变,只拉伸/缩短时长。
场景1:允许音高变化的快速调整
如果只是单纯想改时长,不在意音高跟着变,那超级简单——直接用AudioBufferSourceNode的playbackRate属性就行:
// 加载音频样本的工具函数 async function loadAudio(url) { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); const audioContext = new (window.AudioContext || window.webkitAudioContext)(); return await audioContext.decodeAudioData(arrayBuffer); } // 初始化AudioContext和加载C5样本 const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const c5Buffer = await loadAudio('C5.mp3'); // 播放并调整时长(rate < 1 变慢变长,rate > 1 变快变短) function playWithRate(rate) { const source = audioContext.createBufferSource(); source.buffer = c5Buffer; source.playbackRate.value = rate; source.connect(audioContext.destination); source.start(); } // 示例:把时长翻倍(速率设为0.5) playWithRate(0.5);
但这显然不是你要的——你要的是音高不变,只改时长,接下来重点说这个核心需求。
场景2:保持音高的时长伸缩(两种实现思路)
Web Audio API本身没有原生的“时间拉伸+音高保持”节点,但我们可以用两种方式实现,完全不依赖外部工具:
1. 离线预处理:适合固定时长需求(简单但音质一般)
如果你的需求是提前把样本拉伸到指定时长,之后再播放,这种方法最适合。核心是手动对音频样本数据做重采样(用线性插值实现简单拉伸),音质虽然不如专业算法,但足够应对短的SoundFont单音样本:
async function stretchAudioBuffer(buffer, targetDuration) { const originalDuration = buffer.duration; const stretchRatio = targetDuration / originalDuration; // 创建和原音频参数一致的离线上下文 const offlineCtx = new OfflineAudioContext( buffer.numberOfChannels, Math.floor(buffer.length * stretchRatio), buffer.sampleRate ); // 创建新的音频缓冲区存储拉伸后的数据 const newBuffer = offlineCtx.createBuffer( buffer.numberOfChannels, offlineCtx.length, buffer.sampleRate ); // 逐通道处理样本,用线性插值实现拉伸 for (let channel = 0; channel < buffer.numberOfChannels; channel++) { const originalData = buffer.getChannelData(channel); const newData = newBuffer.getChannelData(channel); for (let i = 0; i < newData.length; i++) { const originalIndex = i / stretchRatio; const floorIndex = Math.floor(originalIndex); const ceilIndex = Math.min(floorIndex + 1, originalData.length - 1); const fraction = originalIndex - floorIndex; // 线性插值计算新样本值 newData[i] = originalData[floorIndex] * (1 - fraction) + originalData[ceilIndex] * fraction; } } return newBuffer; } // 使用示例:把C5样本拉伸到3秒时长 const stretchedC5 = await stretchAudioBuffer(c5Buffer, 3); // 播放拉伸后的音频(音高完全不变) function playStretched() { const source = audioContext.createBufferSource(); source.buffer = stretchedC5; source.connect(audioContext.destination); source.start(); }
2. 实时处理:AudioWorklet实现专业拉伸(音质好,支持动态控制)
如果你想像控制振荡器那样,随时触发播放、调整时长,那就要用AudioWorklet实现时间拉伸算法(比如相位声码器),这种方法支持实时调整,音质也更好,只是代码复杂度稍高。
首先创建一个自定义的AudioWorklet处理器文件stretcher-worklet.js:
class TimeStretchProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [{ name: 'stretchRatio', defaultValue: 1.0, minValue: 0.1, maxValue: 10.0 }]; } constructor() { super(); this.inputBuffer = []; // 存储输入样本的缓冲区 } process(inputs, outputs, parameters) { const input = inputs[0]; const output = outputs[0]; const stretchRatio = parameters.stretchRatio[0]; // 逐通道处理,实现简化版的时间拉伸逻辑(完整实现需要相位声码器) for (let channel = 0; channel < input.length; channel++) { const inputData = input[channel]; const outputData = output[channel]; // 把输入样本存入缓冲区 this.inputBuffer.push(...inputData); // 按拉伸比例从缓冲区取数据输出 for (let i = 0; i < outputData.length; i++) { const targetIndex = Math.floor(i / stretchRatio); outputData[i] = targetIndex < this.inputBuffer.length ? this.inputBuffer[targetIndex] : 0; } // 清理缓冲区中已输出的旧数据 this.inputBuffer = this.inputBuffer.slice(Math.ceil(outputData.length / stretchRatio)); } return true; } } registerProcessor('time-stretch-processor', TimeStretchProcessor);
然后在主脚本中加载并使用:
// 先加载AudioWorklet模块 await audioContext.audioWorklet.addModule('stretcher-worklet.js'); // 封装成类似振荡器的Player类,支持noteOn/noteOff class SoundFontPlayer { constructor(audioContext, buffer) { this.audioContext = audioContext; this.buffer = buffer; this.source = null; this.stretcher = null; } noteOn(targetDuration) { const stretchRatio = targetDuration / this.buffer.duration; // 创建音频源和拉伸处理器 this.source = this.audioContext.createBufferSource(); this.source.buffer = this.buffer; this.stretcher = new AudioWorkletNode(this.audioContext, 'time-stretch-processor'); this.stretcher.parameters.get('stretchRatio').value = stretchRatio; // 连接节点并播放 this.source.connect(this.stretcher); this.stretcher.connect(this.audioContext.destination); this.source.start(); } noteOff() { if (this.source) { this.source.stop(); this.source = null; this.stretcher = null; } } } // 使用示例 const c5Player = new SoundFontPlayer(audioContext, c5Buffer); // 播放3秒时长的C5(音高不变) c5Player.noteOn(3); // 随时停止播放 // c5Player.noteOff();
这样就实现了和振荡器一样简便的noteOn/noteOff控制,还能指定播放时长,完全用SoundFont的自然声音,不需要任何外部工具。
总结
- 允许音高变化的话,
playbackRate是最快的方案,但不符合你的需求; - 要保持音高调整时长,要么用离线重采样(适合固定时长预处理),要么用AudioWorklet实现实时拉伸(支持动态控制,音质更好);
- 完全可以用SoundFont样本实现类似振荡器的时序控制,只要封装好播放逻辑就行。
内容的提问来源于stack exchange,提问作者ajo
相关产品推荐
相关产品推荐

