[Swift/SwiftUI]音声入力の実装

目次

概要

「システム開発をしていると、端末を操作して便利な機能を使える」ということがよくある。
しかし、会社ではない、誰か個人が「こういうものがあったらなあ」と思い浮かべるものは、
大抵の場合、音声認識や画像認識をする機能があることが多い。
その求められるであろう機能のうち、音声入力を行う処理を実装する。

ソースコード

まずはInfo.plistに以下の二項目を追加する。

  • Privacy – Speech Recognition Usage Description
  • Privacy – Microphone Usage Description

ソースコードは以下の通りになる。

import SwiftUI

@main
struct TestApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            LiveAudioView()
        }
    }
}

View表示部分

import SwiftUI
import AVFoundation

struct LiveAudioView: View {
    @ObservedObject var viewModel: LiveAudioViewModel = LiveAudioViewModel()
    
    var body: some View {
        VStack {
            Spacer()
            ScrollView {
                if self.viewModel.voiceText == "" {
                    Text("ここに入力した文字列が表示されます")
                } else {
                    Text(self.viewModel.voiceText)
                }
            }
            Spacer()
            Button {
                self.viewModel.toggleRecording()
            } label: {
                Image(systemName: self.viewModel.audioRunning ? "stop.circle" : "record.circle")
                    .font(.system(size: 120))
                    .foregroundColor(self.viewModel.audioRunning ? Color.black : Color.red)
            }
        }
    }
}

処理部分

import Foundation
import Speech

class LiveAudioViewModel: NSObject, ObservableObject {
    // 音声入力した文字列
    @Published var voiceText: String = ""
    // 音声入力中かどうか
    @Published var audioRunning: Bool = false
    
    private var audioEngine = AVAudioEngine()
    private var speechRecognizer: SFSpeechRecognizer?
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    
    func toggleRecording() {
        if self.audioEngine.isRunning {
            self.stopRecording()
        }
        else{
            self.startRecording()
        }
    }
    
    // 音声認識中止処理
    private func stopRecording() {
        self.recognitionTask?.cancel()
        self.recognitionTask?.finish()
        self.recognitionTask = nil
        
        self.recognitionRequest?.endAudio()
        self.recognitionRequest = nil
        
        self.audioEngine.stop()
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(AVAudioSession.Category.playback)
            try audioSession.setMode(AVAudioSession.Mode.default)
        } catch {
            print("AVAudioSession error")
        }
        self.audioRunning = false
    }
    
    // 音声認識開始処理
    private func startRecording() {
        audioSessionRecordMode()
        recognizeVoice()
        startAudioEngine()
    }
    
    // audioSessionの録音モード
    private func audioSessionRecordMode() {
        // AVAudioSessionのインスタンス化
        let audioSession = AVAudioSession.sharedInstance()
        do {
            // モードとして、音声入力モード
            try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
            // AVAudioSessionをアクティブにする(モードは他の非アクティブなアプリを知らせる)
            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        } catch {
            print("AVAudioSession error")
        }
    }
    
    // audioEngineの開始メソッド
    private func startAudioEngine() {
        self.audioEngine.prepare()
        do {
            try self.audioEngine.start()
        } catch {
            print("AudioEngine error")
        }
        self.audioRunning = true
    }
    
    // 音声認識の処理の準備と認識開始処理
    private func recognizeVoice() {
        self.speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))
        guard let speechRecognizer = self.speechRecognizer else {
            print("speechRecognizer nil")
            return
        }
        
        self.recognitionTask = SFSpeechRecognitionTask()
        self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        guard let recognitionRequest = self.recognitionRequest else {
            self.stopRecording()
            return
        }
        recognitionRequest.shouldReportPartialResults = true
        if #available(iOS 13, *) {
            recognitionRequest.requiresOnDeviceRecognition = false
        }
        
        // 入力用のノードを取得
        let inputNode = audioEngine.inputNode
        inputNode.removeTap(onBus: 0)
        // 0番のaudio node busを表すインスタンス
        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0,
                             bufferSize: 1024,
                             format: recordingFormat) { (buffer: AVAudioPCMBuffer, _: AVAudioTime) in
            recognitionRequest.append(buffer)
        }
        
        self.voiceText = ""
        self.recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in
            if(error != nil){
                print (String(describing: error))
                self.stopRecording()
                return
            }
            var isFinal = false
            if let result = result {
                isFinal = result.isFinal
                self.voiceText = result.bestTranscription.formattedString
                print(result.bestTranscription.formattedString)
            }
            if isFinal {
                print("recording time limit")
                self.stopRecording()
                inputNode.removeTap(onBus: 0)
            }
        }
    }
}

デモ動画

詳細

結論から言うと、以下の手順で実装するわけですな。

  1. AudioSessionを使用して、OSの音響システムにアクセスする準備をする
  2. AVAudioEngineを使用して、データをやり取りするための経路を確保する
  3. SFSpeechRecognizerを使用して、音声認証をするための初期設定を行う
  4. SFSpeechAudioBufferRecognitionRequestを使用して、音声認証をデータを保存するためのバッファを確保する
  5. SFSpeechRecognizerのrecognitionTask(with:resultHandler:)メソッドを使用して音声認証処理を行う

AudioSession

詳細はApple公式ページ(AVAudioSession)を参照。また、本サイトのAudioSessionって?も参照。
AVAudioSessionは、OSとアプリの仲介の役割をしてくれる。
そのため、専門的な知識などなくても、簡単に音響関係の機能が使えるらしい。(余談だが、tvOSではサポート外。)
使い方としては、シングルトンクラスなので、AVAudioSession.sharedInstance()で初期化し、setCategoryメソッドでカテゴリの設定をする。
そして、その処理をアプリ起動時に行うものらしい。

今回の場合、入力するからカテゴリはrecordとなるわけですな。
そして、setActiveメソッドでtrueを設定することで、アクティブ状態にすると。

AVAudioEngine

詳細はApple公式ページ(AVAudioEngine)を参照。また、本サイトのAudioSessionって?も参照。
AVAudioEngineはAVAudioNodeをもっている。このオブジェクトは音響関連の機能がある。

今回の場合、まずAVAudioEngineの入力のノードを取得(audioEngine.inputNode)して、
出力のバス(システムのモジュール間で、データや制御情報などをやり取りするための専用の通信路)からタップ(ローカルネットワーク上のイベントを監視するもの)を取り除く。

SFSpeechRecognitionTask

詳細はApple公式ページ(SFSpeechRecognitionTask)を参照。
speech recognition taskの状態を決めたり、進行中のタスクをキャンセルしたり、終了を検知したりする。
cancelメソッドを使用すると、現在のspeech recognition taskをキャンセルし、
finishメソッドを使用すると、新しくaudioを追加しなくなる。

SFSpeechRecognizer

詳細はApple公式ページ(SFSpeechRecognizer)を参照。
speech recognizer processを使うためのオブジェクト。
主に以下の処理を行う。

  • speech recognition servicesを使うための認証を行う
  • recognition process(認証処理)の言語設定を行う
  • recognition tasks(認証タスク)の初期化

具体的に使う時は以下の手順を踏む

  1. 音声認識の認証を行う
  2. SFSpeechRecognizerのインスタンスを生成する
  3. speech recognizer objectのisAvailableプロパティを使用しているサービスの可用性の確認
  4. 音響のコンテンツを準備
  5. recognition request objectを生成する。
  6.  recognitionTask(with:delegate:) もしくはrecognitionTask(with:resultHandler:)メソッドを読んで認証処理を開始する

そして、認証の時間はおおよそ一分間らしい。

recognitionTask(with:resultHandler:)メソッドは認証処理を行う。その中のレスポンス?はSFSpeechRecognitionResult型。認証した文字列は「bestTranscription.formattedString」に格納されている。

SFSpeechAudioBufferRecognitionRequest

詳細はApple公式ページ(SFSpeechAudioBufferRecognitionRequest)を参照。
リアルタイムで音声認識を行うためのオブジェクト。もしくはaudioバッファをセットする。
マイクから音声を入力する際に使う。

参考ページ:
SERVERNOTE.NET【Swift UI】マイク使用許可を得て音声をテキストに変換する(音声認識)