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.main
的 async
方法调用的,要么是因为那是 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,另一种方法是将整个工作流程本身移动到.concurrent
DispatchQueue
,你可以随心所欲地阻塞它,而不用担心它会阻塞主线程。 AVSpeechSynthesizer
可以毫无问题地在任何地方安排工作。
如果可以选择使用 Swift 5.5,您可以查看它的 async
和 await
关键字。编译器为它们强制执行适当的 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)
}
如何在继续之前等待 @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.main
的 async
方法调用的,要么是因为那是 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,另一种方法是将整个工作流程本身移动到.concurrent
DispatchQueue
,你可以随心所欲地阻塞它,而不用担心它会阻塞主线程。 AVSpeechSynthesizer
可以毫无问题地在任何地方安排工作。
如果可以选择使用 Swift 5.5,您可以查看它的 async
和 await
关键字。编译器为它们强制执行适当的 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)
}