Swift使用AVAudioEngine实现音频播放BPM实时调节的方案咨询
解决方案
方案1:按时间点动态调度(最适配需求,无停顿实时修改BPM)
你当前问题的根源是把BPM对应的节拍时长做进了buffer总长度中,通过循环播放固定长度buffer的方式实现节拍间隔,所以BPM变更必须重建buffer打断播放。
该方案核心逻辑是拆分「原始短音频加载」和「节拍调度」,不要把静音补进buffer里,而是通过控制每次播放的时间点来实现节拍间隔:
- 预加载原始短音频到固定buffer,不做任何长度修改,保留原始音频的实际长度
- 不使用
.loops选项循环播放buffer,改为每次播放完成后,根据当前最新的BPM计算下一个节拍的时间点,调度下一次播放 - 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
相关产品推荐
相关产品推荐

