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

如何解决如何在不使用太多内存的情况下播放循环压缩音轨?

现在我正在使用 AVAudioEngine 和 AVAudioPlayer、AVAudioFile、AVAudioPCMBuffer 来播放一对压缩音轨 (m4a)。我的问题是,如果当我将声音加载到缓冲区中时,原声带未压缩为 40MB,而在 m4a 中为 1.8,则内存使用量会增加 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])

解决方法

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

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

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)
        }
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res