2017/11/28

Web Audio APIとVue.jsでADSRエンベロープ(VCA)をコントロールする

前回までにキーボードをつくったりオシレータをつくったりオシロスコープをつくったりした。
今回は音の三要素の最後の1つ、音量をコントロールする。
エンベロープジェネレータを用い、ADSR(Attack/Decay/Sustain/Release)をコントロールするとともに、エンベロープをグラフ表示する。



ADSRとは?


Attack/Decay/Sustain/Releaseの頭文字をとったもので、音量をコントロールする機能。
  • Attack(立ち上がり)
    • 発音(打鍵)されてから最大音量になるまでの時間
  • Decay(減衰)
    • 最大音量になってからSustainのレベルになるまでの時間
  • Sustain(保持)
    • Decay後の音量。発音中はずっとSustainレベルが保持される
  • Release(余韻)
    • 離鍵されてから音量が0になるまでの時間
この4つのパラメータを弄ることで「ドンッ!!」とか「ほわぁ〜ん」とかいう音を作ることができる。

音量を変える


OscillatorNodeGainNodeに繋ぐことで、時間経過による音量を変更できる。
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つを値を設定する必要がある。

上記コードを例にとると、以下のようなスケジュールになる。

  1. 発音開始時は音量0
  2. 発音から1秒後には音量を線形に0→1に変更する(linearRampToValueAtTime)
  3. 音量が1になってから1秒後に音量が指数関数的に1→0.5に変更する(exponentialRampToValueAtTime)
  4. 離鍵したら音量が線形に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ボタンをクリック
    1. 音量0
    2. attack秒かけて音量を1にする
    3. decay秒かけてsustainの音量にする
    4. 音量をsustainのまま持続させる
  • stopボタンをクリック
    1. すべてのスケジュール(startボタンをクリックしたときのもの)をキャンセルする
    2. 音量をsustainにする
    3. 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 件のコメント :

コメントを投稿