【AudioContext】【Tone.js】ゼットンの声をJavaScriptで再現してみる【Pizzicato.js】【XSound】

きっかけは、[note](https://note.com/kurokky/n/nb56a1f815669)の方に書いてるのでこちらは技術的な部分のお話です。

制作物は以下で

- [こちら](https://bu-kurokky.github.io/vc/wetton)がエフェクトとしてトレモロをかけたもの
- [こちら](https://bu-kurokky.github.io/vc/wetton_base)は無理矢理トレモロを実音として実装したものです

ロジックとしては、

1. クマゼミの音を1/2の音程で再生する
1. 「ウェットン」という音声ファイルを1/2の音程で再生する
1. ド#からオクターブ上のドまでのsine波の音にトレモロをかける

これらだったので、以前にAudioContext、Tone.jsなどを使ってJavaScriptで音を鳴らしたりマイクにエフェクトをかけてWebRTCしてみたりしていた経験もあったので『簡単に実装できるだろうなぁ』と思っていたのですが、意外とつまづきました。

## つまづいたところ 1
音声ファイルをローカルで再生しようとするとCORSで怒られる。  

いや、当たり前っちゃー当たり前なんですし、「live-serverでもなんでも使えや！」って話なんですが、これぐらいlive-serverとか使わずにやりたいんです。  
ローカルのファイルをブラウザにポイってしたら動くのがHTML+CSS+JSのいいところなはずなのに、パラメーターをつけてブラウザを新規で立ち上げたくないのです。

ということで、音声ファイルをbase64化 + [Base64Binary.js](https://gist.github.com/htchaan/108b7aa6b71eb03e38019e64450ea095)を使ってサーバー経由じゃなくても音声が再生されるようにします。

## つまづいたところ 2
[Tone.js](https://github.com/Tonejs/Tone.js)のトレモロが意図した動作にならない

最初、C#の音からオクターブ上のCまでの音を段階的にアップさせる方式で実装していました。

```js
  const option = {
      oscillator:{type:'sine'}
  }
  const synth = new Tone.Synth(option).toDestination();
  synth.volume.value = -20

  const voice = [
  'C#5',null,'D5',null,'D#5',null,'E5',null,
  'F5',null,'F5',null,
  'F#5',null,'F#5',null,
  'G5',null,'G5',null,
  'G#5',null,'G#5',null,
  'A5',null,'A5',null,
  'A#5',null,'A#5',null,
  'B5',null,'B5',null,
  'C6',null,'C6','C6',
  ]
  const seq = new Tone.Sequence((time, note) => {
    synth.triggerAttackRelease(note, '8n', time)
  }, voice, '56n').start(0)
  Tone.Transport.start()

```

で、これだと結局12音階になってるので『ミとファの間の音』が出ないんで段階的に音階を変える（bendingとかギターで言うチョーキング）方法でアプローチしようと思いました。  
Tone.jsではこういう感じです。


```js
  const osc = new Tone.Oscillator().start()
  const signal = new Tone.Signal({
    value: "C#5",
    units: "frequency"
  }).connect(osc.frequency);
  signal.rampTo("C6", 2, "+0.5");
  osc.stop(2.2)
```

↑これでC#5からC6まで音階を区切らず滑らかに音が上がってくれます。  
(AudioContextの[linearRampToValueAtTime](https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/linearRampToValueAtTime)に該当する部分です。)  
あとはこれにトレモロをconnectしたんですが…

私の思っているトレモロと[Tone.jsのトレモロ](https://tonejs.github.io/docs/14.7.77/Tremolo.html)がステレオで振られる形になっていてなんか違ったんですね。

じゃぁ、代理のライブラリ探すかぁと思って出てきたのが[PIZZICATO.JS](https://alemangui.github.io/pizzicato/)です。

## つまづいたところ 3

[こちら](https://alemangui.github.io/pizzicato/#tremolo)のサンプルを再生すれば分かると思いますが、非常にいい感じで音が途切れ途切れになります。  
おお、これはいいぞ！と思って色々やっていると。

PIZZCATO.jsはTone.jsのrampToに該当するものがないんです、というかPitchShiftもなさげ…

じゃぁ、別のライブラリ探そうと思って次に挑戦したのが[XSound](https://xsound.jp/)でした。


## つまづいたところ 4

この[XSound](https://xsound.app/)めっちゃすごいです。webRTCやらmidiやらWebSocketやらもうなんでもできそうです！  
『こりゃぁ、いいぞ！』と思ったところやっぱりTone.jsのrampToに該当するものがないんです。

うーん、PitchShifterがあるから無理矢理実装するかなぁと思ったんですが、そもそもこれらのライブラリってAudioContextをラッピングしてくれるのでAudioContextのlinearRampToValueAtTimeをそのまま自分で使えばいいじゃんって思ったんですね。

なので、最終的にライブラリを使わずAudioContextを生で使って実装することにしました。


## つまづいたところ5
iPhoneだと一部の音が鳴らない…

ピコピコ音を出すのに注視し何も考えずにsetIntervalやsetTimeoutで鳴らしていたんですがPCでうまくいくものの、iPhoneだとセミの音しか鳴らず…
どうしようか迷った挙句iPhoneだった場合はuser-agentで無理矢理判定して1回しか鳴らないように変更しました。  
(PCの場合はloopする)

## がんばったところ

音声が再生されるとオレンジ色のところがアニメーションするようにしたところぐらいでしょうか…

## まとめ

これぐらいの実装ならライブラリに頼らず自分で実装しろや！ってところですね。

あと、今回は使用しませんでしたが[XSound](https://xsound.app/)マジでイチオシです。