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 按预期工作。

但是,有一些细微之处值得一提:

  1. 确保您创建了一个有效的文件 URL,您将其作为参数传递给方法 startSpeakingString(:toURL:) 中的参数 URL

  2. 确保为输出文件选择 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.
}