パンを設定するにあたって時間によってパンの位置を変えたいため設定を保持するクラスをまず作ります。以前周波数を時間で変化させるための周波数マップクラスFrequencyMapというものを作りました。
これは秒で設定した時間double値をサンプルのインデックスlong値に変換してこれを鍵として周波数double値の値として保持するクラスでした。パンの値もdouble値でもつのでこれをそのまま汎用化するクラスにします。
音の高さを変える(1)
音の高さを変える(2)
ステレオについて
時間で変化するdouble値を保持するクラス
以前はインデックスは外から指定していましたがこれからは内部で管理するようにします。nextメソッドを呼ぶごとに内部のインデックスが自動でインクリメントされるようにしています。これで何番目かを意識せず次々と読み出せばよいようになりました。ただし一度読んでしまうと同じ値を返すことはなくなるので使い方には注意が必要です。
他メソッド名を汎用のものにしましたが中のロジックは変わっていません。
package mocha.sound; import java.util.Map.Entry; import java.util.TreeMap; public class DoubleMap extends TreeMap<Long, Double> { double sampleRate; long index; public DoubleMap(double initialValue) { this(WavFileWriter.SAMPLE_RATE, initialValue); } public DoubleMap(double sampleRate, double initialValue) { this.sampleRate = sampleRate; put(0l, initialValue); index = 0; } public void putSecondValue(double second, double value) { put((long) (second * sampleRate), value); } public double next() { return getValue(index++); } private double getValue(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); } }
パンニングを行うクラスを作成する
このマップクラスを使ってパニングを行うクラスを作ります。コンストラクタには読み込み元のSoundReadableとパニングマップを指定します。読み込み元は1チャンネル、つまりモノラルの場合のみ許可するようにしてそれ以外は例外を投げます。1つのデータから2つのデータを作るためreadメソッド保持するためのバッファクラスを作っておきます。
public class Panner implements SoundReadable { SoundReadable readable; DoubleMap panMap; ArrayList buffer; public Panner(SoundReadable readable, DoubleMap panMap) { if (readable.getChannel() != 1) { throw new IllegalArgumentException("readable should be monoral"); } this.readable = readable; this.panMap = panMap; buffer = new ArrayList<>(); }
インタフェースSoundReadableで定義されているメソッドも実装します。読み込むデータの長さはモノラルの2倍、チャンネルは2になります。
@Override public long length() { return readable.length() * 2; } @Override public int getChannel() { return 2; }
readメソッドは工夫が必要です。バッファに何もない場合に読み込み元からサンプルデータを読み込み、パニングマップからパンデータを読み込みます。これは左用のパンデータです。サンプルデータとパンデータを掛け合わせたものを左チャンネルのデータとしてバッファに設定します。右用のパンデータは1.0から左のパンデータを引くことによって求め同じようにバッファに設定します。
あとの処理は常にバッファの先頭から読み込んでメソッドの戻り値として返すだけです。
@Override public double read() { if (buffer.isEmpty()) { double value = readable.read(); double pan = panMap.next(); buffer.add(value * pan); buffer.add(value * (1.0 - pan)); } return buffer.remove(0); }
パニングを行うクラスPannerのソース
以下ソースです。
package mocha.sound; import java.util.ArrayList; public class Panner implements SoundReadable { SoundReadable readable; DoubleMap panMap; ArrayList buffer; public Panner(SoundReadable readable, DoubleMap panMap) { if (readable.getChannel() != 1) { throw new IllegalArgumentException("readable should be monoral"); } this.readable = readable; this.panMap = panMap; buffer = new ArrayList<>(); } @Override public long length() { return readable.length() * 2; } @Override public int getChannel() { return 2; } @Override public double read() { if (buffer.isEmpty()) { double value = readable.read(); double pan = panMap.next(); buffer.add(value * pan); buffer.add(value * (1.0 - pan)); } return buffer.remove(0); } }
次回に続きます。