概要
AppleのSpeechフレークワークサンプルコードを再現します。
iOS10から追加されたSpeech
フレームワークを使って音声認識。
「認識された音声をテキスト化して画面に表示する」という単純なアプリが完成します。
ほとんどApple提供のサンプルコードのままですが、日本語対応させたりコードの記述が違ったりする箇所があります。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
動作環境及び開発環境
Swift3
Xcode8
macOS10.12 Sierra
iOS10(iPad, iPhone)
手順
プロジェクト作成
プロジェクト名は「SpeakWriter」とする。
テンプレートは「iOS > Single View Application」を選択。
プロパティリスト
Info.plist
に許諾説明を追加する。
Privacy - NSSpeechRecognitionUsageDescription: このアプリは音声認識機能を使用します。
Privacy - Microphone Usage Description: このアプリはマイクを使用します。
Storyboard
まずは、ユーザインターフェースをつくる
認識テキストを表示するビュー
UITextView
を配置(認識された音声をテキスト表示: textView)
背景・テキストカラー・フォントサイズを変更
インスペクタエリアからisUserInteraction
プロパティを無効にする
録音開始ボタン
UIButton
をUITextView
の真下に配置
背景・テキストカラー・フォントサイズを変更
Constraintsを付加する
この設定値および設定方法は適当にしてある。
UITextView(Top, Leading, Trailing Space = 0, Bottom to next view = 10)
UIButton(Width = 200, Height = 64, Bottom to = 10, Horizontal center)
Interface Builderと接続
UITextView <=> @IBOutlet: textView
UIButton <=> @IBOutlet: recordButton, @IBAction: recordButtonTapped
ViewController.swiftにコーディング
録音開始ボタンを無効にする
recordButtonはアプリ起動時は無効で、ユーザから録音許可を得た後に有効化する。
recordButton.isEnabled = false
Speechフレームワークを組み込む
音声認識するための機能を盛り込んだフレームワーク
import Speech
SFSpeechRecognizerインスタンスを生成
ローカルを指定しない場合は、端末の地域設定になる。
メンバプロパティで生成・初期化しておく。
明示的アンラップしておく。
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(localeIdentifier: "ja-JP"))!
ユーザから使用許可を得る
コールバックをメインスレッドで実行している
SFSpeechRecognizer.requestAuthorization { (status) in
OperationQueue.main().addOperation {
switch status {
case .authorized: // 許可OK
self.recordButton.isEnabled = true
self.recordButton.backgroundColor = UIColor.blue
case .denied: // 拒否
self.recordButton.isEnabled = false
self.recordButton.setTitle("録音許可なし", for: .disabled)
case .restricted: // 限定
self.recordButton.isEnabled = false
self.recordButton.setTitle("このデバイスでは無効", for: .disabled)
case .notDetermined:// 不明
self.recordButton.isEnabled = false
self.recordButton.setTitle("録音機能が無効", for: .disabled)
}
}
}
一旦、ビルド
アプリ起動直後、ユーザ許可を求められることを確認する
音声認識のタスクを宣言する
メンバプロパティでタスクのオブジェクトを宣言
初期値は設定しないからオプショナル
.
.
.
private var recognitionTask: SFSpeechRecognitionTask?
録音スタート処理を実装する
タスクにリクエストを登録すると、その結果に音声認識された文字列が返ってくる
startRecording()throwsメソッドを実装
エラーハンドリングするためthrowsキーワードをつける(実際はしない)
private func startRecording() throws {
//ここに録音する処理を記述
}
既存のタスクが存在するなら、全てキャンセル
if let recognitionTask = recognitionTask {
// 既存タスクがあればキャンセルしてリセット
recognitionTask.cancel()
self.recognitionTask = nil
}
セッションを準備する
...
...
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(AVAudioSessionCategoryRecord)
try audioSession.setMode(AVAudioSessionModeMeasurement)
try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
認識リクエストのインスタンスを宣言
クラスのメンバプロパティで宣言する
初期値は設定しないからオプショナル
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
認識開始の前に認識リクエストを初期化
...
...
recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
guard let recognitionRequest = recognitionRequest else { fatalError("リクエスト生成エラー") }
shouldReportPartialResultsプロパティ
録音完了前に途中の結果を報告してくれる(デフォルトはfalse)
trueにすると、完了時に結果をさかのぼって修正してくれる
...
...
recognitionRequest.shouldReportPartialResults = true
端末のマイクを使う準備
AVAudioEngine
クラス(iOS8~)を使う
メンバプロパティで生成・初期化する
...
...
private let audioEngine = AVAudioEngine()
inputNode
audioEngine
インスタンスのinputNode
プロパティを取得する
...
...
guard let inputNode = audioEngine.inputNode else { fatalError("InputNodeエラー") }
リクエストを登録してタスクを実行
認識タスクのインスタンスの初期化メソッド内のハンドラで、結果を処理する
生成されるタスクのインスタンスは、SFSpeechRecognitionTask
型オブジェクト
ハンドラ内のresultにはSFSpeechRecognitionResult
型オブジェクト
resultのbestTranscription
プロパティには、最も精度が高かった認識結果のStringオブジェクトが入っている
shouldReportPartialResults
プロパティがtrueなら、bestTranscription
のオブジェクトが変化するのかもしれない
...
...
recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { (result, error) in
var isFinal = false
if let result = result {
self.textView.text = result.bestTranscription.formattedString
isFinal = result.isFinal
}
if error != nil || isFinal {
self.audioEngine.stop()
inputNode.removeTap(onBus: 0)
self.recognitionRequest = nil
self.recognitionTask = nil
self.recordButton.isEnabled = true
self.recordButton.setTitle("Start Recording", for: [])
self.recordButton.backgroundColor = UIColor.blue
}
}
マイクからの録音フォーマット
...
...
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
self.recognitionRequest?.append(buffer)
}
オーディオエンジンで録音を開始して、テキスト表示を変更する
...
...
audioEngine.prepare() // オーディオエンジン準備
try audioEngine.start() // オーディオエンジン開始
textView.text = “(認識中…そのまま話し続けてください)”
デリゲート処理
ViewControllerクラスにデリゲートを採用する
speechRecognizerの状態変化を受け取れるようになる
Class ViewController: UIViewController, SFSpeechRecognitionTaskDelegate {
...
...
自身のクラスをデリゲート先を設定する
override func viewWillAppear(_ animated: Bool) {
speechRecognizer.delegate = self // デリゲート先になる
...
...
デリゲートメソッド実装
音声認識機能の状態が変化するタイミングで呼ばれる
録音ボタンの有効と無効を切り替える
public func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
if available {
// 利用可能になったら、録音ボタンを有効にする
recordButton.isEnabled = true
recordButton.setTitle("Start Recording", for: [])
recordButton.backgroundColor = UIColor.blue
} else {
// 利用できないなら、録音ボタンは無効にする
recordButton.isEnabled = false
recordButton.setTitle("現在、使用不可", for: .disabled)
}
}
IBActionメソッドを実装
recordButtonがタップされたときの処理
audioEngineが動作中なら、音声の認識中ということ
録音開始
@IBAction func recordButtonTapped() {
if audioEngine.isRunning {
// 音声エンジン動作中なら停止
audioEngine.stop()
recognitionRequest?.endAudio()
recordButton.isEnabled = false
recordButton.setTitle("Stopping", for: .disabled)
recordButton.backgroundColor = UIColor.lightGray
return
}
// 録音を開始する
try! startRecording()
recordButton.setTitle("認識を完了する", for: [])
recordButton.backgroundColor = UIColor.red
}
考察
面白い。すごく面白い。
初心者にはいろいろ、ややこしいことは多かったが作業量の割に楽しめるものができた。
テキストビューのリセットボタンつけたり、を外部ライブラリの翻訳機能と連携させたりしたら、使えるアプリも作れるかもしれない。