2

我有使用 AVAudioEngine 播放音频的 iOS 应用程序。我将 AudioFile 中的帧读入 AVAudioPCMBuffer,然后将缓冲区发布到 AVAudioPlayerNode.scheduleBuffer。我在 DispatchQueue 上阅读了帧。我遇到的问题是,当应用程序有很多其他工作要做 - 与服务器同步和下载文件时,DispatchQueue 上的读取帧会延迟几秒钟执行,这会导致声音中断。我已将 DispatchQueueQos 设置为尽可能高的 .userInteractive,并且我在应用程序中没有其他具有相同 qos 的队列,但有时仍需要几秒钟才能在该队列上执行代码。

有什么解决办法吗?某种方式告诉操作系统这个队列是用于音频或类似的?

注意:如果应用程序运行一些繁重的后台操作,我对 AVPlayer 没有这个问题。

编辑:一些代码可以更好地理解。问题是有时需要几秒钟才能在队列上发布任务(从评论 1 到评论 2)。

var audioProcessingQueue = DispatchQueue(label: "audioProcessing", qos: .userInteractive)
var player = AVAudioPlayerNode()
//comment 1
self.audioProcessingQueue.async(flags: .barrier) {
    //comment 2
    //some buffer processing here...
    player.scheduleBuffer(buffer, at: nil)
}

谢谢

4

1 回答 1

0

两件事情:

1)您提供的代码安排在哪个队列上?因为它听起来像那个队列可能会与你的下载任务共享它的 qos。调度到另一个队列确实需要多个步骤。因此,仅仅因为传递了“comment1”,其他队列之一可能会在您调度时获得线程时间来做一些工作。这会导致一些延迟,是的,这似乎是最可能的原因。

2) WWDC 2016 视频Concurrent Programming with GCD in Swift 3可能会有所帮助。如果 cpu 的所有核心都已忙于其他任务,则将新内容分派到更高优先级的队列不会立即自动启动任务。GCD 会尽力而为,但也许您在那些其他队列上的任务非常紧张/正在做中断会导致问题的事情。

因此,如果您有必须立即启动的内容,请确保一个 CPU 内核/线程处于空闲状态。我会尝试使用OperationQueue并设置它的maxConcurrentOperationCount. 然后将所有下载操作放到该操作队列中,让核心/线程准备好启动音频。

但是我认为选项 2 在这里无效,因为您说它有时需要几秒钟。

更新

正如您所说,您提供的代码在一个也用于您的下载任务的队列上运行,这主要是一个错误的架构设计,我很遗憾地说,如果没有看到您的全部内容,任何人都无法提出解决方案项目以帮助更好地构建它。您必须弄清楚何时以及将哪些任务分配到什么优先级。

以下是我对您提供的代码的想法:

你为什么使用.barrier国旗?此标志确保队列不会执行其他并发任务,如果您的数据被多个任务操作并希望避免“竞争条件”,那就太好了。但是,这也意味着在您分派块之后,它将等待在它之前分派的其他任务,然后阻塞队列来执行您的任务。

正如其他人所评论的那样,使用仪器分析您的代码可能会显示这种行为。

以下是我将如何构建您的音频请求:

//A serial (not concurrent) audio queue. Will download one data at a time and play audio
let audioQueue = DispatchQueue(label: "Audio Queue My App", qos: .userInitiated)

//I'm on the main queue here!

audioQueue.async {
    //I'm on the audio queue here, thus not blocking the main thread
    let result = downloadSomeData()
    //Download is done here, and I'm not being blocked by other dispatch queues if their priority is lower.
    switch result {
    case .success(let data):
        //Do something with the data and play audio
    case .failure(let error):
        //Handle Error
    }
}
///Downloads some audio data
func downloadSomeData() -> Swift.Result<Data, Error> {
    //Create dispatch group
    let dg = DispatchGroup()
    dg.enter()
    ///my audio data
    var data: Data?
    //Dispatch onto a background queue OR place a block operation onto your OperationQueue
    DispatchQueue.global(qos: .background).async {
        //Download audio data... Alamofire/ URLSession
        data = Data()
        dg.leave()
    }
    dg.wait()

    //Data was downloaded! We're back on the queue that called us.
    if let nonOptionalData = data {
        return .success(nonOptionalData)
    } else {
        return .failure(NSError(domain: "MyDomain", code: 007, userInfo: [ NSLocalizedDescriptionKey: "No data downloaded, failed?"]))
    }
}

关于选项#2:

可以使用 检索活动核心的数量ProcessInfo().activeProcessorCount。请注意,理论上这个值可能会在运行您的应用程序时由于热节流或其他原因而改变。通过使用此信息并将 a 设置maxConcurrentOperationCountOperationQueue等于活动处理器的数量,您可以确保操作/任务不必共享 cpu 时间。(只要没有其他 DispatchQueue 正在运行。)但是这种方法并不安全。只需要一名团队成员在未来将任务分派到其他地方即可。

但是,此信息的有用之处在于,您可以限制下载任务使用的资源量,方法是拥有一个专用的 OperationQueue,例如最多 2 个并发操作,并将所有后台工作放在此 DispatchQueue 上。您的 CPU 很可能不会充分发挥其潜力并留下一些空间。

于 2019-07-26T13:32:50.417 回答