クモのようにコツコツと

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

【React & Tone.js】ビートプレイヤーを作った(ビートとBPMを変更可能)

Reactアプリの続きです。前回は配色ジェネレーターを作りました。今回からTone.jsを使った音楽アプリ編に入ります。以前CodePenで作成したビートプレイヤーをReact/Next環境に移植してみました。それではいきましょう!

【目次】

※参考:前回記事
【React】配色ジェネレーターを作った(HSBでメインカラー、アクセントカラー、ベースカラーを割り出す!) - クモのようにコツコツと

※参考:【React】ReactでWebアプリを作るシリーズまとめ
qiita.com

作ったもの

ビートプレイヤー f:id:idr_zz:20210914063250j:plain

下記のような用途に活用できる。

  • 楽器やボーカルの練習用のメトロノームとして使う
  • 既存の曲のBPM(Beats Per Minute:テンポ、速度)を調べる
  • 同じBPMの拍子(リズム)を変えるとどんなビートになるか確認できる

作ったアプリはこちら

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

アプリの使い方

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

ソースコード

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

このアプリで実現したかったこと

以前、こちらのTone.jsを使ってCodePen上に作ったビートプレイヤー

※参考:【Tone.js】いろいろなリズムが鳴らせるビート・プレイヤーを作った(BPM切り替え可能) - クモのようにコツコツと

※参考:【Web Audio】Tone.jsを習得するためにやったことまとめ(随時更新) - Qiita


これをReact/Next環境で再現したい。前回の配色ジェネレーターと同じく、Nextスターターキットの環境をベースに作成する。

※参考:GitHub - ryo-i/next-app-started

※参考:【React】ReactでWebアプリを作るシリーズまとめ(随時更新) - Qiita

ソースコード

上記の挙動を実現しているソースコードはこちら。

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

NCUインストール(アップデート確認)

GitHubの画面上で脆弱性からパッケージアップデートを勧められた

NCUインストール

$ npm install -g npm-check-updates

NCU実行

$ ncu -u

パッケージインストール

$ npm install

参考になった記事

※参考:Githubからセキュリティ警告が来たときの対応 – Cntlog

tone.jsのインストール

下記のコマンドでインストールできた

$ npm i tone

※参考:Tone.js

packge.jsonに追加された!

  "dependencies": {
    "next": "11.1.1",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "styled-components": "^5.3.0",
    "tone": "^14.7.77"
  },

※参考:beat-player/package.json at main · ryo-i/beat-player · GitHub

ちなみにtone.jsonは「@type」は無いようだった。

※参考:TypeScript: Search for typed packages

データ:data.json

innerキーの中にビートプレイヤー用のデータ(設定値)を入れている。

    "inner": {
        "settings": {
            // フックの初期値
        },
        "beatParam": {
           // 各ビートの設定値
        },
        "synthParam": {
            // シンセの設定値
        }
    },

settingsキーの中には後述するフックの初期値を入れている

        "settings": {
            "beatPlay": "▶︎",
            "bpm": 120,
            "beatName": "beat8",
            "beatValue": "8拍子"
        },

beatParamキーの中には各種ビートの設定値を入れている

        "beatParam": {
            "beat1": {
                "value": "1拍子",
                "beatNumber": 1,
                "kickRhythm": [0],
                "snareRhythm": [],
                "shaffle": false
            },
            "beat2": {
                "value": "2拍子",
                "beatNumber": 2,
                "kickRhythm": [0],
                "snareRhythm": [1],
                "shaffle": false
            },
            "beat3": {
                "value": "3拍子",
                "beatNumber": 3,
                "kickRhythm": [0],
                "snareRhythm": [1, 2],
                "shaffle": false
            },
            "beat4": {
                "value": "4拍子",
                "beatNumber": 4,
                "kickRhythm": [0, 2],
                "snareRhythm": [1, 3],
                "shaffle": false
            },
            "beat5": {
                "value": "5拍子",
                "beatNumber": 5,
                "kickRhythm": [0],
                "snareRhythm": [3],
                "shaffle": false
            },
            "beat6": {
                "value": "6拍子",
                "beatNumber": 6,
                "kickRhythm": [0],
                "snareRhythm": [3],
                "shaffle": false
            },
            "beat7": {
                "value": "7拍子",
                "beatNumber": 7,
                "kickRhythm": [0, 4],
                "snareRhythm": [2, 6],
                "shaffle": false
            },
            "beat8": {
                "value": "8拍子",
                "beatNumber": 8,
                "kickRhythm": [0, 4],
                "snareRhythm": [2, 6],
                "shaffle": false
            },
            "beat12": {
                "value": "12拍子",
                "beatNumber": 12,
                "kickRhythm": [0, 6],
                "snareRhythm": [3, 9],
                "shaffle": false
            },
            "beatShuffle": {
                "value": "シャッフル",
                "beatNumber": 12,
                "kickRhythm": [0, 6],
                "snareRhythm": [3, 9],
                "shaffle": true
            },
            "beat16": {
                "value": "16拍子",
                "beatNumber": 16,
                "kickRhythm": [0, 8],
                "snareRhythm": [4, 12],
                "shaffle": false
            },
            "beat24": {
                "value": "24拍子",
                "beatNumber": 24,
                "kickRhythm": [0, 12],
                "snareRhythm": [6, 18],
                "shaffle": false
            },
            "beatSwing16": {
                "value": "ハネた16",
                "beatNumber": 24,
                "kickRhythm": [0, 12],
                "snareRhythm": [6, 18],
                "shaffle": true
            },
            "beat32": {
                "value": "32拍子",
                "beatNumber": 32,
                "kickRhythm": [0, 16],
                "snareRhythm": [8, 24],
                "shaffle": false
            }
        },

synthParamにはシンセのオプション設定値を入れている。

        "synthParam": {
            "synthKickOpts": {
                "value": "キック",
                "envelope": {
                    "attack": 0 ,
                    "decay": 0.5 ,
                    "sustain": 0 ,
                    "release": 0
                },
                "volume": 2
            },
            "synthSnareOpts": {
                "value": "スネア",
                "envelope": {
                    "attack": 0 ,
                    "decay": 0.2 ,
                    "sustain": 0 ,
                    "release": 0
                },
                "volume": -10
            },
            "noiseSnareOpts": {
                "value": "スネア",
                "type": "white",
                "envelope": {
                    "attack": 0 ,
                    "decay": 0.4 ,
                    "sustain": 0 ,
                    "release": 0
                },
                "volume": 0
            },
            "noiseHihatOpts": {
                "value": "ハイハット",
                "type": "brown",
                "envelope": {
                    "attack": 0 ,
                    "decay": 0.03 ,
                    "sustain": 0,
                    "release": 0
                },
                "volume": -10
            }
        }

※参考:beat-player/data.json at main · ryo-i/beat-player · GitHub

Inner.tsx(全体)

Inner.tsxはビートプレイヤーの本体にあたるファイル

※参考:beat-player/Inner.tsx at main · ryo-i/beat-player · GitHub

全体的な構成はこうなっている。

import React, { useState }  from 'react';
import styled from 'styled-components';
import Data from '../data/data.json';
import * as Tone from 'tone';


const innerJson = Data.inner;


// CSS in JS
const BeatPlayer = styled.section`
// 中略


// Component
function Inner() {
  // 中略(フック、Tone.js、JSXを設定)
}

export default Inner;
  • reactとフックuseStateをインポート
  • CSS in JSstyled-componentsをインポート
  • 設定データdata.jsonをインポート
  • Tone.jstoneをインポート
  • 変数innerJsondata.jsonからInnerコンポーネントのデータを取得
  • 関数Inner()でInnerコンポーネントを設定(中でフック、Tone.js、JSXを設定)
  • Innerコンポーネントをエクスポート

BeatPlayerコンポーネント(CSS in jS)

BeatPlayerコンポーネントにstyled-componentsによるCSS in JSの設定を書いている。sectionタグになる。

const BeatPlayer = styled.section`
  {
    background: #333;
    font-family:
    "Hiragino Kaku Gothic ProN",
    "Hiragino Sans",
    "Helvetica Neue",
    Arial,Meiryo,
    sans-serif;
    padding: 0;
    h1, h2, p {
      color: #fff;
      text-align: center;
    }
    .pad {
      background: #333;
      padding: 10px;
      display: flex;
      justify-content: center;
      flex-wrap: wrap;
      margin: 0 auto;
      position: relative;
      .beat {
        margin: 5px;
        background: #666;
        border: 0px solid #333;
        width: 100px;
        height: 100px;
        font-family: inherit;
        font-size:30px;
        line-height: 100px;
        border-radius: 5px;
        text-align: center;
        display: inline-block;
        color: #fff;
        box-shadow: 5px 5px 5px rgba(0,0,0, 0.3);
        :hover {
          opacity: 0.7;
          cursor: pointer;
        }
      }
    }
    .setting {
      text-align: center;
      color: #fff;
      max-width: 350px;
      width: 100%;
      margin: 0 auto;
      .range {
        max-width: 300px;
        width: 100%;
        margin: 0 auto;
        :hover {
          cursor: pointer;
        }
      }
      label {
        margin: 0 3px 10px 0;
        display: inline-block;
        :hover {
          opacity: 0.7;
          cursor: pointer;
        }
      }
    }
  }
`;

CodePenのCSSがSass(SCSS)的な書き方になりすっきり♪

Innerコンポーネント(全体)

Innerコンポーネントでフック、Tone.js、JSXを設定している。全体的な構成はこうなっている。

// Component
function Inner() {
  // Hooks
  // 中略


  // シンセ設定
  let kickSynth, snareSynth, hihatSynth;
  const setSynth = () => {
    // 中略
  };


  // リズム取得
  let getRhythmData = (className) => {
    // 中略
  };


  // キック、スネアリズム設定
  const setRhythm = (beatLen, Array) => {
    // 中略
  }


  // ハイハットリズム設定
  const setHihatRhythm = (beatNmb, beatLen, shaffle) => {
    // 中略
  };


  // ビートリズム設定
  const setBeatRhythm = (className) => {
    // 中略
  };


  // ビート再生設定
  const playBeat = (kickRtm, snareRtm, hihatRtm) => {
    // 中略
  };


  // 再生ボタン
  let changeBeatPlay = () => {
    // 中略
  };


  // BPM変更
  let changeBpm = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 中略
  }


  // リズム変更
  const changeRythm = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 中略
  }


  // JSX
  return (
    // 中略
  );
}

フック設定(useState())

冒頭でフック設定をしている。

  // Hooks
  const [beatPlay, setBeatPlay] = useState(innerJson.settings.beatPlay);
  const [bpmRange, setBpmRange] = useState(innerJson.settings.bpm);
  const [beatName, setBeatName] = useState(innerJson.settings.beatName);
  const [beatValue, setBeatValue] = useState(innerJson.settings.beatValue);

beatPlaybpmRangebeatNamebeatValueの4つ。初期値はinnerJsonsettingsキーから読み取っている。

シンセ設定

次にシンセ設定

  // シンセ設定
  let kickSynth, snareSynth, hihatSynth;
  const setSynth = () => {
    const synthKick = new Tone.Synth(innerJson.synthParam.synthKickOpts).toDestination();
    const synthSnare = new Tone.Synth(innerJson.synthParam.synthSnareOpts).toDestination();
    const noiseSnare = new Tone.NoiseSynth(innerJson.synthParam.noiseSnareOpts).toDestination();
    const noiseHihat = new Tone.NoiseSynth(innerJson.synthParam.noiseHihatOpts).toDestination();

    kickSynth = () => {
      synthKick.oscillator.type = "sine";
      synthKick.frequency.rampTo('C0', '0.1');
      synthKick.triggerAttackRelease('C4', '0.5');
    };

    snareSynth = () => {
      synthSnare.oscillator.type = "sine";
      synthSnare.triggerAttackRelease('F4', '0.5');
      noiseSnare.triggerAttackRelease('0.5');
    };

    hihatSynth = () => {
      try {
        noiseHihat.triggerAttackRelease('0.1');
      } catch (error) {
        console.log('error ->', error.message);
      }
    };
  };

setSynth()関数で処理を実行するが、結果を取得するkickSynth, snareSynth, hihatSynthを再利用したいため、関数の外でlet変数で設定した。


ドラムマシン808的な音にしたくて試行錯誤した。

※参考:Roland TR-808 (1980) - Famous Drum Beats - YouTube

synthKicksynthSnarenoiseSnarenoiseHihatでシンセを取得。CodePen時代はtoMaster ()を実行していたが今はtoDestination()が推奨されているようだ。

※参考:Destination


kickSynth()でキック音の設定。CodePen時代はMembraneSynth(Tone.jsのキック&タム用のシンセ)を使っていたが結果、808的なキック音はTone.jsの基本シンセであるSynthで実現できた。

※参考:Synth

波形をサイン波にしたかった。波形の設定値はこちらで調べることができた。

※参考:TypeWithPartials

下記の参考記事のようにキックのアタック時にピッチを時間的変化(エンベロープ)させたかった。

※参考:エレクトロなキック

FrequencyEnvelopeでピッチの時間的変化を実現できた。ピッチがC4からC0に0.1秒かけて下がる。

※参考:FrequencyEnvelope


snareSynth()内でスネアの設定。CodePen時代はNoiseシンセの減衰音を鳴らしていた。

※参考:Noise

それだけだと物たりないため、アタック音に革が響く音を加えたかった。

※参考:エレクトロなスネア

snareSynthでキックと同じSynthシンセを実行しnoiseSnareのと一緒に鳴らした。ピッチはF4が808的な高さだった。


hihatSynth()でハイハットの設定。スネアと同じNoiseシンセ。32ビートにしてBPM240にすると下記のエラーが頻発した。エラーはループの頭で発生し、若干繋ぎがズレる感じがした。

Uncaught Error: Start time must be strictly greater than previous start time

まったく同時に多くのtriggerAttackRelease()を実行するのはよくないようだ。

※参考:javascript - Tone.js Error: 'Start time must be strictly greater than previous start time' - Stack Overflow
※参考:[Tonejs/Tone.js#594:title]

試行錯誤の結果、try...catchで例外設定すると繋ぎのズレはなくなった。(エラー自体はなくならないが)

※参考:try...catch - JavaScript | MDN
※参考:JavaScriptのtry~catch文で例外処理をする方法|TECH PLAY Magazine [テックプレイマガジン]
※参考:https://wa3.i-3-i.info/word1427.html

リズム取得

getRhythmData()関数でリズムを取得

  // リズム取得
  let getRhythmData = (className) => {
    let data = innerJson.beatParam[className];
    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
    }
  };

引数のclassNameを元にinnerJsonbeatParamキーからリズム設定値を取得。そこから先はCodePen時代と同じ。

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

setRhythm()関数でキック、スネアリズムを設定

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

CodePen時代とちょっと変えたのはfor文の中。1000を掛けてから1000で割ってMath.floorで小数2桁で切り捨てている。(エラーや音のズレと格闘しているときに原因の一つかな?と思って試した。結果は変わらなかったw)

ハイハットリズム設定

setHihatRhythm()関数でハイハットリズムを設定

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

shaffleがtrueの場合は2番目のハイハットを鳴らさない。ここでも少数2桁で切り捨てにしている。

ビートリズム設定

setBeatRhythm()関数でビートリズムを設定

  // ビートリズム設定
  const setBeatRhythm = (className) => {
    let data = getRhythmData(className);
    setBeatValue(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(beatLen, kickRhythm),
      snare: setRhythm(beatLen, snareRhythm),
      hihat: setHihatRhythm(beatNum, beatLen, shaffle)
    }
  };

引数でclassNameを取得し先ほどのgetRhythmData()の引数に入れて実行している。

ビートリズム設定

playBeat()で関数ビートリズムを設定

  // ビート再生設定
  const playBeat = (kickRtm, snareRtm, hihatRtm) => {
    setSynth();

    let hihatPart = new Tone.Part(hihatSynth, hihatRtm).start();
    let kickPart = new Tone.Part(kickSynth, kickRtm).start();
    let snarePart = new Tone.Part(snareSynth, snareRtm).start();

    hihatPart.loop = true;
    kickPart.loop = true;
    snarePart.loop = true;
  };

setSynth()でシンセを設定し、Tone.Part()でキック、スネア、ハイハットを鳴らす。loopでビートをループしている。

※参考:Part

再生ボタン

changeBeatPlay()関数で再生ボタンの処理を実行

  // 再生ボタン
  let changeBeatPlay = () => {
    if (beatPlay === "▶︎") {
      setBeatPlay("■");
      Tone.Transport.stop();
      Tone.Transport.cancel();
      Tone.Transport.bpm.value = bpmRange;
      const beatRhythm = setBeatRhythm(beatName);
      playBeat(beatRhythm.kick, beatRhythm.snare, beatRhythm.hihat);
      Tone.Transport.start();
    } else if (beatPlay === "■"){
      setBeatPlay("▶︎");
      Tone.Transport.stop();
      Tone.Transport.cancel();
    }
  };

フックbeatPlayが「▶︎」だったらbeatPlayを「■」に変更。Tone.Transportでキック、スネア、ハイハットを同時に制御している。

※参考:Transport


「▶︎」だったら音を止めて設定をキャンセル。BPMをフックbpmRangeに。setBeatRhythm()の引数にフックbeatNameを入れて実行し、その結果をplayBeat()で再生する。


フックbeatPlayが「■」だったらbeatPlayを「▶︎」に変更。Tone.Transportでキック、スネア、ハイハットをストップし、設定もキャンセルしている。

BPM変更

changeBpm()関数でBPMを変更

  // BPM変更
  let changeBpm = (e: React.ChangeEvent<HTMLInputElement>) => {
    const BPMImput: number = Number(e.target.value);
    setBpmRange(BPMImput);
    Tone.Transport.bpm.value = BPMImput;
  }

引数はJSXのInputタグ。BPMImputでinputタグのvalueを取得し、フックbpmRangeにセット。Tone.TransportのBPMも変更している。

リズム変更

changeRythm()関数でリズムを変更

  // リズム変更
  const changeRythm = (e: React.ChangeEvent<HTMLInputElement>) => {
    const getClassName: string = String(e.target.className);
    const getValue: string = String(e.target.value);
    const beatRhythm = setBeatRhythm(getClassName);
    setBeatName(getClassName);
    setBeatValue(getValue);

    if(beatPlay === "■") {
      Tone.Transport.stop();
      Tone.Transport.cancel();
      Tone.Transport.start();
      playBeat(beatRhythm.kick, beatRhythm.snare, beatRhythm.hihat);
    } else if (beatPlay === "▶︎") {
      Tone.Transport.cancel();
      playBeat(beatRhythm.kick, beatRhythm.snare, beatRhythm.hihat);
    }
  }

こちらも引数はJSXのinputタグ。getClassNameでinputタグのclass名、getValueでvalueを取得。

beatRhythmsetBeatRhythm()を実行(引数にgetClassNameを入れる)。フックbeatNamegetClassNameをセット。フックbeatValuegetValueをセット。


フックbeatPlayが「■」だったらTone.Transportでキック、スネア、ハイハットを停止、キャンセル、再生。playBeat()でリズム変更。引数はbeatRhythmのキック、スネア、ハイハット。

フックbeatPlayが「▶︎」だったらTone.Transportでキック、スネア、ハイハットの設定をキャンセルし、playBeat()playBeat()でリズム変更。

JSX設定

JSX設定

  // JSX
  return (
    <>
      <BeatPlayer>
        <h1>Beat Player</h1>
        <ul className="pad">
          <button className="beat" type="button" onClick={changeBeatPlay}>{beatPlay}</button>
        </ul>
        <div className="setting">
          <section className="bpm">
            <h2>BPM: <span className="val">{bpmRange}</span></h2>
            <input type="range" name="range" min="30" max="240" value={bpmRange} className="range" onChange={changeBpm} />
          </section>
          <section className="rhythm">
            <h2>リズム: <span className="val">{beatValue}</span></h2>
            <label><input type="radio" name="beat" className="beat1" value="1拍子" onChange={changeRythm} />1</label>
            <label><input type="radio" name="beat" className="beat2" value="2拍子" onChange={changeRythm} />2</label>
            <label><input type="radio" name="beat" className="beat3" value="3拍子" onChange={changeRythm} />3</label>
            <label><input type="radio" name="beat" className="beat4" value="4拍子" onChange={changeRythm} />4</label>
            <label><input type="radio" name="beat" className="beat5" value="5拍子" onChange={changeRythm} />5</label>
            <label><input type="radio" name="beat" className="beat6" value="6拍子" onChange={changeRythm} />6</label>
            <label><input type="radio" name="beat" className="beat7" value="7拍子" onChange={changeRythm} />7</label>
            <label><input type="radio" name="beat" className="beat8" value="8拍子" onChange={changeRythm} defaultChecked />8</label>
            <label><input type="radio" name="beat" className="beat12" value="12拍子" onChange={changeRythm} />12</label>
            <label><input type="radio" name="beat" className="beatShuffle" value="シャッフル" onChange={changeRythm} />Shuffle</label>
            <label><input type="radio" name="beat" className="beat16" value="16拍子" onChange={changeRythm} />16</label>
            <label><input type="radio" name="beat" className="beat24" value="24拍子" onChange={changeRythm} />24</label>
            <label><input type="radio" name="beat" className="beatSwing16" value="ハネた16" onChange={changeRythm} />Swing16</label>
            <label><input type="radio" name="beat" className="beat32" value="32拍子" onChange={changeRythm} />32</label>
          </section>
        </div>
      </BeatPlayer>
    </>
  );
}

タグの全体的な構成はCodePenと変わらない。class名はJSXのためclassNameにしている。

ユーザーの操作によってテキスト表示を変える部分は{bpmRange}などのフックにしている。

また、buttonタグやinputタグのイベントはReactのonClickonChangeで関数を実行いしてる。

Headerコンポーネント(音を止める設定)

「About」ページに遷移してもビートの音が止まらなかった。ブラウザがリロードされていないためと思われる。

Headerコンポーネントの中で音を止める設定をした。

※参考:beat-player/Header.tsx at main · ryo-i/beat-player · GitHub

import Link from 'next/link';
import Router from 'next/router';
// 中略
import * as Tone from 'tone';


// 中略(Style設定など)


// 再生停止
const stopBeatPlay = (url) => {
  // console.log('App is changing to: ', url);
  if (url === '/about') {
    Tone.Transport.stop();
    Tone.Transport.cancel();
  }
};
Router.events.on('routeChangeStart', stopBeatPlay);


// Component
function Header() {
  return (
    <HeaderTag>
      <div className="wrapper">
        <h1>{ title }</h1>
        <p dangerouslySetInnerHTML={{ __html: text }}></p>
        <nav>
          <span>MENU:</span>
          <Link href="/"><a>Home</a></Link>
          <Link href="/about"><a>About</a></Link>
        </nav>
      </div>
    </HeaderTag>
  );
}

export default Header;

Tone.jsをここでもインポートする。


stopBeatPlay()関数でTone.Transportでキック、スネア、ハイハットの停止、設定キャンセルを実行


aboutページのaタグでonClickstopBeatPlay()を実行

next/routerのルーティングイベント(events.on)のrouteChangeStartでページ遷移時にstopBeatPlay()を実行

(※2021/09/20修正:onClickだとブラウザの矢印アイコンで音が止まらないため、ルーティングイベントに変更)

※参考:Next.jsのルーティングイベントの発火の覚書 | RyotArchLog ※参考:next/router | Next.js

これでaboutページに遷移したときに音が止まるようになった♪

課題(ハイハットがズレる)

2021/09/14現在、ハイハットの音がズレるという課題が残っている。特に「はねた16ビート」が顕著に感じる。「チッキチッキチッキチッキ」がたまに「チキッチキッチキッチキッ」みたいな感じにズレる。

なんとなくループ処理の時になんらかの事情でタイムラグが生じている気がするが、Tone.jsのループは「hoge.loop = true」とプロパティ化しており、その先はライブラリ内で行われているため、解決方法が見つからない。

この問題はCodePen時代はあまり起こっていなかった(完全にないわけではなさそう)

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

※参考:【Tone.js】いろいろなリズムが鳴らせるビート・プレイヤーを作った(BPM切り替え可能) - クモのようにコツコツと

そのため、React/Next.js環境との相性の問題な気がいている。特にTone.jsはブラウザ側がメインで動き、Next.jsはサーバ側がメインで動くのでその相性が悪いように思う。

今回、React/Next.jsでTone.jsの音が鳴らせるか、というテーマで、それ自体は実現できたので、今後は課題が生じない範囲内で作っていくしか今のところなさそう。また解決策がわかったらここに追記していく予定。

最後に

とうことでReact/Next環境でTone.jsの音を鳴らすことができました〜。ハイハットの音がズレるなどの課題は残っています。。

早いBPMで起こる事象のため、React/Next環境でTone.jsを使うコンテンツはあまり早い速度で動かすと意図通りの音にならない可能性がありそうです。まあ自分はLo-fi Hip Hopなどゆったり目の速度の音楽が好きなのでたいていのことは実現できるんじゃないかな、という気はしています。

Tone.jsシリーズ、次は和音(コード)系のアプリを作る予定です。それではまた!


※参考:【React】ReactでWebアプリを作るシリーズまとめ
qiita.com