この記事の内容
AndroidでWAVファイルの音を再生するだけであればMediaPlayerクラスなどが使えますが、
WAVデータを「事前に」読み込んでデータをいじりたい場合があります。
「事前に」というのは「音を再生する前に」という意味です。
というのもMediaPlayerクラスを使えばVisualizer.setDataCaptureListenerで生データとFFTを取得できますが、
これは再生中の音なのでリアルタイム処理になってしまいます。
外部リンク:VISUALIZERを使ってイコライザーや波形を表示する
そこで本記事ではWAVデータを事前にバイト配列で読み込んでヘッダやらに注意しながら扱う方法を紹介します。
※適当に調べてサクッと作ったものなので、もっと簡単な方法があれば教えてください。
WAVデータの読み込み
res/rawフォルダにWAVファイル(例:music.wav)が格納されていればInputStreamでバイト配列を取得できます。
try {
InputStream is = getResources().openRawResource(R.raw.music);
wavData = new byte[is.available()];
String readBytes = String.format(Locale.US, "read bytes = %d", is.read(wavData));
Log.e(TAG, readBytes);
is.close();
} catch (Exception e){
e.printStackTrace();
}
参考外部リンク: WAV音声ファイルをAudioTrackで再生する
ヘッダの分析
WAVファイルはヘッダが44バイトほどあり、そのあとにデータが続くという形式になっています。
ただ、この44バイトというのが色々追加されたりすることがあるようなので、きちんとヘッダの中身を確認しながらデータを取得したほうがよさそうです。
参考外部リンク:音ファイル(拡張子:WAVファイル)のデータ構造について
ヘッダの種類はchunkと呼ばれており、'fmt'と'data'がわかればよさそうです。(詳細は上の外部リンクを見てもらえればと思います)
そこでこの'fmt ' == 0x6d7420と'data'==0x64617461を確認してそれぞれのインデックスを保存しておきます。
int fmtIdx = 0;
for(int i = 0; i < wavData.length - 4; i ++){
if(wavData[i] == 0x66 && wavData[i + 1] == 0x6d
&& wavData[i + 2] == 0x74 && wavData[i + 3] == 0x20){ // 'fmt ' chunk
fmtIdx = i;
Log.i("Test", "fmtIdx:" + fmtIdx);
break;
}
}
if(fmtIdx == 0){
Log.e(TAG, "No fmt chunk");
}
int dataIdx = 0;
for(int i = 0; i < wavData.length - 4; i ++){
if(wavData[i] == 0x64 && wavData[i + 1] == 0x61
&& wavData[i + 2] == 0x74 && wavData[i + 3] == 0x61){ // 'data' chunk
dataIdx = i;
Log.i("Test", "dataIdx:" + dataIdx);
break;
}
}
if(dataIdx == 0){
Log.e(TAG, "No data chunk");
}
次にチャネル数、サンプリングレート、ビット数(ここではバイト数)、データサイズを取得しましょう。
データサイズは基本的にはヘッダを除いた部分ということですが、WAVフォーマットはフッタも自由につけてよいということらしいので取得しておきます。
実際に私が試した音源ではフッタが結構な量で含まれていました。
int wavChannel = (int)(wavData[fmtIdx + 10]);
Log.i("Test", "wavChannel:" + wavChannel);
//int wavSamplingRate = ((int)(wavData[fmtIdx + 15]) << 24) + ((int)(wavData[fmtIdx + 14]) << 16)
// + ((int)(wavData[fmtIdx + 13]) << 8) + (int)(wavData[fmtIdx + 12]);
// ↑の書き方はよくなさそうなので修正
byte[] bytes1 = {wavData[fmtIdx + 15], wavData[fmtIdx + 14], wavData[fmtIdx + 13], wavData[fmtIdx + 12]};
int wavSamplingRate = ByteBuffer.wrap(bytes1).getInt();
Log.i("Test", "wavSamplingRate:" + wavSamplingRate);
int wavByte = (int)(wavData[fmtIdx + 22]) / 8;
Log.i("Test", "wavByte:" + wavByte);
//int wavDataSize = ((int)(wavData[dataIdx + 7]) << 24) + ((int)(wavData[dataIdx + 6]) << 16)
// + ((int)(wavData[dataIdx + 5]) << 8) + (int)(wavData[dataIdx + 4]);
byte[] bytes2 = {wavData[dataIdx + 7], wavData[dataIdx + 6], wavData[dataIdx + 5], wavData[dataIdx + 4]};
int wavDataSize = ByteBuffer.wrap(bytes2).getInt();
Log.i("Test", "wavDataSize:" + wavDataSize);
int wavHeaderSize = dataIdx + 8;
int[] musicDataRight = new int[wavDataSize / wavByte / wavChannel];
int[] musicDataLeft = new int[wavDataSize / wavByte / wavChannel];
4バイトで格納されているところはシフト演算ByteBufferでintにしています。
また、あとで使うのでヘッダサイズを計算しています。
チャネルが2のときは左右の音源が取得できますので、これをmusicDataRight、musicDataLeftに入れていきます。
byte[] bytes_temp = {0, 0, 0, 0}; // 4byteないとBufferUnderflowExceptionになる
if(wavByte == 1 && wavChannel == 1){
for(int i = 0, j = wavHeaderSize; i < musicDataRight.length; i ++, j ++){
musicDataRight[i] = (int)wavData[j];
musicDataLeft[i] = (int)wavData[j];
}
} else if(wavByte == 1 && wavChannel == 2){
for(int i = 0, j = wavHeaderSize; i < musicDataRight.length; i ++, j += 2){
musicDataRight[i] = (int)wavData[j];
musicDataLeft[i] = (int)wavData[j + 1];
}
} else if(wavByte == 2 && wavChannel == 1){
for(int i = 0, j = wavHeaderSize; i < musicDataRight.length; i ++, j += 2){
//musicDataRight[i] = ((int)wavData[j + 1] << 8) + (int)wavData[j];
//musicDataLeft[i] = ((int)wavData[j + 1] << 8) + (int)wavData[j];
bytes_temp[2] = wavData[j + 1];
bytes_temp[3] = wavData[j];
musicDataRight[i] = ByteBuffer.wrap(bytes_temp).getInt();
musicDataLeft[i] = ByteBuffer.wrap(bytes_temp).getInt();
}
} else if(wavByte == 2 && wavChannel == 2){
for(int i = 0, j = wavHeaderSize; i < musicDataRight.length; i ++, j += 4){
//musicDataRight[i] = ((int)wavData[j + 1] << 8) + (int)wavData[j];
//musicDataLeft[i] = ((int)wavData[j + 3] << 8) + (int)wavData[j + 2];
bytes_temp[2] = wavData[j + 1];
bytes_temp[3] = wavData[j];
musicDataRight[i] = ByteBuffer.wrap(bytes_temp).getInt();
bytes_temp[2] = wavData[j + 3];
bytes_temp[3] = wavData[j + 2];
musicDataLeft[i] = ByteBuffer.wrap(bytes_temp).getInt();
}
}
チャネル数とバイト数別にデータを格納しています。
やろうと思えば式をまとめることもできる(実際やってみた)のですが、可読性が落ちてしまうので分けて書いています。
チャネル数が1のときは左右に同じ音を入れています。
また、チャネル数やバイト数が3以上の場合は別途追加する必要があります。
以上でWAVデータをバイト配列で読み込む手順の説明を終わります。
おわりに
読み込んだ波形をグラフで表示したり、データを加工したりといったことがこれで可能になります。
ただ、冒頭にも述べましたがもっと簡単な方法があれば教えてください。