🎹

Tone.jsを使ってみた

2024/11/06に公開

Tone.jsとは

Tone.jsは、Web Audio APIを使いやすくラップしたライブラリです。
これを使えばブラウザ上で簡単に音を鳴らすことができます。

ここではTone.jsを主に音程に沿った音を鳴らすために使います。
他にも音源の再生や加工をすることもできますが、今回は扱いません。

ドキュメントはこちらです。
https://tonejs.github.io/docs/15.0.4/index.html

入門にはこちらこちらこちらのブログが参考になりました。

導入

導入には主に2つの方法があります。

  • CDNを使う(http://unpkg.com/tone
  • パッケージマネージャでtoneパッケージをインストールする

また、ここから手軽に試すことができます。

使ってみる

Tone.jsはToneオブジェクトにその機能が(ほぼ)全て詰まっています。
CDNから使う場合はグローバルにToneオブジェクトがあると思います。

toneパッケージからimportする際は、デフォルトエクスポートを使うのではなく、以下のように* as Toneを使ってください。

import * as Tone from 'tone'

このように書かないと、私の環境ではエラーになりました。

一つの音を鳴らす

いわゆるドレミファソラシドの音を鳴らすにはシンセサイザーを使います。
Tone.jsのシンセサイザー(Synth)にはtriggerAttackReleaseというメソッドが用意されており、これを使うと任意の音を鳴らせます。

以下はド(C4)を4分音符の長さで鳴らす例です。

const synth = new Tone.Synth().toDestination()
synth.triggerAttackRelease('C4', '4n')

toDestinationの詳細はこちらをご覧ください。

このtriggerAttackReleaseメソッドは、triggerAttacktriggerReleaseの2つの機能を組み合わせたものです。
これらのメソッドには以下の役割があります。

  • triggerAttack: 指定された高さの音を鳴らす
  • triggerRelease: 音を止める

つまりtriggerAttackReleaseは、triggerAttackの呼び出し後、引数で指定された時間が経ったらtriggerReleaseを呼び出すイメージです。
Tone.jsは独自のスケジューリングを実装しており、それに乗ることで正確な長さの音を鳴らせます。

ボタンを押したら音を鳴らす

Tone.jsで音を鳴らす際に、2つ注意すべきことがあります。

  • ブラウザはボタンを押すなどのユーザーアクションがないと音を鳴らさない
  • Tone.jsのコードに到達する前にTone.startを呼び出す必要がある

これを踏まえ、ボタンが押されたら↑のコードを実行するようにしてみます。
以下はReactでの例です。

function App() {
  const play = async () => {
    await Tone.start()
    const synth = new Tone.Synth().toDestination()
    synth.triggerAttackRelease('C4', '4n')
  }
  return <button onClick={play}>play</button>
}

複数の音を鳴らす

複数の音を順番に鳴らしたい場合、以下のようにコードを書いても順次再生されません。

const synth = new Tone.Synth().toDestination()
synth.triggerAttackRelease('C4', '4n')
synth.triggerAttackRelease('D4', '4n')

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

これはtriggerAttackRelease再生が終わるまで待つメソッドではないためです。
また、Tone.Synthは同時に音を鳴らすことができないからでもあります。同時再生にはPolySynthが使えますが、それはまた別の話です。

順番に鳴らす場合、第3引数に再生を開始するタイミングを設定する必要があります。
例えばこの場合、2つ目の音の再生を4nだけ待ってからにします。

synth.triggerAttackRelease('C5', '4n')
synth.triggerAttackRelease('D5', '4n', '4n') // 1音目が鳴り終わってから再生

データから音を再生する

例えば、音程と長さのみが定義されたデータがあるとします。

const melodyArray = [
  { note: 'C5', duration: '4n' },
  { note: 'D5', duration: '8n' },
  { note: 'E5', duration: '4n' },
]

この音を順番に再生するための適したAPIが見つからなかったので、自分で実装してみます。
おおかた以下のような手順を踏むと再生できます。

  1. 合計の時間を保管するための変数を宣言する
  2. melodyArrayでループする
  3. ループ内で音をスケジュールする
  4. ループ内で1の変数を音の長さ分加算する

コードはこちらです。

const synth = new Tone.Synth().toDestination()
let time = Tone.now() // 時間を保管する変数を宣言する

melodyArray.forEach(({ note, duration }, index) => {
  // timeを使って音をスケジュールする
  synth.triggerAttackRelease(note, duration, time)
  // durationを使ってtimeを更新する
  time += Tone.Time(duration).toSeconds()
})

スケジューリングAPI

Tone.jsは独自にスケジューリングAPIを実装しています。
それがTransportと呼ばれるクラスです。

Transportオブジェクトには以下のメソッドがあります。

  • schedule: 指定した時間で関数をスケジュールする
  • start: スケジュールされた関数を実行する

メインのTransportオブジェクトはTone.getTransportを呼び出すことで取得でき、これを操作することでスケジューリングができます。
例えば開始直後にC4を鳴らし、その4n(4分音符の期間)後にD4の音を鳴らす場合、以下のように書きます。

const synth = new Tone.Synth().toDestination();

Tone.getTransport().schedule(time => {
  synth.triggerAttackRelease('C5', '8n', time)
}, 0) // 0 = 開始直後

Tone.getTransport().schedule(time => {
  synth.triggerAttackRelease('D5', '4n', time)
}, '4n') // 4nだけ経った後

// 再生する
Tone.getTransport().start()

詳細はv14のドキュメントをご覧ください。

再生を停止する

Transport.start()メソッドで再生した音は、pauseメソッドで一時停止できます。
うまくスケジューリングを利用することで、簡単に一時停止や再生を実現できるというわけです。

また、停止系の名前がついているメソッドは他にも下記があります。

  • pause: 再生を一時停止する、startメソッドで再開可能
  • stop: 再生を停止する
  • cancel: 指定された時間以降のスケジュールを削除する
  • clear: イベントIDからスケジュールを削除する
  • dispose: リソースを解放する、呼び出すとそのオブジェクトは使えなくなる

ループする

Transportを使うと簡単にループが実現できます。
方法は以下の2種類があります。

  • scheduleRepeatを呼び出す: ループしたいイベントをスケジュールする
  • loopプロパティをtrueにする: スケジュール全体でループする

例えばscheduleRepeatを使ってC4の音をずっと鳴らすには、以下のようにします。

Tone.getTransport().scheduleRepeat(time => {
  synth.triggerAttackRelease('C4', '4n', time)
}, '4n')

また、正しい使い方なのかはわかりませんが、ループに状態を持たせることで各ループごとに違う音を鳴らすこともできます。
以下はC3C#3D3、...、B3, C4と順番に音を鳴らすサンプルです。

const letters = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
const count = {
  letter: 'B',
  octave: 2,
  next() { // noteを順番に返す
    if (this.letter === letters.at(-1)) { // 繰り上がり
      this.letter = letters[0]
      this.octave++
    } else { // 通常
      this.letter = letters[letters.findIndex(l => l === this.letter) + 1]
    }
    return this.letter + this.octave
  }
}

Tone.Transport.scheduleRepeat(time => {
  const letter = count.next()
  synth.triggerAttackRelease(letter, '8n', time)
}, "8n")
Tone.Transport.start()

エフェクト

Tone.jsでは、専用のオーディオノード(のラッパー?)を通して音にエフェクトを加えることができます。
エフェクトには例えば以下のようなものがあります。(By ChatGPT)

  • 空間的なエフェクト:Reverb, Delay, Chorus
  • 音を変形するエフェクト:Distortion, Filter, Phaser, Flanger
  • ダイナミクスを調整するエフェクト:Compressor, EQ, AutoFilter
  • リズム的なエフェクト:Tremolo, Gater

詳細はドキュメントを確認してください。

使い方

ここでは音量を調整するGainを使って、シンセサイザーの音量を2倍にする例を紹介します。

まずはGainオブジェクトを作成します。
ここでは最後にtoDestinationを呼び、ノードの出力をスピーカーに接続します(後述)。

  const gain = new Tone.Gain(2).toDestination()

次にシンセを初期化し、出力をgainに接続します。
接続にはconnectメソッドを利用します。
なお、ここではtoDestinationは呼ばないので注意してください。

  const gain = new Tone.Gain(2).toDestination()
+ const synth = new Tone.Synth()
+ synth.connect(gain)

最後にこのシンセで音を鳴らす記述を追加します。
これで音量が2倍になってC4の音が流れるはずです。

  const gain = new Tone.Gain(2).toDestination()
  const synth = new Tone.Synth()
  synth.connect(gain)

+ synth.triggerAttackRelease('C4', '4n')

toDestinationとは

先ほどからシンセサイザーの初期化のたびに呼び出しているtoDestinationですが、これは重要な役割を持っています。
その役割とは、出力をスピーカーに接続するというものです。

先ほどの例では、シンセの初期化でtoDestinationを呼んでいませんでした。

const gain = new Tone.Gain(2).toDestination()
const synth = new Tone.Synth() // ここ
synth.connect(gain)

synth.triggerAttackRelease('C4', '4n')

その代わり、Gainの初期化でこれを呼んでいます。
なぜかというと、この場合はシンセの出力を直接(PCなどの)スピーカーに送るわけではなく、一度Gainを通してから送りたいからです。

おまけ: Player

Playerを使うと、外部のサウンドファイルの音を鳴らすことができます。
しかも、上で紹介したようなエフェクトを適用することが可能です。

以下はここのコードを少し改変したものです。
ぜひコメントアウトしたり解除したりして聴き比べてみてください。

// サウンドファイルを読み込む
const player = new Tone.Player({
  url: "https://tonejs.github.io/audio/berklee/gurgling_theremin_1.mp3",
  loop: true,
  autostart: true,
});
// Distortionエフェクトを作成する
const distortion = new Tone.Distortion(0.4).toDestination();

// 以下の2行のどちらかをコメントアウトするとエフェクトの適用が切り替えられます
player.connect(distortion);
// player.toDestination()

Discussion