THETA XでBluetooth機器のマイクを利用する。 

## はじめに

リコーの @KA-2 です。

弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
<B>RICOH THETA V</B>, <b>RICOH THETA Z1</b>, <b>RICOH THETA X</b>は、OSに<B>Android</B>を採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。


RICOH THETA Xは、[以前の記事の以下引用](https://qiita.com/KA-2/items/35d7ef550d2aead50cb4#bluetooth%E3%81%AE%E3%83%9A%E3%82%A2%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%8C%E6%A5%BD%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F)

>A2DP, AVRCP,HSPのプロファイルに対応したマイク・スピーカを接続することもできます。過去機種と比べるとマイクにも対応したのが進化です。ただし、マイクを利用するプラグインを作成しないとマイク利用はできません。

でも触れていたとおり、A2DP, AVRCP,HSPのプロファイルを持つBluetooth機器のマイクを利用することができます（多くのBluetoothイヤホンやスピーカーがマイクも内蔵しています）。
今回は、この方法を紹介します。


## 今回作成したサンプルの概要

マイク利用というと、THETA VやZ1の本体マイクを利用する以下の記事
<b>「[THETAプラグインでマイクを使って録音する #thetaplugin 【追記あり】](https://qiita.com/kushimoto/items/e2ee2b1abd05dc50ffcf)」</b>
のように、「録音（ファイル保存）する」「再生する」というサンプルが思い浮かびます。

しかし、上記例では、ファイル保存をMediaRecorderに任せていますので、ファイル保存のコードを簡単に書けるメリットがあるものの、音データを自身でサンプリングし、そのデータを自身でFFT処理したり、音声認識など他のライブラリへ流し込みたい場合には向きません。

そこで、今回は、

- <b>Bluetoothのマイクを利用する手続きをする</b>
- <b>AudioRecordを利用して、音データを自身でサンプリングする</b>
- <b>サンプリングした音データを直ぐにスピーカ出力へ流す</b>

というサンプルにしてみました。

単に「Bluetoothのマイクを扱う」というだけでなく、音データの取り扱い方のバリエーションを広げて頂けると幸いです。
なお、「Bluetoothのマイクを利用する手続き」の部分を[前述の記事](https://qiita.com/kushimoto/items/e2ee2b1abd05dc50ffcf)に移植すれば、Bluetooth機器のマイクを利用した録音&再生のサンプルにもなりますのでご安心ください。この記事が理解できれば、その作業はご自身の力だけでできるはずです。

また、画面があるTHETA Xのサンプルではありますが、今回のサンプルでは画面を作成していません。
ボタン操作だけで動作確認ができるサンプルとなっています。


## ソースコード

[こちらのプロジェクト一式](https://github.com/theta-skunkworks/theta-plugin-x-bt-mic-sample)を参照してください。

### ファイル構成

[THETA Plug-in SDK](https://github.com/ricohapi/theta-plugin-sdk)をベースに作業しています。
現在では、このサンプルを作成したときよりもバージョンが上がっていますので、新しいSDKをベースに作業されることをおすすめします。

新規作成、または、変更を加えたファイルは以下のとおりです。

```
theta-plugin-x-bt-mic-sample\app
└src\main
　├assets　　　　　　　 // ベースとしたプロジェクトのままです。
　└java\com\theta360
　　　└pluginapplication　//以下２つのファイルがサンプルコードとなります。
　　　　　|　　　　　　　　//BluetoothUtil.javaを追加（BTマイク利用の要のクラス）
　　　　　|　　　　　　　　//MainActivity.javaに手を加えました（サンプルコード本体）
　　　　　|　
　　　　　├model　　 　　　// 未使用:ベースとしたプロジェクトのままです。
　　　　　├network　 　　　// 未使用:ベースとしたプロジェクトのままです。
　　　　　└task　　　　　　// 未使用:ベースとしたプロジェクトのままです。
```



### パーミッションを定義する。

```AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
```
上の2行は元々設定されていたパーミッションです。
下の2行をオーディオを取り扱うために追加しました。

### Bluetoothマイクとの接続(BluetoothUtil.javaの作成)

Bluetooth機器のマイクは、ペアリングし接続が完了しただけでは利用できません。
利用開始時には、AudioManager#startBluetoothSco()を実行する必要があり、
利用終了時には、AudioManager#stopBluetoothSco()を実行する必要があります。

これらのことは、[Googleのこちらのドキュメント](https://developer.android.com/reference/android/media/AudioManager#startBluetoothSco())に記載があります。

特にstartBluetoothSco()を実行したあと、オーディオ機器が有効になるまでには数秒の時間を要すことがあるため、インテントを監視する必要があります。
また、一度接続に失敗したからといって接続を諦めることもなく、作り手都合でリトライをさせることができます。

今回のサンプルでは、こういったBluetooth機器のマイク利用に関する処理を。
[BluetoothUtilというクラス](https://github.com/theta-skunkworks/theta-plugin-x-bt-mic-sample/blob/main/app/src/main/java/com/theta360/pluginapplication/BluetoothUtil.java)にまとめました。
大抵の場合、ご自身でこの部分のコードを書く必要はなく、このクラスを利用して頂けると思います。

なお、今回の例では約500msの待ち時間を挟んで5回までのリトライをするように作成してあります。

- 5回の接続を試みても接続できなかった場合→onError()
- 5回までのトライの間に接続できた場合→onSuccess()

というインターフェースメソッドを介して接続の結果を受け取れます。
このインターフェースは、MainActivityにimplementsして利用しますので、次の説明を参照してください。

### MainActivity(PluginActivity)にBluetoothUtil.IBluetoothConnectListenerをimplementsする

前述のインターフェースをMainActivityにimplementsします。

```MainActivity.java
public class MainActivity extends PluginActivity implements BluetoothUtil.IBluetoothConnectListener {
```

Android Studioのエディタでは、行全体に赤く波線が付くと思います。
その後、行を選択すると左端にヒントがでて、「Implements methods」が選択できる状態となります。
クリックすると、記述が必要なメソッドのスケルトンがファイル末尾に追加されます。
追加されるのは以下2つのメソッドです。

```MainActivity.java
    @Override
    public void onError(String error) {

    }

    @Override
    public void onSuccess() {

    }
```

どちらのメソッドについても、Bluetooth機器のマイクを有効にする手続きをした後に呼ばれます。
前述のとおり、onError()は手続きに失敗した場合、onSuccess()は手続きに成功した場合です。

[今回のサンプル](https://github.com/theta-skunkworks/theta-plugin-x-bt-mic-sample/blob/539590c38e1bbc280f4766f454d257c72d3a5dcc/app/src/main/java/com/theta360/pluginapplication/MainActivity.java#L179-L189)では、onError()が呼ばれてもonSuccess()が呼ばれても、サンプリングの処理を開始するようにしています。
前者の場合本体マイクが利用され、後者の場合Bluetooth機器のマイクが利用されるという振る舞いの違いになります。


### サンプリング処理の開始

サンプリングには、[AudioRecord](https://developer.android.com/reference/android/media/AudioRecord)というクラスを利用しています。
[startRecord()というメソッド](https://github.com/theta-skunkworks/theta-plugin-x-bt-mic-sample/blob/539590c38e1bbc280f4766f454d257c72d3a5dcc/app/src/main/java/com/theta360/pluginapplication/MainActivity.java#L141-L171)にまとめてあります。

以下コードで、AudioRecordのインスタンスを作成しサンプリングを開始したあと、
```MainActivity.java
        //AudioRecordの作成
        audioRec = new AudioRecord(
                MediaRecorder.AudioSource.MIC,
                SAMPLING_RATE,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT,
                bufSize * 2);

        audioRec.startRecording();
```

スレッドのwhile文内の以下コードでサンプリングをしています。

```MainActivity.java
int numRead = audioRec.read(buf, 0, buf.length);
```

厳密には、AudioRecordのインスタンスが管理しているサンプリング用のバッファがあふれる前に、バッファからデータを読みだしているという処理となります。
したがって、読みだしたデータにFFTをかける、音声認識をかけるといった処理がしたい場合には、データを処理用のバッファにコピーしたあと、別のスレッドで演算処理を行わせることをお勧めします。

参考として、今回のサンプルコードは、バッファから読みだしたデータを直ぐにスピーカーに流していますが、再生される音に1～2秒のディレイが生じます。このディレイはサンプリング用のバッファで生じていると思われます。
（アプリケーションレベルではサンプリング周期を守れませんから、AudioRecordにてサンプリング周期を守り、そのデータをバッファしている都合から生じているディレイと予想できます。）


### サンプリング処理の停止

[stopRecord()というメソッド](https://github.com/theta-skunkworks/theta-plugin-x-bt-mic-sample/blob/539590c38e1bbc280f4766f454d257c72d3a5dcc/app/src/main/java/com/theta360/pluginapplication/MainActivity.java#L173-L177)で、前述のwhile文を抜けさせるようにしています。

ループを抜けたあとはスレッドの中で以下を実行して

```MainActivity.java
                audioRec.stop();
                audioRec.release();
```

リソースを開放しています。


### サンプリングした音データの再生

[Googleが提供しているaudioTrack](https://developer.android.com/reference/android/media/AudioTrack)というクラスを利用しています。

onCreateの以下コードで再生の準備をしておき

```MainActivity.java
        audioTrack = new AudioTrack(AudioManager.STREAM_VOICE_CALL, SAMPLING_RATE,
                AudioFormat.CHANNEL_OUT_MONO,
                AudioFormat.ENCODING_PCM_16BIT, bufSize, AudioTrack.MODE_STREAM);
        audioTrack.play();
```

音データを取得するループの以下コードでデータをスピーカーに流しています。

```MainActivity.java
                    if (numRead > 0) {
                        audioTrack.write(buf, 0, numRead);
                    }
```

リソースの開放はonPause()の以下コードで行っています。
```MainActivity.java
        audioTrack.stop();
        audioTrack.release();
```

### その他注意点

- 今回のサンプルでは、本体スピーカーを使用することを想定していないため、Bluetooth機器を接続していない状態では再生音が聞こえません。（本体内蔵スピーカーの特性のため致し方ないこととなります）

- 動作確認の手順は「THETA Xを起動する」「Bluetooth機器を正規の手順で接続する」「サンプルプラグインを起動し動作させる（シャッターボタン押下でサンプリング開始/停止）」です。プラグインを動かす前にBluetooth機器を接続しておいてください。

- ハウリングに注意してください。Bluetooth機器接続後かつプラグイン起動前に、スピーカー音量をある程度抑えておくことをお勧めします。

- 本体マイクが使われているのか、Bluetooth機器のマイクが使われているのか判断がしにくい場合、
それぞれの筐体を爪などで軽くコンコンと叩くとよいです。マイクが使われている筐体をコンコンとした場合大きな音になります。


## おわりに

THETA X にて、A2DP, AVRCP,HSPのプロファイルに対応したBluetooth機器のマイクを利用することができました。また、AudioRecordによってサンプリングしたデータの利用幅が広がります。記事中に記載したようにFFTをかけたり音声認識を行うだけでなく、映像と共にライブ配信に音データを流すこともできます。（AudioRecordの部分はTHETA VやTHETA Z1で本体マイクを利用する時にも応用できます）

THETAの映像だけでなく、外部マイクや内蔵マイクも併せた利用方法が増えていくと嬉しいです。色々なTHETAプラグイン作成にトライしてみてください。


## RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方は[こちら](https://github.com/ricohapi/theta-api-specs/blob/main/core/products/theta-plugin.md)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発者コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡