前回コードも作り実行結果の音声ファイルもできましたがあまり詳しい説明はしていませんでした。ここでは処理の詳細を説明します。
周期を求める
一定の音の高さの場合波の周期は一定です。一秒間にサンプルする数のことをサンプリングレートと呼んでいますが1周期あたりのサンプル数を求めます。サンプリングレートを周波数で割ります。
double period = sample_rate / freq;
たいていの場合は割り切れないので結果の値もdouble値にしておきます。
音量を求める
サンプルサイズのビット数をxとすると最大音量は以下のように求められました。
最大値 = 2(x – 1) – 1
これを実際の出力値として使います。最小音量はこの数字を負にしたものとします。最小音量というと無音に近いように思えますが実際には最大値と同じ音圧が負の方向に働いているという意味です。最大値がスピーカーが一番飛び出した状態とすると最小値は一番奥に戻っている状態のことになります。
処理は以下のようにしています。
volume = Math.pow(2, sample_size_byte * 8 - 1) - 1;
インデックスを保持する
サンプルを取得する際に自分が今どこにいるのかを保持しておきます。初期値は0にします。
double index = 0;
サンプルを取得するごとにインクリメント、つまり1増やします。1秒分のサンプルを取得するとサンプリングレートと同じ数になります。
index++; index %= period;
ただ長い時間のサンプルを取るとインデックスの値もどんどん増えていきます。この二番目の処理は周期で割ったあまりをインデックスに設定し直しています。周期は常に繰り返すので1周期のどこに位置すれば良いかが分かれば処理はできます。この結果インデックスは常に0以上、1周期未満の数になります。
周期上の位置によって出力する音量を決める
インデックスを元に自分のいる場所を求めます。矩形波は周期の前半であれば最大出力、後半であれば最小出力です。1周期を1とすると0.5より大きいか小さいかを判定します。
long value; if(index / period < 0.5){ value = (long)volume; }else{ value = (long)-volume; }
InputStreamの仕様に沿った値を返す
InputStreamのreadメソッドを実装するにあたって戻す値を考慮します。
@Override public int read() throws IOException
もどす値はintですが実際にはbyteの値の範囲内にしなくてはいけません。Javaのbyteは符号があるものなので-128〜127までの値になるのですがこちらのreadメソッドの結果は0〜255の値を返すことになっています。このため通常のbyte値のうち-128〜-1までの値は256を足す必要があります。幸いByteクラスのstaticメソッドにはこの変換をしてくれるものがあるので利用します。
int ret = Byte.toUnsignedInt(byte_value); return ret;
サンプルデータをバッファする
一つのサンプルを取得した場合サンプルサイズによって取得できるデータ量が異なります。よく使う16bitのサンプルデータの場合は8bitが1バイトですから2バイトのサイズになります。上記のreadメソッドが2回呼ばれないとサンプルデータは読めない事になります。このため一度取得したデータをバッファしておきます。ここではArrayListを使っていますが他の方法でも良いと思います。
ArrayList<Byte> list = new ArrayList<>();
long値でサンプルした値をバイトに変換してバッファするには以下のようにします。まずlong値は8バイトのデータ量なのでこれが入りきるバッファを用意します。そこにlong値を入れます。とはいえ実際に入るデータはサンプルサイズを超えないので下の方の位だけ取得します。16bit2バイトのサンプルサイズの場合は下二桁を取得するようにします。
ByteBuffer buffer = ByteBuffer.allocate(8); buffer.putLong(value); byte[] array = buffer.array(); for (int i = 8 - sample_size_byte; i < 8; i++) { list.add(array[i]); }
バッファしたサンプルデータをreadメソッドが呼ばれるごとにint型に変換します。removeメソッドはその値をバッファから消して返します。
int ret = Byte.toUnsignedInt(list.remove(0));
バッファがなくなった場合にサンプルを取ります。
if(list.isEmpty()){
呼び出し処理を書く
これでおよそ処理内容は書けました。次は実際に呼び出してファイルを作るところです。
public static void main(String[] arg) throws IOException { SampleSquare ss = new SampleSquare(440, 3); AudioSystem.write( new AudioInputStream(ss, ss.getFormat(), ss.length()), AudioFileFormat.Type.WAVE, new File("/Users/myaccount/mywork/square_440.wav")); }
SampleSquareのコンストラクタは二つの引数を取っています。一つ目は周波数、二つ目はサンプルの秒数です。ここでは440Hzの矩形波を3秒作る事にしています。次にAudioSystem.writeのメソッドを呼んで必要な引数を設定すればファイルが書き出されます。
最初の引数はAudioInputStreamです。これにはInputStreamを実装した今回のクラスSampleSquareのインスタンスとオーディオフォーマットとサンプル数を引数に設定します。オーディオフォーマットとサンプル数は計算して設定するようにメソッドを作りました。
public long length(){ return (long)(sample_rate * channels * seconds); }
public AudioFormat getFormat(){ return new AudioFormat(sample_rate, sample_size_byte * 8, channels, signed, big_endian); }
今回の基本設定は以下のようになっています。
boolean signed = true; boolean big_endian = true; float sample_rate = 48000; int sample_size_byte = 2; int channels = 1;
あとはファイルのフォーマットをWAVに指定して書き出し先のファイルを指定すれば出来上がりです。