swift OSX: 使用 GCD 串行生成文件
swift OSX: serially generating files using GCD
我正在尝试使用 NSSpeechSynthesizer.startSpeakingString() 生成 .aiff 文件,并使用 GCd 使用串行队列,因为 NSSpeechSynthesizer 接受一个字符串并在指定的 NSURL 地址创建一个 aiff 文件。我对 [String:[String]] 中的字符串列表使用了标准的 for 循环方法,但这会创建一些具有 0 个字节的文件。
这里是生成语音的函数:
func createSpeech(type: String, name: String) {
if !NSFileManager.defaultManager().fileExistsAtPath("\(dataPath)\(type)/\(name)/\(name).aiff"){
do{
try NSFileManager().createDirectoryAtPath("\(dataPath)\(type)/\(name)/", withIntermediateDirectories: true, attributes: nil)
let URL = NSURL(fileURLWithPath: "\(dataPath)\(type)/\(name)/\(name).aiff")
print("Attempting to save speech \(name).aiff")
self.synth.startSpeakingString(name, toURL: URL)
}catch{
print("error occured")
}
}
}
下面是遍历字典来创建文件的函数:
for key in self.nodeLibrary.keys{
dispatch_sync(GlobalBackgroundQueue){
let type = self.nodeLibrary[key]?.0
let name = key.componentsSeparatedByString("_")[0]
if !speechCheck.contains(name){
mixer.createSpeech(type!, name: name)
}
}
}
为了便于阅读,globalBackgroundQueue 是 GCD 队列调用 _T 的别名。
例程运行良好,根据另一个外部函数的要求创建文件夹和子文件夹,然后合成语音,但在我的情况下,我总是得到一个或一些不能正确加载的,给出 0 个字节或太少的使文件无法使用的字节数。
我阅读了以下 post 并且使用这些 GCD 方法已有一段时间了,但我不确定我哪里错了:
http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1
像往常一样非常感谢任何帮助
编辑:更新完成关闭并发现可能存在错误
我创建了一个如下所示的闭包函数,并在另一个辅助方法中使用它,该方法检查是否存在任何错误,例如 sourceFile.length 在加载后为 0。但是,所有文件都显示为 0 长度,这是不可能的,因为我使用 finder 的 属性 命令+i.
检查了每个文件的音频属性
func synthesise(type: String, name: String, completion: (success: Bool)->()) {
if !NSFileManager.defaultManager().fileExistsAtPath("\(dataPath)\(type)/\(name)/\(name).aiff"){
do{
try NSFileManager().createDirectoryAtPath("\(dataPath)\(type)/\(name)/", withIntermediateDirectories: true, attributes: nil)
let URL = NSURL(fileURLWithPath: "\(dataPath)\(type)/\(name)/\(name).aiff")
let success = self.synth.startSpeakingString(name, toURL: URL)
completion(success: success)
}catch{
print("error occured")
}
}
}
func loadSpeech(type: String, name: String){
synthesise(type, name: name, completion: {(success: Bool)->Void in
if success{
print("File \(name) created successfully with return \(self.synthSuccess), checking file integrity")
let URL = NSURL(fileURLWithPath: "\(self.dataPath)\(type)/\(name)/\(name).aiff")
do{
let source = try AVAudioFile(forReading: URL)
print("File has length: \(source.)")
}catch{
print("error loading file")
}
}else{
print("creation unsuccessful, trying again")
self.loadSpeech(type, name: name)
}
})
}
文件是用它们的文件夹生成的,方法 startSpeakingString->Bool 和我在 class 中更新 synthSuccess 属性 的委托函数都显示为真。所以我加载了一个 AVAudioFile 来检查它的长度。所有文件长度均为0。除一个外,它们都不例外。
当我说错误时,这是来自应用程序的另一部分,我在其中加载 AVAudioEngine 并开始加载缓冲区,并将 frameCount 参数设置为 sourceAudioFile.length,这给出了诊断错误,但这是断章取义的现在。
在上面的代码中,检查调用synth.startSpeakingString(name, toURL: URL)
的结果,如果合成器无法开始说话,可以return false
。如果失败,找出原因,或者重试。
另外,添加 [NSSpeechSynthesiserDelegate][1]
,并在那里寻找 speechSynthesizer:didFinishSpeaking:
回调。当合成器认为它已完成讲话时,请检查文件大小。如果为零,请重试该操作。
startSpeakingString(_:toURL:)
将在后台启动一个 异步 任务。实际上,您的代码会同时启动多个 运行 异步任务。这可能是您遇到问题的原因。
解决方案需要确保一次只有 一个 任务处于活动状态。
startSpeakingString(_:toURL:)
的问题在于,它启动了一个异步任务 - 但函数 本身 无法提供在该任务完成时获得通知的方法。
但是,您需要设置一个 delegate 才能收到通知。
因此,您的解决方案将需要定义 NSSpeechSynthesizerDelegate.
您可能想要创建自己的助手 class 以公开具有完成处理程序的异步函数:
func exportSpeakingString(string: String, url: NSURL,
completion: (NSURL?, ErrorType?) -> ())
在内部,class 创建了 NSSpeechSynthesizer
和 NSSpeechSynthesizerDelegate
的实例并相应地实现了委托方法。
要完成挑战,您需要搜索 运行 多个异步函数 顺序 的方法。 SO上已经有解决方案了。
编辑:
我设置自己的项目来确认或忽略 NSSpeechSynthesizer
系统框架中可能存在的问题。到目前为止,可能自己的测试确认 NSSpeechSynthesizer
按预期工作。
但是,有一些细微之处值得一提:
确保您创建了一个有效的文件 URL,您将其作为参数传递给方法 startSpeakingString(:toURL:)
中的参数 URL
。
确保为输出文件选择 NSSpeechSynthesizer
和播放此文件的系统框架已知的扩展名,例如 .aiff
。不幸的是,这里非常缺乏文档——所以我不得不反复试验。 QuickTime 支持的音频文件格式列表可能对您有所帮助。不过,我不知道 NSSpeechSynthesizer
如何选择输出格式。
以下两个class组成了一个简单易用的库:
import Foundation
import AppKit
enum SpeechSynthesizerError: ErrorType {
case ErrorActive
case ErrorURL(message: String)
case ErrorUnknown
}
internal class InternalSpeechSynthesizer: NSObject, NSSpeechSynthesizerDelegate {
typealias CompletionFunc = (NSURL?, ErrorType?) -> ()
private let synthesizer = NSSpeechSynthesizer(voice: nil)!
private var _completion: CompletionFunc?
private var _url: NSURL?
override init() {
super.init()
synthesizer.delegate = self
}
// CAUTION: This call is not thread-safe! Ensure that multiple method invocations
// will be called from the same thread!
// Only _one_ task can be active at a time.
internal func synthesize(input: String, output: NSURL, completion: CompletionFunc) {
guard _completion == nil else {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
completion(nil, SpeechSynthesizerError.ErrorActive)
}
return
}
guard output.path != nil else {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
completion(nil, SpeechSynthesizerError.ErrorURL(message: "The URL must be a valid file URL."))
}
return
}
_completion = completion
_url = output
if !synthesizer.startSpeakingString(input, toURL: output) {
fatalError("Could not start speeaking")
}
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
willSpeakWord characterRange: NSRange,
ofString string: String)
{
NSLog("willSpeakWord")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
willSpeakPhoneme phonemeOpcode: Int16)
{
NSLog("willSpeakPhoneme")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
didEncounterErrorAtIndex characterIndex: Int,
ofString string: String,
message: String)
{
NSLog("didEncounterErrorAtIndex")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
didFinishSpeaking finishedSpeaking: Bool)
{
assert(self._url != nil)
assert(self._url!.path != nil)
assert(self._completion != nil)
var error: ErrorType?
if !finishedSpeaking {
do {
error = try self.synthesizer.objectForProperty(NSSpeechErrorsProperty) as? NSError
} catch let err {
error = err
}
}
let url: NSURL? = NSFileManager.defaultManager().fileExistsAtPath(self._url!.path!) ? self._url : nil
let completion = self._completion!
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
if url == nil && error == nil {
error = SpeechSynthesizerError.ErrorUnknown
}
completion(url, error)
}
_completion = nil
_url = nil
}
}
public struct SpeechSynthesizer {
public init() {}
private let _synthesizer = InternalSpeechSynthesizer()
public func synthesize(input: String, output: NSURL, completion: (NSURL?, ErrorType?) -> ()) {
_synthesizer.synthesize(input, output: output) { (url, error) in
completion(url, error)
}
}
}
您可以如下图使用:
func testExample() {
let expect = self.expectationWithDescription("future should be fulfilled")
let synth = SpeechSynthesizer()
let url = NSURL(fileURLWithPath: "/Users/me/Documents/speech.aiff")
synth.synthesize("Hello World!", output: url) { (url, error) in
if let url = url {
print("URL: \(url)")
}
if let error = error {
print("Error: \(error)")
}
expect.fulfill()
}
self.waitForExpectationsWithTimeout(1000, handler: nil)
// Test: output file should exist.
}
我正在尝试使用 NSSpeechSynthesizer.startSpeakingString() 生成 .aiff 文件,并使用 GCd 使用串行队列,因为 NSSpeechSynthesizer 接受一个字符串并在指定的 NSURL 地址创建一个 aiff 文件。我对 [String:[String]] 中的字符串列表使用了标准的 for 循环方法,但这会创建一些具有 0 个字节的文件。
这里是生成语音的函数:
func createSpeech(type: String, name: String) {
if !NSFileManager.defaultManager().fileExistsAtPath("\(dataPath)\(type)/\(name)/\(name).aiff"){
do{
try NSFileManager().createDirectoryAtPath("\(dataPath)\(type)/\(name)/", withIntermediateDirectories: true, attributes: nil)
let URL = NSURL(fileURLWithPath: "\(dataPath)\(type)/\(name)/\(name).aiff")
print("Attempting to save speech \(name).aiff")
self.synth.startSpeakingString(name, toURL: URL)
}catch{
print("error occured")
}
}
}
下面是遍历字典来创建文件的函数:
for key in self.nodeLibrary.keys{
dispatch_sync(GlobalBackgroundQueue){
let type = self.nodeLibrary[key]?.0
let name = key.componentsSeparatedByString("_")[0]
if !speechCheck.contains(name){
mixer.createSpeech(type!, name: name)
}
}
}
为了便于阅读,globalBackgroundQueue 是 GCD 队列调用 _T 的别名。
例程运行良好,根据另一个外部函数的要求创建文件夹和子文件夹,然后合成语音,但在我的情况下,我总是得到一个或一些不能正确加载的,给出 0 个字节或太少的使文件无法使用的字节数。
我阅读了以下 post 并且使用这些 GCD 方法已有一段时间了,但我不确定我哪里错了:
http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1
像往常一样非常感谢任何帮助
编辑:更新完成关闭并发现可能存在错误
我创建了一个如下所示的闭包函数,并在另一个辅助方法中使用它,该方法检查是否存在任何错误,例如 sourceFile.length 在加载后为 0。但是,所有文件都显示为 0 长度,这是不可能的,因为我使用 finder 的 属性 命令+i.
检查了每个文件的音频属性func synthesise(type: String, name: String, completion: (success: Bool)->()) {
if !NSFileManager.defaultManager().fileExistsAtPath("\(dataPath)\(type)/\(name)/\(name).aiff"){
do{
try NSFileManager().createDirectoryAtPath("\(dataPath)\(type)/\(name)/", withIntermediateDirectories: true, attributes: nil)
let URL = NSURL(fileURLWithPath: "\(dataPath)\(type)/\(name)/\(name).aiff")
let success = self.synth.startSpeakingString(name, toURL: URL)
completion(success: success)
}catch{
print("error occured")
}
}
}
func loadSpeech(type: String, name: String){
synthesise(type, name: name, completion: {(success: Bool)->Void in
if success{
print("File \(name) created successfully with return \(self.synthSuccess), checking file integrity")
let URL = NSURL(fileURLWithPath: "\(self.dataPath)\(type)/\(name)/\(name).aiff")
do{
let source = try AVAudioFile(forReading: URL)
print("File has length: \(source.)")
}catch{
print("error loading file")
}
}else{
print("creation unsuccessful, trying again")
self.loadSpeech(type, name: name)
}
})
}
文件是用它们的文件夹生成的,方法 startSpeakingString->Bool 和我在 class 中更新 synthSuccess 属性 的委托函数都显示为真。所以我加载了一个 AVAudioFile 来检查它的长度。所有文件长度均为0。除一个外,它们都不例外。
当我说错误时,这是来自应用程序的另一部分,我在其中加载 AVAudioEngine 并开始加载缓冲区,并将 frameCount 参数设置为 sourceAudioFile.length,这给出了诊断错误,但这是断章取义的现在。
在上面的代码中,检查调用synth.startSpeakingString(name, toURL: URL)
的结果,如果合成器无法开始说话,可以return false
。如果失败,找出原因,或者重试。
另外,添加 [NSSpeechSynthesiserDelegate][1]
,并在那里寻找 speechSynthesizer:didFinishSpeaking:
回调。当合成器认为它已完成讲话时,请检查文件大小。如果为零,请重试该操作。
startSpeakingString(_:toURL:)
将在后台启动一个 异步 任务。实际上,您的代码会同时启动多个 运行 异步任务。这可能是您遇到问题的原因。
解决方案需要确保一次只有 一个 任务处于活动状态。
startSpeakingString(_:toURL:)
的问题在于,它启动了一个异步任务 - 但函数 本身 无法提供在该任务完成时获得通知的方法。
但是,您需要设置一个 delegate 才能收到通知。
因此,您的解决方案将需要定义 NSSpeechSynthesizerDelegate.
您可能想要创建自己的助手 class 以公开具有完成处理程序的异步函数:
func exportSpeakingString(string: String, url: NSURL,
completion: (NSURL?, ErrorType?) -> ())
在内部,class 创建了 NSSpeechSynthesizer
和 NSSpeechSynthesizerDelegate
的实例并相应地实现了委托方法。
要完成挑战,您需要搜索 运行 多个异步函数 顺序 的方法。 SO上已经有解决方案了。
编辑:
我设置自己的项目来确认或忽略 NSSpeechSynthesizer
系统框架中可能存在的问题。到目前为止,可能自己的测试确认 NSSpeechSynthesizer
按预期工作。
但是,有一些细微之处值得一提:
确保您创建了一个有效的文件 URL,您将其作为参数传递给方法
startSpeakingString(:toURL:)
中的参数URL
。确保为输出文件选择
NSSpeechSynthesizer
和播放此文件的系统框架已知的扩展名,例如.aiff
。不幸的是,这里非常缺乏文档——所以我不得不反复试验。 QuickTime 支持的音频文件格式列表可能对您有所帮助。不过,我不知道NSSpeechSynthesizer
如何选择输出格式。
以下两个class组成了一个简单易用的库:
import Foundation
import AppKit
enum SpeechSynthesizerError: ErrorType {
case ErrorActive
case ErrorURL(message: String)
case ErrorUnknown
}
internal class InternalSpeechSynthesizer: NSObject, NSSpeechSynthesizerDelegate {
typealias CompletionFunc = (NSURL?, ErrorType?) -> ()
private let synthesizer = NSSpeechSynthesizer(voice: nil)!
private var _completion: CompletionFunc?
private var _url: NSURL?
override init() {
super.init()
synthesizer.delegate = self
}
// CAUTION: This call is not thread-safe! Ensure that multiple method invocations
// will be called from the same thread!
// Only _one_ task can be active at a time.
internal func synthesize(input: String, output: NSURL, completion: CompletionFunc) {
guard _completion == nil else {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
completion(nil, SpeechSynthesizerError.ErrorActive)
}
return
}
guard output.path != nil else {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
completion(nil, SpeechSynthesizerError.ErrorURL(message: "The URL must be a valid file URL."))
}
return
}
_completion = completion
_url = output
if !synthesizer.startSpeakingString(input, toURL: output) {
fatalError("Could not start speeaking")
}
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
willSpeakWord characterRange: NSRange,
ofString string: String)
{
NSLog("willSpeakWord")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
willSpeakPhoneme phonemeOpcode: Int16)
{
NSLog("willSpeakPhoneme")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
didEncounterErrorAtIndex characterIndex: Int,
ofString string: String,
message: String)
{
NSLog("didEncounterErrorAtIndex")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
didFinishSpeaking finishedSpeaking: Bool)
{
assert(self._url != nil)
assert(self._url!.path != nil)
assert(self._completion != nil)
var error: ErrorType?
if !finishedSpeaking {
do {
error = try self.synthesizer.objectForProperty(NSSpeechErrorsProperty) as? NSError
} catch let err {
error = err
}
}
let url: NSURL? = NSFileManager.defaultManager().fileExistsAtPath(self._url!.path!) ? self._url : nil
let completion = self._completion!
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
if url == nil && error == nil {
error = SpeechSynthesizerError.ErrorUnknown
}
completion(url, error)
}
_completion = nil
_url = nil
}
}
public struct SpeechSynthesizer {
public init() {}
private let _synthesizer = InternalSpeechSynthesizer()
public func synthesize(input: String, output: NSURL, completion: (NSURL?, ErrorType?) -> ()) {
_synthesizer.synthesize(input, output: output) { (url, error) in
completion(url, error)
}
}
}
您可以如下图使用:
func testExample() {
let expect = self.expectationWithDescription("future should be fulfilled")
let synth = SpeechSynthesizer()
let url = NSURL(fileURLWithPath: "/Users/me/Documents/speech.aiff")
synth.synthesize("Hello World!", output: url) { (url, error) in
if let url = url {
print("URL: \(url)")
}
if let error = error {
print("Error: \(error)")
}
expect.fulfill()
}
self.waitForExpectationsWithTimeout(1000, handler: nil)
// Test: output file should exist.
}