【Flutter_iOS】TeachableMachineを使って簡単に音声認識アプリを実装する

## はじめに
TeachableMachineでは、ノーコードで音声認識を簡単に実装することができます。今回はそれをアプリに取り込む方法をお伝えします。

## 今回作るもの
ボタンを押すと録音が開始し、完了すると音を分類してその割合を表示するアプリを作りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/7ee943d3-ffe4-bd5b-827d-ccb2302d9522.png)


## 開発環境
- Flutter: 3.14.0
- Dart: 3.2.0
- tflite_audio: 0.3.0

## TeachableMachineで音声を学習させる
TeachableMachineを用いて音声を学習します。学習の仕方は以下の記事で説明しています。ご参照ください。

https://qiita.com/TajimaMiho/items/b865a05bea69bf70e107


## 学習した音声のデータをエクスポートする
以下の手順で`Tensorflow Lite`をエクスポートしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/689082ca-2ce4-6662-6d72-f500c8332f41.png)

## riveライブラリの追加
以下のコマンドを実行するか、手動でpubspec.yamlに指定のバージョンを追記してください。
`flutter pub add tflite_audio`

https://pub.dev/packages/tflite_audio/install

## ダウンロードしたファイルを追加
`/assets`ディレクトリを作成し、TeachableMachineからダウンロードしたフォルダの中にある`.txt`, `.tflite`ファイルを挿入します。名前は`labels.txt`, `soundclassifier`にしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/08253e04-6058-9e53-9045-06fdaff25762.png" width = 50%>

また、pubspec.yamlに以下の記述を追加してください。
```yaml
flutter:
  uses-material-design: true
  # 以下を追記
  assets:
    - assets/labels.txt
    - assets/soundclassifier.tflite
```

## iOSの設定
① `ios/Runner/Info.plist`に以下を追記してください。
```
	<key>NSMicrophoneUsageDescription</key>
	<string>録音するためにマイクへの許可をお願いします。</string>
```

② `ios/Podfile`で以下を編集してください
- `platform :ios`を`12.0`以上にする
```
platform :ios, '12.0'
```
- `pod'TensorFlowLiteSelectTfOps','~> 2.6.0'`を以下に追記
```
target 'Runner' do
  use_frameworks!
  use_modular_headers!
  　#　追記箇所
  pod'TensorFlowLiteSelectTfOps','~> 2.6.0'

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end
```
③ `Targets > Runner > BuildSettings > All and add the following line to Linking > Other Linker`に以下を追加

```
![スクリーンショット 2024-03-16 1.01.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/60853eea-e81a-e58e-80b4-dab1110327e6.png)
-force_load $(SRCROOT)/Pods/TensorFlowLiteSelectTfOps/Frameworks/TensorFlowLiteSelectTfOps.framework/TensorFlowLiteSelectTfOps
```
![スクリーンショット 2024-03-16 1.01.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/5cae91ee-dcb0-ff7d-710b-3c055837524d.png)


④ `ios/Flutter/Release.xcconfig`に以下を追記
```
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
```

⑤ `/iOS`ディレクトリで以下を実行
```
$ flutter pub get
$ pod install
$ flutter clean
```

## main.dartで音声認識の実装
音声認識のコードは以下のようになっています。

```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tflite_audio/tflite_audio.dart';
import 'dart:convert'; // JSON文字列を扱うために必要

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData.dark(),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _sound = "Press the button to start";
  bool _recording = false;
  late Stream<Map<dynamic, dynamic>> result;

  @override
  void initState() {
    super.initState();
    TfliteAudio.loadModel(
      model: 'assets/soundclassifier.tflite',
      label: 'assets/labels.txt',
      outputRawScores: true, // Rawスコア(それぞれの割合を出力)を出力する
      numThreads: 1,
      isAsset: true,
      inputType: 'rawAudio',　// Rawスコア(それぞれの割合を出力)を出力する
    );
  }

  void _recorder() {
    if (!_recording) {
      setState(() => _recording = true);
      result = TfliteAudio.startAudioRecognition(
        numOfInferences: 1,
        sampleRate: 44100,
        bufferSize: 22016,
      );
      result.listen((event) async {
        // ラベルリストを非同期で取得
        final labels = await fetchLabelList();
        // recognitionResultからスコアのリストを取得
        final rawScores = json.decode(event["recognitionResult"]);
        List<double> scores = List<double>.from(rawScores);

        // スコアをパーセント表示に変換し、それぞれのラベルと結合する
        String recognitionResults = "";
        for (int i = 0; i < scores.length; i++) {
          recognitionResults +=
              "${labels[i]}: ${(scores[i] * 100).toStringAsFixed(2)}%\n";
        }

        setState(() {
          _recording = false;
          _sound = recognitionResults;
        });
      });
    }
  }

  Future<List<String>> fetchLabelList() async {
    final labelData = await rootBundle.loadString('assets/labels.txt');
    return LineSplitter().convert(labelData);
  }

  void _stop() {
    TfliteAudio.stopAudioRecognition();
    setState(() {
      _recording = false;
      _sound = "Press the button to start";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Container(
                padding: EdgeInsets.all(20),
              ),
              MaterialButton(
                onPressed: _recorder,
                color: _recording ? Colors.grey : Colors.pink,
                textColor: Colors.white,
                child: Icon(Icons.mic, size: 60),
                shape: CircleBorder(),
                padding: EdgeInsets.all(25),
              ),
              Text(
                _sound,
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.headline5,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

## エラー対処
実行時に
`error (xcode): sandbox: dart(52307) deny(1) file-write-create`
というエラーが出た場合、
xcodeの`Build Settings`の`User Script Sandboxing (ENABLE_USER_SCRIPT_SANDBOXING)` を `No`にすると解決しました。

https://zenn.dev/feb19/articles/c3e6ec545383bb

## 最後に
TeachableMachineを使用するとこんなにも簡単にアプリで音声認識を実装することができました。色々なツールを組み合わせてアプリを作るのは楽しいですね。これからも面白いものにたくさん触れていこうと思います。