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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 07:00:14