クモのようにコツコツと

フロントエンドエンジニア イイダリョウのブログ。略称「クモコツ」

【Tone.js】メロディ再生、シンセ音源加工、エフェクター接続(マリオを打ち込んだ!)

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()の第一引数は音は直接かかずnotesetPlay()の第二引数)とする。
  • 変数melodyTone.Sequence()でメロディを設定。
    第一引数はsetPlayの再生設定読み込み、第二引数はmelodyLineのメロディ読み込む。
  • melodystart()で再生設定

最初に書いたmelodyLineのメロディを読み込んでsetPlayの8分音符で再生させる設定。

イベントの中には他にも再生設定を書き込む

//ループ回数
melody.loop = 0; 

//テンポ
Tone.Transport.bpm.value = 400;
  
//再生実行
Tone.Transport.start(); 
  • melodyにのloopプロパティにループ回数(0回)を設定
  • ToneTransport.bpm.valueプロパティでテンポを設定。400bpm(1分あたり400ビート)
  • ToneTransport.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();
  • ToneTransport.stop();で停止実行
  • ToneTransport.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,
  );
  • 変数melodyLineintroを入れる
  • 最初の配列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();
  • 変数reverbTone.Freeverb()でリバーブを設定。
  • 第一引数は部屋の大きさ[ roomSize ]、第二引数は湿気[ dampening ]

※参考:Freeverb

再生ボタンのクリックイベントの中でシンセとエフェクトを接続する。

// シンセとエフェクトを接続
synth.connect(reverb);
  • synthconnect()メソッドで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