Edited at

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

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

2018/10/9 追記: THETAにスピーカーを接続して大きな音量で再生できることを確認しました!

リコーの @kushimoto です。

いきなりですが、RICOH THETA Vは、AndroidベースのOSなので、アプリをインストールすることで機能拡張でき、THETA界隈ではアプリのことをプラグインと呼んでいます。

THETAプラグインをご存じない方はこちらをご覧ください。

興味を持たれた方はTwitterのフォローとTHETAプラグイン開発コミュニティ(Slack)への参加もよろしくお願いします。

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


ポイント

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


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

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


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

THETA Vは4chマイクによる360°空間音声の記録が可能です。

Web APIを使って動画撮影すると空間音声付きの動画を取得できますし、Camera APIを使って動画撮影すると空間音声のwavファイルを取得することも可能です(Camera APIに関しては今後コミュニティのメンバーが記事にする予定です)。

ただし、スタンダードなAndroidのAPIを利用する場合は、360°空間音声には対応していないため、モノラルで録音する必要があります。

THETA VはAudioManager APIにいくつかのパラメータを追加しています。

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

モノラルに設定するには、下記のように、マイクを使う前にAudioManagerのsetParameters()"RicUseBFormat=false"を渡します。

    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

audioManager.setParameters("RicUseBFormat=false");


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

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

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

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


サンプルコード

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

この記事ではサンプルコードとして、MediaRecorderを使って録音するプラグインを作ってみました。

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

無線ボタンを押すと録音開始し、もう一度無線ボタンを押すと録音終了します。

録音した音声はシャッターボタンを押すと再生できます。

サンプルコードを動かしていただくとわかるのですが、再生される音量は結構小さめです。 → 大きくできました!【2019/1/21 追記】

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

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

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


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

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

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

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


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

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



大きな音量で再生することができました!

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


  • USB DAC(USB Audio Class 1.0のもの)

  • USB OTGアダプター(USB DACがType Aだったのでmicro Bに変換)

  • アナログスピーカー(アンプとバッテリーを内蔵)

この実験では、ソースコードを変更することなく、外部スピーカーで再生できています。

つまり、OTG対応のUSB機器をそのままTHETAで使うことができる!ことがわかりました。

もちろん、製品によって動く動かないはあるので、各人で実験してみてください!

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


まとめ

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

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


ソースコード


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");
}
}

}