クモのようにコツコツと

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

【Tone.js】いろいろなリズムが鳴らせるビート・プレイヤーを作った(BPM切り替え可能)

Tone.jsの続きです。前回Tone.Part()を使ってシャッフル、16ビートなどのリズムを作りました。今回はこれまで作ったリズムを合体させて、BPMとリズムの切り替えができるビート・プレイヤーを作りました!演奏練習用、BPM(曲の速度)調査など実用的なプレイヤーになったと思います。それではいきましょう!

【目次】

※参考:前回記事
【Tone.js】Tone.Part()でいろいろなリズムを鳴らす ※12〜32拍子&シャッフル編 - クモのようにコツコツと

※参考:Tone.jsを習得するためにやった事まとめ
qiita.com

前回までのおさらい

ボタンを押すとリズムがなる仕組みで1〜32拍子までいろいろ作った。

See the Pen tone.js-DrumBeat_Swing 16beat by イイダリョウ (@i_ryo) on CodePen.

詳細は前回までの記事を参照

※参考:前々回(1〜8拍子)
【Tone.js】Tone.Part()でいろいろなリズムを鳴らす ※1〜8拍子編 - クモのようにコツコツと

※参考:前回(12〜32拍子)
【Tone.js】Tone.Part()でいろいろなリズムを鳴らす ※12〜32拍子&シャッフル編 - クモのようにコツコツと

今回はこれらを合体させてリズムを切り替えて鳴らしたい。ついでにBPM(速度)も変更する。

ビート・プレイヤー(完成品)

ここ数日、これを作るのに時間を費やしていた。ついに完成!!

See the Pen tone.js-BeatPlayer by イイダリョウ (@i_ryo) on CodePen.

音が鳴らない場合はこちら(スマホ画面でもちょうどいい幅、高さになった♪)

※参考:https://codepen.io/i_ryo/full/dyGpbxL

仕様はこんな感じ

  • 再生ボタン「▶︎」を押すと音がなる(もう一回押すと止まる)
  • 初期設定は120BPMの8拍子(エイトビート)
  • 「BPM」のスライダーを動かすと速度が変わる(30〜250BPM)
  • 「リズム」のラジオボタンを押すとリズムが変わる

こんな使い方ができます!

  • 拍子によって変わるリズムの違いを知る
  • 流れている音楽に合わせてBPMを調べる
  • ビートを鳴らしながら楽器演奏の練習を行う

BPMとは

BPMとは「Beats Per Minute」の略。1分あたりのビート数(4拍子ベース)。

※参考:ビーピーエム:BPMとは | 偏ったDTM用語辞典 - DTM / MIDI 用語の意味・解説 | g200kg Music & Software

ジャンルによって使われるBPMが異なる。

※参考:【効果絶大!? - BPM(テンポ)設定集 -】|さがはじめ/Polyamory house|note

ちなみにBPM116は「絶対テンポ」らしい。集中力が増すとか。0.5秒に1回くらいの速さか。

※参考:記事の作成スピードが爆速的にあがるテンポ「BPM116」を聴いてみて! | 暮らしの自動化メディアASHETPIA【アシェトピア】

今回作ったビート・プレーヤーのBPMは30〜240の範囲にした(初期値は120)。

  • 最小値:30は1拍=2秒
  • 初期値:120は1拍=0.5秒(絶対テンポに近い!)
  • 最大値:240は1拍=0.25秒。

HTMLコード全体

以下、書いたコードを解説。まずHTMLから。

<h1>Beat Player</h1>
<ul id="pad">
   <li id="beat">▶︎</li>
</ul>
<div class="setting">
<section class="bpm">
  <h2>BPM: <span class="val"></span></h2>
  <input type="range" name="range" min="30" max="240" value="120" class="range">
    </section>
<section class="rhythm">
  <h2>リズム: <span class="val"></span></h2>
    <label><input type="radio" name="beat" class="beat1" value="1拍子">1</label>  
    <label><input type="radio" name="beat" class="beat2" value="2拍子">2</label>  
    <label><input type="radio" name="beat" class="beat3" value="3拍子">3</label>  
    <label><input type="radio" name="beat" class="beat4" value="4拍子">4</label>  
    <label><input type="radio" name="beat" class="beat5" value="5拍子">5</label>  
    <label><input type="radio" name="beat" class="beat6" value="6拍子">6</label>  
    <label><input type="radio" name="beat" class="beat7" value="7拍子">7</label>
    <label><input type="radio" name="beat" class="Beat8" value="8拍子" checked="checked">8</label>
    <label><input type="radio" name="beat" class="beat12" value="12拍子">12</label>
    <label><input type="radio" name="beat" class="beatShuffle" value="シャッフル">Shuffle</label>
    <label><input type="radio" name="beat" class="beat16" value="16拍子">16</label>
    <label><input type="radio" name="beat" class="beat24" value="24拍子">24</label>
    <label><input type="radio" name="beat" class="beatSwing16" value="ハネた16">Swing16</label>
    <label><input type="radio" name="beat" class="beat32" value="32拍子">32</label>
    </section>
</div>
  • #beatが再生・停止ボタン
  • . bpm.rangeがBPM変更のスライダー
  • .rangeの設定、最小値min30、最大値max240、初期値value120
  • . rhythmのinputタグ(name属性がbeat)がリズム変更のラジオボタン
  • value属性8拍子のinputタグにcheckedが入って初期に選択されている

スライダーのinputタグ(type="range")の設定値を属性に書いている。

※参考:<input type="range"> - HTML: HyperText Markup Language | MDN

(CSSは割愛します。気になる方はCodePen参照)

※参考:https://codepen.io/i_ryo/pen/dyGpbxL

JSコード(全体)

次はJSコードの全体。

// DOM
const beat = document.querySelector('#beat');
const BPMVal = document.querySelector('.bpm .val');
const BPMRange = document.querySelector('.bpm .range');
const rythmVal = document.querySelector('.rhythm .val');
const rythmRadio = document.querySelectorAll('.rhythm input[name="beat"]');


// エンベロープ(キック)
let membraneKickOpts = {
  pitchDecay: 0.001,
  envelope: {
    attack: 0.001 ,
    decay: 0.75 ,
    sustain: 0.01 ,
    release: 0.01 
  },
  volume: 25
}


// エンベロープ(スネア)
let noiseSnareOpts = {
  envelope: {
    attack: 0.001 ,
    decay: 0.5 ,
    sustain: 0.01
  }
}


// エンベロープ(ハイハット)
let noiseHihatOpts = {
  type: "brown",
  envelope: {
    attack: 0.001 ,
    decay: 0.03 ,
    sustain: 0
  }
}


// シンセ生成
const membraneKick = new Tone.MembraneSynth(membraneKickOpts).toMaster();
const noiseSnare = new Tone.NoiseSynth(noiseSnareOpts).toMaster();
const noiseHihat = new Tone.NoiseSynth(noiseHihatOpts).toMaster();


// シンセ実行
const kickSynth = () => {
  membraneKick.triggerAttackRelease('C0','2n'); 
};
const snareSynth = () => {
  noiseSnare.triggerAttackRelease('8n');
};
const hihatSynth = () => {
  noiseHihat.triggerAttackRelease('32n');
};


// リズム設定値
let rhythmData = [
  {
    value: '1拍子',
    beatNumber: 1,
    kickRhythm: [0],
    snareRhythm: []
  },
  {
    value: '2拍子',
    beatNumber: 2,
    kickRhythm: [0],
    snareRhythm: [1]
  },
  {
    value: '3拍子',
    beatNumber: 3,
    kickRhythm: [0],
    snareRhythm: [1, 2]
  },
  {
    value: '4拍子',
    beatNumber: 4,
    kickRhythm: [0, 2],
    snareRhythm: [1, 3]
  },
  {
    value: '5拍子',
    beatNumber: 5,
    kickRhythm: [0],
    snareRhythm: [3]
  },
  {
    value: '6拍子',
    beatNumber: 6,
    kickRhythm: [0],
    snareRhythm: [3]
 },
  {
    value: '7拍子',
    beatNumber: 7,
    kickRhythm: [0, 4],
    snareRhythm: [2, 6]
  },
  {
    value: '8拍子',
    beatNumber: 8,
    kickRhythm: [0, 4],
    snareRhythm: [2, 6]
  },
  {
    value: '12拍子',
    beatNumber: 12,
    kickRhythm: [0, 6],
    snareRhythm: [3, 9]
  },
  {
    value: 'シャッフル',
    beatNumber: 12,
    kickRhythm: [0, 6],
    snareRhythm: [3, 9],
    shaffle: true
  },
  {
    value: '16拍子',
    beatNumber: 16,
    kickRhythm: [0, 8],
    snareRhythm: [4, 12]
  },
  {
    value: '24拍子',
    beatNumber: 24,
    kickRhythm: [0, 12],
    snareRhythm: [6, 18]
  },
  {
    value: 'ハネた16',
    beatNumber: 24,
    kickRhythm: [0, 12],
    snareRhythm: [6, 18],
    shaffle: true
  },
  {
    value: '32拍子',
    beatNumber: 32,
    kickRhythm: [0, 16],
    snareRhythm: [8, 24]
  },
];


// リズム取得
let getRhythmData = (i) => {
  let data = rhythmData[i];
  let beatLen = 4 / data.beatNumber;
  return {
    data: data,
    beatVal: data.value,
    beatNum: data.beatNumber,
    beatLen: beatLen,
    kickRhythm: data.kickRhythm,
    snareRhythm: data.snareRhythm,
    shaffle: data.shaffle
   }
};


// キック、スネアリズム設定
const setRhythm = (beatNmb, beatLen, Array) => {
  let rhythm = [];
  for(i = 0; i < Array.length ; i++) {
    rhythm.push('0:' + beatLen * Array[i] + ':0');
  }
  return rhythm;
}


// ハイハットリズム設定
const setHihatRhythm = (beatNmb, beatLen, shaffle) => {
  let rhythm = [];
  for(i = 0; i < beatNmb ; i++) {
    if (shaffle  && i % 3 == 1) {
      // 鳴らさない
    } else {
      rhythm.push('0:' + beatLen * i + ':0');
    }
  }
  return rhythm;
};


// ビートリズム設定
let setBeatRhythm = (i) => {
  let data = getRhythmData(i);
  rythmVal.innerHTML = data.beatVal;
  const beatNum = data.beatNum;
  const beatLen = data.beatLen;
  const kickRhythm = data.kickRhythm;
  const snareRhythm = data.snareRhythm;
  const shaffle = data.shaffle;
  return {
    kick: setRhythm(beatNum, beatLen, kickRhythm),
    snare: setRhythm(beatNum, beatLen, snareRhythm),
    hihat: setHihatRhythm(beatNum, beatLen, shaffle)
  }
};


// ビート再生設定
const playBeat = (kickRtm, snareRtm, hihatRtm) => {
  let kickPart = new Tone.Part(kickSynth, kickRtm).start();
  let snarePart = new Tone.Part(snareSynth, snareRtm).start()
  let hihatPart = new Tone.Part(hihatSynth, hihatRtm).start();
  kickPart.loop = true;
  snarePart.loop = true;
  hihatPart.loop = true;
}


// ビート初期値
let defRhythm = setBeatRhythm(7);
playBeat(defRhythm.kick, defRhythm.snare, defRhythm.hihat);


// 再生判定
let play = false; 


// 再生ボタン
beat.addEventListener('click', () => {
  if(play){
    play = false;
    beat.innerHTML = "▶︎";
    Tone.Transport.stop();
  } else {
    play = true;
    beat.innerHTML = "■";
    Tone.Transport.start();    
  }
}, false);


// BPM設定
let BPMSet = () => {
  let BPMImput = BPMRange.value;
  BPMVal.innerHTML = BPMImput;
  Tone.Transport.bpm.value = BPMImput;
}
BPMSet();

// BPM変更
BPMRange.addEventListener('input', BPMSet, false);


// リズム変更
function selectRythm() {
  for(let i = 0; i < rhythmData.length; i++){ 
    rythmRadio[i].addEventListener('input', ()=> {
      let beat = setBeatRhythm(i);
      const kick = beat.kick;
      const snare = beat.snare;
      const hihat = beat.hihat;
      if( rythmRadio[i].checked && play ) {
        Tone.Transport.cancel();
        Tone.Transport.start();
        playBeat(kick, snare, hihat);
      } else {
        Tone.Transport.cancel();
        playBeat(kick, snare, hihat);
      }
    }, false);
  }
}
selectRythm();

コードがちょっと長くなったが、なるべく機能ごとのブロックを分けるリファクタリングをした。変数名、関数名も規則性を保つために前回とちょっと変えている。

上記のコードが何をしているのか。以下、ブロックごとに解説する。

JSコード(DOM、シンセ設定)

まずはDOMやシンセ音の設定から。

DOM取得

冒頭で処理に使うDOMを取得する。

// DOM
const beat = document.querySelector('#beat');
const BPMVal = document.querySelector('.bpm .val');
const BPMRange = document.querySelector('.bpm .range');
const rythmVal = document.querySelector('.rhythm .val');
const rythmRadio = document.querySelectorAll('.rhythm input[name="beat"]');
  • 変数beatquerySelector()メソッドを実行し、#beatのDOMを取得
  • 変数BPMValquerySelector()メソッドを実行し、.bpm .valのDOMを取得
  • 変数BPMRangequerySelector()メソッドを実行し、.bpm .rangeのDOMを取得
  • 変数rythmValquerySelector()メソッドを実行し、.rhythm .valのDOMを取得
  • 変数rythmRadioquerySelectorAll()メソッドを実行し、.rhythm input[name="beat"]のDOMを取得

最後の変数rythmRadio(ラジオボタン)は複数あるため、querySelectorAll()で配列に入れる。

※参考:Document.querySelectorAll() - Web API | MDN

エンベロープ

次にキック、スネア、ハイハットのエンベロープ(ADSR)を設定。

ADSRは「Attack」「Decay」「Sustain」「Release」の略。

※参考:【Tone.js】ドラムパッドにエンベロープ(ADSR)とエフェクトを設定する - クモのようにコツコツと

キックのADSR

// エンベロープ(キック)
let membraneKickOpts = {
  pitchDecay: 0.001,
  envelope: {
    attack: 0.001 ,
    decay: 0.75 ,
    sustain: 0.01 ,
    release: 0.01 
  },
  volume: 25
}
  • 変数membraneKickOptsに連想配列でキックのエンベロープを設定

キックはmembraneシンセのオプション設定。

※参考:MembraneSynth

スネアのADSR

// エンベロープ(スネア)
let noiseSnareOpts = {
  envelope: {
    attack: 0.001 ,
    decay: 0.5 ,
    sustain: 0.01
  }
}
  • 変数noiseSnareOptsに連想配列でスネアのエンベロープを設定

ハイハットのエンベロープ

// エンベロープ(ハイハット)
let noiseHihatOpts = {
  type: "brown",
  envelope: {
    attack: 0.001 ,
    decay: 0.03 ,
    sustain: 0
  }
}
  • 変数noiseHihatOptsに連想配列でハイハットのエンベロープを設定

スネアとハイハットはどちらもnoiseシンセのオプション設定だがエンベロープ値は異なる。

※参考:NoiseSynth

シンセ生成

シンセ音を生成する。

// シンセ生成
const membraneKick = new Tone.MembraneSynth(membraneKickOpts).toMaster();
const noiseSnare = new Tone.NoiseSynth(noiseSnareOpts).toMaster();
const noiseHihat = new Tone.NoiseSynth(noiseHihatOpts).toMaster();
  • 変数membraneKickMembraneSynth()を生成(引数はmembraneKickOpts
  • 変数noiseSnareNoiseSynth()を生成(引数はnoiseSnareOpts
  • 変数noiseHihatNoiseSynth()を生成(引数はnoiseHihatOpts

シンセの引数に先ほどのオプション設定(エンベロープ値)を入れている。

シンセ実行

先ほど生成したシンセを実行する。

// シンセ実行
const kickSynth = () => {
  membraneKick.triggerAttackRelease('C0','2n'); 
};
const snareSynth = () => {
  noiseSnare.triggerAttackRelease('8n');
};
const hihatSynth = () => {
  noiseHihat.triggerAttackRelease('32n');
};
  • 変数kickSynthmembraneKicktriggerAttackRelease()メソッド実行(引数は左からC0, '2n
  • 変数snareSynthnoiseSnaretriggerAttackRelease()メソッド実行(引数は8n
  • 変数hihatSynthnoiseHihattriggerAttackRelease()メソッド実行(引数は32n

noiseシンセは音程がないため、引数は音の長さのみ。

JSコード(リズム設定値)

ここからは各リズムの設定値を設定する。

リズム設定値の書式

リズムの設定値を入れる。

// リズム設定値
let rhythmData = [
  {
    value: '●拍子',
    beatNumber: ●,
    kickRhythm: [●, ●],
    snareRhythm: [●, ●]
  },
];
  • 変数rhythmDataの値は配列
  • 配列の値は連想配列
  • 連想配列のvalueキーは文字列
  • beatNumberキーは数値
  • kickRhythmキーは配列
  • snareRhythmキーも配列

リズムの設定値は連想配列でオブジェクトになる。

ハイハットは全ての拍子で鳴らすのでbeatNumberを拍子数分for文でループする。

キックとスネアは配列に入れる。

1〜8拍子の設定値

以下、リズムの設定値。前々回の1〜8拍子の設定値。

※参考:前々回(1〜8拍子)
【Tone.js】Tone.Part()でいろいろなリズムを鳴らす ※1〜8拍子編 - クモのようにコツコツと

1拍子

  {
    value: '1拍子',
    beatNumber: 1,
    kickRhythm: [0],
    snareRhythm: []
  },

1拍子はキックのみ。

2拍子

  {
    value: '2拍子',
    beatNumber: 2,
    kickRhythm: [0],
    snareRhythm: [1]
  },

2拍子からスネアが加わる。

3拍子

  {
    value: '3拍子',
    beatNumber: 3,
    kickRhythm: [0],
    snareRhythm: [1, 2]
  },

ここから4拍子ベースで3/4拍子。ワルツのリズム。

4拍子

  {
    value: '4拍子',
    beatNumber: 4,
    kickRhythm: [0, 2],
    snareRhythm: [1, 3]
  },

4拍子は2拍子の倍でマーチのリズム。

5拍子

  {
    value: '5拍子',
    beatNumber: 5,
    kickRhythm: [0],
    snareRhythm: [3]
  },

ここから8拍子ベースで5/8拍子。ハチロク-1のリズム。

6拍子

  {
    value: '6拍子',
    beatNumber: 6,
    kickRhythm: [0],
    snareRhythm: [3]
 },

6/8拍子=ハチロクのリズム。

7拍子

  {
    value: '7拍子',
    beatNumber: 7,
    kickRhythm: [0, 4],
    snareRhythm: [2, 6]
  },

7/8拍子。エイトビート-1のリズム。

8拍子

  {
    value: '8拍子',
    beatNumber: 8,
    kickRhythm: [0, 4],
    snareRhythm: [2, 6]
  },

8拍子=エイトビート

12〜32拍子の設定値

ここからは前回の12〜32拍子の設定値。

※参考:前回(12〜32拍子)
【Tone.js】Tone.Part()でいろいろなリズムを鳴らす ※12〜32拍子&シャッフル編 - クモのようにコツコツと

12拍子

  {
    value: '12拍子',
    beatNumber: 12,
    kickRhythm: [0, 6],
    snareRhythm: [3, 9]
  },

ここから16分音符ベースで12/16拍子。ハチロクの倍のリズム。

シャッフル

  {
    value: 'シャッフル',
    beatNumber: 12,
    kickRhythm: [0, 6],
    snareRhythm: [3, 9],
    shaffle: true
  },
  • shaffleキーの値をtrue、を追加

12拍子からハイハットの一部を抜いたシャッフルリズム。

16拍子

  {
    value: '16拍子',
    beatNumber: 16,
    kickRhythm: [0, 8],
    snareRhythm: [4, 12]
  },

16拍子=16ビート。

24拍子

  {
    value: '24拍子',
    beatNumber: 24,
    kickRhythm: [0, 12],
    snareRhythm: [6, 18]
  },

ここから32分音符ベースで24/32拍子。

ハネた16ビート

  {
    value: 'ハネた16',
    beatNumber: 24,
    kickRhythm: [0, 12],
    snareRhythm: [6, 18],
    shaffle: true
  },
  • shaffleキーの値をtrue、を追加

24拍子からハイハットの1部を抜くのでこれもshaffletrueに。

32拍子

  {
    value: '32拍子',
    beatNumber: 32,
    kickRhythm: [0, 16],
    snareRhythm: [8, 24]
  },

43拍子=32ビート。ハイハットがヘリコプターw

JSコード(リズム設定)

リズム取得

先ほど設定したリズムを取得する処理

// リズム取得
let getRhythmData = (i) => {
  let data = rhythmData[i];
  let beatLen = 4 / data.beatNumber;
  return {
    data: data,
    beatVal: data.value,
    beatNum: data.beatNumber,
    beatLen: beatLen,
    kickRhythm: data.kickRhythm,
    snareRhythm: data.snareRhythm,
    shaffle: data.shaffle
   }
  • 変数getRhythmDataは無名関数(引数はi
  • 変数dataは配列rhythmData[]i番目
  • 変数beatLendataオブジェクトのbeatNumberプロパティを4で割る
  • returnを連想配列で複数の値を返す
  • dataキーの値はdata
  • beatValキーの値はdataオブジェクトのvalueプロパティ
  • beatNumキーの値はdataオブジェクトのbeatNumberプロパティ
  • beatLenキーの値はbeatLen
  • kickRhythmキーの値はdataオブジェクトのkickRhythmプロパティ
  • snareRhythmキーの値はdataオブジェクトのsnareRhythmプロパティ
  • shaffleキーの値はdataオブジェクトのshaffleプロパティ

変数dataで配列rhythmData[]i番目を取得する。

BPMは四分音符ベースのため、変数beatLenは拍子数を4で割る式。これで1拍の長さが出る(あとでハイハットのループで使う)。

returnで複数の値(処理結果)を返したい時は配列や連想配列に入れる。

※参考:【JavaScript入門】returnの使い方と戻り値・falseのまとめ! | 侍エンジニアブログ

キック、スネアリズム設定

キックとスネアのリズム設定は共通のため、一つの関数にまとめた。

// キック、スネアリズム設定
const setRhythm = (beatNmb, beatLen, Array) => {
  let rhythm = [];
  for(i = 0; i < Array.length ; i++) {
    rhythm.push('0:' + beatLen * Array[i] + ':0');
  }
  return rhythm;
}
  • 変数setRhythmの値は無名関数(引数は左からbeatNmb, beatLen, Array
  • 変数rhythmの値は空の配列
  • for文の条件:iの初期値は0iArrayの数(length)まで繰り返す、iを1つずつ加算する
  • 配列rhythmpush()メソッドで追加(引数は文字列0:beatLenArrayi番目を割るった数と文字列:0
  • returnで配列rhythmを返す

Arrayは配列でキックやスネアを鳴らす数値になる。これをfor文でループしてTone.jsのリズム設定「0:●:0」という文字列にする。

ハイハットリズム設定

ハイハットのリズム設定はキック、スネアとは異なるので別の関数にした

// ハイハットリズム設定
const setHihatRhythm = (beatNmb, beatLen, shaffle) => {
  let rhythm = [];
  for(i = 0; i < beatNmb ; i++) {
    if (shaffle && i % 3 == 1) {
      // 鳴らさない
    } else {
      rhythm.push('0:' + beatLen * i + ':0');
    }
  }
  return rhythm;
};
  • 変数setHihatRhythmは無名関数(引数は左からbeatNmb, beatLen, shaffle
  • 変数rhythmは空の配列
  • for文の条件:iの初期値は0ibeatNmbの数まで繰り返す、iを1つずつ加算する
  • if文の条件もしshaffleの値がtrue、かつi3で割った余りが1なら何も鳴らさない
  • さもなくば配列rhythmpush()メソッドで追加(引数は文字列0:beatLenArrayi番目を割るった数と文字列:0
  • returnで配列rhythmを返す

beatNmblengthがないのは配列ではなくただの数値だから。ハイハットはすべての拍子を鳴らすので拍子数でOK。

if文の最初の条件でshaffleの値は真偽値だがshaffle == trueを書かなくても単体でtrueを意味する(!を付けるとfalseを意味する)。

※参考:【JavaScript入門】boolean型の使い方(文字列変換/判定/反転) | 侍エンジニアブログ

これの条件でshaffleの時は3拍セットの真ん中の1拍のハイハットを鳴らさない。

JSコード(ビート設定)

ビートリズム設定

先ほど設定したキック、スネア、ハイハットのリズムを組み合わせてビートのリズムを設定する。

// ビートリズム設定
let setBeatRhythm = (i) => {
  let data = getRhythmData(i);
  rythmVal.innerHTML = data.beatVal;
  const beatNum = data.beatNum;
  const beatLen = data.beatLen;
  const kickRhythm = data.kickRhythm;
  const snareRhythm = data.snareRhythm;
  const shaffle = data.shaffle;
  return {
    kick: setRhythm(beatNum, beatLen, kickRhythm),
    snare: setRhythm(beatNum, beatLen, snareRhythm),
    hihat: setHihatRhythm(beatNum, beatLen, shaffle)
  }
};
  • 変数setBeatRhythmは無名関数(引数はi
  • 変数datagetRhythmData()関数(引数はi
  • rythmValの中身(innerHTMLプロパティ)をdataオブジェクトのbeatVaプロパティに
  • 変数beatNumdataオブジェクトのbeatNumプロパティに
  • 変数beatLendataオブジェクトのbeatLenプロパティに
  • 変数kickRhythmdataオブジェクトのkickRhythmプロパティに
  • 変数snareRhythmdataオブジェクトのsnareRhythmプロパティに
  • 変数shaffledataオブジェクトのshaffleプロパティに
  • returnは連想配列で複数の値を返す
  • kickキーの値はsetRhythm()関数(引数は左からbeatNum, beatLen, kickRhythm
  • snareキーの値はsetRhythm()関数(引数は左からbeatNum, beatLen, snareRhythm
  • hihatキーの値はsetHihatRhythm()関数(引数は左からbeatNum, beatLen, shaffle

getRhythmData関数のi番目を読み込んでキック、スネア、ハイハットのリズムを設定している。

このi番目はgetRhythmData関数では配列rhythmDatai番目を意味している。

キックとスネアはsetRhythm()関数でハイハットはsetHihatRhythm()関数。

ビート再生設定

先ほど設定したビートリズムを再生する設定

// ビート再生設定
const playBeat = (kickRtm, snareRtm, hihatRtm) => {
  let kickPart = new Tone.Part(kickSynth, kickRtm).start();
  let snarePart = new Tone.Part(snareSynth, snareRtm).start()
  let hihatPart = new Tone.Part(hihatSynth, hihatRtm).start();
  kickPart.loop = true;
  snarePart.loop = true;
  hihatPart.loop = true;
}
  • 変数playBeatは無名関数(引数は左からkickRtm, snareRtm, hihatRtm
  • 変数kickPartTone.Part()メソッドを生成(引数は左からkickSynth, kickRtm)、start()メソッドを実行
  • 変数snarePartTone.Part()メソッドを生成(引数は左からsnareSynth, snareRtm)、start()メソッドを実行
  • 変数hihatPartTone.Part()メソッドを生成(引数は左からhihatSynth, hihatRtm)、start()メソッドを実行
  • kickPartloopプロパティをtrue
  • snarePartloopプロパティをtrue
  • hihatPartloopプロパティをtrue

Tone.Part()が前回まで使っていた細かいタイミングで音を再生するメソッド。引数でキック、スネア、ハイハットのシンセ音とリズムを読み込んでいる。

※参考:Part

そしてキック、スネア、ハイハットいずれもlooptrueにしてループ再生している。

次にビートの初期値を設定する。

// ビート初期値
let defRhythm = setBeatRhythm(7);
playBeat(defRhythm.kick, defRhythm.snare, defRhythm.hihat);
  • 変数defRhythmsetBeatRhythm()関数を実行(引数は7
  • playBeat()関数を実行(引数は左からdefRhythm.kick, defRhythm.snare, defRhythm.hihat

defRhythmsetBeatRhythm()関数の引数7は0からカウント開始なので配列rhythmDataの8番目=8拍子になる。

playBeat()関数の引数にはdefRhythmのキック、スネア、ハイハット設定値を入れる。

これで初期値がエイトビートになる。

JSコード(ボタン設定)

ここからは画面上のボタン操作の設定。

再生設定

まず画面の一番上にある再生ボタンの設定から。

// 再生判定
let play = false; 
  • 変数playの値はfalse

この真偽値(truefalseか)は再生状態の判定に使う。初期値はfalseなので音は鳴らない。

次に再生・停止を切り替える設定

// 再生ボタン
beat.addEventListener('click', () => {
  if(play){
    play = false;
    beat.innerHTML = "▶︎";
    Tone.Transport.stop();
  } else {
    play = true;
    beat.innerHTML = "■";
    Tone.Transport.start();    
  }
}, false);
  • beatのクリックイベントを設定
  • もしplaytrureなら
    playfalse
    beatの中身(innerHTML)を▶︎
    Tone.Transportstop()メソッド実行
  • さもなくばplaytrue
    beatの中身(innerHTML)を
    Tone.Transportstart()メソッド実行

if文の条件、playの真偽値は先ほどのshaffleと同じでplay == trueではなくplayだけでOK。

処理はplayの真偽値、beatの文字「▶︎」と「■」、Tone.Transportstop()start()を切り替えている。

※参考:Transport

BPM変更設定

次はBPMのスライダー設定

// BPM設定
let BPMSet = () => {
  let BPMImput = BPMRange.value;
  BPMVal.innerHTML = BPMImput;
  Tone.Transport.bpm.value = BPMImput;
}
  • 変数BPMSetは無名関数(引数はなし)
  • 変数BPMImputBPMRangevalue属性
  • BPMValの中身(innerHTMLBPMImput
  • Tone.Transportbpm.valueBPMImput

BPMRangeのスライダーを動かすとBPMValの文字変更し再生のbpm.valueも変更する。

BPMSet();
  • BPMSet()関数を実行

BPMSet()関数を実行するとBPMRangevalue属性の初期値120が適用される。

BPMRangeスライダーのイベント設定

// BPM変更
BPMRange.addEventListener('input', BPMSet, false);
  • BPMRangeinput値が変更されたらBPMSet()関数を実行

これでスライダーを動かすとBPMSet()関数が実行される。

リズム変更設定

最後はリズム変更の設定

// リズム変更
function selectRythm() {
  for(let i = 0; i < rhythmData.length; i++){ 
    rythmRadio[i].addEventListener('input', ()=> {
      let beat = setBeatRhythm(i);
      const kick = beat.kick;
      const snare = beat.snare;
      const hihat = beat.hihat;
      if( rythmRadio[i].checked && play ) {
        Tone.Transport.cancel();
        Tone.Transport.start();
        playBeat(kick, snare, hihat);
      } else {
        Tone.Transport.cancel();
        playBeat(kick, snare, hihat);
      }
    }, false);
  }
}
  • for文の条件:変数iの初期値0irhythmDataの数length繰り返す、iに1つずつ加算
  • 配列rythmRadio[]i番目の'input'値が変更されたらイベントを実行
  • 変数beatsetBeatRhythm()i番目
  • 変数kickbeatkickプロパティ
  • 変数snarebeatsnareプロパティ
  • 変数hihatbeathihatプロパティ
  • if文の条件:もし配列rythmRadio[]i番目がcheckedになっていて、かつplaytrueなら
    Tone.Transportcancel()メソッドを実行
    Tone.Transportstart()メソッドを実行
    playBeat()関数を実行(引数は左からkick, snare, hihat
  • さもなくばTone.Transportcancel()メソッドを実行
    playBeat()関数を実行(引数は左からkick, snare, hihat

変数beatsetBeatRhythm()i番目を取得する。

もしrythmRadio[]がチェック状態でかつ、プレイヤーが再生状態だったら、Tone.Transportをいったんキャンセル*1した後にスタート。

playBeat()関数にrythmRadio[]i番目のキック、スネア、ハイハットを読み込んで実行。これでリズムが切り替わる。

プレイヤーが停止状態であればTone.TransportはキャンセルのみでplayBeat()関数のリズム切り替えのみ行う。

selectRythm();
  • selectRythm()関数を実行

selectRythm()関数を実行することでrythmRadio[]で最初にチェックが入っている8拍子が適用される。

音楽の三要素(メロディ、ハーモニー、リズム)

音楽の三要素(メロディ、ハーモニー、リズム)をTone.jsで音楽の三要素が体験できる!

※参考:音楽 - Wikipedia

メロディ

マリオメロディを打ち込んだ。

See the Pen Mario BGM-fix by イイダリョウ (@i_ryo) on CodePen.

音が鳴らない場合はこちら

※参考:https://codepen.io/i_ryo/full/VOzedR

ハーモニー

コードを切り替えて鍵盤で引ける楽器を作った。

See the Pen tone.js-12cord- 20 by イイダリョウ (@i_ryo) on CodePen.

音が鳴らない場合はこちら

※参考:https://codepen.io/i_ryo/full/xxbPQNv

リズム

そして今回作ったビート・プレイヤー。

See the Pen tone.js-BeatPlayer by イイダリョウ (@i_ryo) on CodePen.

音が鳴らない場合はこちら

※参考:https://codepen.io/i_ryo/full/dyGpbxL

マリオメロディを鳴らしながらコードやビートを調べたり♪

これまでTone.jsでやってきたことはこちらを参照。

※参考:Tone.jsを習得するためにやった事まとめ
qiita.com

React/Next版ビートプレイヤー作成!

※2021/09/17追記

本投稿のCodrPen版を元にReact/Next版のビートプレイヤーを作成しました!

※参考:【React & Tone.js】ビートプレイヤーを作った(ビートとBPMを変更可能) - クモのようにコツコツと

作ったものこちら

※参考:ビートプレイヤー

使い方

※参考:このアプリについて | ビートプレイヤー

ソースコード

※参考:GitHub - ryo-i/beat-player: Tone.jsを使って作ったビートプレイヤー

最後に

f:id:idr_zz:20200620052512j:plain

今回はTone.jsとしては新しいことはやってなかったんですが、下記の体験ができたのが良かったです。

  • inputタグのrange属性(スライダー)とJS処理を紐付ける方法がわかった
  • returnで連想配列を使うと複数の値を返すことができて便利
  • 真偽値truefalseを使うとif文の条件がシンプルになる(==不要で!の有無だけ)

特に長いコードを機能ごとにリファクタリングするときにreturnの使い方は重要になるな、と感じました。

ビート・プレイヤー、実際に音楽を流しながらBPMを調べてみましたが、結構実用的で満足感がありました♪

リズム編はここで一区切り。次は音楽の三要素を組み合わせて音楽全体を作っていく段階!ミキシングなどの音響系の理解も必要になってきます。

それではまた!


※参考:Tone.jsを習得するためにやった事まとめ
qiita.com

*1:このキャンセルを行わない場合、スタートで音が二重に鳴ってしまう