シンセサイザーを作ろう
シンセサイザーの構成
ざっくりと、こんな作りになる。
実験場
See the Pen実験場by lidjar (@lidjar1) on CodePen.
「サクッと試したい場合はここにコードを貼り付けて」と言いたかったのだけど、Qiita側から禁止されてるのね。残念。
オシレーター
オシレーター, oscillator, 発振器
シンセサイザーの出発点で、音を生み出すユニットだ。
音を出す
先ずは音を出してみよう。
できる限りの最小構成で作ってみるぞ。
<!--再生ボタンを設置-->
<button id="play">再生</button>
// オーディオとノードを接続する
const context = new AudioContext();
const oscillator = new OscillatorNode(context);
oscillator.connect(context.destination);
// ボタン入力を検知して再生する
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => oscillator.start());
Q. 最小構成と言いながら「ボタンを押す」という一手間をかけるのはどうして?
A. ページを開くと同時に音が鳴ったらビックリするよね。そういったことを避けるために、Web Audio API は 自動再生ができない 作りになっているんだ。今回の例ではclickイベントを採用したけど、chengeやinputやkeydownなどユーザー操作が検知できれば何を使ってもOKだ。
音を止める
次は音を止めてみよう。停止は自動実行OKだ。
音を出す処理に続けて、タイマーで音を止める処理を追加しよう。
<button id="play">再生</button>
const context = new AudioContext();
const oscillator = new OscillatorNode(context);
oscillator.connect(context.destination);
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000) // ←停止する
});
もう一度音を出す
気付いた人も居るかもしれないけど、もう一度ボタンを押しても音は出ない。
これは oscillator の仕様が使い捨てることを前提としているからだ。
ボタンを押すたびに oscillator を作るように修正しよう。
<button id="play">再生</button>
const context = new AudioContext();
// const oscillator = new OscillatorNode(context); // ←削除
// oscillator.connect(context.destination); // ←削除
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context); // ←ここへ移動。clickのたびに作る。
oscillator.connect(context.destination); // ←ここへ移動。繋ぐのも忘れずに。
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
音色を変える (波形を変える)
これまでの例では正弦波を使ってきた。
これをシンセサイザーで好まれる鋸波に変えてみよう。
| 波形 | 周波数特性 | 電圧特性 | 音色 | 備考 |
|---|---|---|---|---|
| 正弦波 | 単一の周波数の波。 最も単純な波 |
sinθで計算した波 | 音叉 | 加工のしようがない純粋な音。故にあまり使われない。 |
| 鋸波 | n倍音の周波数を含む波 | 最大まで直線的に増加した後に瞬時に最小に戻るのを繰り返す波。 | 弦楽器に喩えられる音 | 最も多くの周波数を含む音。加工のしがいがあるため頻繁に使われる。 |
| 矩形波 | 奇数倍の周波数を含む波 | 等間隔でONとOFFを繰り返す波。 最も単純な波 |
いわゆる電子音 | あえて加工せずに使いたくなる音。 |
| 三角波 | 高いほど減衰する奇数倍の周波数を含む波 | 最大まで直線的に増加した後に同じ傾きで最小まで減少するのを繰り返す波。 | 管楽器に喩えられる音 | そのままでも良い感じに聞こえる音。 |
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth', // ← sine,sawtooth,square,triangle が選択できる
});
oscillator.connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
Q. 正弦波を指定した覚えは無いのだけど...
A. 何も指定しないと自動で正弦波が選択されるよ。
音色を変える (周波数を変える)
これまでの例では440Hzのラの音を使ってきた。もちろんコレも変更できる。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 220, // ← 2倍にすると1オクターブ上がるよ
});
oscillator.connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
Q. 440Hzを...(ry
A. 何も指定しないと自動で440Hzが選択されるよ。
音色を変える (周波数をセントで変える)
とは言え、ドレミの音階を出すためにいちいち周波数を計算するのは大変だよね。
そこで活躍するのが「セント」という単位と概念。
どんな音程でも100セントごとに半音上がる(下がる)から難しい計算が必要ないのだ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440, // ← A4ラの周波数で固定して
detune: 300, // ← 3度上のC5ドの音を出してみよう
});
oscillator.connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
ちなみにその面倒な周波数の計算式は次のとおり。
基準の音の周波数 × 2 基準の音からの度数/12
詳しく知りたい場合は「2の12乗根」で検索しよう。
音色を変える (波形を作る)
オシレーターパートはこのセクションが最後。
しかし、労力の割に使い道が限られるので、読み飛ばしてしまってOK。
波形の選択肢にはもう一つ「custom」という選択肢がある。frequencyで指定した周波数の 1倍音,2倍音....n倍音の強さを個別に指定して、正弦波の合成波を作れる機能だ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'custom', // ← customを指定
frequency: 440,
detune: 300,
periodicWave: new PeriodicWave(context, { // ←PeriodicWaveを作る。contextを忘れずに。
real: new Float32Array([0, 3, 2, 1]), // 1番目の要素が1倍音、2番目の要素2倍音...以降同じ
imag: new Float32Array([0, 0, 0, 0]), // realとimagの配列は同じサイズにする必要がある。
}),
});
oscillator.connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
波を描くんじゃなくて波を合成する。楽しいっちゃ楽しいけどまぁ使わないよね。UIとか面倒だもん。
フィルター
フィルター, filter, 和名は...特になし。
オシレーターの後ろで音色を変えるユニットだ。
フィルターを通す
さっそくフィルターを通過させるぞ。
オシレーターと出力の間に接続だ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context); // ←フィルターを作って
oscillator.
connect(filter). // オシレーターと出力の間に接続
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
Q. フィルターも使い捨て?
A. いいえ、フィルターは永続的に使えます。
しかしシンセサイザーで固定のフィルターを使うと、オシレーターで「この音を出したい」と生んだはずの波がカットされて聞こえなくなってしまう。ということになりかねないので、使い捨て(オシレーターに追従する)構成になるのです。
特性を変える (カットオフを変える)
続いてパラメーターを弄って特性を変えていこう。
先ずは周波数。一般にカットオフと呼ばれるモノだ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
frequency: 440, // ←周波数特性を変える
});
oscillator.
connect(filter).
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
特性を変える (カットオフをセントで変える)
続いてデチューン。
オシレーターと同じようにセントを使って変更できるぞ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
frequency: 440,
detune: 300, // ←やっていることはオシレーターのそれと同じ。
});
oscillator.
connect(filter).
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
特性を変える (レゾナンスを変える)
お次はQ値。一般に呼ばれるレゾナンスとほぼ同義で、品質係数やクオリティーファクターとも呼ばれるね。
{{Q値の説明は難しい}}
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
frequency: 440,
detune: 300,
Q: 2, // ←q値はこれ。大文字であることに注意
});
oscillator.
connect(filter).
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
特性を変える (種別を変える)
蛇足その1。
これまで使ってきたのはローパスフィルター。
Web Audio API では、これ以外にも ハイパス、バンドパス、ローシェルフ、ハイシェルフ、ピーキング、ノッチ、オールパス のフィルターを選択できる。
が、シンセサイザーが使うのは主にデフォルト値ローパスフィルターなので、お世話になる機会はあまりないだろう。(次点でバンドパスフィルター)
エフェクターではどれも大活躍するから、そのときに思い出してやってくれ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
type: 'highshelf', // ←lowpass, highpass, bandpass, lowshelf, highshelf, peaking, notch, allpass から選択できる。
frequency: 440,
detune: 300,
Q: 2,
});
oscillator.
connect(filter).
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
特性を変える (利得を変える)
蛇足その2。
ローシェルフ、ハイシェルフ、ピーキング の3種のフィルターだけが参照するパラメーター。
先述のとおりシンセサイザーが使うのは主にローパスフィルターなので、こちらもまたお世話になる機会はほぼないだろう。エフェクターを作るときに思い出してやってくれ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
type: 'highshelf',
frequency: 440,
detune: 300,
Q: 2,
gain: -12, // ←利得はここで指定する
});
oscillator.
connect(filter).
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
アンプ
アンプ, amplifier, 増幅器
シンセサイザーの出口で音量を制御する役割のユニットだ。
アンプを通す
アンプもちゃちゃっと接続していこう。
フィルターと出力の間に繋ぐぞ。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
type: 'lowpass',
frequency: 440,
detune: 300,
Q: 2,
});
const amplifier = new GainNode(context); // ←アンプを追加
oscillator.
connect(filter).
connect(amplifier). // ←フィルターと出力の間に接続
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
Q. アンプも使い捨て?
A. このあとに出てくる変調器(LFOとエンベロープ)を個別に適用するために、オシレーターと同じレベルに置いて使い捨ててるよ。
音量を変える (利得を変える)
アンプのパラメーターは1つだけ。
ゲインを増減...と言いたいところだけど、設定範囲は0~1で初期値は1だから、実質的には減衰器としての利用が目的だね。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
type: 'lowpass',
frequency: 440,
detune: 300,
Q: 2,
});
const amplifier = new GainNode(context, {
gain: 0.2, // ←利得を変更。0~1倍なのでその中身は減衰器
});
oscillator.
connect(filter).
connect(amplifier).
connect(context.destination);
oscillator.start();
setTimeout(_ => oscillator.stop(), 3000);
});
LFO 低周波数発振器
変調器その1。
Low Frequency Oscillatorの頭文字を取ってLFO、日本語だと低周波発振器。
可聴音(20Hz~20kHz)より低い波を使って、音を周期的に揺らすユニット。
ユニット作成
LFOの中身はこれまで見てきたユニットとは異なり、発振器部分と増幅部分に分かれる。
各ユニットへ接続する前に、先ずはGainNodeを拡張したLfoNodeを作成しておこう。
class LfoNode extends GainNode {
constructor(context, {
type = undefined,
detune = undefined,
depth = undefined,
} = {}) {
// 増幅部
super(context, {
gain: depth,
});
// 発振部
this.oscillator = new OscillatorNode(context, {
type: type,
frequency: 20,
detune: detune,
});
this.oscillator.connect(this);
}
start(time = this.context.currentTime) {
this.oscillator.start(time);
}
stop(time = this.context.currentTime) {
this.oscillator.stop(time);
}
}
接続
LFOを定義できたので、各ユニット用に作って接続していく。
LFOや後述のエンベロープなどの変調器は、ユニット本体ではなくユニットのパラメーターへ接続することに注意しよう。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
type: 'lowpass',
frequency: 440,
detune: 300,
Q: 2,
});
const amplifier = new GainNode(context, {
gain: 0.2,
});
oscillator.
connect(filter).
connect(amplifier).
connect(context.destination);
const lfo1 = new LfoNode(context); // ←LFOを作成
const lfo2 = new LfoNode(context); // ←LFOを作成
const lfo3 = new LfoNode(context); // ←LFOを作成
lfo1.connect(oscillator.detune); // ←LFOをパラメーターに接続
lfo2.connect(filter.detune); // ←LFOをパラメーターに接続
lfo3.connect(amplifier.gain); // ←LFOをパラメーターに接続
oscillator.start();
lfo1.start(); // ←LFOを起動
lfo2.start(); // ←LFOを起動
lfo3.start(); // ←LFOを起動
setTimeout(_ => {
oscillator.stop();
lfo1.stop(); // ←LFOを停止
lfo2.stop(); // ←LFOを停止
lfo3.stop(); // ←LFOを停止
}, 3000);
});
パラメーター変更
パラメーター変更の要領は今までと同じ。
長くなって疲れてきたので、すべてのパラメーターを一気に解説します。
depthの単位が変調対象によって異なることだけ注意しよう。
<button id="play">再生</button>
const context = new AudioContext();
const playButton = document.querySelector('#play');
playButton.addEventListener('click', _ => {
const oscillator = new OscillatorNode(context, {
type: 'sawtooth',
frequency: 440,
detune: 300,
});
const filter = new BiquadFilterNode(context, {
type: 'lowpass',
frequency: 440,
detune: 300,
Q: 2,
});
const amplifier = new GainNode(context, {
gain: 0.2,
});
oscillator.
connect(filter).
connect(amplifier).
connect(context.destination);
const lfo1 = new LfoNode(context, {
type: 'square', // ←squareは救急車のように2音を繰り返すようになる
detune: 200, // ←繰り返しの速さ。基準は20Hzで単位はセント。
depth: 200, // ←detuneに繋ぐのでdepthの単位はセント
});
const lfo2 = new LfoNode(context, {
type: 'sawtooth', // ←sawtoothは緊急地震速報のようなスイープ音
detune: 300, // ←
depth: -500, // ←マイナス値を指定すると 下げ→上げ の順で繰り返すようなる
});
const lfo3 = new LfoNode(context, {
type: 'triangle', // ←triangleはsineに似た素直な揺れ方をする
detune: -100, // ←
depth: 0.2, // ←gainに繋ぐのでdepthの単位は...何になるんだろう
});
lfo1.connect(oscillator.detune);
lfo2.connect(filter.detune);
lfo3.connect(amplifier.gain);
oscillator.start();
lfo1.start();
lfo2.start();
lfo3.start();
setTimeout(_ => {
oscillator.stop();
lfo1.stop();
lfo2.stop();
lfo3.stop();
}, 3000);
});
エンベロープ
変調器その2
エンベロープ。
音を時間で変化させるらすユニット。基本的な考え方はLFOと一緒だが、あちらは勝手に周期変化したのに対して、こちらは何秒後に1、更に何秒後に0.5、更に更に何秒後に0。と、時間と値を予約する必要がある。
coming soon...