SpeechFrameworkで音声認識されなくなる問題

はじめに

iOSのSpeechFrameworkを使って開発を進めていく上で、特定の文字を受け取ったらイベントを走らせて再度音声認識を行うというプログラムを作っていたのですが、一定時間放置していると音声認識しなくなってしまうという問題にぶち当たりました。

調べてみたところ、SpeechFrameworkには認識回数制限タイムアウトが設定されているようでした。

制限

StackOverFlowにSpeech Recognition Limits for iOS 10という質問がありその中で1時間あたり1000回のリクエスト制限、1回のリクエストで最長1分まで音声を許可されるとありました。

この2つを守って実装しなければいけないようです、他にも僕が出会ったいくつかの問題について記載していきます。

クラッシュ

'com.apple.coreaudio.avfaudio', reason: 'required condition is false: nullptr == Tap()'

解決策は以下のコードをストップさせている箇所で呼び出せば良い!

audioEngine.inputNode?.removeTap(onBus: 0)

ログにエラーが出る

以下のようなエラーが出ている場合はSFSpeechRecognitionTaskが裏で走ってしまっているために起こっている可能性が高いです。

+[AFAggregator logDictationFailedWithError:] Error Domain=kAFAssistantErrorDomain Code=209 "(null)"

音声ん入力をストップさせている箇所に以下のコードを追記すると直ります

     guard let task = self.recognitionTask else {
            fatalError("Error")
        }
        task.cancel()
        task.finish()

音声認識でUIを動かす検証コード

赤と青という音声が入力された場合Viewの色を変更するコードです。

60秒ごとにタイマーでループ呼び出しを行っているので半永久的に音声入力をすることが可能です。

1時間に1000回までの入力については想定できていません。

class ViewController: UIViewController {
    //認識する言語
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private var audioEngine = AVAudioEngine()

    override func viewDidLoad() {
        super.viewDidLoad()

        SFSpeechRecognizer.requestAuthorization { (status) in
            OperationQueue.main.addOperation {
                    switch status {
                    case .authorized:
                            do {
                                try self.startSpeech()
                            } catch{
                                print("startError: \(error)")
                            }
                            self.startTimer()
                    default:
                        print("error")
                }
            }
        }
    }
    
    func startSpeech() throws {
        if let recognitionTask = recognitionTask {
            recognitionTask.cancel()
            self.recognitionTask = nil
        }

        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(.record, mode: .measurement, options: [])
        try audioSession.setActive(true, options: .notifyOthersOnDeactivation)

        let recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        self.recognitionRequest = recognitionRequest
        recognitionRequest.shouldReportPartialResults = true
        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] (result, error) in
            guard let `self` = self else { return }
            
            if let result = result {
                print(result.bestTranscription.formattedString)
                //特定の文字を発話した際に処理を行う
                if result.bestTranscription.formattedString.contains("赤") {
                    self.setBackgroundColor(color: .red)
                    
                } else if result.bestTranscription.formattedString.contains("青") {
                    self.setBackgroundColor(color: .blue)
                    
                }
            }
        }
        let recordingFormat = audioEngine.inputNode.outputFormat(forBus: 0)
        audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.recognitionRequest?.append(buffer)
        }

        audioEngine.prepare()
        try? audioEngine.start()
    }

    func stopSpeech() {
        
        guard let task = self.recognitionTask else {
            fatalError("Error")
        }
        task.cancel()
        task.finish()
        
        self.audioEngine.inputNode.removeTap(onBus: 0)
        self.audioEngine.stop()
        self.recognitionRequest?.endAudio()
    }
    
    private func setBackgroundColor(color: UIColor) {
        DispatchQueue.main.async {
            self.view.backgroundColor = color
        }
        
        self.stopSpeech()
        do {
            try self.startSpeech()
        } catch {
            print("startError: \(error)")
        }
    }
    
    //1分でタイムアウトして音声認識されなくなるのでタイマーでループ
    private func startTimer(){
        Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
            self.stopSpeech()
            do {
                try self.startSpeech()
            } catch{
                print("startError: \(error)")
            }
        }
    }
}

参考文献

Terminating app due to uncaught App crashes while using Speech kit ios

Error Domain=kAFAssistantErrorDomain Code=209 “(null)”

Speech Recognition API

Speech Recognition Limits for iOS 10