ブラウザ上で音楽を演奏する
κeenです。めずらしくWebの話題でも。ブラウザ上で音出したいときってどうすればいいんだっけとなって調べた結果です。
Web Audio API
個人的にはSVGのオーディオ版の、楽譜っぽいものをテキストで入力したら勝手に音が鳴ってくれるフォーマットをさがしてたのですがみつかりませんでした(辛うじてMusicXMLが近いくらい?)。MIDIとかにも期待したんですがあれはバイナリ
代わりに、Web Audio APIというのを使えばブラウザ上で音が出せそうというのがみつかりました。
この中の OscillatorNode
を使えば手で音が出せそうです。
OscillatorNode
OscillatorNode
を使ったサンプルを実装してみたのがこれ(クリックすると大きめの音が鳴ります)。
コードは以下。
<div id="oscPlayer">▶</div>
<script>
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let oscillator = null;
const playButton = document.getElementById("oscPlayer");
playButton.addEventListener("mousedown", function(event) {
if (oscillator === null) {
playButton.innerHTML = "⏸ Stop";
oscillator = audioCtx.createOscillator();
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); // value in hertz
oscillator.connect(audioCtx.destination);
oscillator.start();
} else {
playButton.innerHTML = "▶️ Play";
oscillator.stop();
oscillator.disconnect(audioCtx.destination);
oscillator = null
}
}, false);
</script>
ノードをコネクトしていって有向グラフを作り、音源から audioCtx.destination
まで到達できたら音が鳴る仕組みです。
その音源にあたるのが OscillatorNode
で、コード中のこの部分で作って実行してます。
oscillator = audioCtx.createOscillator();
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); // value in hertz
oscillator.connect(audioCtx.destination);
oscillator.start();
440Hzの矩形波を鳴らしています。440Hzというのはおなじみラの音です。
音源をstopする手段はあるのですが、一度stopすると再開できないのでこういう再生/停止を繰り返す場合は音源を削除、disconnectするのが常套手段のようです。
今回は使ってませんが、 OscillatorNode
には frequency
の他に detune
というパラメータがあります。
これは平均律でのセントを指定します。
12音階での隣の音がちょうど100セントで、1オクターブ上がるとちょうど1200セントです。
上記のラが440Hzなのでこれに100セントずつ+-していけば音階を表現できるという仕組みです。
演奏時間を指定する
周波数と演奏する長さがあればひとまず演奏できそうですね。 上記のAPIで周波数は指定できるようになったので演奏時間を指定するのが目標です。
OscillatorNode
などの AudioScheduledSourceNode
の start
(と呼応する stop
)はオプショナルな引数 when
でいつ操作をするか選べるのでそれを使います。
すなわち、1秒演奏するなら start(startTime)
と stop(startTime + 1)
を呼んであげればいい訳です。
ただし演奏が終わったら disconnect
したり null
を代入したりする処理が必要になります。そういうのは onended
にコールバックで指定できるのでそうします。
それを実装したのが以下。
コードは以下。
<div id="oscPlayer">▶</div>
<script>
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let oscillator = null;
const playButton = document.getElementById("oscPlayer2");
playButton.addEventListener("mousedown", function(event) {
if (oscillator === null) {
playButton.innerHTML = "⏸ Stop";
oscillator = audioCtx.createOscillator();
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); // value in hertz
oscillator.connect(audioCtx.destination);
// 1秒だけ鳴らしたあとお片付けする
let currentTime = audioCtx.currentTime;
oscillator.onended = function() {
oscillator.disconnect(audioCtx.destination);
oscillator = null
}
oscillator.start(currentTime);
oscillator.stop(currentTime + 1);
} else {
playButton.innerHTML = "▶️ Play";
oscillator.stop();
oscillator.disconnect(audioCtx.destination);
oscillator = null
}
}, false);
</script>
音量を調整する
GainNode
というのでできます。
音源とスピーカの間に挟まるエフェクト扱いです。
こんな感じ(動作例略)
const gainNode = audioCtx.createGain();
gainNode.gain.value = 0.025;
gain.connect(audioCtx.destination);
osc.connect(gain)
演奏する
まあ、あとはプログラマなら適当にコード書けば演奏できるようになるでしょう。
実装したのがこちら。今度はボリュームが調整できます。
コードは後程gistを貼りますが、だいたい以下のようなコードを書いたら演奏できるようになってます。
const music = Music.parse("\
C C G G | A A G2 |\
F4 F E E | D D C2 |\
G4 G F F | E E D2 |\
G4 G F F | E E D2 |\
C4 C G G | A A G2 |\
F4 F E E | D D C2 |");
const player = new Player(90, new SimpleTone('sine'));
player.playMusic(music)
ひとまずやりたいことができました。
ピアノの音色?
ところで、演奏したときの音色気になりませんでしたか? 正弦波なのでちょっと機械っぽい音色になります。
しかしあらゆる波は正弦波の組み合わせで表現できますし、ピアノの鍵盤を叩いたときの音の起伏もプログラムで表現できるので今までにでてきた道具でピアノの音色っぽいものも作れます。ちょっと挑戦してみましょう。
やってみたのがこれです。
どうですか?少なくとも先程の正弦波よりは楽器っぽくきこえないですか?
実装は以下のようなコードです。
class PianoTone extends Tone {
createOsc(detune, gain, start, duration, output) {
const attack = 0.2;
const decay = 0.1;
const sustain = gain * 0.7;
const release = 0.3;
const osc = this.ctx.createOscillator();
osc.frequency.value = this.concertPitch;
osc.detune.value = detune;
osc.type = 'sine';
const gainNode = this.ctx.createGain();
const t0 = start;
const t1 = t0 + attack;
const t2 = t1 + decay;
const t3 = t0 + duration;
const t4 = t3 + release;
gainNode.gain.linearRampToValueAtTime(gain, t1);
gainNode.gain.setTargetAtTime(sustain, t1, decay)
gainNode.gain.setTargetAtTime(0, t3, release);
osc.connect(gainNode);
gainNode.connect(output);
osc.onended = function() {
osc.disconnect(gainNode);
gainNode.disconnect(output);
}
osc.start(t0);
osc.stop(t4);
return osc;
}
play(detune, start, duration, output) {
let o1 = this.createOsc(detune - 3600, 0.512, start, duration * 0.064, output);
let o2 = this.createOsc(detune - 2400, 0.64 , start, duration * 0.16 , output);
let o3 = this.createOsc(detune - 1200, 0.8 , start, duration * 0.4 , output);
let o4 = this.createOsc(detune , 1 , start, duration , output);
let o5 = this.createOsc(detune + 1200, 0.4 , start, duration * 0.8 , output);
let o6 = this.createOsc(detune + 2400, 0.16 , start, duration * 0.64 , output);
let o7 = this.createOsc(detune + 3600, 0.064, start, duration * 0.512, output);
return {
oscs: [o1, o2, o3, o4, o5, o6, o7],
stop: function(when) {
for(let osc of this.oscs) {
osc.stop(when)
}
}
};
}
}
ポイントは2つ。1つはADSRで、もう1つは複数の波の組み合わせです。 どっちも私は詳しい訳じゃないんですがちょっと解説してみます。
ADSR
ADSRはAttack、Decay、Sustain、Releaseの略でそれぞれ立ち上がり、減衰、減衰後の保持、余韻と訳されるようです。
ピアノ的に解釈すると鍵盤に触れてからを叩き終わるまでがAttack、叩いた瞬間から音が落ち着くまでがDecay、鍵盤を押しっぱなしの時間がSustain、鍵盤を離したあとにも鳴ってる時間がReleaseですかね?
音を作る人はこういうパラメータで音(エンベロープ)を作るらしいです。
上記のコードでは以下の部分ですね。
const attack = 0.35;
const decay = 0.2;
const sustain = gain * 0.7;
const release = 0.3;
// ...
const gainNode = this.ctx.createGain();
const t0 = start;
const t1 = t0 + attack;
const t2 = t1 + decay;
const t3 = t0 + duration;
const t4 = t3 + release;
gainNode.gain.linearRampToValueAtTime(gain, t1);
gainNode.gain.setTargetAtTime(sustain, t1, decay)
gainNode.gain.setTargetAtTime(0, t3, release);
duration
が全体の演奏時間(鍵盤に触れてから離すまで)で、そのうち物理特性で attack
と decay
に使われる時間が決まります。
本当は鍵盤の叩き方で attack
の方は変わると思いますがこの人は同じ強さで叩く人と思いましょう。
波の組み合わせ
コードでいうとこの部分です。 detune
が1200で1オクターブなので、対象の音の3オクターブ下から3オクターブ上までの音を合成しています。
let o1 = this.createOsc(detune - 3600, 0.512, start, duration * 0.064, output);
let o2 = this.createOsc(detune - 2400, 0.64 , start, duration * 0.16 , output);
let o3 = this.createOsc(detune - 1200, 0.8 , start, duration * 0.4 , output);
let o4 = this.createOsc(detune , 1 , start, duration , output);
let o5 = this.createOsc(detune + 1200, 0.4 , start, duration * 0.8 , output);
let o6 = this.createOsc(detune + 2400, 0.16 , start, duration * 0.64 , output);
let o7 = this.createOsc(detune + 3600, 0.064, start, duration * 0.512, output);
これは数学的に解釈すればフーリエ変換的に複数の周波数の正弦波を組み合わせてピアノの音色を再現しようとしています。 物理的に解釈すれば固有振動数が倍数関係にあるピアノ線が共鳴するはずなので、それを表現しています。 本当はピアノ線の長さ(音の高さ)で物理特性が変わるはずなのでADSRも変わるはずですが面倒なので無視しましょう。
まとめ
Web Audio APIを使って音を鳴らしてみました。 Web Audio APIは周波数を指定して定音を鳴らす機能しかないので、自分で演奏機能や音色などを再現してみる試みをしました。その過程でADSRや波の合成などを知りました。
参考文献
- Example and tutorial: Simple synth keyboard - Web APIs | MDN
- Controlling multiple parameters with ConstantSourceNode - Web APIs | MDN
- エンベロープジェネレータ | Web Audio APIの基本処理 | WEB SOUNDER - Web Audio API 解説 -
- Web Audio API でピアノの音色に近づけたい | q-Az
付録: コード
今回実装したPlayerのコードはこちらに置いておきます。 楽譜部分を書き換えたら他の楽曲も演奏できるので気に入った方は試してみて下さい。
ライセンスはMIT/Apache-2.0のデュアルライセンスとします。