给一个 mp3 音频资源,循环播放其中的一个片段
本文说一下,两种实现的思路
第一种,主要用 seek 方法
把整个音频资源分配给播放器,跳转到片段开始的地方,就是 seek 过去,
正常播放到片段结尾,再 seek 到片段开头
面临的挑战: seek 会有杂音
每一次 seek ,就是先调用 AVAudioPlayerNode 的 stop 方法,
把分配 AVAudioPlayerNode 的音频缓冲资源清空,
再把分配音频资源的指针,放到片段开始的地方,重新给 AVAudioPlayerNode 分配需要的音频资源缓冲
最后调用 AVAudioPlayerNode 的 play 方法,又播放起来
挑战的普通解决方法:
关闭 AVAudioPlayerNode 之前,把音量调为 0,就是修改 engine.mainMixerNode.outputVolume
又开始播放了, 把 AVAudioPlayerNode 的音量恢复正常
第 2 种,把分配音频缓冲的角度
把需要的片段音频缓冲,从音频文件中抽离,分配需要的次数给 AVAudioPlayerNode
无杂音
( 上一个,播放到片段结尾,一 seek , 就出杂音 )
具体实现
1,seek 方法途径
控制的数据结构,
-
总共需要重复多少遍, countStdRepeat
-
当前重复了几次,howManyNow
片段播放完后,需要停顿,
-
循环播放多次,停顿 1 次后,可以播放下一个片段了,toClimb
-
音频片段有很多,记录当前的片段序号 currentX
判断当前片段播放完了没有,拿当前的播放时间,与该片段的结束时间比较,
用 currentX,找到该片段的结束时间
-
pauseWork, 每一个片段,循环播放完成后,需要停顿一次
-
currentMoment, 记录当前的暂停时刻,过了需要的暂定时间 stdPauseT,继续播放
-
stdPauseT,一轮播放完成后,的暂停持续时间
struct AudioRecord{
// 重复
var countStdRepeat = 0
var howManyNow = 0
var toClimb = true
var currentX = 0
// pause , 停顿
var pauseWork = false
var currentMoment = Date()
var stdPauseT: TimeInterval = BottomPopData.interval[3]
mutating
func doPause(at index: Int){
// 停顿之后,播放时间,与下一个片段的结束时间比较
currentX = index + 1
// 停顿之后,可以播放下一个片段了
toClimb = true
// 当前循环次数 0
howManyNow = 0
// 记录当前时刻,过了规定的时间,继续播放
currentMoment = Date()
// 当前需要暂停
pauseWork = true
}
}
对应的播放代码
func scheduleNextBuffer() {
guard let reader = reader_ha else {
os_log("No reader yet...", log: Streamer.logger, type: .debug)
return
}
guard repeatControl.pauseWork == false else {
// 暂停的逻辑,恢复播放
if firstPause == false, Date().timeIntervalSince(repeatControl.currentMoment) >= repeatControl.stdPauseT{
playS()
repeatControl.pauseWork = false
}
return
}
var shouldReturn = false
let i = repeatControl.currentX
let count = timeNode.count
if repeatControl.toClimb{
// 循环的逻辑
if repeatControl.howManyNow < repeatControl.countStdRepeat{
if i < count, currentTime > timeNode[i]{
repeatControl.howManyNow += 1
if i == 0{
try? seek(to: 0)
}
else{
try? seek(to: timeNode[i - 1])
}
shouldReturn = true
}
}
else{
// 继续去,下一个片段
repeatControl.toClimb = false
}
}
else {
// 暂停的逻辑,开始暂停
if i < count, currentTime > timeNode[i]{
repeatControl.doPause(at: i)
pauseS()
shouldReturn = true
}
}
guard shouldReturn == false else {
return
}
// 文件,读完了,就不要再继续调度了
guard !isFileSchedulingComplete else {
return
}
// 前面是调度资源的逻辑
// 下面是,简单的分配音频资源
do {
let nextScheduledBuffer = try reader.read(readBufferSize)
// 这个方法,很有意思,timer 给他塞的 buffer, 比他自己消费的速度, 快多了
playerNode.scheduleBuffer(nextScheduledBuffer)
} catch ReaderError.reachedEndOfFile {
os_log("Scheduler reached end of file", log: Streamer.logger, type: .debug)
isFileSchedulingComplete = true
} catch {
os_log("Cannot schedule buffer: %@", log: Streamer.logger, type: .debug, error.localizedDescription)
}
}
2,分配缓冲的方案
public var sourceURL: URL? {
didSet {
// ...
var file: AVAudioFile?
do {
file = try AVAudioFile(forReading: url)
} catch {
print(error)
}
guard let f = file else {
return
}
let buffer = try! AVAudioPCMBuffer(file: f)
// 抽取需要的音频资源缓冲
guard let piece = buffer?.extract(from: timeNode[1], to: timeNode[2]) else { return }
// 循环 40 次,配置 40 个
// 不采用数组,用个指针 index 记录,应该也行
dataSource.append(contentsOf: [AVAudioPCMBuffer](repeating: piece, count: 40))
isReady = true
}
}
// 分配资源很简单
func scheduleNextBuffer() {
guard isReady, dataSource.count > 0 else {
return
}
let nextScheduledBuffer = dataSource[0]
playerNode.scheduleBuffer(nextScheduledBuffer)
dataSource.removeFirst()
}
其中,抽取 extract buffer, 主要用到了拷贝 buffer
@discardableResult public func copy(from buffer: AVAudioPCMBuffer,
readOffset: AVAudioFrameCount = 0,
frames: AVAudioFrameCount = 0) -> AVAudioFrameCount {
let remainingCapacity = frameCapacity - frameLength
if remainingCapacity == 0 {
print("AVAudioBuffer copy(from) - no capacity!")
return 0
}
if format != buffer.format {
print("AVAudioBuffer copy(from) - formats must match!")
return 0
}
let totalFrames = Int(min(min(frames == 0 ? buffer.frameLength : frames, remainingCapacity),
buffer.frameLength - readOffset))
if totalFrames <= 0 {
print("AVAudioBuffer copy(from) - No frames to copy!")
return 0
}
let frameSize = Int(format.streamDescription.pointee.mBytesPerFrame)
// 前面的代码,计算开始位置,和长度 length
// 确保开始位置安全,
// 确保长度安全
// 剩下的,就是填充数据
// 主要用到 memcpy,简单理解,就是 assign , =
// 对于内存里面的二进制数据,从 src 中掏出来,塞进去到 dest, 不能简单用 =
if let src = buffer.floatChannelData,
let dst = floatChannelData {
for channel in 0 ..< Int(format.channelCount) {
memcpy(dst[channel] + Int(frameLength), src[channel] + Int(readOffset), totalFrames * frameSize)
}
} else {
// ...
}
frameLength += AVAudioFrameCount(totalFrames)
return AVAudioFrameCount(totalFrames)
}