前回からの続きです。
音がブツブツ鳴るのを防ぐ
前回、とりあえずキーボードを押すと音が鳴るところまで行きましたが(前回の成果はこちら)、キーを離す際に、「ブツッ」という途切れた音がします。
なぜこのような音が鳴るかというと、音量が急激にゼロになってしまうためです。
音の波は、1 ~ -1の範囲の中で、値が変化していくのですが(CD音源の場合だと、1秒間に44,100回変化します)、前回作ったコードでは、キーを離した瞬間に即座にオシレーターが停止 = 音量がゼロになる仕様です。
このような急激な音量の変化は、「ブツッ」というクリックノイズを発生させます。
音の波を扱う際は、必ず
- 最初は音量0の地点から音を出し、
- 終了する際は0の地点で音を止める
ようにしましょう。
最悪、音響機器を傷める可能性があります。
(傷めるようなコードを公開して申し訳ありません...)
コード
そんなわけで、以下がブツブツ音が鳴らないよう、修正したコードです。
音を止める直前に、フェードアウトするようにしました。
このフェードアウトの時間は、0.01秒といった、ごく短い間で構いません。
ためしに、変数releaseの値を変更してみると、違いがわかりやすいかもしれません。
サンプルはこちらです。
'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とは全く違いますが、サウンドプログラミングの基本がわかりやすく学べましたので、おすすめです。