前回TimeLineクラスを分離してモノラルでも利用できうる抽象クラスを作りましたのでそれを利用して楽器で「演奏された音」クラス、Playedクラスを作ります。
演奏された音は音源から発せられているはずなのでステレオのような音の広がりは必要ありません。これは通常の楽器の話です。シンセサイザーの中にはステレオでの音の広がりを含めた音作りをしているものがありますがここではモノラルのままとして、音をミックスするときに考慮したいと思います。
Playedクラスの実装
実装するところはそれほど多くありません。継承元クラスにモノラルである1を渡してフォーマットではモノラルの音源のみそのまま登録するようにするだけです。
package mocha.sound; public class Played extends AbstractTimeLine { public Played() { super(1); } @Override protected SoundReadable formatReadable(SoundReadable readable) { int channel = readable.getChannel(); switch (channel) { case 1: return readable; default: return null; } } }
楽器インタフェースの定義
個々の音色を作り出す楽器クラスを作る前にその仕様をインタフェースにまとめて置きたいと思います。
MIDIの仕様を参考にして楽器は以下の三つの情報を元に音を作り出すことにします。
変数名 | 意味 |
---|---|
note | 音の高さ |
duration | 音の長さ |
velocity | 音の速さ |
「音の長さ」はピアノでいうとどのくらいの時間鍵盤を押しているかと言う意味になります。これは秒数で指定するようにします。lengthはこのシステムでデータ量を意味しているので別な名前にするのは混同を避けるためにも有効です。
「音の速さ」とはピアノにたとえて言うとどれぐらい早く鍵盤を叩いたかという意味です。早く叩くと音は大きくなりますが音色も変わりますので「大きさ」と「音色」の両方を操作する値になります。
インタフェースInstrumentalは以下の一つのメソッドだけ定義するようにします。これだけ実装されていればあとは楽器は自由にデザインして良いことになります。
package mocha.sound; public interface Instrumental { public Played play(double note, double duration, double velocity); }
今度は楽器の実装をしますがこれは次回行います。
抽象クラスAbstractTimeLineのソース
前回ソース全体を載せていなかったので以下抽象クラスのソースになります。
package mocha.sound; import java.util.ArrayList; import java.util.TreeMap; 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<>(); } 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); } @Override public long length() { long length = 0; for (long key : keySet()) { for (SoundReadable readable : get(key)) { length = Math.max(length, key * channel + readable.length()); } } return length; } @Override public int getChannel() { return channel; } @Override public double read() { if (buffer.isEmpty()) { if (containsKey(index)) { for (SoundReadable readable : get(index)) { reading.add(new ReadableCounter(readable)); } } index++; for (int i = reading.size() - 1; i >= 0; i--) { if (reading.get(i).rest <= 0) { reading.remove(i); } } 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]); } } return buffer.remove(0); } class ReadableCounter { long rest; SoundReadable readable; ReadableCounter(SoundReadable readable) { this.readable = readable; rest = readable.length(); } } }