[Astro] #188 ブラウザで動くボイスチェンジャー — マイクの声を変え、ギターのエフェクターを並べる

[Astro] #188 ブラウザで動くボイスチェンジャー — マイクの声を変え、ギターのエフェクターを並べる

はじめに

きっかけは、Gemini との壁打ちで出た「ヘリウムガスのボイスチェンジャーはどうか」という案でした。実用性のない、お遊びのアプリです。ただ、ブラウザでマイクを扱うのは、私にとって初めてでした。マイクの声を、その場で変えて返すだけなら、重くないはずです。そう考えて、作り始めました。

作ったあとで、「ギターのエフェクターみたいなものがあると面白い」と思いつき、EQ と 8 種のペダルまで足しました。この記事は、そこまでの記録です。

結論から言うと、マイクの声は、ちゃんと変わります。そして、ペダルを並べるのは、想像以上に面白かったです。 実際に声を出して、全部のプリセットとペダルを試しました。大きな問題は、出ていません。ただし、ブラウザは Chrome しか試していません。

スクリーンショット

起動画面

ボイスチェンジャー — 起動画面

presetとエフェクトを追加して再生

ボイスチェンジャー — presetとエフェクトを追加して再生

Help

F1キーで操作ヘルプ表示。

ボイスチェンジャー — Help

動画

ボイスチェンジャー — 操作の様子(動画)

作ったもの

ページは /voice-changer です。音声は、このブラウザの中だけで処理して、どこにも送りません。

  • マイク: MIC ON を押すと、声が変換されて、ヘッドホンから聞こえます
  • ファイル: WAV などの音声ファイルをドロップして、再生や、変換した WAV の保存ができます
  • REC: 変換後の音を、そのまま録音して、WAV で保存します
  • プリセット 10 個: HELIUM、DEEP、ROBOT、ECHO、PHONE、LO-FI、CAVE、FUNK、DRIVE、NORMAL
  • VOICE: PITCH(高さ)、BRIGHT(高音)、VOLUME、PHONE(電話の声)
  • EQUALIZER: 5 バンドの縦フェーダー
  • ペダル 8 種: WAH、DISTORTION、BITCRUSH、ROBOT、CHORUS、TREMOLO、DELAY、REVERB

普段の画面は、できるだけ少なくしました。細かい説明は、F1 のヘルプに入れています。

音の流れ

声は、次の順に、1 本の道を通ります。

入力 → ピッチ → 電話 → 明るさ → EQ
     → WAH → DISTORTION → BITCRUSH → ROBOT
     → CHORUS → TREMOLO → DELAY → REVERB → 音量 → リミッター → 出力

この道は、マイクでも、ファイルの再生でも、WAV への変換でも、同じものを使います。そのため、聞いている音と、保存される音が、同じになります。

マイクを取り込む

ブラウザでマイクを使うのは、getUserMedia です。ここで、1 つ、設定を変えました。

navigator.mediaDevices.getUserMedia({
  audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false },
});

ブラウザは、初期状態で、エコー除去・ノイズ抑制・自動の音量調整をかけます。通話には便利ですが、声を変えるアプリでは、邪魔になります。声の形が、先に変えられてしまうからです。そこで、3 つとも OFF にしました。

その代わり、スピーカーで聞くと、ハウリングします。画面にも、ヘルプにも、「ヘッドホン推奨」と書きました。

ピッチシフトの仕組み

ヘリウムの声は、声の高さを上げれば作れます。ただし、再生速度を上げると、高さと一緒に、話す速さも変わってしまいます。速さを変えずに、高さだけを変える必要があります。

使ったのは、遅延線を 2 本使う方法です。

  • 音を少しずつ遅らせる読み取り位置を、2 つ用意します
  • 読み取り位置は、一定の速さでずれていきます(これが、高さを変える仕組みです)
  • 2 つを、窓の形(Hann 窓)で、交互に混ぜて、つなぎ目の音を消します
  • 窓の長さは、約 60 ms にしました
  • 高さを変えないとき(比が 1)は、素通しにします。2 つを混ぜたままにすると、くしの形の音(コムフィルター)が出るためです

高さの比は、半音の数から、次の式で出します。

ratio = Math.pow(2, semitones / 12);

この処理は、AudioWorklet(音を、別スレッドで処理する仕組み)で書きました。

声質(フォルマント)は、別に動かしていません。 そのため、これは「声の高さを上げる」処理で、本物のヘリウムの声(声の通り道が変わる)とは、少し違います。それでも、HELIUM プリセット(+8 半音と、高音を少し持ち上げる)で、十分に、それらしくなりました。

ヘッドレス Chrome で、440 Hz の音を通して測ると、次のようになりました。

設定出てきた周波数
0 半音440 Hz
+12 半音874 Hz
-12 半音224 Hz
+8 半音696 Hz

理論値は、+12 で 880 Hz、-12 で 220 Hz、+8 で 698 Hz です。ほぼ合っています。長さも、変わりません。

WAV を変換して保存する

ファイルは、decodeAudioData で読み込みます。WAV に限らず、ブラウザが読める音声なら、通ります。

保存するときは、OfflineAudioContext を使いました。これは、再生せずに、計算だけで音を作る仕組みです。音の流れは、ライブと同じ関数で作っているので、聞いた音と同じ結果になります。そして、再生時間を待たずに、一気に終わります。

書き出しは、16 bit の PCM にして、44 バイトのヘッダーを付けた WAV を、自分で作りました。保存した WAV を読み直して、周波数と長さが同じになることも確認しました。

ディレイやリバーブを使うと、余韻が出ます。変換結果の長さを、そのぶん、延ばしています(ディレイは、時間とフィードバックから計算します。リバーブは、広さに 0.2 秒を足します)。

REC は、変換後の音を、ライブで録音する機能で、録音用の AudioWorklet を、道の最後につないで、一定量ずつ、メインに送っています。

ペダルの作り方

ここからが、あとから足した部分です。

原音の道と、効果音の道

ペダルは、全部、同じ形にしました。原音が通る道(dry)と、効果音が通る道(wet)の 2 本を持ちます。

  • 効果音だけにしたいペダル(歪み、ワウ、ロボット、トレモロ、クラッシュ)は、ON にすると、dry を 0、wet を 1 にします
  • 原音に足したいペダル(ディレイ、リバーブ、コーラス)は、dry を 1 のままにして、wet の量を、MIX で変えます
  • OFF のときは、dry が 1、wet が 0 です

これで、8 種の ON/OFF が、1 つの関数で済みました。

各ペダル

ペダル作り方
WAHバンドパスフィルターの中心周波数(350〜2500 Hz)を動かします
DISTORTIONWaveShaper に、tanh の曲線を入れます。DRIVE は、手前の音量で変えます。4 倍オーバーサンプリングで、折り返しの雑音を抑えます
BITCRUSHビット数を減らして、サンプルを間引きます。AudioWorklet で書きました
ROBOTリングモジュレーション(音に、低い周波数の波を掛ける)です
CHORUS揺れる短いディレイを、原音に混ぜます
TREMOLO音量を、周期的に揺らします
DELAYディレイ。フィードバックで、繰り返しを作ります
REVERB減衰するノイズから残響を作って、ConvolverNode に入れます

EQ は、5 つのバンド(100 / 400 / 1k / 2.5k / 6k Hz)を、±12 dB で動かします。

つまみは、input[type=range] ではなく、上下にドラッグする丸いつまみを自分で作りました。ギターのペダルらしくしたかったからです。Shift で細かく、ダブルクリックで初期値に戻ります。

ワウは、画面のペダルで動かす

ワウは、本物のペダルを踏んで、中心周波数を動かすものです。私の手元には、アナログの入力機器がありません。そこで、画面の縦長のペダルを、マウスでドラッグして動かすことにしました。離すと、元の位置に戻ります。

もう 1 つ、AUTO も付けました。一定の周期で、自動で動かします。

注意点が、1 つあります。ファイルを変換して保存するとき、手動のワウは、半開きの位置で固定されます。マウスの動きは、変換には残らないからです。動きごと残したいときは、REC を使います。

詰まったこと

ペダルを OFF にしても、音が出ていた

ペダルを全部 OFF にしても、テストの音が、元の 4 倍近い大きさで出ていました。原因は、次の 1 行でした。

set(box.wet, 0);        // 間違い: GainNode そのもの
set(box.wet.gain, 0);   // 正しい: AudioParam

音量の値は、GainNode ではなく、その中の gain(AudioParam)に入れます。前者は、エラーにならず、何も起きません。ペダルを 1 つずつ外して測る方法で、見つけました。この件は、テストで音の大きさを測っていなかったら、気づかなかったはずです。

つまみが、スタイルなしで出た

ペダルの画面を、JavaScript で作ったところ、CSS が全く効かず、つまみが文字だけで並びました。Astro の <style> は、そのページの HTML に書いた要素にしか、効かないためです。あとから JavaScript で作った要素には、付きません。

そこで、ペダルまわりのスタイルを、is:global の別ブロックに分けました。他のページに影響しないように、すべて .board の中だけに限定しています。

テスト用のページでは、スタイルを全体に当てていたので、この差に気づけませんでした。実際に Chrome で開いて、初めて分かりました。

画面の配置を、何度も変えた

最初は、ペダルを中央の下に置き、右にログのパネルを置きました。ところが、PLAY ボタンが左の下の方にあって、スクロールしないと押せませんでした。右のログは、場所を取るだけで、見る場面が、ほとんどありませんでした。

最終的には、次の形にしました。

  • 左: INPUT、OUTPUT(PLAY など)、PRESET
  • 中央の上: 波形
  • 中央の下: VOICE、EQ、ペダルを、2 段で
  • ログのパネルは、廃止

幅が狭くなると、操作部が 4 列、2 列と減って、縦に積まれます。この調整は、操作のしやすさを見て、私が決めました。

全力の音で、音が割れる

テスト用の擬似マイクは、最大の音量で鳴ります。HELIUM の高音を足すと、波形が、上限に張りつきました。最後に、リミッター(DynamicsCompressor)を入れて、割れを抑えました。本物の声では、ここまでの音量にならないはずです。

確認できたこと、できていないこと

確認できたこと

  • 音の高さは、理論値に近い値になりました(上の表)
  • 保存した WAV を読み直して、周波数と長さが同じでした
  • EQ は、-12 dB で小さく、+12 dB で大きくなります
  • BITCRUSH を 3 bit にすると、音の段数が 40 段に減ります
  • TREMOLO と AUTO のワウは、音量が周期的に揺れます
  • ディレイとリバーブは、元の音が終わったあとも、音が残ります
  • 実機(私の Chrome とマイク)で、全プリセットと、ペダル、EQ を、一通り試しました

確認できていないこと

  • Quest、スマホ、Safari、Firefox での動作
  • 数分以上の長い WAV の変換時間と、メモリ
  • 埋め込みの窓(サイトの中の窓)の中での表示
  • ペダルを多数 ON にしたときの、マイクの遅延
  • ゲームコントローラーでのワウの操作(未実装)

おわりに

ブラウザの Web Audio API は、フィルター、ディレイ、歪みといった部品が、最初から揃っています。マイクの取り込みも、最初に思ったより、ずっと簡単でした。

詰まったのは、音そのものではなく、GainNode と AudioParam の取り違えや、CSS の効く範囲のような、地味なところでした。これは、聞いてもすぐには分からず、数値で測って、初めて分かりました。