音声をマイクで取得してテキスト化する方法をご紹介します。

サンプルコード(Xcode8、Swift3.0)

info.plist

ViewController.swift

import UIKit
import Speech

class ViewController: UIViewController {

    // 音声認識
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private let audioEngine = AVAudioEngine()
    private var recognitionResult: SFSpeechRecognitionResult?
    //private var inputNode:AVAudioInputNode?

    // View
    let textView = UITextView()
    let button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 音声認識初期化
        speechRecognizer.delegate = self
        self.requestRecognizerAuthorization()

        viewInit()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    //--------------------------------
    // MARK: - View Init
    //--------------------------------
    private func viewInit(){
        // TextView表示
        textView.frame = CGRect(x: 0, y: 0, width: 300, height: 200)
        textView.center = self.view.center
        textView.layer.borderWidth = 1
        textView.layer.borderColor = UIColor.black.cgColor
        textView.layer.cornerRadius = 5
        self.view.addSubview(textView)
        // Button表示
        button.frame = CGRect(x: 0, y: 0, width: 300, height: 45)
        button.center = CGPoint(x: self.view.center.x, y: self.view.center.y+140)
        button.setTitle("音声認識開始", for: .normal)
        button.backgroundColor = UIColor.red
        button.titleLabel?.textColor = UIColor.white
        button.addTarget(self, action: #selector(ViewController.buttonTapped), for: .touchUpInside)
        self.view.addSubview(button)
    }

    //--------------------------------
    // MARK: - Action
    //--------------------------------
    func buttonTapped(){
        if audioEngine.isRunning {
            audioEngine.stop()
            recognitionRequest?.endAudio()
            button.isEnabled = false
            button.setTitle("音声認識を再開", for: .normal)
            print("#### 停止 ####")
        } else {
            try! startRecording()
            button.setTitle("音声認識を中止", for: .normal)
            print("#### スタート ####")
        }
    }

    //--------------------------------
    // MARK: - 音声認識
    //--------------------------------
    // 認証処理
    private func requestRecognizerAuthorization() {
        SFSpeechRecognizer.requestAuthorization { authStatus in
            // メインスレッドで処理したい内容のため、OperationQueue.main.addOperationを使う
            OperationQueue.main.addOperation { [weak self] in
                guard let `self` = self else { return }
                switch authStatus {
                case .authorized:
                    self.textView.text = "音声認識へのアクセスが許可されています。"
                case .denied:
                    self.textView.text = "音声認識へのアクセスが拒否されています。"
                case .restricted:
                    self.textView.text = "音声認識へのアクセスが制限されています。"
                case .notDetermined:
                    self.textView.text = "音声認識はまだ許可されていません。"
                }
            }
        }
    }
    // 録音開始
    private func startRecording() throws {
        refreshTask()

        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(AVAudioSessionCategoryRecord)
        try audioSession.setMode(AVAudioSessionModeMeasurement)
        try audioSession.setActive(true, with: .notifyOthersOnDeactivation)

        recognitionRequest = SFSpeechAudioBufferRecognitionRequest()

        guard let inputNode = audioEngine.inputNode else {
          fatalError("Audio engine has no input node")
        }
        guard let recognitionRequest = recognitionRequest else {
          fatalError("Unable to created a SFSpeechAudioBufferRecognitionRequest object")
        }

        recognitionRequest.shouldReportPartialResults = true
        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in
            guard let `self` = self else { return }

            var isFinal = false

            if let result = result {
                self.textView.text = result.bestTranscription.formattedString
                isFinal = result.isFinal
            }

            if error != nil || isFinal {
                print("error:(error)")
                self.audioEngine.stop()
                inputNode.removeTap(onBus: 0)
                self.recognitionRequest = nil
                self.recognitionTask = nil
                self.button.isEnabled = true
                self.textView.text = "音声認識スタート"
            }
        }

        // マイクから取得した音声バッファをリクエストに渡す
        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.recognitionRequest?.append(buffer)
        }

        try startAudioEngine()
    }

    private func refreshTask() {
        if let recognitionTask = recognitionTask {
            recognitionTask.cancel()
            self.recognitionTask = nil
        }
    }

    private func startAudioEngine() throws {
        // startの前にリソースを確保しておく。
        audioEngine.prepare()
        try audioEngine.start()
        button.setTitle("音声認識停止", for: .normal)
        print("#### 音声認識停止 ####")
    }

}

extension ViewController: SFSpeechRecognizerDelegate {
    // 音声認識の可否が変更したときに呼ばれるdelegate
    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        if available {
            button.isEnabled = true
            button.setTitle("音声認識スタート", for: .normal)
            print("#### 音声認識スタート ####")
        } else {
            button.isEnabled = false
            button.setTitle("音声認識ストップ", for: .normal)
            print("#### 音声認識ストップ ####")
        }
    }
}

解説

ユーザ許可設定

ユーザにマイク利用、音声認識機能を許可してもらうためにinfo.plistNSMicrophoneUsageDescriptionNSSpeechRecognitionUsageDescriptionを追加します。

info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
:
:
:
  //////////////// ▼▼ 追加 ▼▼ ////////////////
    <key>NSMicrophoneUsageDescription</key>
    <string>学習のためにマイクを利用します。</string>
    <key>NSSpeechRecognitionUsageDescription</key>
    <string>学習のために音声認識を利用します。</string>
  //////////////// ▲▲ 追加 ▲▲ ////////////////
</dict>
</plist>

フレームワークインポート

まずSpeechフレームワークをインポートします。

import Speech

初期化

次にロケールを指定してSFSpeechRecognizerをインスタンス化します。

let speechRecognizer = SFSpeechRecognizer(locale: Locale(localeIdentifier: "en_US"))!

ユーザー承認

SFSpeechRecognizerのrequestAuthorization(_:)メソッドを呼び出し、
ユーザーに許可を求めます。

ハンドラーの引数には認証の状態が入って来るので、場合に応じて処理します。

SFSpeechRecognizer.requestAuthorization { [weak self] authStatus in
    // some operation
}

リクエストの作成

SFSpeechRecognitionRequestオブジェクトを作成します。
実際には以下の二つのどちらかです。

オブジェクト名 説明
SFSpeechAudioBufferRecognitionRequest マイク等のオーディオバッファを利用
SFSpeechURLRecognitionRequest オーディオファイルを利用

マイクを利用する場合

SFSpeechAudioBufferRecognitionRequestを使用します。

recognitionRequest = SFSpeechAudioBufferRecognitionRequest()

オーディオファイルを使用する場合

SFSpeechURLRecognitionRequestを使用します。

生成時にファイルのパスを指定します。

let audioPath = Bundle.main().pathForResource("steve", ofType: "aiff")!
recognitionRequest = SFSpeechURLRecognitionRequest(url: URL(fileURLWithPath: audioPath))

リクエストの開始

作成したSFSpeechRecognizerに、先ほど作成したSFSpeechRecognitionRequestを投げると同時に、結果のコールバックの設定をします。

コールバックの設定には以下の二つの方法があります。

  • recognitionTask(with:resultHandler:)による、クロージャーでの登録
  • recognitionTask(with:delegate:)による、デリゲートでの登録

デリゲートの方が状況に応じて呼ばれるメソッドが分かれているので、詳細に処理することができます。

クロージャーの場合

SFSpeechRecognitionResult?型のオブジェクトが渡されます。

このオブジェクトのbestTranscription.formattedStringにアクセスし、文字列を受け取ります。
また、isFinalには終了かどうかの状態が入っているので、場合に応じて処理します。

recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in
    var isFinal = false

    if let result = result {

        let text = result.bestTranscription.formattedString
        isFinal = result.isFinal
    }

    if isFinal {
        // 終了時の処理
    }
}

デリゲートの場合

SFSpeechRecognitionTaskDelegateを実装することで結果を受け取ることができます。

受け取るメソッドはspeechRecognitionTask(_:didHypothesizeTranscription:)です。
これは、中間結果を受け取るメソッドです。

先ほどのSFSpeechRecognitionResultのbestTranscriptionと同じ、SFTranscription型の引数を受け取るので、formattedStringを取り出します。

func speechRecognitionTask(_ task: SFSpeechRecognitionTask, didHypothesizeTranscription transcription: SFTranscription) {

    let text = transcription.formattedString
}

デリゲートにはこの他、終了時に呼ばれる、speechRecognitionTask(_:didFinishSuccessfully:)メソッド
処理のキャンセル時に呼ばれる、speechRecognitionTaskWasCancelled(_:)メソッド
など合計7つのメソッドがあります。

Github URL

https://github.com/imagepit/ios-speech-to-text-sample