今回は音の三要素の最後の1つ、音量をコントロールする。
エンベロープジェネレータを用い、ADSR(Attack/Decay/Sustain/Release)をコントロールするとともに、エンベロープをグラフ表示する。
ADSRとは?
Attack/Decay/Sustain/Releaseの頭文字をとったもので、音量をコントロールする機能。
- Attack(立ち上がり)
- 発音(打鍵)されてから最大音量になるまでの時間
- Decay(減衰)
- 最大音量になってからSustainのレベルになるまでの時間
- Sustain(保持)
- Decay後の音量。発音中はずっとSustainレベルが保持される
- Release(余韻)
- 離鍵されてから音量が0になるまでの時間
音量を変える
OscillatorNodeをGainNodeに繋ぐことで、時間経過による音量を変更できる。
const ctx = new AudioContext();
const vco = ctx.createOscillator();
const vca = ctx.createGain();
// Oscillator -> Gain -> destination
vco.connect(vca);
vca.connect(ctx.destination);
// 打鍵イベントなど
// 現在の時間の音量を「0」に設定する
const t0 = ctx.currentTime;
vco.start(t0);
vca.gain.setValueAtTime(0, t0);
// t0から1秒後に音量が「1」になるよう線形に変更する
const t1 = t0 + 1;
vca.gain.linearRampToValueAtTime(1, t1);
// t1から1秒後に音量が「0.5」になるよう指数関数的に変更する
const t2 = t1 + 1;
vca.gain.exponentialRampToValueAtTime(0.5, t2);
// 離鍵イベントなど
// 現在の時間から音量を「0」になるよう線形に変更する
const t3 = ctx.currentTime;
const gain = vca.gain.value;
vca.gain.cancelScheduledValues(t3) // 打鍵イベントで設定したスケジュールをキャンセル
vca.gain.setTargetAtTime(gain, t3, 0.5);
const stopTime = setInterval(() => {
// 完全に「0」にならない(なるまでに時間がかかる)ので、音量が0.01未満なら音が鳴っていないと判断する
if (vca.gain.value < 0.01) {
vco.stop();
clearInterval(stopTime)
}
}, 10);
音量はスケジューリングすることで変更できる。
スケジューリングするときに「いつ」「どのような変化で」「音量をどうする」の3つを値を設定する必要がある。
上記コードを例にとると、以下のようなスケジュールになる。
- 発音開始時は音量0
- 発音から1秒後には音量を線形に0→1に変更する(linearRampToValueAtTime)
- 音量が1になってから1秒後に音量が指数関数的に1→0.5に変更する(exponentialRampToValueAtTime)
- 離鍵したら音量が線形に0.5→0に変更する
ADSRエンベロープをつくる
<div id="app">
<form class="envelope-controller">
<div>
<label>Attack</label>
<input type="range" min="0" max="1" step="0.01" v-model.number="form.attackTime">
</div>
<div>
<label>Decay</label>
<input type="range" min="0" max="1" step="0.01" v-model.number="form.decayTime">
</div>
<div>
<label>Sustain</label>
<input type="range" min="0" max="1" step="0.01" v-model.number="form.sustainLevel">
</div>
<div>
<label>Release</label>
<input type="range" min="0" max="1" step="0.01" v-model.number="form.releaseTime">
</div>
<button type="button" @click="start">Start</button>
<button type="button" @click="stop">Stop</button>
</form>
</div>
// import vue@2.5
new Vue({
el: '#app',
data() {
return {
form: {
attackTime: 0.5,
decayTime: 0.3,
sustainLevel: 0.5,
releaseTime: 1.0
},
ctx: new AudioContext(),
osc: null,
amp: null
}
},
methods: {
start() {
this.osc = this.ctx.createOscillator();
this.amp = this.ctx.createGain();
// osc -> gain -> output
this.osc.connect(this.amp);
this.amp.connect(this.ctx.destination);
const t0 = this.ctx.currentTime;
this.osc.start(t0);
// vol:0
this.amp.gain.setValueAtTime(0, t0);
// attack(t1秒かけて音量を1にする)
const t1 = t0 + this.form.attackTime;
this.amp.gain.linearRampToValueAtTime(1, t1);
// decay(t1秒からt2秒の間に音量をsustainの値にする)
const t2 = this.form.decayTime;
this.amp.gain.setTargetAtTime(this.form.sustainLevel, t1, t2);
},
stop() {
const t = this.ctx.currentTime;
this.amp.gain.cancelScheduledValues(t);
// 初期値の設定(gain.valueはsustainと同じ)
this.amp.gain.setValueAtTime(this.amp.gain.value, t);
// t秒からreleaseTime秒の間に音量を0にする
this.amp.gain.setTargetAtTime(0, t, this.form.releaseTime);
const stop = setInterval(() => {
// 完全に0になるまでには時間がかかるため、0.01になったら音量0になったことにする
if (this.amp.gain.value < 0.01) {
this.osc.stop();
clearInterval(stop);
}
}, 10);
},
}
});
Attack, Decay, Sustain, Releaseをスライダーで変更できるようにしている。
そして、ボタンをクリックするとこで以下のフローで発音される。
- startボタンをクリック
- 音量0
- attack秒かけて音量を1にする
- decay秒かけてsustainの音量にする
- 音量をsustainのまま持続させる
- stopボタンをクリック
- すべてのスケジュール(startボタンをクリックしたときのもの)をキャンセルする
- 音量をsustainにする
- release秒かけて音量を0にする
注意すべき点は、いくらrelease秒かけて音量を0にしようとしても「0.00001」とか「0.0000001」みたいになかなか0にならないので、0.01未満は音が鳴っていないと判断してオシレーターを停止している。
エンベロープを折れ線グラフで表す
ADSRは折れ線グラフで表されることが多い。
ということで、せっかくなのでAttack, Decay, Sustain, Releaseの値を折れ線グラフで表示する。
<script type="text/x-template" id="adsr">
<svg :width="width" :height="height" xmlns="http://www.w3.org/2000/svg" baseProfile="full">
<path :d="path" stroke="#666666" stroke-width="3" fill="none"></path>
</svg>
</script>
const EnvelopeGenerator = Vue.component('envelope-generator', {
name: 'EnvelopeGenerator',
template: "#adsr",
props: {
width: {
type: Number,
default: 640
},
height: {
type: Number,
default: 480
},
attack: {
type: Number,
required: true,
validaor: v => 0 <= v && v <= 1
},
decay: {
type: Number,
required: true,
validaor: v => 0 <= v && v <= 1
},
sustain: {
type: Number,
required: true,
validaor: v => 0 <= v && v <= 1
},
release: {
type: Number,
required: true,
validaor: v => 0 <= v && v <= 1
}
},
data () {
return {
// SVGでグラフを書くためのパス
path: ''
}
},
mounted() {
this.draw();
},
watch: {
// 親コンポーネントで変更された値を監視して、変更があれば再描画する
attack: function () { this.draw(); },
decay: function () { this.draw(); },
sustain: function () { this.draw(); },
release: function () { this.draw(); }
},
methods: {
draw() {
const wRetio = this.width / 4;
const hRetio = this.height / 1;
const paths = [];
let x, y;
x = y = 0;
// attack
x = this.attack * wRetio;
y = 0;
paths.push(`${x} ${y}`);
// decay
x += this.decay * wRetio;
y = this.height - this.sustain * hRetio;
paths.push(`${x} ${y}`);
// sustain
x += 1 * wRetio;
paths.push(`${x} ${y}`);
// release
x += this.release * wRetio;
y = this.height;
paths.push(`${x} ${y}`);
this.path = `M0 ${this.height},` + paths.join(',');
}
}
});
エンベロープグラフはコンポーネントとして定義する。
そして、親コンポーネントからAttack, Decay, Sustain, Releaseの値をもらい、SVGのPathを使って表示する。
watchでその4つの値を監視することで、親コンポーネントで変更された場合もグラフが再描画されるようにしている。
以上
written by @bc_rikko
0 件のコメント :
コメントを投稿