THETAプラグインでマイクを使って録音する #thetaplugin 【追記あり】

**2019/1/21 追記: [設定を変更することで音量を大きくすることができました！](#2019121-追記設定を変更することで音量を少し大きくすることができました)**
**2018/10/9 追記: [THETAにスピーカーを接続して大きな音量で再生できることを確認しました！](#2018109-追記thetaにスピーカーを接続してみた)**

リコーの @kushimoto です。

いきなりですが、[RICOH THETA V](https://theta360.com/ja/about/theta/v.html)は、AndroidベースのOSなので、アプリをインストールすることで機能拡張でき、THETA界隈ではアプリのことをプラグインと呼んでいます。

THETAプラグインをご存じない方は[こちら](https://github.com/ricohapi/theta-api-specs/blob/main/core/products/theta-plugin.md)をご覧ください。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発コミュニティ（Slack）](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNDU5Nzg4MTg4NzA2LTYyOGQ1YjRkMTllY2RhNGE2ZTNhNjIxZDY4MzgyZjYyOWYxYmM4OWE5MjZlM2QwZGQ4ZTIyZDc1MjQ0ODcwZjM)への参加もよろしくお願いします｡

この記事では、THETAプラグインでマイクを使って録音する方法を紹介します。

## ポイント

ポイントとしては以下の2点です。

1. モノラルで録音するように設定する
2. オーディオ設定を変更する権限を追加する

### 1. モノラルで録音するように設定する

THETA Vは4chマイクによる360°空間音声の記録が可能です。
[Web API](https://github.com/ricohapi/theta-api-specs/blob/main/ricoh-theta-plugin/web-api.md)を使って動画撮影すると空間音声付きの動画を取得できますし、[Camera API](https://github.com/ricohapi/theta-api-specs/blob/main/ricoh-theta-plugin/camera-api.md)を使って動画撮影すると空間音声のwavファイルを取得することも可能です（Camera APIに関しては今後コミュニティのメンバーが記事にする予定です）。
ただし、スタンダードなAndroidのAPIを利用する場合は、360°空間音声には対応していないため、モノラルで録音する必要があります。

THETA Vは[AudioManager](https://developer.android.com/reference/android/media/AudioManager) APIに[いくつかのパラメータを追加](https://github.com/ricohapi/theta-api-specs/blob/main/ricoh-theta-plugin/audio-manager-api.md)しています。

**B-format Selection**パラメータを利用すると、モノラルで録音するように設定できます。

モノラルに設定するには、下記のように、マイクを使う前にAudioManagerの[setParameters()](https://developer.android.com/reference/android/media/AudioManager.html#setParameters(java.lang.String))に`"RicUseBFormat=false"`を渡します。

```java
    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    audioManager.setParameters("RicUseBFormat=false");
```

### 2. オーディオ設定を変更する権限を追加する

1.のようにAudioManagerを使ってオーディオの設定を変更するので、`AndroidManifest.xml`に下記の権限を追加します（`RECORD_AUDIO`も必ず必要になるので記載しました）。

```xml
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
```

## サンプルコード

以上のポイントをおさえれば、あとは一般的なAndroidアプリ開発と同じです。

この記事ではサンプルコードとして、[MediaRecorder](https://developer.android.com/reference/android/media/MediaRecorder)を使って録音するプラグインを作ってみました。

ソースコードはこの記事の下部にあります。

無線ボタンを押すと録音開始し、もう一度無線ボタンを押すと録音終了します。
録音した音声はシャッターボタンを押すと再生できます。

サンプルコードを動かしていただくとわかるのですが、再生される音量は結構小さめです。 → [大きくできました！【2019/1/21 追記】](#2019121-追記設定を変更することで音量を少し大きくすることができました)

これはTHETA Vのスピーカーはオリジナルの操作音を再生するためだけに設計されているからです。

電子音はそれなりに鳴らせますが、今回のような録音した自然音には強くないので、スピーカーを使ったプラグインを作る際にはご注意ください。

動かしてみるとこんな感じです（↓はYouTubeへのリンクです）。

[![YouTube動画](http://img.youtube.com/vi/0ipgLlGikMM/0.jpg)](https://www.youtube.com/watch?v=0ipgLlGikMM)

### 【2019/1/21 追記】設定を変更することで音量を少し大きくすることができました

@shrhdk_ さんから下記の設定にすると音量が大きくなるとアドバイスをいただきました。

* 録音時の[サンプリング周波数](https://developer.android.com/reference/android/media/MediaRecorder#setAudioSamplingRate(int))を44.1kHzにする
* 再生時の[StreamType](https://developer.android.com/reference/android/media/AudioAttributes.Builder#setLegacyStreamType(int))を[STREAM_RING](https://developer.android.com/reference/android/media/AudioManager#STREAM_RING)または[STREAM_ALARM](https://developer.android.com/reference/android/media/AudioManager#STREAM_ALARM)にする

シャッター音ほどではありませんが確かに大きくなりました！

ソースコードは修正済みなのでぜひお試しください。

### 【2018/10/9 追記】THETAにスピーカーを接続してみた

THETAで再生すると音量が小さかったので、THETAにUSB OTG経由でスピーカーを接続してみました（↓はYouTubeへのリンクです）。

[![YouTube動画](http://img.youtube.com/vi/eIyVM0iEQLY/0.jpg)](https://www.youtube.com/watch?v=eIyVM0iEQLY)
大きな音量で再生することができました！

今回使用した機材は↓です。

* USB DAC（USB Audio Class 1.0のもの）
* USB OTGアダプター（USB DACがType Aだったのでmicro Bに変換）
* アナログスピーカー（アンプとバッテリーを内蔵）

この実験では、ソースコードを変更することなく、外部スピーカーで再生できています。
つまり、OTG対応のUSB機器をそのままTHETAで使うことができる！ことがわかりました。
もちろん、製品によって動く動かないはあるので、各人で実験してみてください！

**これらの機材での動作を保証しているわけではないのでご注意ください。**

## まとめ

この記事では、THETAでマイクを使う方法を紹介しました。

THETA V固有のポイントをおさえれば、一般的なAndroidアプリと同じように開発できるので、Android開発者の方は気軽に試してみてはいかがでしょうか。

## ソースコード

```java:MainActivity.java
package com.theta360.pluginapplication;

import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.MediaRecorder;
import android.media.MediaRecorder.AudioEncoder;
import android.media.MediaRecorder.AudioSource;
import android.media.MediaRecorder.OutputFormat;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import com.theta360.pluginlibrary.activity.PluginActivity;
import com.theta360.pluginlibrary.callback.KeyCallback;
import com.theta360.pluginlibrary.receiver.KeyReceiver;
import com.theta360.pluginlibrary.values.LedColor;
import com.theta360.pluginlibrary.values.LedTarget;
import java.io.File;

public class MainActivity extends PluginActivity {

    private static final String RECORDER_TAG = "Recorder";
    private static final String PLAYER_TAG = "Player";

    private boolean isRecording = false;
    private MediaRecorder mediaRecorder;
    private String soundFilePath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        soundFilePath = getFilesDir() + File.separator + "mySound.wav";

        setKeyCallback(new KeyCallback() {
            @Override
            public void onKeyDown(int keyCode, KeyEvent event) {
                if (keyCode == KeyReceiver.KEYCODE_WLAN_ON_OFF) {
                    if (!isRecording) {
                        startRecorder();
                        notificationLedBlink(LedTarget.LED7, LedColor.RED, 2000);
                    } else {
                        stopRecorder();
                        notificationLedHide(LedTarget.LED7);
                    }
                } else if (keyCode == KeyReceiver.KEYCODE_CAMERA && !isRecording) {
                    startPlayer();
                }
            }

            @Override
            public void onKeyUp(int keyCode, KeyEvent event) {
            }

            @Override
            public void onKeyLongPress(int keyCode, KeyEvent event) {
            }
        });
    }

    @Override
    protected void onPause() {
        super.onPause();
        releaseMediaRecorder();
    }

    private void startRecorder() {
        new MediaRecorderPrepareTask().execute();
    }

    private void stopRecorder() {
        try {
            mediaRecorder.stop();
        } catch (RuntimeException e) {
            Log.d(RECORDER_TAG, "RuntimeException: stop() is called immediately after start()");
            deleteSoundFile();
        } finally {
            isRecording = false;
            releaseMediaRecorder();
        }
        Log.d(RECORDER_TAG, "Stop");
    }

    private void startPlayer() {
        File file = new File(soundFilePath);
        if (!file.exists()) {
            return;
        }
        file = null;

        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        int maxVol = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING); // 2019/1/21追記
        audioManager.setStreamVolume(AudioManager.STREAM_RING, maxVol, 0); // 2019/1/21追記

        MediaPlayer mediaPlayer = new MediaPlayer();
        AudioAttributes attributes = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .setLegacyStreamType(AudioManager.STREAM_RING) // 2019/1/21追記
                .build();
        try {
            mediaPlayer.setAudioAttributes(attributes);
            mediaPlayer.setDataSource(soundFilePath);
            mediaPlayer.setVolume(1.0f, 1.0f);
            mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    mp.release();
                }
            });
            mediaPlayer.setOnPreparedListener(new OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                }
            });
            mediaPlayer.prepare();
            Log.d(PLAYER_TAG, "Start");
        } catch (Exception e) {
            Log.e(RECORDER_TAG, "Exception starting MediaPlayer: " + e.getMessage());
            mediaPlayer.release();
            notificationError("");
        }
    }

    private boolean prepareMediaRecorder() {
        Log.d(RECORDER_TAG, soundFilePath);
        deleteSoundFile();

        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        audioManager.setParameters("RicUseBFormat=false");

        mediaRecorder = new MediaRecorder();
        mediaRecorder.setAudioSource(AudioSource.MIC);
        mediaRecorder.setAudioSamplingRate(44100); // 2019/1/21追記
        mediaRecorder.setOutputFormat(OutputFormat.DEFAULT);
        mediaRecorder.setAudioEncoder(AudioEncoder.DEFAULT);
        mediaRecorder.setOutputFile(soundFilePath);

        try {
            mediaRecorder.prepare();
        } catch (Exception e) {
            Log.e(RECORDER_TAG, "Exception preparing MediaRecorder: " + e.getMessage());
            releaseMediaRecorder();
            return false;
        }

        return true;
    }

    private void releaseMediaRecorder() {
        if (mediaRecorder != null) {
            mediaRecorder.reset();
            mediaRecorder.release();
            mediaRecorder = null;
        }
    }

    private void deleteSoundFile() {
        File file = new File(soundFilePath);
        if (file.exists()) {
            file.delete();
        }
        file = null;
    }

    /**
     * Asynchronous task for preparing the {@link android.media.MediaRecorder} since it's a long
     * blocking operation.
     */
    private class MediaRecorderPrepareTask extends AsyncTask<Void, Void, Boolean> {

        @Override
        protected Boolean doInBackground(Void... voids) {
            if (prepareMediaRecorder()) {
                mediaRecorder.start();
                isRecording = true;
                return true;
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean result) {
            if (!result) {
                Log.e(RECORDER_TAG, "MediaRecorder prepare failed");
                notificationError("");
                return;
            }
            Log.d(RECORDER_TAG, "Start");
        }
    }

}

```