音楽素人でも思い付きで簡単なメロディーを作れたらなぁと思い、ちょっと Web API でどんなことが出来るか試します。
https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API
正直できることが多すぎて何していいか困りますが、とりあえず1曲鳴らすまで。
周波数の計算
ドレミファソラシドを周波数に変えます。
基準音A(ラ)を440(442)Hzとして、十二平均律で以下のように計算できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const base = 440; //ラ=440(442)Hz const d = Math.pow(2, 1 / 12); console.log(base * Math.pow(d, -9)); //ド console.log(base * Math.pow(d, -8)); //ド# console.log(base * Math.pow(d, -7)); //レ console.log(base * Math.pow(d, -6)); //レ# console.log(base * Math.pow(d, -5)); //ミ console.log(base * Math.pow(d, -4)); //ファ console.log(base * Math.pow(d, -3)); //ファ# console.log(base * Math.pow(d, -2)); //ソ console.log(base * Math.pow(d, -1)); //ソ# console.log(base * Math.pow(d, 0)); // ラ console.log(base * Math.pow(d, 1)); // ラ# console.log(base * Math.pow(d, 2)); // シ console.log(base * Math.pow(d, 3)); // ド |
1オクターブ(12音)上げると周波数は倍、下げると半分になります。
周波数を音にする
Web_Audio_API を参考にしますが、できることが多すぎる。
音源として周波数を音にする OscillatorNode
と GainNode
で音量指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
const HzBase = Math.pow(2, 1 / 12); const getHz = (semitone: number, octave: number = 4): number => { const _o = (octave - 4) * 12; const _s = semitone - 9; return 440 * Math.pow(HzBase, _s + _o); }; const doremi = () => { const audioContext = new AudioContext(); const t0 = audioContext.currentTime; let t = 0; var oscillator = audioContext.createOscillator(); var gain = audioContext.createGain(); oscillator.type = "square";//sine, square, sawtooth, triangle, custom [0, 2, 4, 5, 7, 9, 11, 12].forEach((s) => { const hz = getHz(s); oscillator.frequency.setValueAtTime(hz, t0 + t); gain.gain.setValueAtTime(0.01, t0 + t); t += 1; }); oscillator.start(t0); oscillator.stop(t0 + t); oscillator.connect(gain); gain.connect(audioContext.destination); }; |
oscillator.type
によって雰囲気が変わりますが、square
が一番それっぽい。
ユーザーアクションに関わらず自動で再生させようとすると以下のエラーが出ることがあります。
The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page.
音の長さを決める
4分音符を基準としてテンポが60の曲を考えます。
テンポはBPM(Beat Per Minute)なので1分間に60拍、1拍は1秒。
テンポが倍になれば1拍は半分、8分音符も半分、2分音符は倍。
つまり1拍は (60/テンポ)*(4/音符の長さ)
になります。
1音の強弱をつける
最初のサンプルコードの gain.setValueAtTime()
だけでは音が平坦で、同じ音を続けた場合にわからなくなります。
AudioParam を見て適当に音量の強弱を付けます。
1 2 3 4 5 6 7 8 |
const d = (60 / 80) * (4 / 4); //1音の時間 //1音の半分をかけて音量を上げる gain.gain.setValueAtTime(0.01, t0 + t); gain.gain.linearRampToValueAtTime(0.02, t0 + t + d / 2); //音の時間中の音量をスケジュールする gain.gain.setValueCurveAtTime([0.01, 0.02, 0.02, 0.005], t0 + t, d); |
きらきら星
ここまでの知識を利用して「きらきら星」を鳴らします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
const score = [ { hz: getHz(0), div: 4, long: 1 }, { hz: getHz(0), div: 4, long: 1 }, { hz: getHz(7), div: 4, long: 1 }, { hz: getHz(7), div: 4, long: 1 }, { hz: getHz(9), div: 4, long: 1 }, { hz: getHz(9), div: 4, long: 1 }, { hz: getHz(7), div: 4, long: 2 }, { hz: getHz(5), div: 4, long: 1 }, { hz: getHz(5), div: 4, long: 1 }, { hz: getHz(4), div: 4, long: 1 }, { hz: getHz(4), div: 4, long: 1 }, { hz: getHz(2), div: 4, long: 1 }, { hz: getHz(2), div: 4, long: 1 }, { hz: getHz(0), div: 4, long: 2 }, ]; const kirakira = () => { const audioContext = new AudioContext(); const t0 = audioContext.currentTime; let t = 0; var oscillator = audioContext.createOscillator(); var gain = audioContext.createGain(); oscillator.type = "square"; score.forEach((s) => { const hz = s.hz; const d = (60 / 80) * (4 / s.div) * s.long; //テンポ80 oscillator.frequency.setValueAtTime(hz, t0 + t); gain.gain.setValueCurveAtTime([0.01, 0.02, 0.02, 0.005], t0 + t, d); t += d; }); oscillator.start(t0); oscillator.stop(t0 + t); oscillator.connect(gain); gain.connect(audioContext.destination); }; |
d
(1音の長さ)が2分音符などで長くなると、それに依存した強弱がなんとなく気持ち悪い。実際の音はどうなってるかよく聞いて作らないといけないですね。
所感
簡単にそれなりの音を出せたけど、こだわると沼にハマりそう。
Web API
はどうしても限定的な用途になるのであまりコストをかけたくないんですが、思ったより面白いからもう少し続けてみよう。