Swift:如何等待异步的@escaping 闭包(内联)

Swift: How to wait for an asynchronous, @escaping closure (inline)

如何在继续之前等待 @escaping 闭包完成内联?

我正在使用 AVSpeechSynthesizer 的 write 方法,它使用 @escaping 闭包,因此回调中的初始 AVAudioBuffer 将在 createSpeechToBuffer 完成后 return。

func write(_ utterance: AVSpeechUtterance, toBufferCallback bufferCallback: @escaping AVSpeechSynthesizer.BufferCallback)

我的方法是将语音写入缓冲区,然后对输出进行重新采样和操作,以实现语音比实时速度更快的工作流。

目标是内联执行任务,以避免将工作流更改为 'didFinish' 代表

的备用状态
speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance)

相信这个问题可以概括为处理function\method

中的@escaping闭包
import Cocoa
import AVFoundation

let _speechSynth = AVSpeechSynthesizer()

func resampleBuffer( inSource: AVAudioPCMBuffer, newSampleRate: Float) -> AVAudioPCMBuffer
{
    // simulate resample data here
    let testCapacity     = 1024
    let audioFormat      = AVAudioFormat(standardFormatWithSampleRate: Double(newSampleRate), channels: 2)
    let simulateResample = AVAudioPCMBuffer(pcmFormat: audioFormat!, frameCapacity: UInt32(testCapacity))
    return simulateResample!
}

func createSpeechToBuffer( stringToSpeak: String, sampleRate: Float) -> AVAudioPCMBuffer?
{
    var outBuffer    : AVAudioPCMBuffer? = nil
    let utterance    = AVSpeechUtterance(string: stringToSpeak)
    var speechIsBusy = true
    utterance.voice  = AVSpeechSynthesisVoice(language: "en-us")
    let semaphore = DispatchSemaphore(value: 0)
    
    _speechSynth.write(utterance) { (buffer: AVAudioBuffer) in

        guard let pcmBuffer = buffer as? AVAudioPCMBuffer else {
            fatalError("unknown buffer type: \(buffer)")
        }
        
        if ( pcmBuffer.frameLength == 0 ) {
            print("buffer is empty")
        } else {
            print("buffer has content \(buffer)")
        }
        
        outBuffer    = resampleBuffer( inSource: pcmBuffer, newSampleRate: sampleRate)
        speechIsBusy = false
//        semaphore.signal()
    }
    
    // wait for completion of func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance)
    
//        while ( _speechSynth.isSpeaking )
//        {
//            /* arbitrary task waiting for write to complete */
//        }
//
//        while ( speechIsBusy )
//        {
//            /* arbitrary task waiting for write to complete */
//        }
//    semaphore.wait()
    return outBuffer
}

print("SUCCESS is waiting, returning the non-nil output from the resampleBuffer method.")

for indx in 1...10
{
    let sentence  = "This is sentence number \(indx). [[slnc 3000]] \n"
    let outBuffer = createSpeechToBuffer( stringToSpeak: sentence, sampleRate: 48000.0)
    print("outBuffer: \(String(describing: outBuffer))")
}

在我编写了 createSpeechToBuffer 方法但未能产生所需的输出(内联)之后,我意识到它 returns 在获得重采样结果之前。回调正在转义,因此回调中的初始 AVAudioBuffer 将在 createSpeechToBuffer 完成后 return。实际的重采样确实有效,但是我目前必须保存结果并在委托“didFinish utterance”通知后继续。

尝试等待 _speechSynth.isSpeaking、speechIsBusy 标志、调度队列和信号量正在阻止写入方法(使用 _speechSynth.write)完成。

等待内联结果与根据委托“didFinish utterance”重新创建工作流有何可能?

我使用的是 macOS 11.4 (Big Sur),但我相信这个问题适用于 macOS 并且 ios

在我看来,如果 @escaping 闭包同时为 运行,DispatchSemaphore 的注释掉的代码将起作用,我认为问题在于它是 运行 串行,或者更准确地说,根本不是运行,因为它被安排为串行运行。我不是特别熟悉 AVSpeechSynthesizer API,但是根据你的描述,我觉得它好像在调用主调度队列,它是一个 serial 队列。您调用 wait 来阻塞直到 _speechSynth.write 完成,但这会阻塞主线程,从而阻止它继续进行 运行 循环的下一次迭代,因此 _speechSynth.write 的实际工作永远不会甚至开始。

让我们后退。在幕后的某个地方,你的闭包几乎肯定是通过 DispatchQueue.mainasync 方法调用的,要么是因为那是 speechSynth.write 工作的地方,然后在当时的当前线程上同步调用你的闭包,或者因为它在主线程上显式调用它。

很多程序员有时对 async 的作用感到困惑。所有 async 的意思是“立即安排此任务并 return 控制权给调用者”。而已。它 不是 意味着任务将 运行 并发,只是它会在 运行 之后。 运行 concurrently 还是 serially 是 DispatchQueue 的一个属性,其 async 方法被调用。并发队列为它们的任务启动线程,这些任务可以 运行 在不同的 CPU 核心上并行(真正的并发),或者与同一核心上的当前线程交错(抢占式多任务处理)。另一方面,串行队列有一个 运行 循环,如 NSRunLoop 和 运行 它们的计划任务 同步 出队后。

为了说明我的意思,主 运行 循环看起来像这样,其他 运行 循环也类似:

while !quit
{
    if an event is waiting {
        dispatch the event <-- Your code is likely blocking in here
    }
    else if a task is waiting in the queue 
    {
        dequeue the task
        execute the task <-- Your closure would be run here
    }
    else if a timer has expired {
       run timer task
    }
    else if some view needs updating {
        call the view's draw(rect:) method
    }
    else { probably other things I'm forgetting }
}

createSpeechToBuffer 几乎肯定是 运行 响应某些事件处理,这意味着当它阻塞时,它不会 return 回到 运行 循环继续下一次迭代,检查队列中的任务...从您描述的行为来看,似乎包括 _speechSynth.write 正在完成的工作...您正在等待的事情。

您可以尝试显式创建 .concurrent DispatchQueue 并使用它在显式 async 调用中包装对 _speechSynth.write 的调用,但是 可能 不会起作用,即使它起作用,它也很容易受到 Apple 可能对 AVSpeechSynthesizer 的实现所做的更改的影响。

安全的方法是不阻止...但这意味着稍微重新考虑您的工作流程。基本上,在 createSpeechToBuffer return 之后调用的任何代码都应该在闭包结束时调用。当然,正如目前所写的那样,createSpeechToBuffer 不知道该代码是什么(也不应该知道)。解决方案是将其作为参数注入...意味着 createSpeechToBuffer 本身也会采用 @escaping 闭包。当然,这意味着它不能 return 缓冲区,而是将其传递给闭包。

func createSpeechToBuffer(
    stringToSpeak: String,
    sampleRate: Float,
    onCompletion: @escaping (AVAudioPCMBuffer?) -> Void) 
{
    let utterance    = AVSpeechUtterance(string: stringToSpeak)
    utterance.voice  = AVSpeechSynthesisVoice(language: "en-us")
    let semaphore = DispatchSemaphore(value: 0)
    
    _speechSynth.write(utterance) { (buffer: AVAudioBuffer) in

        guard let pcmBuffer = buffer as? AVAudioPCMBuffer else {
            fatalError("unknown buffer type: \(buffer)")
        }
        
        if ( pcmBuffer.frameLength == 0 ) {
            print("buffer is empty")
        } else {
            print("buffer has content \(buffer)")
        }
        
        onCompletion(
            resampleBuffer(
                inSource: pcmBuffer, 
                newSampleRate: sampleRate
            )
        )
    }
}

如果您真的想要维护现有的API,另一种方法是将整个工作流程本身移动到.concurrentDispatchQueue,你可以随心所欲地阻塞它,而不用担心它会阻塞主线程。 AVSpeechSynthesizer 可以毫无问题地在任何地方安排工作。

如果可以选择使用 Swift 5.5,您可以查看它的 asyncawait 关键字。编译器为它们强制执行适当的 async 上下文,这样您就不会阻塞主线程。

更新回答如何调用我的版本。

假设您调用 createSpeechToBuffer 的代码当前如下所示:

guard let buffer = createSpeechToBuffer(stringToSpeak: "Hello", sampleRate: sampleRate)
else { fatalError("Could not create speechBuffer") }

doSomethingWithSpeechBuffer(buffer)

你会这样称呼新版本:

createSpeechToBuffer(stringToSpeak: "Hello", sampleRate: sampleRate) 
{
    guard let buffer = [=13=] else {
        fatalError("Could not create speechBuffer")
    }

    doSomethingWithSpeechBuffer(buffer)
}