LoginSignup
12
7

More than 5 years have passed since last update.

Web Audio APIで、簡単なシンセサイザーを作ってみる / 2: 音をフェードアウトさせる

Posted at

前回からの続きです。

音がブツブツ鳴るのを防ぐ

前回、とりあえずキーボードを押すと音が鳴るところまで行きましたが(前回の成果はこちら)、キーを離す際に、「ブツッ」という途切れた音がします。

なぜこのような音が鳴るかというと、音量が急激にゼロになってしまうためです。
音の波は、1 ~ -1の範囲の中で、値が変化していくのですが(CD音源の場合だと、1秒間に44,100回変化します)、前回作ったコードでは、キーを離した瞬間に即座にオシレーターが停止 = 音量がゼロになる仕様です。
このような急激な音量の変化は、「ブツッ」というクリックノイズを発生させます。

音の波を扱う際は、必ず

  • 最初は音量0の地点から音を出し、
  • 終了する際は0の地点で音を止める

ようにしましょう。
最悪、音響機器を傷める可能性があります。
(傷めるようなコードを公開して申し訳ありません...)

コード

そんなわけで、以下がブツブツ音が鳴らないよう、修正したコードです。
音を止める直前に、フェードアウトするようにしました。
このフェードアウトの時間は、0.01秒といった、ごく短い間で構いません。

ためしに、変数releaseの値を変更してみると、違いがわかりやすいかもしれません。

サンプルはこちらです。

synth.js
'use strict';

// Web Audio APIを利用するためのインスタンス生成
var audioContext = new AudioContext();
// キーごとにMIDIノートナンバーを割り振る
var keymap = {
  // Zキー = C4
  90: 60,
  // Sキー = C#4
  83: 61,
  // Xキー = D4
  88: 62,
  // Dキー = D#4
  68: 63,
  // Cキー = E4
  67: 64,
  // Vキー = F4
  86: 65,
  // Gキー = F#4
  71: 66,
  // Bキー = G4
  66: 67,
  // Hキー = G#4
  72: 68,
  // Nキー = A4
  78: 69,
  // Jキー = A#4
  74: 70,
  // Mキー = B4
  77: 71,
  // ,キー = C5
  188: 72
};

// キーダウンした際の処理
document.onkeydown = function(keyDownEvent) {
  // キー押しっぱなしの状態で発火した場合は、動作を終了する
  if (keyDownEvent.repeat === true) {
    return;
  }

  // オシレーターを作成
  var osciillatorNode = audioContext.createOscillator();
  // エンベロープジェネレーターを作成
  var envelopeGen = audioContext.createGain();

  // MIDIノートナンバーを周波数に変換
  var freq = 440.0 * Math.pow(2.0, (keymap[keyDownEvent.keyCode] - 69.0) / 12.0);

  // オシレーターの周波数を決定
  osciillatorNode.frequency.value = freq;

  // オシレーターをエンベロープジェネレーターに接続
  osciillatorNode.connect(envelopeGen);
  // エンベロープジェネレーターを最終出力にx接続
  envelopeGen.connect(audioContext.destination);
  // 現在の時間を取得
  var t1 = audioContext.currentTime;

  envelopeGen.gain.cancelScheduledValues(t1);
  envelopeGen.gain.setValueAtTime(1, t1);
  // オシレーター動作
  osciillatorNode.start();

  // キーを離した際に音が止まるよう、イベントを登録する
  document.addEventListener('keyup', checkKeyUp);

  // キーを離したかどうかチェック
  function checkKeyUp(keyUpEvent) {
    var release = 0.1;

    // 離したキーが、押下したキーで無い場合は処理を行わない
    if (keyUpEvent.keyCode !== keyDownEvent.keyCode) {
      return;
    }
    var keyUpTime = audioContext.currentTime;

    // キーをリリースした後にクリック音がなるのを防ぐため、現在の音量をセットする
    envelopeGen.gain.setValueAtTime(envelopeGen.gain.value, keyUpTime);

    envelopeGen.gain.linearRampToValueAtTime(0, keyUpTime + release);

    // オシレーターを停止する
    osciillatorNode.stop(keyUpTime + release);
    // 自身のイベントを削除
    removeEventListener('keyup', checkKeyUp);
  }
};

変更があった箇所から解説します。

  // エンベロープジェネレーターを作成
  var envelopeGen = audioContext.createGain();

新しい概念がでてきました。エンベローブジェネレーターです。
簡単に言えば、ボリュームのつまみを新しく作成した、と思っていただければ問題ありません。
このボリュームのつまみを使って、音をブツブツ言わせることを防ぎます。

今までは、オシレーターを最終出力先であるスピーカーに直接接続していましたが、オシレーターとスピーカーの間に、ボリュームのつまみを接続します。

// オシレーターをエンベロープジェネレーターに接続
  osciillatorNode.connect(envelopeGen);
  // エンベロープジェネレーターを最終出力にx接続
  envelopeGen.connect(audioContext.destination);

osciillatorNode.connect()関数で、接続先を先ほど作成したボリュームのつまみに接続します。
さらに、ボリュームのつまみは、envelopeGen.connect()で、スピーカーを接続先に指定します。
これで間にボリュームを噛ませることに成功しました。ギターのエフェクターみたいなもんですね。

さて、そんなエンベローブジェネレーターですが、初期のボリュームは0になっているので、きちんとボリュームのつまみをあげなければ、音が出ません。
そこで、上記のように、ボリュームを設定しなければなりません。

  // 現在の時間を取得
  var t1 = audioContext.currentTime;

  envelopeGen.gain.cancelScheduledValues(t1);
  envelopeGen.gain.setValueAtTime(1, t1);
  // オシレーター動作
  osciillatorNode.start();

まずは、audioContext.currentTimeですが、こちらはWeb Audio APIにて使用可能な時計です。
返す値は「ファイルを開いてから経過した秒数」です。これを使うことにより、音楽を扱う上で非常に重要な、時間を自由自在に操ることが可能です。

(例えば、今から5秒間音を鳴らしたい、といった場合には、
開始時にaudioContext.currentTimeにて現在時間を取得し、
終了時間を(audioContext.currentTime + 5)と指定すればいいわけです。)

setValueAtTime()は、ボリュームのつまみの位置を決定する関数です。第一引数がボリューム、第二引数が、音量を変更する時間です。
よって、ボリュームは最大の1、変更時間は現在とします。

cancelScheduledValues()を使用することにより、引数以降のスケジュールをキャンセルすることが可能です。連続で操作した場合を踏まえ、このような操作を行う場合は、その前に過去のスケジュールをキャンセルすることをおすすめします。

  function checkKeyUp(keyUpEvent) {
    var release = 0.1;

    // 離したキーが、押下したキーで無い場合は処理を行わない
    if (keyUpEvent.keyCode !== keyDownEvent.keyCode) {
      return;
    }
    var keyUpTime = audioContext.currentTime;

    // キーをリリースした後にクリック音がなるのを防ぐため、現在の音量をセットする
    envelopeGen.gain.setValueAtTime(envelopeGen.gain.value, keyUpTime);

    envelopeGen.gain.linearRampToValueAtTime(0, keyUpTime + release);

    // オシレーターを停止する
    osciillatorNode.stop(keyUpTime + release);
    // 自身のイベントを削除
    removeEventListener('keyup', checkKeyUp);
  }

自分で書いてて、果たして正解なのか、よくわかりませんが...

まず、キーを離した瞬間に、現在の音量を改めてsetValueAtTime()します。
なんだかおかしな話ですが、このようにした場合、クリック音を発生させずに音量を減少させることができました。

その後、linearRampToValueAtTime()にて、音量を減少させていきます。
第一引数は最終的な音量、第二引数が最終音量にたどり着くまでの時間です。
この関数は、setValueAtTime()と違い、第二引数の時間に最終的な音量にたどり着くよう、徐々に値が変化します。

音量が0になった時点でオシレーターを停止させます。
このようにすれば、不快なクリック音を発生させずに音を停止することが可能です。

ADSRエンベローブジェネレーターの作成を断念

さて、変数releaseの値を1に変更して鳴らしてみましょう。
音が弦楽器のように、さらにゆっくり減衰することに気がつくはずです。

シンセサイザーでは、音の立ち上がり(Attack)、音の減衰(Decay)、減衰後の保持(Sustain)、余韻(Release)のそれぞれの時間を調節することにより、バイオリンのようなゆっくりした立ち上がりの音から、スネアドラムのような鋭角な音まで、幅広く音をシミュレートすることが可能です。
この機能をADSRといったりします。

が、Web Audio APIでは、linearRampToValueAtTime()等で音を減衰、増加時にキャンセルをかけると音量が跳ね上がってしまったり、ここらへんのエンベローブの調節に難があるように見受けられます。
Web Audio APIで作られたシンセを見てみても、UIでごまかしていて、実際にはほとんど機能していないものも見受けられ。。

そんなわけで、早々にADSRの機能を作ることを断念したのでした。
どなたか、良い実装をご存知であれば、教えて下さい。。。

気を取り直して、次回は、シンセ以外のこともやっていきたいと思います。

参考文献

Web Audio APIとは全く関係無いですが、
サウンドプログラミングについては、以下の書籍が非常にわかりやすいかと思います。

ビジュアルプログラミング言語Pure Data用の書籍なので、jsとは全く違いますが、サウンドプログラミングの基本がわかりやすく学べましたので、おすすめです。

12
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7