ブラウザで動くシンセサイザーの制作
当ブログの初記事となります。張り切って書きたいと思います。
今回は、シンセサイザーの基礎とWeb Audio APIの勉強もかねて、ブラウザ上で動作するシンセサイザー (以下ブラウザシンセと呼びます) を制作してみたので紹介いたします。実際に動くデモもあるのでぜひ触ってみてください。
目次
ブラウザシンセを作ったきっかけ
Advent Calendarに参加しませんかというお誘いを受けて、せっかくなので何か新しいことを勉強してみようと思い、前々から触ってみたいと思っていたWeb Audio APIを使って何かを作ってみようと思い立ったことがきっかけです。
もともと趣味でパソコンを用いた楽曲制作 (DTM) を10年以上やっていたこともあり、いつか自分でソフトウェア・シンセサイザーを作ってみたい気持ちもありました。そんな個人的背景と今回の機会がうまく結びつき、じゃあWeb Audio APIでシンセを作ってみようという事になった訳です。
Web Audio API自体は2010年代前半から存在する技術仕様なので、もちろんブラウザシンセを作ったのは自分が最初ではなく、既に先人たちが作った沢山のすばらしいブラウザシンセの作例があります。ここで一部紹介いたします。
今回自分でブラウザシンセを作るうえで、先人たちとはまた違ったコンセプトにしようと考え、以下のことを意識して作りました。
- 簡単な操作で多彩な音作りができるようにする。ビギナーの方でも音作りの楽しさに目覚められるような入門機になることが理想。
- どんな音になっているか一目でわかるように、パラメータ数を可能な限り少なくする。
- せっかくブラウザシンセを作るので、いわゆる「ホームページ感」のあるUIにする。パラメータ操作にはフェーダーや回転ツマミなどではなく、ラジオボタンやレンジ入力等のHTML5標準のUIを使用する。
今回制作したもの
- ブラウザ上で動作するシンセサイザー
- HTMLファイル一つだけで作られており、ブラウザ上でそのページを表示するだけで動作する
- PCのキーボード操作でピアノのようにドレミファ……の音程を再生できる
- Midiキーボードを繋いで演奏することも可能
- 再生する音の波形、音量カーブ、フィルタ処理などをUI上で変更することによって、音色を自在に変えることができる
- 音色を変えて遊んでいるうちにシンセサイザーの仕組みが分かってくるので勉強になる (と良いな……)
- 音色を作ったらアプリ内のプリセットに保存できる (画面を更新すると消える)
実際に動作するデモはこちらになります。
※ すみませんが現状Google Chromeのみ対応です。他のブラウザでは動かないと思います (未確認) 。あとMacのGoogle Chromeでの動作も未確認です。
シンセとか音楽とか全然詳しくないですよという方向けに、このシンセサイザーの操作説明を兼ねたデモ動画をYouTubeにあげてみました。こちらも是非ご覧ください。
シンセサイザー基礎
シンセサイザーの原理を学ぶ教材は山ほどあるのですが、個人的にはAbletonの「Learning Synth」が非常にわかりやすく、しかもウェブサイト上でインタラクティブに学べるのでオススメです(実際に音が出ます!)。
私もいままでちゃんと勉強したことがなかったので、上記のサイトでシンセサイザーの原理について一通り学びました。以下に学んだことをまとめてみます。
シンセサイザーとは
まず、シンセサイザーとは何かというと、電気的に (もしくはソフトウェア上で) 音声波形を生成、合成、および編集し、それらを楽音として出力する機材 (またはソフトウェア) となります。バンド等で使用される電子キーボードと呼ばれる楽器もシンセサイザーの一種という事になります。
シンセサイザーの構成要素
今回作成するシンセサイザーは以下の構成要素からなります。なお、以下に登場する略語 (OSCなど) は便宜上の表記であり、一般的な表記ではないかもしれませんのでご注意ください。
オシレーター (Oscillator, OSC)
シンセサイザーの出音の基本となる波を生成する。
オシレーターの波形は周期的で、波形の種類によって音色が変わる。またオシレーターの音程は波の周波数によって決定される。
今回は波形の種類として
の4種類を用意している。
また2台のOSCを使って異なる種類の波形を同時発音できることから、波形の種類の組み合わせで様々な音色を楽しめる。
フィルター
音を周波数レベルでカットしたり増幅したりする処理。
今回は、
- 高周波成分のカット (Low Pass Filter, LPF)
- 低周波成分のカット (High Pass Filter, HPF)
の二種類を用意している。
高域をカットすると音のキンキンした成分を取り除くことができ、低域をカットすると軽い音となる。
カットの基準となる周波数 (カットオフ周波数, Cutoff Frequency) を調整することでフィルタ経由後の音の質感を調整できる。
また、カットオフ周波数をダイナミックに変化させることで、スカスカな音が徐々にボリューミーになっていくような曲を盛り上げる効果を出す事が出来る (filter sweepと呼ばれる手法)。
音量エンベロープ (Amplifier Envelope, AMP ENV)
パラメータが時間的にどう変化するかをエンベロープとして記述する。今回はこのエンベロープを音量に対して適用するので音量エンベロープと呼ぶ。一般的なエンベロープは、パラメータとして以下の四つをもつことからADSRと呼ばれることが多い。
- Attack Time:鍵盤を押した瞬間 (音量がゼロ) から最大音量に到達するまでの時間。Attack Timeを長めに取ると音の立ち上がりが遅くなり、ふわっとした音になる。
- Decay Time:音量が最大になってから、音量が後述のSustain Levelに減衰するまでの時間。
- Sustain Level:音量がAttack Timeを経て一度最大になったあと、Decay Timeが経過した後に到達する音量。鍵盤を押し続けている間、音はこのSustain Levelで鳴り続ける。4つのパラメータのうちSustain Levelのみが音量をさすことに注意する (他は時間をさす)。
- Release Time:鍵盤を離してから音量が完全にゼロになるまでの時間。音の余韻のようなものと思っていただいてよい。リリースタイムがゼロだと鍵盤を離した瞬間に音が鳴らなくなる。
今回はこのADSRエンベロープを音量に対して適用しているが、音量以外のパラメータ (オシレーターの周波数など?) にも適用することがある(らしい)。
低周波オシレーター (Low Frequency Oscillator, LFO)
周期的な低周波を生成する。先述のオシレーターが音そのものを生成するのに対し、LFOは他の構成要素のパラメータに対して適用し、そのパラメータを周期的に揺さぶるような使い方をするという違いがある。
例えば、LFOをシンセの音量に対して適用すると、音量が周期的に増減するのでトレモロのような効果を出す事ができる。また、LFOをオシレータの周波数に対して適用すると、音程が周期的に変化するのでビブラートのような効果を出すことが出来る。
独自要素 (ミキサー)
今回作ったシンセはオシレーター(OSC)が二つある構成となっている。通常は、OSC1, OSC2それぞれに音量コントロールがついているが、実際に操作してみるとそれぞれの音量を調整するのが若干面倒である。そこで、今回は音作りを簡単にするため、OSC1, 2の音量バランスを整える要素 (ミキサー) を独自要素として用意し、OSC1, 2を重ね合わせた音量はミキサーを経由して常に一定となるようにした。
要素間の接続
これらの構成要素と、入力機器 (キーボードなど)、および出力機器 (スピーカーなど) を接続 (ルーティング) することによって、入力に応じた音の生成、加工、出力までを行う。
シンセサイザーの歴史
あまり詳しくないので割愛いたします。 とても面白いトピック (というか深すぎる沼) かと思うのでぜひ調べてみてください。私も勉強中です。
Web Audio API基礎
Web Audio APIはブラウザ上で音声を扱うためのAPI群です。
ブラウザ上で使える機能のため、Webページ内のJavaScriptから使用することができます。
今回は概要のみ説明します。詳細な解説は以下のページがわかりやすいです。
基本概念
- ブラウザAPIなのでJavaScriptで叩ける。
- 音声の操作はブラウザのオーディオコンテキスト (Audio Context) 内で行われる。
- 各操作はオーディオノード (Audio Node) として実現される。
波形生成 (オシレータ, LFOに使用) やフィルタリング、ゲイン調整 (音量エンベロープに使用) などの主要な操作はオーディオノードとして既に実装されている。
=> 既にあるものをそのまま使えばよいので、よほど複雑な事をしない限り簡単に実装できます! - オーディオノード同士を入力—出力の関係で接続することができる。上述のMDNのページではこの特徴をモジュラールーティングが可能であると表現している。モジュラールーティングが可能であることはWeb Audio APIの主要な特徴であり、ブロックを組み合わせるかのように簡単に音声信号の処理ルーティーンを組むことができるので便利である。
- オーディオノード同士を接続したグラフ構造をオーディオグラフ (Audio Graph) と呼ぶ。
- オーディオノードの接続先はオーディオノードそのものでも良いし、オーディオノードのパラメータであってもよい。たとえば、オシレーター周波数をLFOで揺らしたい場合、LFOのノードの接続先をオシレーターの周波数パラメータに対して適用する。
- チャンネル構成の異なるオーディオ (モノラル、ステレオ) も同一のオーディオコンテキスト内でまとめて扱うことが出来る (今回は深入りしない)。
オーディオノードの種類
今回使用したオーディオノード3種類を紹介します。この3つ (特に上二つ) を覚えればとりあえずブラウザシンセは作れるということになります。
- Oscillator Node:周期的な波形を出力するノード。オシレーター、低周波オシレーター (LFO) はこのノードで実現する。
- Gain Node:入力音の音量を調整して出力するノード。使用箇所は同時発音時の音量調整や、音量エンベロープの実現など多岐にわたる。
- BiquadFilterNode:入力音に二次フィルタを適用して出力するノード。今回は出音に対して適用するローパスフィルター、ハイパスフィルターに使用している。
オーディオノードの接続
node1.connect(node2).connect(node3.audioparam) ... のような形でどんどん繋いでいきます。
Audio Nodeから直接Audio Nodeに接続するだけでなく、ノードのパラメーター (Audio Param)にも接続できるので、たとえば波形出力 (Oscillator Node)の周波数 (Audio Param)に対して別の波形の出力 (Oscillator Node)を適用するといったこともできます。
以下の例はOSC 1の音程がOSC 2によってウニョウニョ揺れるような接続になります。ビブラートみたいなやつですね。
OSCNode2.connect(OSCNode1.frequency);
サンプルコード
「ボタンを押すと440Hz (ラの音, A4)のサイン波を再生する」という、Web Audio APIにおけるHello World的なものを実装してみます。
※ はてなブログの見たままモードで記事を書き始めてしまったので、コード部分を後付けでhtml編集することになり、シンタックスハイライト等全くないので見づらくなっていると思います。すみません。
let audioCtx = null;
const main = () => {
// avoid multiple clicking
if (audioCtx) return;
// construct Audio Context
audioCtx = new AudioContext();
// osc1 represents 440Hz sine wave
const osc1 = new OscillatorNode(audioCtx, { frequency: 440, type: 'sine' });
// gain represents amplification level of input sound before it goes to output
const gain1 = new GainNode(audioCtx, { gain: 0.5 });
// connect osc1 -> gain -> your audio output device (speakers, headphones, etc...)
osc1.connect(gain1).connect(audioCtx.destination);
// start to play osc1
osc1.start();
}
上記コードはJSFiddle上で動作確認できます。
https://jsfiddle.net/608hr327/
ちなみになぜ440Hzなのかというと、音楽における一般的な調律 (平均律) における基準の音として定義されているためです。
Web Midi API基礎
MIDIとは
MIDI (Musical Instrument Digital Interface) とは、音楽の演奏情報をデータ化し、複数の音楽機器間で転送・共有できるようにする共通規格です。
例えば、MIDI出力に対応しているキーボードを、MIDI入力に対応しているシンセサイザーに接続し、「ラ(A4)」の鍵盤を弾きます。すると、「ラ(A4)の音を発音開始する」というMIDI命令がシンセサイザーに行くので、シンセサイザーはA4キーに割り当てられている音 (ほとんどの場合は440Hz) を再生する、というような使い方となります。
今回のブラウザシンセもMIDI入力対応のため、MIDI対応のキーボードを接続して鍵盤演奏が可能です。このMIDI入力をブラウザ側でハンドリングするためにWeb MIDI APIを使用しています。
MIDIについては、語るだけで単独の記事が書けそうなくらい重要な概念なのですが、今回はMIDIキーボード入力の処理に使うだけなので詳細説明はスキップします。
Web Midi APIとは
ブラウザ上でMIDIデータの入力・出力を扱えるようにするAPI群です。
Web Midi APIの基本的な説明、使い方は以下のサイトがわかりやすいです。
ブラウザシンセ制作過程
一つ一つコードを載せて解説しようとすると長くなりそうなので、今回は実装の説明はスキップさせていただきます。どちらかというと、どんなことを考えながら作ったのかという設計指針の方に寄せた紹介をしようと思います。
ソースコードを確認したい方はブラウザ上で直接右クリックしたりデベロッパーツールを使ってみていただければと思います。(コードはそんなにきれいではないですが……)
コンセプトスケッチ
まずどういうものを作るか考えてスケッチします。以下の図は完成後にPowerPointで清書したものです (本物のスケッチは汚すぎてお見せできない)
図にもありますが、音作りの幅が広がるという点から、2つのオシレーターの実装と、ADSRを用いた音量コントロールの2点は絶対に実装したいと思っていました。
LFOは適用先パラメータのスイッチングが難しそうな予感はしましたが、LFOがあった方がシンセっぽいと思ったので無理してでも入れようと思いました。
逆に、フィルタはそこまで音作りに寄与しない (と個人的には思っている) のと、興味がなかったので最低限の機能のみとしました。
次に、この構成を実現するために、大体どのような配線にすればよいかをブロック図にまとめました。それぞれのブロックがWeb Audio APIのオーディオノードに対応します (キーボード入力は別)。
図に描き起こすことによって、例えばオシレーターやフィルタなどは対応するノードが用意されているので実装は簡単そうだなとか、逆にミキサーやADSRは工夫が必要そうだなという見積もりができます。
オシレーターモジュールの実装
コンセプトスケッチとブロック図を基に実装に入ります。まずは何かしら音が出たほうが楽しいので、出音の元となる音声波形の出力モジュールを実装しました。これをオシレーターモジュールと呼びます。
オシレーターモジュールは以下の要素から構成されます。
- オシレーター1, 2:出音の基本となる波形。OscillatorNodeをそのまま使用して実装する。パラメータとして波形の種類 (サイン波、ノコギリ波など…) と、オクターブピッチを選択できる。また、二つのオシレーターは独立しており、違うパラメータを設定できる。
- ミキサー:オシレータ1, 2の音量の混ぜ合わせ具合を調整する。パラメータとしてratioを持ち、ratio = 0の場合はオシレータ1の音のみ、ratio = 1の場合はオシレータ2の音のみが発音される。ミキサーは2つのGain Nodeを使って実装し、各Nodeのgainの値をそれぞれ1 - ratio, ratio とすることで求める機能を表現できる。ミキサーを介して2つのオシレーターを混ぜ合わせた音量は一定となる。
OscillatorNode - Web APIs | MDN
また、鍵盤を押すと音が出て、離すと音が消えるという機能を実現するために、オシレーターモジュールの後段にもう一つGain Nodeを配置します。キーボードが押されたらこのGain Nodeのgainの値を1、離されたらgainの値を0 (= ミュート) にすればよいというわけです。
これらを図にすると以下のようになります。
複数同時発音 (Polyモード) 対応
先に述べたオシレーターモジュール一つだけの構成だと、複数の音を同時に出すこと (シンセの言葉ではPolyモードと言ったりします) ができません。つまり、「ドミソ」のような和音が弾けなくなってしまいます。
今回はPolyモードに対応したかったので、どういう構成にすればよいか迷った結果、10個のオシレーターモジュールを常駐させておいて、押された鍵盤の数だけオシレーターモジュールにキーボードに対応する周波数を適用、およびミュートを解除する、という下図のような構成にしました。
10という数字は両手の指の数からきています。実際に演奏する場合は両手指の数を超えないだろうという想定で決め打ちしました。
「ドミソ」を鳴らす場合、オシレーターモジュール1~3を使用し、残り7つは使用しません。使用しないオシレーターモジュールはミュートします。
また、鳴っているオシレーターモジュールの音量 (厳密にはオシレーターモジュールの後段のGain Nodeのgain値) は以下の式で補正しています (コードは擬似コードです)。
OSC Gain.gain.value = 1 / (1 + log(鳴っているオシレーターモジュールの数))
なぜ補正するかというと、オシレーターモジュールの音量をすべて1にしたまま混ぜ合わせてしまうと、次のノードに入る時のgainが大きすぎて音が割れてしまうためです。
上の式自体には特に根拠はありませんが、色々試した中で聞こえ方が一番よさそうなものを採用しました。
Filterの実装
フィルタは特に難しいことはなくBiquad Filter Nodeをそのまま使用しました。
BiquadFilterNode - Web APIs | MDN
ADSRの実装
ここが一番の難関でした。
まず、ADSRはGain Nodeのgain値を経過時間で変動させる方式がよさそうだったので採用しました。gain値は、鍵盤を押したタイミング、離したタイミングを起点として以下の図のように変化させます。
実装はlinearRampToValueAtTimeメソッドを使用します。
AudioParam.linearRampToValueAtTime() - Web APIs | MDN
コードは擬似コードですが以下のように書きます。
// 音の立ち上がり時
OSC Gain.gain.linearRampToValueAtTime(1 / (1+log(発音数)), 鍵盤を押したタイミング + attack time);
// sustain levelに落ちる
OSC Gain.gain.linearRampToValueAtTime((1 / (1+log(発音数))) * sustain level, 鍵盤を押したタイミング + attack time + decay time);
// 鍵盤を離して音が減衰する時
OSC Gain.gain.linearRampToValueAtTime(0, 鍵盤を離したタイミング + release time);
上記のように実装してみたのですが、音が立ち上がらないなど、思ったような動きにならずハマりました。原因は、例えば音の立ち上げが完了する前に鍵盤を離して音の減衰が始まった後、(本来キャンセルされるべきだった) 最大音量からの減衰が始まる……、という風に、様々なスケジューリングが混在してしまうことにありました。
解決策は、音の立ち上げもしくは減衰時にそれまでにスケジュールされていた未完の処理を全てキャンセルすることでした。
OSC gain.gain.cancelScheduledValues(鍵盤が押されたまたは離されたタイミング);
ADSRの実装、スケジューリングのキャンセルについては、以下のサイトが非常に参考になりました。
エンベロープジェネレータ | Web Audio APIの基本処理 | WEB SOUNDER - Web Audio API 解説 -
LFOのルーティング
最後にLFOのルーティングについて解説します。
LFO自体はオシレーターと同じように、Oscillator Nodeをそのまま使って実装できます。問題はLFOの適用先を自由に切り替えられるようにする部分です。
ノード同士もしくはノードとパラメータの接続にはconnectというメソッド、接続解除にはdisconnectedというメソッドを使うのですが、接続先を動的に切り替えるとなるとconnect/disconnectを正しくコントロールしなければならず面倒だと思いました。そこで、以下の図のような方式で代替しました。
まず、LFOはすべてのパラメータに対して、Gain Nodeを介して接続しっぱなしにします。パラメータ適用先の選定はどうするかというと、適用したいパラメータに接続されるGain Nodeのgain値を1、それ以外をゼロにすることで実現しています。適用先パラメータを切り替える場合はGain Nodeのゲイン値をいじるだけでよいので、connect/disconnectをいちいち呼ばずに済みます。
また、この図では端折っていますが、LFOの振幅を可変にするために、図のLFO Gainの前段にもう一つGain Nodeを挟み、そのgain値をUIから自由に設定できるようにしています。
所感
今回ブラウザシンセを実際に作ってみて、様々なことを学びました。
ブラウザは年々進化している
時代の変化とともにブラウザ内のAPIが充実してきており、様々なことができるようになってきていることを実感しました。
私がHTMLを書き始めたころはせいぜいブラクラを作って遊ぶ程度のことしかできなかったのに、今やシンセサイザーが作れるというだけでも驚きです。
ここでは触れませんがWeb GLなど、高度なグラフィックをブラウザ上で表示するためのAPIも充実してきています。
これからも時代の変化とともにブラウザだけで完結出来ることが増えていくのかもしれません。
ブラウザシンセはサウンドプログラミングの入門に最適
シンセサイザーの基本に始まり、Web Audio APIやWeb Midi APIといったブラウザで音データを扱うための方法まで一気通貫で学んだことで、サウンドプログラミングの基礎を身に着けることができたと思います。
良かったのはこれがHTML上でJavaScriptを書いてブラウザ上で実行するだけという手軽さで実現できたことです。
サウンドプログラミングを学ぶには、他にもMAX言語やVSTを使うなどいくつか方法はありますが、ブラウザを使って試すのが一番手っ取り早いのではないかと思います。ブラウザであればだれでも簡単に導入できるのでおすすめです。
実用にはやや難あり
今回の成果物だけを見るとあたかも実用レベルのシンセが出来たかのように見えますが、実用で使うには不十分な点も残っています。
- 同時発音の制御。複数のオシレーターを常駐させて使いまわす方式がよくなかったのかもしれませんが、複数同時発音の際に不自然な音の立ち上がり方をすることがあります。
- レイテンシ。これも実装が良くないのかもしれませんが、打鍵から発音開始までに若干のラグがあるのが気になります。本当にレイテンシをギリギリまで下げたいのであれば、JavaScriptではなくWeb Assemblyの使用を考えたほうがいいのかもしれませんが、そこまでしてブラウザで動かしたいか?とも思います。
- スタンドアロンでしか使えない事。音楽制作で使う場合はVST (Macの場合はAudio Unit) プラグイン形式で配布してDAWソフトウェアで読み込めるようにするのが便利です。
これらの理由から、音楽制作やライブ演奏で用いる場合は別のソフトシンセを使うのがよいでしょう。また実用に耐えうるソフトシンセを自作したい場合は、MAX言語を使ってみたり、VSTプラグインを作ってみたりするのが良いと思います。
正直、無料のソフトシンセとしてはSynth1がすごすぎるので、わざわざ自作せずともSynth1を使っておけばよいのではないかと思います。
まとめ
Web Audio APIを使ってブラウザで動作するシンセサイザーを制作しました。
ADSR実装やLFOのルーティングなど、若干工夫が必要だったところはありましたが基本的にはWeb Audio APIのAudio Nodeをそのまま使えばよく、思ったより簡単に実装できました。
以下のデモ動画のように、シンセ入門機として使いやすいものが出来上がったのでよかったです。
シンセサイザーの基礎やサウンドプログラミングを学ぶには、ブラウザとWeb Audio APIを使えば手っ取り早く手を動かして試せるのでオススメです。
逆に、本格的なソフトウェアシンセサイザーを作るには音楽制作ソフトにプラグインとして読み込めるよう、VSTなどの形式やMAX言語を使用するのが良いかと思います。また、自分で作らなくとも無料で使えるSynth1のような優秀なシンセもあるので、興味のある方は使ってみてください。
この記事で ほぼ厚木の民 Advent Calendar 2020 - Adventar に参加しています。
最後まで読んでいただきありがとうございます。
【おまけ】このシンセの音だけを使って作った曲を貼っておきます。
関連リンク
W3C仕様