今までは音は鳴りっぱなしでしたが音量を調節したいと思います。一つの音は始まりと終わりで音量が違います。最初は無音ですが音が大きく最大になってからまた小さくなりまた無音になります。この音量の変化のことをエンベロープenvelopeと言います。
エンベロープの機能
例えばピアノは立ち上がりは大きな音をしますがすぐに小さくなります。オルガンは鳴り出したら鍵盤を話すまで同じ音量で鳴り続けます。このように音量を操作すると音に特徴をつけることができます。このコントロールをするのがエンベロープの役割です。
以前作ったOscillatorReaderを少し改造します。
既存のクラスを修正する
フィールドにエンベロープ用のDoubleMapを追加します。このマップには音量を設定することにします。最大は1として無音を0とします。
DoubleMap envMap;
コンストラクタを改修します。
public OscillatorReader(Oscillatable oscillatable, DoubleMap freqMap, DoubleMap envMap, double seconds) { this.oscillatable = oscillatable; this.freqMap = freqMap; this.envMap = envMap; this.seconds = seconds; }
以前のエンベロープのマップがないものも保持するようにします。このようにするとクラスの改修を行っても互換性を保てます。エンベロープは1固定にすれば前と同じ音になります。
public OscillatorReader(Oscillatable oscillatable, DoubleMap freqMap, double seconds) { this(oscillatable, freqMap, new DoubleMap(1), seconds); }
readメソッドで返すときにエンベロープのマップから取得した値を掛け合わせるようにします。
@Override public double read() { return oscillatable.read(freqMap.next(), 1) * envMap.next(); }
実際に音を出す
では次に音声を作って見ましょう。だいたい次のようなものを作ります。
- ドレミファソラシドの8音を鳴らす。各音は0.5秒
- エンベロープは音の初めは無音
- 0.05秒後に最大
- 0.2秒後に半分の音量
- 0.5秒後に無音に戻る
- 最初の音は右からだんだん移動して最後の音は左から出るようにパニングする
8つの音を番号で定義します。
double[] notes = new double[]{60, 62, 64, 65, 67, 69, 71, 72};
タイムラインを作成します。
TimeLine tl = new TimeLine();
8つの音それぞれについて上記の仕様にそって設定していきます。
for(int i = 0;i < notes.length;i++){
パンを設定します。最初は右つまり0、最後は左つまり1になるようにします。
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));
これで1音の処理が終了します。
} WavFileWriter.create(tl, new File("wav/envelope.wav"));
結果
実行した結果は以下のようになります。今回は静止画ですがyoutube経由でアップロードしました。
ソース
以下がソースです。
package mocha.sound; public class OscillatorReader implements SoundReadable { public static final int CHANNELS = 1; Oscillatable oscillatable; DoubleMap freqMap; DoubleMap envMap; double seconds; public OscillatorReader(Oscillatable oscillatable, DoubleMap freqMap, double seconds) { this(oscillatable, freqMap, new DoubleMap(1), seconds); } public OscillatorReader(Oscillatable oscillatable, DoubleMap freqMap, DoubleMap envMap, double seconds) { this.oscillatable = oscillatable; this.freqMap = freqMap; this.envMap = envMap; this.seconds = seconds; } @Override public long length() { return (long) (SAMPLE_RATE * CHANNELS * seconds); } @Override public int getChannel() { return CHANNELS; } @Override public double read() { return oscillatable.read(freqMap.next(), 1) * envMap.next(); } }
import java.io.File; import java.io.IOException; import mocha.sound.DoubleMap; import mocha.sound.OscillatorReader; import mocha.sound.Panner; import mocha.sound.Temperament; import mocha.sound.TimeLine; import mocha.sound.TriangleOscillator; import mocha.sound.WavFileWriter; public class EnvelopeTest { public static void main(String[] arg) throws IOException { 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)); } WavFileWriter.create(tl, new File("wav/envelope.wav")); } }
次回はこれに関連してタイムラインのクラスを改良します。