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



在继续之前,如何等待@转义闭包以内联方式完成?

我使用的是AVSpeechSynthesizer的write方法,它使用了一个@转义闭包,因此回调中的初始AVAudioBuffer将在createSpeechToBuffer完成后返回。

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

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

目标是内联执行任务,以避免将工作流更改为"didFinish"代理的备用

speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance)

我相信这个问题可以推广到处理函数\方法中的@转义闭包

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方法后,它未能产生所需的输出(内联),我意识到它在获得重新采样的结果之前就返回了。回调正在转义,因此回调中的初始AVAudioBuffer将在createSpeechToBuffer完成后返回。实际的重新采样确实有效,但我目前必须保存结果,并在收到代表的通知后继续;didFinish话语";继续。

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

如何可能在线等待结果而不是根据委托重新创建工作流;didFinish话语";?

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

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

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

很多程序员有时会对async究竟做了什么感到困惑。所有CCD_;调度这个任务并立即将控制权返回给呼叫者";。就是这样。并不意味着任务将同时运行,只是意味着它将在以后运行。它是并发运行还是串行运行是调用其async方法的DispatchQueue的一个属性。并发队列为其任务旋转线程,这些任务可以在不同的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几乎可以肯定是在响应一些事件处理而运行的,这意味着当它阻塞时,它不会返回到运行循环,继续到下一次迭代,在那里它检查队列中的任务。。。从你描述的行为来看,似乎包括_speechSynth.write正在做的工作。。。正是你在等待的东西。

您可以尝试显式创建一个.concurrentDispatchQueue,并使用它将对_speechSynth.write的调用封装在一个显式async调用中,但可能不起作用,即使起作用,它也很容易受到苹果可能对AVSpeechSynthesizer的实现所做的更改的影响。

安全的方法是不要阻塞。。。但这意味着要重新思考一下你的工作流程。基本上,无论createSpeechToBuffer返回后调用什么代码,都应该在闭包结束时调用。当然,正如目前编写的createSpeechToBuffer不知道该代码是什么(也不应该知道)。解决方案是将其作为参数注入。。。意味着CCD_ 23本身也将采用CCD_。当然,这意味着它不能返回缓冲区,而是将其传递给闭包。

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 = $0 else {
fatalError("Could not create speechBuffer")
}
doSomethingWithSpeechBuffer(buffer)
}

最新更新