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

Swift使用AVAudioEngine实现音频播放BPM实时调节的方案咨询

解决方案

方案1:按时间点动态调度(最适配需求,无停顿实时修改BPM)

你当前问题的根源是把BPM对应的节拍时长做进了buffer总长度中,通过循环播放固定长度buffer的方式实现节拍间隔,所以BPM变更必须重建buffer打断播放。
该方案核心逻辑是拆分「原始短音频加载」和「节拍调度」,不要把静音补进buffer里,而是通过控制每次播放的时间点来实现节拍间隔:

  1. 预加载原始短音频到固定buffer,不做任何长度修改,保留原始音频的实际长度
  2. 不使用.loops选项循环播放buffer,改为每次播放完成后,根据当前最新的BPM计算下一个节拍的时间点,调度下一次播放
  3. BPM修改时不需要做重建buffer、停止播放器的操作,下一次调度自动使用新的间隔参数

修改后参考代码:

var bpm: Float = 30
// 存储原始无静音的音频buffer
var originalTickBuffer: AVAudioPCMBuffer?
let engine = AVAudioEngine()
var player = AVAudioPlayerNode()
var audioFile: AVAudioFile?
// 存储下一次播放的基准时间
var nextPlayTime: AVAudioTime?

override func viewDidLoad() {
    super.viewDidLoad()
    audioFile = loadfile(from: "sound.wav")
    // 加载原始音频buffer,不补静音
    originalTickBuffer = loadOriginalBuffer(audioFile: audioFile!)
    
    engine.attach(player)
    engine.connect(player, to: engine.mainMixerNode, format: audioFile?.processingFormat)
    do {
        engine.prepare()
        try engine.start()
    } catch {
        print(error)
    }
}

// 加载原始无修改的音频buffer
func loadOriginalBuffer(audioFile: AVAudioFile) -> AVAudioPCMBuffer {
    let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: AVAudioFrameCount(audioFile.length))!
    try! audioFile.read(into: buffer)
    buffer.frameLength = AVAudioFrameCount(audioFile.length)
    return buffer
}

func play() {
    nextPlayTime = AVAudioTime.now()
    scheduleNextTick()
    player.play()
}

// 递归调度下一个节拍
func scheduleNextTick() {
    guard let buffer = originalTickBuffer, let sampleRate = audioFile?.processingFormat.sampleRate else { return }
    // 用当前最新的BPM计算节拍间隔(秒)
    let beatInterval = 60.0 / Double(bpm)
    // 调度当前buffer在nextPlayTime播放
    player.scheduleBuffer(buffer, at: nextPlayTime, options: []) { [weak self] in
        guard let self = self else { return }
        // 播放完成后计算下一次播放时间,递归调度
        let frameOffset = AVAudioFrameCount(beatInterval * sampleRate)
        self.nextPlayTime = self.nextPlayTime?.offset(bySamples: frameOffset)
        self.scheduleNextTick()
    }
}

// 滑块修改BPM时直接调用即可,不需要额外操作
func updateBPM(_ newBPM: Float) {
    bpm = newBPM
}

func stop() {
    player.stop()
    nextPlayTime = nil
}

private func loadfile(from fileName: String) -> AVAudioFile? {
    let path = Bundle.main.path(forResource: fileName, ofType: nil)!
    let url = URL(fileURLWithPath: path)
    do {
        let audioFile = try AVAudioFile(forReading: url)
        return audioFile
    } catch {
        print("Error loading buffer1 \(error)")
    }
    return nil
}

方案2:使用变速节点调整播放速率(适合需要音频本身随BPM变速的场景)

如果你的短音频不是短促的打击乐,需要音频本身的播放速度随BPM同步变化,可以在音频链路中插入AVAudioUnitTimePitch节点,通过调整rate参数实现变速,不需要修改buffer,也不需要停止播放,实时生效。这种方式下可以用固定长度的buffer循环播放,修改BPM时同步修改变速节点的速率参数即可。

内容的提问来源于stack exchange,提问作者sergii.tsyban

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 18:54:03