JavaScriptでMIDIを読み取り、シンセっぽいのを作りたい
MIDIとは
パソコンからシンセサイザーなどの電子楽器をコントロールするための規格です。
midiの情報には基本的に「音の高さ」「長さ」「大きさ」の情報数値で入っているデジタル機器で音を鳴らす時にmidiを読み取って音を鳴らしています。
オクターブ
https://gyazo.com/95f07e9bf848a8f366929c939033e997
0-127でドレミファソラシドを表現
大きさ(ベロシティ)
0-127で表現 => 127が一番音が大きい
長さ(ノートオン /オフ)
いつ鍵盤を押さえたか/ 離したか
Web Audio APIで音を鳴らす
code:js
const context = new AudioContext();
インスタンスを生成して使う。
code:js
const context = new AudioContext();
// オシレーターを呼び出す
const o = context.createOscillator();
// context.currentTimeになったら周波数を440Hzにする
o.frequency.setTargetAtTime(440, context.currentTime, 0);
// オシレータを出力に接続
o.connect(context.destination);
// オシレータ動作開始
o.start(0);
オシレーターとは、音色の元となるサイン波、ノコギリ波、三角波、矩形波、ノイズなどの基本的な波形を作り出します。
setTargetAtTime()で周波数(音の高さ)を決める。例では、440Hzのラの音が出る。
https://gyazo.com/5ad1c6c0e2536b2ec2713c82b1973dea
音色を変える
code:js
const oscillator = context.createOscillator();
// Sine波の音が鳴る
oscillator.type = 'sine';
// 矩形波の音が鳴る
oscillator.type = 'square';
// ノコギリ波の音が鳴る
oscillator.type = 'sawtooth';
// 三角波の音が鳴る
oscillator.type = 'triangle';
MIDIのキーを変える
MIDI番号を適切な周波数に変換するのに最適な式(mはmidiの番号
https://gyazo.com/6797a655cf6cabe872e902bf21acbad9
これをJavaScriptで再現すると
code:js
function playSound(m) {
o.frequency.setTargetAtTime(Math.pow(2, (m-69)/12)*440, context.currentTime, 0);
}
パソコンのキーボードにノートをマッピングする
code:js
// C4-C5 q-iキーのキーボードエミュレーション
const emulatedKeys = {
a: 60, // C
w: 61, // C#
s: 62, // D
e: 63, // D#
d: 64, // E
f: 65, // F
t: 66, // F#
g: 67, // G
y: 68, // G#
h: 69, // A
u: 70, // A#
j: 71, // B
k: 72 // C
};
// タイプされたら呼ばれる
document.addEventListener('keydown', function(e) {
if (emulatedKeys.hasOwnProperty(e.key)) {
noteOn(emulatedKeyse.key); }
});
// タイプが終わったら呼ばれる
document.addEventListener('keyup', function(e) {
if (emulatedKeys.hasOwnProperty(e.key)) {
noteOff();
}
});
midiプレイヤーを作る
midiの配列。pitchは高さ、beatsは拍(長さ)
code:js
export const kirby = {
bpm: 160,
source: [
{ pitch: 67, beats: 8 }, // G
{ pitch: 72, beats: 8 }, // C
{ pitch: 72, beats: 2 }, // C
{ pitch: 72, beats: 8 }, // C
{ pitch: 77, beats: 8 }, // F
{ pitch: 79, beats: 8 }, // G
{ pitch: 84, beats: 8 }, // C
{ pitch: 83, beats: 8 }, // B
{ pitch: 81, beats: 8 }, // A
{ pitch: 79, beats: 4 }, // G
{ pitch: 76, beats: 8 }, // E
{ pitch: 79, beats: 8 }, // G
{ pitch: 77, beats: 4 }, // F
{ pitch: 74, beats: 8 }, // D
{ pitch: 77, beats: 8 }, // F
{ pitch: 76, beats: 4 }, // E
{ pitch: 76, beats: 8 }, // E
{ pitch: 74, beats: 8 }, // D
{ pitch: 72, beats: 4 } // C
]
};
BPMとは曲の速さをあらわす単位。一般的には1分間の4分音符の数を表す。
code:js
const bpm = L.kirby.bpm;
const length = (60 * 4) / bpm;
const eps = 0.01;
function Play() {
getOrCreateContext(); //オシレーターを呼び出す
oscillator.start(0);
let time = context.currentTime + eps;
L.kirby.source.forEach((note) => {
const freq = Math.pow(2, (note.pitch - 69) / 12) * 440;
oscillator.frequency.setTargetAtTime(0, time - eps, 0.001);
oscillator.frequency.setTargetAtTime(freq, time, 0.001);
time += length / note.beats; // 一音の長さ
});
}
まだ実装してないがエンベロープもできそう
エンベロープとは単音における「音量」を変化させる部分のこと。
エンベロープのパラメーターにはAttack, Decay, Sustain, Releaseの四つがある。
Attack: 音量の立ち上がりが決まる部分。 どの位の時間をかけて最大の音量になるかを決める
Decay: Attack(最大音量)」から「サスティーン」へ繋げるまでの時間
Sustain: 鍵盤を押している間、どの音量で鳴り続けるのかを決定します。
Release: 鍵盤を離した時の音量の残り具合(残響)です。
https://gyazo.com/7a0499a5cbdb6ec818d2508bb2aca8a4
code:js
const context = new AudioContext();
const oscillator = new OscillatorNode(context); // 周波数と波形を指定して連続した信号を出力
const gain = new GainNode(context, { gain: 0 }); // 音量を調整
const initTime = context.currentTime;
gain.gain.setValueAtTime(0, initTime); // initTimeで音量を0
gain.gain.linearRampToValueAtTime(1, attackTime); // attackTimeに音量を1になるように直線的変化(アタック)
gain.gain.setTargetAtTime(sustain, attackTime, decayTime); // attackTimeになったらdecayTimeかけてsustainに向かって漸近的に変化(サステイン)
gain.gain.setTargetAtTime(0, decayTime, releaseTime); // decayTimeになったらreleaseTimeかけて0に向かって漸近的に変化(リリース)
oscillator.connect(gain).connect(context.destination);// gainをオシレーターにコネクト
oscillator.start();
linearRampToValueAtTime(value, endTime): endTime に丁度 value になるように現在値から直線的に変化させる
setValueAtTime(value, startTime): startTime になったら値を value にする
setTargetAtTime(target, startTime, timeConstant): startTime になったら現在値から target に向かって漸近的に変化させる
これを用いてmidiプレイヤーにもエンベロープを組み込めそう。
フィルターも簡単にできそう
Web Audio Apiのノード
AnalyserNode
信号を周波数分析してパワースペクトルを取り出したり、波形データを取り出せます。主な用途としてはスペクトルアナライザーがあります。
AudioBufferSourceNode
メモリバッファにあるデータを音源として信号を出力します。再生速度の可変やループの設定ができます。
AudioWorkletNode
JavaScript で直接信号のデータを処理するためのノードです。ScriptProcessor ノードが非推奨になり、後継として作られました。
BiquadFilterNode
入力信号にフィルターをかけて出力します。フィルターの種類は LPF / HPF / BPF など8種類あります。
ChannelMergerNode
それぞれ独立した信号をまとめてステレオ LR や 5.1ch の信号を出力します。
ConstantSourceNode
常に一定値を出力します。オートメーション関数と併用して複雑なエンベロープカーブを作る時などに使用します。
ConvolverNode
コンボリューション演算を行います。主にリバーブに用いられますが任意の特性のフィルターなども作れます。
DelayNode
入力した信号を遅らせて出力します。パラメータは delayTime の1つだけです。
DynamicsCompressorNode
いわゆるコンプレッサーです。入力信号の音量の変化を抑えて出力します。
GainNode
入力した信号を増幅または減衰させて出力します。パラメータは gain の1つだけです。
IIRFilterNode
こちらもフィルターですが、フィルター係数を自由に設定できますが、その代りにリアルタイムに特性を変化させる事はできません。固定的で特殊な特性のフィルターが必要な場合に使用します。
MediaStreamAudioDestinationNode
MediaStream の出力として扱える形で出力します。
MediaElementAudioSourceNode
HTML上に組み込まれている audio エレメントを音源にします。
MediaStreamAudioSourceNode
WebRTC でも使われる MediaStream を信号源にします。例えばマイクから入ってくる信号を扱うような場合に使用します。
MediaStreamTrackAudioSourceNode
MediaStreamTrack を信号源にします。MediaStreamAudioSourceNode はストリームの最初のオーディオトラックしか扱えないため、追加されたノードです。
PannerNode
三次元空間内でのパンニングが設定できます。
OscillatorNode
周波数と波形を指定して連続した信号を出力します。
ScriptProcessorNode
入力と出力の間で Javascript で自由に処理が行えます。このノードは今後 AudioWorklet という仕組みに置き換えられる事が決まっており、現在はまだ使えますが非推奨となっています。
StereoPannerNode
ステレオ (2ch) でのパンニングが設定できます。
WaveShaperNode
入力信号に入力対出力のカーブを適用して出力します。ディストーションなどを作る事ができます。
jsでmidiファイルを読み込む