今までは音量が最大1になるようなオシレーターをwavファイルのフォーマットに合わせて最大音量にしていました。しかし今後複数のオシレーターを重ねようとすると1以上の音量になる可能性があります。二つのオシレーターを同時に鳴らす場合、最大で2倍の音量になります。
毎回最大値がいくつか調べてそれに合わせて音量を最大にするという作業は面倒なので自動で最大音量にできるようにします。
最大化するときの問題点
例えばサンプルサイズが16ビットの場合の最大音量は32767です。読み込むデータの最大音量が1であれば音量は32767倍すればよいですが最大音量が32767であれば1倍です。最大音量になるのは音声ファイルのいつのタイミングかわかりません。最初は1だからといって32767倍と決めてしまった場合最後に1より大きい音量がくると値が溢れてしまいます。音声データは一度全部読み込まないと最大値がわからないのです。
解決方法
解決策として一度読み込んで最大値を計測し、その後最大音量にする方法をとります。一度読み出したデータはもしデータ量が多いとメモリを圧迫してしまうのでローカルの一時ファイルに書き出すようにします。次に読み込んだ最大音量をファイルのフォーマットが許容できる最大の音量になるように係数を計算します。最後に一時ファイルを今度は読み込んで読み込んだデータに係数をかけるようにします。
クラスの宣言はこのような形です。SoundReadableインタフェースを実装するので読み込み機能の他に長さやチャンネル情報も返すことが担保されます。
public class Maximizer implements SoundReadable
読み込み元はSoundReadableを実装しているものであればなんでも良いものとします。
SoundReadable readable;
最終的に最大音量になるべき値を決めておきます。これはサンプルサイズできまります。
double maxVolume;
添付ファイルを作成します。一時ファイルなのですぐに削除するものですが一応この処理のためのものであることを示すために接頭辞”maximizer”をつけておきます。一時ファイルに対してデータを書き出すようにDataOutputStreamを開きます。このコンストラクタにはさらにファイル書き出し用のFileOutputStreamのインスタンスを指定しています。
- ファイルのバイナリデータ書き出し用にFileOutputStreamを作成する。
- 基本データ型のdoubleを書き出せるようにこれをDataOutputStreamで包み込む(wrapする)。
tempFile = File.createTempFile("maximizer", ""); DataOutputStream out = new DataOutputStream(new FileOutputStream(tempFile));
あとはreadableの長さ分読み込み最大値を都度計測してデータをファイルに書き出していきます。最後まで読み込んだ時点で最大値がわかります。また一時ファイルに読み込んだデータと同じものが書き出されています。
ちなみに最大値を測るときに注意しなければいけないのは最小値も考慮することです。このため読み込んだデータは「絶対値に変換」してから比較をします。
ファイルの書き出しは最後にflushするのを忘れないようにしてください。途中でバッファリングをしていると全て書き出されないままになってしまう可能性があります。
double max = 0; for(int i = 0;i < readable.length();i++){ double value = readable.read(); max = Math.max(max, Math.abs(value)); out.writeDouble(value); } out.flush(); out.close();
係数を計算します。maxが0のとき、つまり音声データがすべて無音だったときは係数は0にします。あとの場合はフォーマットの許容する最大値を計測した最大値で割ります。
ratio = max == 0?0:maxVolume / max;
データ読み込みのストリームを開きます。先ほどの書き出しと同じようにします。
DataInputStream in = new DataInputStream(new FileInputStream(tempFile));
読み込みのメソッドです。
@Override public double read() { try { return in.readDouble() * ratio; } catch (IOException ex) { throw new RuntimeException(ex); } }
読み込んだデータに対して係数をかけてから返します。
データの長さとチャンネル数は読み込み元の情報をそのまま返すようにします。
@Override public long length() { return readable.length(); } @Override public int getChannel() { return readable.getChannel(); }
一時ファイルを消すための終了処理を定義します。これを呼ばないと一時ファイルがハードディスクにどんどんたまってしまいます。
public void terminate() throws IOException{ in.close(); tempFile.delete(); }
コード
以上の記述をクラスに対して行います。次回はこのクラスを利用する側の処理を作ります。
package mocha.sound; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class Maximizer implements SoundReadable { SoundReadable readable; File tempFile; DataInputStream in; double ratio; public Maximizer(SoundReadable readable, double maxVolume) throws IOException { this.readable = readable; tempFile = File.createTempFile("maximizer", ""); System.out.println(tempFile.getAbsolutePath()); DataOutputStream out = new DataOutputStream(new FileOutputStream(tempFile)); double max = 0; for (int i = 0; i < readable.length(); i++) { double value = readable.read(); max = Math.max(max, Math.abs(value)); out.writeDouble(value); } out.flush(); out.close(); ratio = max == 0 ? 0 : maxVolume / max; in = new DataInputStream(new FileInputStream(tempFile)); } @Override public long length() { return readable.length(); } @Override public int getChannel() { return readable.getChannel(); } @Override public double read() { try { return in.readDouble() * ratio; } catch (IOException ex) { throw new RuntimeException(ex); } } public void terminate() throws IOException { in.close(); tempFile.delete(); } }