Agoraで話すときに動くシンプルな音波を作成する

# Agoraについて
Agoraは、ビデオ、音声、対話的な放送ソリューションのAPIを提供するリアルタイムコミュニケーションプラットフォームです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991103/1048987b-c261-bf42-8bed-cf4d40469aa9.png)

# 準備
1\. 新しいプロジェクトを作成

```
flutter create agora_sample
```

2\. Agora SDKをプロジェクトに追加

```
flutter pub add agora_rtc_engine
flutter pub add permission_handler
```

3\. シンプルな音波を表示するためのパッケージを追加

```
flutter pub add loading_indicator
```

参考：https://pub.dev/packages/loading_indicator

4\. [Agora.io](https://docs.agora.io/en/) で開発者アカウントにサインアップする

**準備は完了です。はじめましょう！**

# 実装
## Agoraプロジェクトを設定
Agora.ioのプロジェクト管理画面でプロジェクトを選択して、設定画面に移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991103/048a0428-37f8-9408-3c5a-15ec5cde9913.png)

`Generate temp RTC Token`リンクをクリックしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991103/526b93da-1413-ed9c-a905-6819429eb820.png)

ここで、チャネル名を入力して、トークンを生成しましょう。このトークンは後でチャネルに参加するために使用されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2991103/96247a5b-e4a9-2f41-868b-65ef9fe1e166.png" width="400">

## 音波Widgetを作成
```sound_wave.dart
class SoundWave extends StatelessWidget {
  const SoundWave({super.key, required this.hasVoiceCome});

  final bool hasVoiceCome;

  @override
  Widget build(BuildContext context) {
    List<Color> soundWaveColors = const [
      Color(0xFFFF5500),
      Color(0xFFFF6600),
      Color(0xFFFF7700),
      Color(0xFFFF8800),
      Color(0xFFFF9900),
      Color(0xFFFAA400),
      Color(0xFFFBAD00),
      Color(0xFFFCB400),
      Color(0xFFFDBA00),
      Color(0xFFFECC00),
    ];

    return Row(
      children: List.generate(
        2,
        (index) {
          return Padding(
            padding: const EdgeInsets.only(right: 8.0),
            child: LoadingIndicator(
              indicatorType: Indicator.lineScalePulseOutRapid,
              colors: soundWaveColors.sublist(index * 5, (index + 1) * 5),
              pause: !hasVoiceCome,
            ),
          );
        },
      ),
    );
  }
}
```
## 通話画面を作成
```call_screen.dart
class CallScreen extends StatefulWidget {
  const CallScreen({super.key});

  @override
  State<CallScreen> createState() => _CallScreenState();
}

class _CallScreenState extends State<CallScreen> {
  late RtcEngine _agoraEngine;

  bool _muted = false;
  bool _speakerOff = false;
  bool _hasVoiceCome = false;
  int? _remoteUid;
  bool _isJoined = false;

  final appId = '';        // コピーしたアプリID
  final uid = 0;           // 参加するユーザーID
  final channelId = '';    // 設定したチャネルID（test)
  final token = '';        // 生成されたトークン

  @override
  void initState() {
    super.initState();
    setupVoiceSDKEngine().onError((error, stackTrace) => {
          debugPrint(error.toString()),
          debugPrint(stackTrace.toString()),
        });
  }

  @override
  void dispose() {
    super.dispose();
    _agoraEngine.leaveChannel();
  }

  void _onToggleMute() {
    setState(() {
      _muted = !_muted;
    });
    _agoraEngine.muteLocalAudioStream(_muted);
  }

  void _onToggerSpeaker() {
    setState(() {
      _speakerOff = !_speakerOff;
    });
    _agoraEngine.setEnableSpeakerphone(!_speakerOff);
  }


  void _onJoin() async {
    ChannelMediaOptions options = const ChannelMediaOptions(
      clientRoleType: ClientRoleType.clientRoleBroadcaster,
      channelProfile: ChannelProfileType.channelProfileCommunication,
    );

    try {
      await _agoraEngine.joinChannel(
        token: token,
        channelId: channelId,
        options: options,
        uid: uid,
      );
    } catch (e) {
      debugPrint(e.toString());
    }
  }
  
  void _onLeave() {
    _agoraEngine.leaveChannel();
  }

  Future<void> setupVoiceSDKEngine() async {
    try {
      // Retrieve or request microphone permission
      await [Permission.microphone].request();

      // Create an instance of the Agora engine
      _agoraEngine = createAgoraRtcEngine();
      await _agoraEngine.initialize(RtcEngineContext(appId: appId));

      // Enables the audioVolumeIndication
      await _agoraEngine.enableAudioVolumeIndication(
          interval: 250, smooth: 8, reportVad: true);

      // Register the event handler
      _agoraEngine.registerEventHandler(
        RtcEngineEventHandler(
          onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
            setState(() {
              _isJoined = true;
            });
          },
          onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
            setState(() {
              _remoteUid = remoteUid;
            });
          },
          onUserOffline: (RtcConnection connection, int remoteUid,
              UserOfflineReasonType reason) {
            setState(() {
              _remoteUid = null;
            });
          },
          onAudioVolumeIndication: (
            RtcConnection connection,
            List<AudioVolumeInfo> speakers,
            int speakerNumber,
            int totalVolume,
          ) {
            setState(() {
              _hasVoiceCome = speakers.any((speaker) => speaker.vad == 1);
            });
          },
          onError: (err, msg) => {
            debugPrint(err.toString()),
            debugPrint(msg.toString()),
          },
        ),
      );
    } catch (err) {
      debugPrint(err.toString());
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Get started with Voice Calling'),
      ),
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            SizedBox(height: 40, child: Center(child: _status())),
            Column(
              children: [
                SizedBox(
                  height: 60.0,
                  child: SoundWave(hasVoiceCome: _hasVoiceCome),
                ),
                const SizedBox(height: 40.0),
                BottomActionBar(
                  muted: _muted,
                  speakerOff: _speakerOff,
                  onToggleMute: _onToggleMute,
                  onToggleSpeaker: _onToggerSpeaker,
                  onJoin: _onJoin,
                  onLeave: _onLeave,
                ),
              ],
            )
          ],
        ),
      ),
    );
  }

  Widget _status() {
    String statusText;

    if (!_isJoined) {
      statusText = 'Join a channel';
    } else if (_remoteUid == null) {
      statusText = 'Waiting for a remote user to join...';
    } else {
      statusText = 'Connected to remote user, uid:$_remoteUid';
    }

    return Text(
      statusText,
    );
  }
}

```
* `enableAudioVolumeIndication(interval:, smooth:, reportVad:)`: ユーザーの音量インジケーターを報告するようにするために使用されます。
    * `smooth`はオーディオボリュームインジケーターの感度です。 値の範囲は 0 ～ 10 です。
    * `reportVad`: ローカルユーザーの音声アクティビティの検出を有効にします。 有効にすると、`onAudioVolumeIndication`コールバックの`vad`パラメータがローカルユーザーの音声アクティビティ ステータスを報告します。
* `onAudioVolumeIndication`: `_hasVoiceCome = speakers.any((speaker) => speaker.vad == 1)`: 少なくとも1人の話者が話しているかどうかを確認します。

**実装が完了しました。試してみましょう！**

# 結果
https://webdemo.agora.io/basicVideoCall/index.html

会話に参加するには、上記のサイトにアクセスし、以前に設定したチャネルに関する情報を入力します。

そして結果を体験しましょう！:raised_hands:

**終わり。**