Tone.jsシリーズ、前回まではTone.jsに用意されているシンセ音源たちを見ていきました。今回はメロディの再生、シンセ音源加工、エフェクター接続を行います。それではいきまSHOW!
【目次】
※前回:【Tone.js】十三音鍵盤のシンセ音を変更してみる(その2) - クモのようにコツコツと
マリオのメロディ打ち込んだ!
まずは完成品をご覧ください。
スーパーマリオの地上面のメロディを耳コピで打ち込んでみました(以前作ったCSSドット絵マリオのBGMにする野望ありw)。
※参考;CSSドット絵マリオをCSSアニメでジャンプさせてみる - クモのようにコツコツと
どうやって作ったかを以下に書いていきます。ロード to マリオ!!
一音鍵盤を再生ボタンに変更
最初に再生ボタンを作った。以前作った「一音鍵盤」をベースにしている。
※参考:はじめの一音!tone.js事始める - クモのようにコツコツと
ボタン型に変更したのがこちら。
See the Pen tone.js-btn01 by イイダリョウ (@i_ryo) on CodePen.
//DOM const btn = document.getElementById("play"); //イベントリスナ btn.addEventListener("click", function () { //シンセ生成 const synth = new Tone.Synth().toMaster(); //ドが8分音符の長さ鳴る synth.triggerAttackRelease("C4", "8n"); }, false);
- 変数
btn
でDOM#play
を紐付け btn
にクリックイベントを設定- 変数
synth
でシンセ音Synth()
を設定 triggerAttackRelease()
でド(C4
)の音とを八分音符(8n
)で鳴らす
Synth()
はTone.jsの基本的なシンセ音源。ボタンを押すと「ポーン」とドの音がなる。
C4
はC(=ド)から始まる音程表記(CDEFGAB)の4オクターブ目という意味。
8n
は音の長さで八分音符。この数値が4だと四分音符、16だと十六分音符などになる。
なお、シンセ音源はクリックイベントの中に書いて、ボタンクリックのタイミングで生成しないとiOS環境で音が再生されないことがわかった。
【Tone.js】iOSで音が鳴らない現象の対処 - Qiita
メロディを流す
先ほどはtriggerAttackRelease()
の引数にドの単音を書いたが、今度はボタンを押すとメロディを鳴らすようにした。
See the Pen Mario BGM01 by イイダリョウ (@i_ryo) on CodePen.
まずはマリオのイントロ部分を書いてみる。
//メロディ //イントロ const melodyLine = [ 'E5', 'E5', null, 'E5', null, 'C5', 'E5', null, 'G5', null, null, null, 'G4', null, null, null, ]
- 変数
melodyLine
の配列に音符を書く。 null
は休符
次に再生ボタンのクリックイベントの中に手を加える。
//音源 var synth = new Tone.Synth().toMaster(); //8分音符で再生 function setPlay(time, note) { synth.triggerAttackRelease(note,'8n', time); } //メロディをセット const melody = new Tone.Sequence(setPlay, melodyLine); //メロディ再生 melody.start();
play
ボタンのクリックイベント- 変数
synth
シンセ音設定(変更なし) - 関数
setPlay()
に再生設定synth.triggerAttackRelease()
を入れる
synth.triggerAttackRelease()
の第一引数は音は直接かかずnote
(setPlay()
の第二引数)とする。 - 変数
melody
にTone.Sequence()
でメロディを設定。
第一引数はsetPlay
の再生設定読み込み、第二引数はmelodyLine
のメロディ読み込む。 melody
にstart()
で再生設定
最初に書いたmelodyLine
のメロディを読み込んでsetPlay
の8分音符で再生させる設定。
イベントの中には他にも再生設定を書き込む
//ループ回数 melody.loop = 0; //テンポ Tone.Transport.bpm.value = 400; //再生実行 Tone.Transport.start();
melody
にのloop
プロパティにループ回数(0回)を設定Tone
にTransport.bpm.value
プロパティでテンポを設定。400bpm(1分あたり400ビート)Tone
にTransport.start()
で再生実行
ループ回数(繰り返さない場合も、書かないと無限ループになった)、テンポ、そして最後に再生の実行。
長いメロディで再生、停止ボタン
メロディの再生が成功したので、曲をフルに打ち込んでみる。
See the Pen Mario BGM02 by イイダリョウ (@i_ryo) on CodePen.
ボタン設定
メロディは音の長さが長いので再生ボタンだけでなく、静止ボタンも作りたい。HTMLでボタンを2つ書く。
<div id="btn"> <div id="play">Play</div> <div id="stop">Stop</div> </div>
最初は再生ボタンと停止ボタンを最初は横に並べていた。ところが再生中にもう一回再生ボタンを押すとダブって音が再生されてしまった。(停止を押すと両方止まりはするが)。イベントリスナの解除や一度だけの発火などいくつかの方法を試したがうまくいかなかった。。
CSSの非表示(display: none
)を使ってこのような動きを試してみた。
- 最初は再生ボタンのみ表示する(停止ボタンは非表示)
- 再生ボタンを押すと再生ボタンは非表示→停止ボタンを表示
- 停止ボタンを押すと停止ボタンは美表示→再生ボタンを表示
これによって再生ボタンを複数回押す操作を防ぐことができた!
ページ読み込み時(停止ボタンを非表示に)
//STOPボタン非表示 stop.style.display = "none";
再生ボタンのイベントリスナ(再生ボタンは非表示→停止ボタンを表示)
//再生ボタン play.addEventListener("click", function () { //ボタン表示切替 play.style.display = "none"; stop.style.display = "block"; }, false);
停止ボタンのイベントリスナ(停止ボタンは非表示→再生ボタンを表示)
//停止ボタン stop.addEventListener("click", function () { //ボタン表示切替 stop.style.display = "none"; play.style.display = "block"; }, false);
さらに停止ボタンのクリックイベントの中に再生停止の設定も追記する。
//停止実行 Tone.Transport.stop(); //イベントクリア Tone.Transport.cancel();
Tone
にTransport.stop();
で停止実行Tone
にTransport.cancel();
でイベントキャンセル
停止実行だけで再生イベントをキャンセルしないと、再生ボタンを再開するたびに音の数が重なってしまったので、イベントキャンセルも必要。
フルのメロディを打ち込む
ダブっているパーツがあるので、パーツを分割して打ち込んだ。
//メロディ //イントロ const intro = [ 'E5', 'E5', null, 'E5', null, 'C5', 'E5', null, 'G5', null, null, null, 'G4', null, null, null, ] //Aメロ const A1 = [ 'c5',null, null,'G4', null, null,'E4', null, null, 'A4', null, 'B4', null, 'A#4', 'A4', null, ] const A2 = [ 'G4', 'C5', 'E5', 'A5', null, 'F5', null, 'G5', null, 'E5', null, 'C5', 'D5', 'B4', null, null, ] //Bメロ const B1 = [ null, null,'G5', 'F#5', 'F5', 'D5#', null,'E5', null, 'G4', 'A4', 'C5', null, 'A4', 'C5', 'D5', ] const B2 = [ null, null, 'G5', 'F#5', 'F5', 'D5', null, 'E5', null, 'C6', null, 'C6', 'C6', null, null, null, ] const B3 = [ null, null, 'D#5', null, null, 'D5', null, null, 'C5', null, null, null, null, null, null, null, ] //Cメロ const C1 = [ 'C5', 'C5', null, 'C5', null, 'C5', 'D5', null, 'E5', 'C5', null, 'A4', 'G4', null, null, null, ] const C2 = [ 'C5', 'C5', null, 'C5', null, 'C5', 'D5', 'E5', null, null, null, null, null, null, null, null, ] //Dメロ const D1 = [ 'E5','C5',null,'G4', null,null,'G#4',null, 'A4','F5',null,'F5', 'A4',null,null,null, ] const D2 = [ 'B4','A5',null,'A5', 'A5','G5', null,'F5', 'E5','C5',null,'A4', 'G4',null,null,null, ] const D3 = [ 'B4','F5',null,'F5', 'F5','E5', null,'D5', 'C5',null,null,null, null,null,null,null, ]
分割した配列を結合したい。.concat()
メソッドを使う。
//曲構成 var melodyLine = //イントロ intro.concat( //Aメロ A1, A2, A1, A2, //Bメロ B1, B2, B1, B3, B1, B2, B1, B3, //Cメロ C1, C2, C1, intro, //Aメロ A1, A2, A1, A2, //Dメロ D1, D2, D1, D3, D1, D2, D1, D3, //Cメロ C1, C2, C1, );
- 変数
melodyLine
にintro
を入れる - 最初の配列
intro
に.concat()
メソッドで配列を結合 .concat()
の引数の中で曲構成に従った順番で配列を入れる
結合した配列は先ほどのメロディと同じ変数名melodyLine
のため、再生ボタンのクリックイベントの中のmelody
にそのままフルの曲が入る。
//メロディをセット const melody = new Tone.Sequence(setPlay, melodyLine);
上記は変更なし。
そしてループ回数は5回に増やした。
//ループ回数
melody.loop = 5;
シンセ音源のオプション設定
メロディを再生するという目的は達せられたので他のことをためしたい。シンセ音源を変更して、さらにオプションで音源を加工した。
See the Pen Mario BGM-03 by イイダリョウ (@i_ryo) on CodePen.
まずシンセ音源を変更。
//音源 const synth = new Tone.MonoSynth(option).toMaster();
Tone
のシンセ音源をSynth()
からMonoSynth()
に。(引数にoption
を入れる)
MonoSynth()
の引数に入れたoption
は外部で変数として設定する。
//音源オプション const option = { envelope: { attack: 0.05, decay: 3, sustain: 0.5, release: 3, }, portamento: 0.1, volume: -20, }
- 変数
option
設定(連想配列) envelope
でエンベロープ(ADSR)設定
(中にADSRの連想配列が入れ子になっている)portamento
ポルタメント設定volume
ボリューム設定
エンベロープ(ADSR)は音量の立ち上がりや減衰などです。打楽器はタン!とアタック音が立ち上がって徐々に減衰する。バイオリンは徐々に立ち上がる、など。
- A(アタック):立ち上がりから最大音量までにかかる時間
- D(ディケイ):最大音量から安定音量までにかかる時間
- S(サスティーン):安定的に楽器が鳴り続ける音量(ピアノでいえば鍵盤を押し続けている間の音量)
- R(リリース):音がなくなる(減衰)までにかかる時間(ピアノでいえば鍵盤を離したあとの時間)
こちらの記事の解説がわかりやすい!
※参考:AMPをエンベロープ(ADSR)でコントロールする シンセサイザー 初心者講座
ポルタメントは音程と音程の間がなめらかにつながる設定。バイオリンやトロンボーン、スチールギターなどのイメージ。
ボリュームは音源全体のボリューム。MonoSynth()
は少しうるさかったのでマイナス値を入れて抑えた。
これらの設定がMonoSynth(option)
の引数option
に反映されて、先ほどと違う音色になる。
エフェクターと接続
最後にエフェクターを設定してシンセ音源に接続する。
先ほどより音の反響が増えている。リバーブを少しかけた。エフェクターには「歪み系」「モジュレーション系」「空間系」などいくつかの種類がある。
※参考:【今さら聞けない】エフェクターの基礎知識編 ~エフェクターの種類と効果~ - 島村楽器 ギタセレ(Guitar Selection)ニュース
このうち、リバーブは「空間系」のエフェクター。空間系エフェクターのうち、ディレイは山彦のように繰り返す。リバーブはお風呂や教会のように反響する。
// エフェクト(リバーブ) const reverb = new Tone.Freeverb(0.8,500).toMaster();
- 変数
reverb
にTone.Freeverb()
でリバーブを設定。 - 第一引数は部屋の大きさ[ roomSize ]、第二引数は湿気[ dampening ]
※参考:Freeverb
再生ボタンのクリックイベントの中でシンセとエフェクトを接続する。
// シンセとエフェクトを接続
synth.connect(reverb);
synth
にconnect()
メソッドでreverb
を接続
これで音に反響が加わった。
JSコード(全体)
最終的に完成したコードはこちら
//DOM const btn = document.getElementById("btn"); const play = document.getElementById("play"); const stop = document.getElementById("stop"); //STOPボタン非表示 stop.style.display = "none"; //メロディ //イントロ const intro = [ 'E5', 'E5', null, 'E5', null, 'C5', 'E5', null, 'G5', null, null, null, 'G4', null, null, null, ] //Aメロ const A1 = [ 'c5',null, null,'G4', null, null,'E4', null, null, 'A4', null, 'B4', null, 'A#4', 'A4', null, ] const A2 = [ 'G4', 'C5', 'E5', 'A5', null, 'F5', null, 'G5', null, 'E5', null, 'C5', 'D5', 'B4', null, null, ] //Bメロ const B1 = [ null, null,'G5', 'F#5', 'F5', 'D5#', null,'E5', null, 'G4', 'A4', 'C5', null, 'A4', 'C5', 'D5', ] const B2 = [ null, null, 'G5', 'F#5', 'F5', 'D5', null, 'E5', null, 'C6', null, 'C6', 'C6', null, null, null, ] const B3 = [ null, null, 'D#5', null, null, 'D5', null, null, 'C5', null, null, null, null, null, null, null, ] //Cメロ const C1 = [ 'C5', 'C5', null, 'C5', null, 'C5', 'D5', null, 'E5', 'C5', null, 'A4', 'G4', null, null, null, ] const C2 = [ 'C5', 'C5', null, 'C5', null, 'C5', 'D5', 'E5', null, null, null, null, null, null, null, null, ] //Dメロ const D1 = [ 'E5','C5',null,'G4', null,'G4','G#4',null, 'A4','F5',null,'F5', 'A4',null,null,null, ] const D2 = [ 'B4','A5',null,'A5', 'A5','G5', null,'F5', 'E5','C5',null,'A4', 'G4',null,null,null, ] const D3 = [ 'B4','F5',null,'F5', 'F5','E5', null,'D5', 'C5',null,null,null, null,null,null,null, ] //曲構成 var melodyLine = //イントロ intro.concat( //Aメロ A1, A2, A1, A2, //Bメロ B1, B2, B1, B3, B1, B2, B1, B3, //Cメロ C1, C2, C1, intro, //Aメロ A1, A2, A1, A2, //Dメロ D1, D2, D1, D3, D1, D2, D1, D3, //Cメロ C1, C2, C1, ); //音源オプション const option = { envelope: { attack: 0.05, decay: 0.05, sustain: 0.5, release: 3, }, portamento: 0.1, volume: -20, } // エフェクト(リバーブ) const reverb = new Tone.Freeverb(0.8,500).toMaster(); //再生ボタン play.addEventListener("click", function () { //ボタン表示切替 play.style.display = "none"; stop.style.display = "block"; //音源 const synth = new Tone.MonoSynth(option).toMaster(); // シンセとエフェクトを接続 synth.connect(reverb); //8分音符で再生 function setPlay(time, note) { synth.triggerAttackRelease(note,'8n', time); } //メロディをセット const melody = new Tone.Sequence(setPlay, melodyLine); //メロディ再生 melody.start(); //ループ回数 melody.loop = 5; //テンポ Tone.Transport.bpm.value = 400; //再生実行 Tone.Transport.start(); }, false); //停止ボタン stop.addEventListener("click", function () { //ボタン表示切替 stop.style.display = "none"; play.style.display = "block"; //停止実行 Tone.Transport.stop(); //イベントクリア Tone.Transport.cancel(); }, false);
最後に
ようやくTone.jsで音楽らしいことができた。今後やりたいことはこちら。
- 均一な8分音符ではなく一音ごとに音の長さを調整したい(今回、マリオの曲のところどころにある「三連符」が再現できなかった)
- 和音を鳴らしたい
- リズム音をならしたい
- それらを同時に鳴らしたい
順不同です。やりやすそうなものから着手していきます。それではまた!!
ブラウザで音を鳴らすライブラリはTone.jsが一番定番だと思うのですが、それでも情報がとても少ないです。。今回参考にさせていただい記事に感謝を込めて…。
※参考:初心者でも音楽アプリが作れる!Web Audio APIを簡略化したJSライブラリ「Tone.js」でピアノ鍵盤を作ってみよう! - paiza開発日誌
※参考:Tone.jsを使ってテキストからメロディを鳴らしてみた - Qiita
※参考:Web上での音声の同時再生・連続再生や編集をスムーズに実装できるTone.jsの使い方 │ よねぴ
※参考:【Tone.js】iOSで音が鳴らない現象の対処 - Qiita
※参考:Tone.jsを習得するためにやった事まとめ qiita.com