前回のエンベロープを使う音作りで改善したい点があります。今回は音の高さを決めるところと音色作りを分離したいと思います。
対象となる処理
double[] notes = new double[]{60, 62, 64, 65, 67, 69, 71, 72}; TimeLine tl = new TimeLine(); for (int i = 0; i < notes.length; i++) { DoubleMap pan = new DoubleMap((double) i / (double) (notes.length - 1)); DoubleMap env = new DoubleMap(0); env.putSecondValue(0.05, 1); env.putSecondValue(0.2, 0.5); env.putSecondValue(0.5, 0); DoubleMap freq = new DoubleMap(Temperament.getFreq(notes[i])); tl.addReadable(i * 0.5, new Panner(new OscillatorReader(new TriangleOscillator(), freq, env, 0.5), pan)); }
前回のforの中の処理は音色を作っているところです。同じ楽器から音を鳴らす場合、高さによって多少変わるものの本質的には同じ音色になるはずです。そしてその同じ音色を作っているのが上記のfor文の内側になります。パニングはここでは除きます。
演奏する音の高さはfor文の外で定義されています。
このように考えたとき音色を作る処理は「楽器」クラスを定義し音作りの一切はそこで管理し、どの高さの音を演奏するかはその「楽器」クラスを利用する側、「演奏者」の側で決めるようにすれば今後は演奏者は楽曲に集中できるようになります。
演奏された音のオプティマイズ
楽器は演奏したとき音を作り出します。これを以下のように定義します。
- SoundReadabaleを実装しているクラス
- モノラル
- 複数のSoundReadableを内部に持つ
- この複数の内部のSoundReadableは時間差で読み出しを開始することがある
まずSoundReadableのインタフェースを見てみましょう。
package mocha.sound; public interface SoundReadable extends SoundConstants { public long length(); public int getChannel(); public double read(); }
自分自身のデータ量を返すlengthメソッド、チャンネル数を返すgetChannelメソッド、実際に一つづつデータを返すreadメソッドでできています。
また以前作ったTimeLineクラスはステレオであることを除けば上記の条件に当てはまります。このためTimeLineをモノラルでも利用できるように修正します。
抽象クラス | 継承クラス | チャンネル |
---|---|---|
AbstractTimeLine | TimeLine | 2 |
Played | 1 |
TimeLineをチャンネルに関係ない部分を抽象クラスに吸い出しAbstractTimeLineクラスを作ります。これをチャンネルに関係するTimeLineクラスが継承するようにします。
これによりモノラルの「演奏された音」クラスはAbstractTimeLineクラスを利用して簡単に作ることができます。
AbstractTimeLineクラスの作成
public abstract class AbstractTimeLine extends TreeMap<Long, ArrayList> implements SoundReadable
チャンネルを引数でとるようにします。
protected long index; protected ArrayList reading; protected int channel; ArrayList buffer; public AbstractTimeLine(int channel) { this.channel = channel; index = 0; reading = new ArrayList<>(); buffer = new ArrayList<>(); }
SoundReadableを追加するところです。抽象メソッドformatReadableを置きそこで登録しようとするSoundReadableのチャンネル数を揃えたり必要な処理を行えるようにします。これは継承先で実装されます。
既存のaddReadableはフォーマットされたSoundReadableがnullだったりチャンネル数が異なる場合には処理を中断します。
protected abstract SoundReadable formatReadable(SoundReadable readable); public void addReadable(double second, SoundReadable readable) { long key = (long) (second * SAMPLE_RATE); if (!containsKey(key)) { put(key, new ArrayList<>()); } SoundReadable formatted = formatReadable(readable); if(formatted == null || formatted.getChannel() != channel){ throw new IllegalArgumentException("unexpected channel=" + readable.getChannel()); } get(key).add(formatted); }
長さを計算する箇所は今まではkeyにチャンネル数として固定で2をかけていましたが変数channelをかけるようにします。
length = Math.max(length, key * channel + readable.length());
チャンネル数を返すところも同様に変数にします。
@Override public int getChannel() { return channel; }
読み出す箇所も今まで2回固定で読み出していたものを変数channelによって読み出す回数を変えられるようにします。
double[] values = new double[channel]; for (ReadableCounter counter : reading) { for(int i = 0;i < channel;i++){ values[i] += counter.readable.read(); } counter.rest -= channel; } for(int i = 0;i < channel;i++){ buffer.add(values[i]); }
モノラルの場合はそもそもバッファリングする必要がないのですがこのままにして置きます。モノラル専用の処理を作るとバグが散在する恐れがあるからです。ただ今後速度に問題が出てきたら再検討することにします。
継承クラスTimeLineの再作成
ほとんどの機能を抽象クラスで行うようにしたためTimeLineクラスは大幅に処理が減ります。
コンストラクタで継承元抽象クラスにチャンネル数を渡すようにします。
また登録するSoundReadableがステレオになるように揃えます。以前と同じ実装です。
- モノラルであれば中央にパニングする
- ステレオであればそのまま
- それ以外のチャンネル数の場合はnull(これは継承元で中断されるケース)
package mocha.sound; public class TimeLine extends AbstractTimeLine { public TimeLine() { super(2); } @Override protected SoundReadable formatReadable(SoundReadable readable) { int channel = readable.getChannel(); switch (channel) { case 1: return new Panner(readable, new DoubleMap(0.5)); case 2: return readable; default: return null; } } }
これでTimeLineクラスを利用している側には影響なくクラスを分離できました。
次回に続きます。