Tone.jsを使ってみた
Tone.jsとは
Tone.jsは、Web Audio APIを使いやすくラップしたライブラリです。
これを使えばブラウザ上で簡単に音を鳴らすことができます。
ここではTone.jsを主に音程に沿った音を鳴らすために使います。
他にも音源の再生や加工をすることもできますが、今回は扱いません。
ドキュメントはこちらです。
導入
導入には主に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
メソッドは、triggerAttack
とtriggerRelease
の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が見つからなかったので、自分で実装してみます。
おおかた以下のような手順を踏むと再生できます。
- 合計の時間を保管するための変数を宣言する
-
melodyArray
でループする - ループ内で音をスケジュールする
- ループ内で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')
また、正しい使い方なのかはわかりませんが、ループに状態を持たせることで各ループごとに違う音を鳴らすこともできます。
以下はC3
、C#3
、D3
、...、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