なんちゃってイケボメーカーはこちらに置いてあります。
タイトルでネタバレしてますがWeb Speech APIで喋らせた音声をTone.jsで加工してリモート会議で使おうと思ったんです。
取り掛かりをWeb Speech APIの方からやればよかったものの、こんなライブラリもあるしなんとかなるだろ…と思ったのが間違いでした。
(上記のライブラリを使っても、マイク入力のバイナリが生成されるだけで、Speechした音声のバイナリは生成できませんでした。)
Tone.jsでマイク入力の音声をちょっとだけイケボにしてみる
イケボの定義は色々あると思いますが、
- PitchiShift(1音以内)
- EQ(Equalizer)でブースト
- Reverb
- ShortDelay
これぐらいの加工すれば、普段の声よりはイケボになるじゃないかなぁと思っていました。(もちろんコンプとか噛ませばもっとよいんでしょうけど)
で、JSで音の加工の有名ライブラリと言えばTone.jsです。
エフェクトのページを見ても
上記のeffect以外にもDistortionやTremolo、ChorusからAutoWah、Phaserまであるので、マイクの代わりにギターを刺せばそれなりのオモチャになれそうです。(アンプシミュと併用したら面白いでしょうね)
実践してみよう
ということで、順を追って実装していきましょう。
まず、マイクから音声を拾って聞こえるようにします。
const mic = new Tone.UserMedia()
mic.open()
mic.toDestination()
はい、違和感ありますよね?
早速罠です。マイクの音声がモノラルで片側に行かないので気持ち悪いのです。
なので、マイクを入力するときはPanで音声を中央に寄せてあげなければなりません。
また、これ系のdebugで大きな声出すのは非常に勇気がいるので入力ボリュームを上げます。
//引数にintを指定することでvolumeを調整できる
const mic = new Tone.UserMedia(10)
const pan = new Tone.Panner(1)
mic.open()
mic.chain(pan)
pan.toDestination()
これで違和感は減りました、これにリバーブをかけます。
const mic = new Tone.UserMedia(10)
const pan = new Tone.Panner(1)
const reverb = new Tone.Reverb()
mic.open()
mic.chain(pan)
pan.chain(reverb)
reverb.toDestination()
はい、変ですよね。Reverbのかかったチャンネルがまた片方だけに寄ってる気がします。
もう、面倒なので両方のchを出力(toDestination())します。
const mic = new Tone.UserMedia(10)
const pan = new Tone.Panner(1)
const reverb = new Tone.Reverb()
mic.open()
mic.chain(reverb)
reverb.chain(pan)
//無理矢理ステレオで同じものを出力
reverb.toDestination()
pan.toDestination()
こう言った感じで実装していきます。
実際は、いちいちconnectを書くのが面倒なのでchainで繋げてもいいと思います。
あと、Reverbはdecayの値に0を指定するとエラーになります。
また、Delayは入力に対してのDelayになって原音そのものがなくなってしまうので(ギターとかやってる方はこの表現で分かると思います)
ShortDelayみたいな使い方をしたい場合は、前述の処理のようにDelay側もtoDestination()する必要があります。
マイクを録音してFileURLにする
レコーディング処理については新しいcontextを作ってあげて、ストリーム処理をMediaRecorderに流す感じになります。
レコーダーのイベントにondataavailableとonstopがあるので、そこでチャンクしたバイナリデータをpushしていったり、URL.createObjectURLでURLに変換する感じになります。
const dest = Tone.context.createMediaStreamDestination()
const recorder = new MediaRecorder(dest.stream)
recorder.start()
//色々処理(仮にmicをそのまま録音する場合
mic.connect(dest)
recorder.ondataavailable = (event) => {
//chunksの宣言は別でしておく
chunks.push(event.data)
}
recorder.onstop = () => {
//chromeのブラウザのデフォのコーデックがopusなのでそのまま使ってます
const blob = new Blob(chunks, { type: 'audio/ogg; codecs=opus' })
const file_url = URL.createObjectURL(blob)
}
私のサンプルページだとaudioタグに入れているんですが、すぐに反映されないバグとかaudioだと大きなファイルだとダメみたいな話があるのでhrefの中身も書き換える処理を入れています。
余談 SpeechSynthesisRecorder が使えなかった件
はい、前述の通りこの辺までは割とスンナリいんたんですが、元々はWeb Speech APIで喋らせた音声を〜って話なので、SpeechSynthesisRecorderを使って色々やってみたんですが、ダメでした。
- arrayBuffer
- audioBuffer
- blob
- readableStream
- mediaSource
- mediaStream
全部ダメで、「うーん、リアルタイムでのストリームがダメなら一度録音して、それを加工しよう!」と思ったんですが「お、ちゃんとURL発行されてるじゃん!」ってなっても無音(実際はマイクを拾っているのでマイクの音声は録音できる)状態で無理でした。
ということでWebRTCに流し込む
現状、WebRTCの候補として上がるのはSkywayとagora.ioって所だと思います。
で、Skywayに関してはこちらの記事にドンズバがあったので、そちらを参考にしてください。(知らなくて全く同じ事やっちゃったよorz)
Agora.ioでは以下のようにアプローチします。
async function callWithEffect(){
const micAudio = new Tone.UserMedia()
//便宜上agoraのサンプルにあるpromise.allを使います
await Promise.all([
micAudio.open().then( () => {
const reverb = new Tone.Reverb()
const dest = Tone.context.createMediaStreamDestination()
micAudio.connect(reverb)
navigator.mediaDevices.getUserMedia({video: false, audio: true}).then( stream => {
//元の音声から削除
const deleteTrack = stream.getAudioTracks()[0]
stream.removeTrack(deleteTrack)
//一応、削除したところにdestにあるものを追加
const newTrack = dest.stream.getAudioTracks()[0]
stream.addTrack(newTrack);
//agoraに加工済みのを入れる
const agoraAudioTrack = AgoraRTC.createCustomAudioTrack({ mediaStreamTrack: newTrack})
AgoraRTC.createCameraVideoTrack()
client.publish([agoraAudioTrack])
}).catch( error => {
console.error(error)
return
})
}),
])
}
感想
Tone.jsもAgoraもSkywayもブラウザにあるAPIを駆使していくので、便宜上は「元のAPIを使えば色々できる」とは言え、私のようにJSに疎い人でもやろうと思った事が「ライブラリ化していることでかなり簡単に書ける」というのはよいなぁと思いました。
あと、パッと見実現できそうなライブラリがあっても、先に検証してからやらないとダメダメだったという反省にもなりました。