如何在不使用太多内存的情况下播放循环压缩的配乐?

Posted

技术标签:

【中文标题】如何在不使用太多内存的情况下播放循环压缩的配乐?【英文标题】:How to play looping compressed soundtrack without using to much ram? 【发布时间】:2021-01-23 23:17:43 【问题描述】:

现在我正在使用 AVAudioEngine,与 AVAudioPlayer、AVAudioFile、AVAudioPCMBuffer 一起播放压缩音轨 (m4a)。我的问题是,如果当我在缓冲区中加载声音时,原声带是 40MB 未压缩和 1.8 在 m4a 中,内存使用量会跳跃 40MB(文件的未压缩大小)。如何优化它以使用尽可能少的内存?

谢谢。

let loopingBuffer : AVAudioPCMBuffer!
do let loopingFile = try AVAudioFile(forReading: fileURL)
    loopingBuffer = AVAudioPCMBuffer(pcmFormat: loopingFile.processingFormat, frameCapacity: UInt32(loopingFile.length))!
    do 
        try loopingFile.read(into: loopingBuffer)
     catch
    
        print(error)
    
 catch

    print(error)

// player is AVAudioPlayerNode
player.scheduleBuffer(loopingBuffer, at: nil, options: [.loops])

【问题讨论】:

【参考方案1】:

好吧,作为一种解决方法,我决定创建一个包装器,将音频分割成几秒钟的块,并同时播放和缓冲它们到 AVAudioPlayerNode 中。 因此,任何时候只有几秒钟的 RAM(缓冲时的两倍)。 它使我的用例的内存使用量从 350Mo 减少到不到 50Mo。

这里是代码,不要犹豫使用它或改进它(它是第一个版本)。欢迎任何cmets!

import Foundation
import AVFoundation

public class AVAudiostreamPCMPlayerWrapper

    public var player: AVAudioPlayerNode
    public let audioFile: AVAudioFile
    public let bufferSize: TimeInterval
    public let url: URL
    public private(set) var loopingCount: Int = 0
    /// Equal to the repeatingTimes passed in the initialiser.
    public let numberOfLoops: Int
    /// The time passed in the initialisation parameter for which the player will preload the next buffer to have a smooth transition.
    /// The default value is 1s.
    /// Note : better not go under 1s since the buffering mecanism can be triggered with a relative precision.
    public let preloadTime: TimeInterval

    public private(set) var scheduled: Bool = false

    private let framePerBuffer: AVAudioFrameCount
    /// To identify the the schedule cycle we are executed
    /// Since the thread work can't be stopped when they are scheduled
    /// we need to be sure that the execution of the work is done for the current playing cycle.
    /// For exemple if the player has been stopped and restart before the async call has executed.
    private var scheduledId: Int = 0
    /// the time since the track started.
    private var startingDate: Date = Date()
    /// The date used to measure the difference between the moment the buffering should have occure and the actual moment it did.
    /// Hence, we can adjust the next trigger of the buffering time to prevent the delay to accumulate.
    private var lastBufferingDate = Date()

    /// This class allow us to play a sound, once or multiple time without overloading the RAM.
    /// Instead of loading the full sound into memory it only reads a segment of it at a time, preloading the next segment to avoid stutter.
    /// - Parameters:
    ///   - url: The URL of the sound to be played.
    ///   - bufferSize: The size of the segment of the sound being played. Must be greater than preloadTime.
    ///   - repeatingTimes: How many time the sound must loop (0 it's played only once 1 it's played twice : repeating once)
    ///                     -1 repeating indéfinitly.
    ///   - preloadTime: 1 should be the minimum value since the preloading mecanism can be triggered not precesily on time.
    /// - Throws: Throws the error the AVAudioFile would throw if it couldn't be created with the URL passed in parameter.
    public init(url: URL, bufferSize: TimeInterval, isLooping: Bool, repeatingTimes: Int = -1, preloadTime: TimeInterval = 1)throws
    
        self.url = url
        self.player = AVAudioPlayerNode()
        self.bufferSize = bufferSize
        self.numberOfLoops = repeatingTimes
        self.preloadTime = preloadTime
        try self.audioFile = AVAudioFile(forReading: url)

        framePerBuffer = AVAudioFrameCount(audioFile.fileFormat.sampleRate*bufferSize)
    

    public func scheduleBuffer()
    
        scheduled = true
        scheduledId += 1
        scheduleNextBuffer(offset: preloadTime)
    

    public func play()
    
        player.play()
        startingDate = Date()
        scheduleNextBuffer(offset: preloadTime)
    

    public func stop()
    
        reset()
        scheduleBuffer()
    

    public func reset()
    
        player.stop()
        player.reset()
        scheduled = false
        audioFile.framePosition = 0
    


    /// The first time this method is called the timer is offset by the preload time, then since the timer is repeating and has already been offset
    /// we don't need to offset it again the second call.
    private func scheduleNextBuffer(offset: TimeInterval)
    
        guard scheduled else return
        if audioFile.length == audioFile.framePosition
        
            guard numberOfLoops == -1 || loopingCount < numberOfLoops else return
            audioFile.framePosition = 0
            loopingCount += 1
        

        let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: framePerBuffer)!
        let frameCount = min(framePerBuffer, AVAudioFrameCount(audioFile.length - audioFile.framePosition))
        print("\(audioFile.framePosition/48000) \(url.relativeString)")
        do
        
            try audioFile.read(into: buffer, frameCount: frameCount)

            DispatchQueue.global().async(group: nil, qos: DispatchQoS.userInteractive, flags: .enforceQoS)  [weak self] in
                self?.player.scheduleBuffer(buffer, at: nil, options: .interruptsAtLoop)
                self?.player.prepare(withFrameCount: frameCount)
            

            let nextCallTime = max(TimeInterval( Double(frameCount) / audioFile.fileFormat.sampleRate) - offset, 0)
            planNextPreloading(nextCallTime: nextCallTime)
         catch
        
            print("audio file read error : \(error)")
        
    

    private func planNextPreloading(nextCallTime: TimeInterval)
    
        guard self.player.isPlaying else return

        let id = scheduledId
        lastBufferingDate = Date()
        DispatchQueue.global().asyncAfter(deadline: .now() + nextCallTime, qos: DispatchQoS.userInteractive)  [weak self] in
            guard let self = self else return
            guard id == self.scheduledId else return

            let delta = -(nextCallTime + self.lastBufferingDate.timeIntervalSinceNow)
            self.scheduleNextBuffer(offset: delta)
        
    

【讨论】:

以上是关于如何在不使用太多内存的情况下播放循环压缩的配乐?的主要内容,如果未能解决你的问题,请参考以下文章

Qt:如何在不阻塞主线程的情况下播放声音?

如何在不重复单词并遍历整个数组的情况下从数组中随机播放文本? (迅速)

如何在不提取Java的情况下读取压缩文件的内容

如何在不使用 Ruby 保存到磁盘的情况下生成 zip 文件?

如何在不使用第三方程序的情况下重新映射我的 OSX 键盘?

如何在不使用 Qt 内部头文件的情况下压缩 QEvents?