片段循环播放器的两种实现思路

1,553 阅读4分钟

给一个 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)
    }

github repo