前回周波数が変化するときの計算ロジックを考えましたがあのままでは使いにくいのでクラスにしました。これを使ってサイン波の音の高さを変化させて見ます。
周波数マップクラス
以下クラスのソースです。
package mocha.sound;
import java.util.Map.Entry;
import java.util.TreeMap;
public class FrequencyMap extends TreeMap<Long, Double> {
double sampleRate;
public FrequencyMap(double sampleRate, double initialValue){
this.sampleRate = sampleRate;
put(0l, initialValue);
}
public void putFreq(double second, double freq){
put((long)(second * sampleRate), freq);
}
public double getFreq(long index) {
if (containsKey(index)) {
return get(index);
}
Entry<Long, Double> a = floorEntry(index);
Entry<Long, Double> b = ceilingEntry(index);
if (a == null && b == null) {
throw new IllegalStateException("map should contain at least one entry");
}
if (a == null) {
return b.getValue();
} else if (b == null) {
return a.getValue();
}
double xa = a.getKey();
double xb = b.getKey();
double ya = a.getValue();
double yb = b.getValue();
return ya + (yb - ya) * (index - xa) / (xb - xa);
}
}
クラス化するにあたりサンプリングレートと種は数の初期値をコンストラクタにとるようにしました。またサンプリングレートを内部で持っているためインデックスではなく秒を鍵の代わりとしてマップに設定できるようになりました。「96000番目で880Hzになる」という設定のしかたよりも「2秒目で880Hzになる」とした方が設定がしやすいです。
FrequencyMap freqMap = new FrequencyMap(SAMPLE_RATE, 440.0); freqMap.putFreq(2.0, 880.0);
wavファイルを作成するクラスのロジックの変更
この周波数マップクラスを使えるようにwav書き出しクラスも変更を加えます。まずコンストラクタの変更です。
public SampleWav(Oscillatable oscillatable, FrequencyMap freqMap, double seconds) {
次にオシレーターから音量を読み出すときのロジックも変更します。
double value = oscillatable.read(freqMap.getFreq(index++), volume);
今まではreadメソッドに固定の周波数を指定していましたが今回は周波数マップから都度計算された周波数を呼び出しています。index++とすることで読み込んだあとでインクリメント、つまり1増やす操作が行われます。
メインの処理は以下のようにします。
public static void main(String[] arg) throws IOException {
FrequencyMap freqMap = new FrequencyMap(SAMPLE_RATE, 440.0);
freqMap.putFreq(2.0, 880.0);
SampleWav ss = new SampleWav(new SineOscillator(SAMPLE_RATE), freqMap, 3);
AudioSystem.write(
new AudioInputStream(ss, ss.getFormat(), ss.length()),
AudioFileFormat.Type.WAVE, new File("/Users/myaccount/mywork/sine_440_880.wav"));
}
実行する
ファイルが出力されます。
最初から2秒間の間に周波数が倍になり3秒目はそのまま固定されているのが聞こえましたか?
ファイル出力処理をするコード
最後にwavファイル出力をするコードを載せておきます。
package mocha.sound;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
public class SampleWav extends InputStream {
public static final float SAMPLE_RATE = 48000;
boolean signed = true;
boolean big_endian = true;
int sample_size_byte = 2;
int channels = 1;
ArrayList list;
double volume;
Oscillatable oscillatable;
FrequencyMap freqMap;
double seconds;
long index;
public SampleWav(Oscillatable oscillatable, FrequencyMap freqMap, double seconds) {
this.freqMap = freqMap;
this.seconds = seconds;
list = new ArrayList<>();
volume = Math.pow(2, sample_size_byte * 8 - 1) - 1;
this.oscillatable = oscillatable;
}
@Override
public int read() throws IOException {
if (list.isEmpty()) {
double value = oscillatable.read(freqMap.getFreq(index++), volume);
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.putLong((long) value);
byte[] array = buffer.array();
for (int i = 8 - sample_size_byte; i < 8; i++) {
list.add(array[i]);
}
}
int ret = Byte.toUnsignedInt(list.remove(0));
return ret;
}
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);
}
public static void main(String[] arg) throws IOException {
FrequencyMap freqMap = new FrequencyMap(SAMPLE_RATE, 440.0);
freqMap.putFreq(2.0, 880.0);
SampleWav ss = new SampleWav(new SineOscillator(SAMPLE_RATE), freqMap, 3);
AudioSystem.write(
new AudioInputStream(ss, ss.getFormat(), ss.length()),
AudioFileFormat.Type.WAVE, new File("/Users/minaberger/mywork/sine_440_880.wav"));
}
}