発端
- Jupyter Lab で音を録りたい。
- localhost で動かすなら、
sounddevice
で録音できるが、遠隔サーバで動く Jupyter Lab ではそうもいかない - クライアントPCのウェブブラウザで音を録れたらいいな。Web Audio APIに手を出すか!
- localhost で動かすなら、
-
ipywebrtc は欲しいものに近いのだが、やはり ScriptProcessor を使っているようである。
- 現状では、ScriptProcessor を使った例を公開しているウェブページが多い印象。
- 映像はいいから、音声をサクッと手に入れたいのだ。
Jupyter extension の開発
ipyaudioworklet という Jupyter Lab Extension 作りました。
正確には、ipywidgets の custom widgets なのかもしれませんが、Jupyter Lab で動かすことしか考えていません。
Binder を使って、デモとして、録音GUIの表示と、録音後のちょっとしたアプリケーション例を公開しています。
ipywidgets に依存しているので、Google colabでは、多分動かないと思われます。
ウリ
- どこかの開発ノード上で動かしている Jupyter Lab を使っていても、
- クライアントPCのマイクで収録した音を、
- OPUS encoding されたWAVファイルではなく、生のPCMとして録音できるし、
- 一発で numpy.ndarray(dtype=numpy.float32) として音声データ列が手に入るよ
という言葉が刺さる音声研究者は多い・・・と思っています。
オフィシャル寄りな情報源
だいたい、以下を参考にしながら作ることになりました。
ただ、公式の情報通りに各ブラウザが実装しているとも限らない。。。
興味深かった点
どうやら最近の API では、Auto Gain Control, noise suppression とか echo cancel が内蔵(?)されているんですね。
明示的に無効化しないと、自動ですべて有効化されるようです。(デバイス依存?)
ipyaudioworklet では、あえて無効化しましたが・・・
開発上のつらかった点
Jupyter Lab と Web Audio API が混ざることで、つらさの化学反応が発生します。
以下、乱文。
主に Web Audio API 関係
OSやブラウザごとに挙動が違いすぎる、の一言に尽きるのです。
例えば:
sampleRate を変える実装は実現できるのだろうか・・・?
- 基本的には、AudioContext と MediaDevices.getUseMedia() の constraints の両方で sampleRate を指定すればよさそうだが、まぁ動かない。
- そもそも AudioContext で指定したところで、返ってくるインスタンスは sampleRate が固定される。
- getUserMedia の constraints でsampleRateを指定するようなサンプルが、巷にあふれかえっているが、すごくヤバい。
- 何を指定しようが AudioContext のsampleRate依存で録っている動きをするブラウザがある。
- そして、AudioTrack インスタンス の getSettings() の戻り値を確認すると、constraints で指定した値を返してくれたりする。(だが、この数字はまったく意味がない)- 44,100 Hz を指定したつもりが、実際は 48,000 Hz で録られている、なんてのもある。
- そのぐらいの差だと、自分の音声とかを聞きなれていない人は「こんなものか」と思ってしまうのではないか。
- AudioContext はデフォルトのまま生成されるがままに利用、getUserMedia の constraints の sampleRate には AudioContextインスタンスのsampleRate を与える、という実装が一番無難な形だった。
32 bit float で音データが得られるが、ハードウェア的に量子化ビット数が 32 bit というわけではない
- 量子化ビット数が 16 bit だろうが 24 bit だろうが、ともかく得られるのは -1 ~ +1 というだけ。
- 16 bitの整数を32768($=2^{15}$)で割った実数を手に入れたところで、収録時点の量子化ビット数は16 bitなわけで。。。
- getUserMedia の後で、実際に設定された量子化ビット数(sampleSize)を手に入れられるが、これまたブラウザ依存であり、値を返すブラウザと、そんな項目すら返してくれないブラウザが存在する。
- 結局のところ、constraints で sampleSize は与えられそうだが、sampleRateに関する挙動を思うと、正しく働いているのかどうか、定かではない。
Noise supplession 等の機能は面白い、が、、、チャネル数が変わってしまう
マルチチャネル信号を処理して、シングルチャネル信号を作ってると考えれば、それはそう、と納得できる。
だが・・・
-
autoGainControl
,echoCancellation
,noiseSuppression
をFalse
にした場合、チャネル数がマチマチになる- ブラウザによって、2チャネル返ってきたり、1チャネル返ってきたり。
- constraints でチャネルを 1 にしたら 1 にしてほしい。(exact指定しても効かない)
- 無駄が多いなと思いつつ、ipyaudioworklet では、全チャネルを平均する処理を強制している。
Jupyter Lab (or, ipywidget extension) 関係
- Jupyter Lab 側の公式のチュートリアルを実行しようとすると、日本語圏のWindowsユーザは、おそらくテンプレを用意することすらできない
- 参考:https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
-
copier の問題。
-
https://peps.python.org/pep-0540/ を参考に、windows端末なら
set PYTHONUTF8=1
で環境変数設定すれば、問題なく作れるようになる。
-
https://peps.python.org/pep-0540/ を参考に、windows端末なら
- Jupyter Labチュートリアルが難しい
- これは、自分の理解力&技術力がなさすぎるせいなので、どうしようもない。。
- ipywidgets のチュートリアルに沿って作ると比較的楽だとわかったので、そちらに移行。
化学反応
- Web Audio API のいくつかについては、HTTPSコンテキスト が必要とされている。
- 氾濫している ScriptWorkletProcessor なら考えなくてもよい
- Jupyter Lab の拡張機能として動かしたいのだから、Jupyter Lab でもSSL証明書を与えないといけない
- 適切な管理者がいるような Jupyter Hub Lab でも使ってない限り、http で使い続ける人が多いのでは。(音の研究者では、そんなこと考える人は限りなくレアと思われる。)
- オレオレ証明書の作成経験があるとどうにかなる。(そんな経験がある、オーディオエンジニアがどれぐらいいるというのか・・・?)
- AudioWorkletProcessor の実装は、呼び出し側とは独立した JavaScript ファイルとして用意する必要がある
- ipywidget の custom widget テンプレで typescript による実装を選ぶと、webpack がもれなく1つのjsファイルにまとめて minify してくれるし、なんならファイル名もユニークな名前にしてくれる。(固定の名前で参照できない。)
- 強引な方法で独立した js ファイルをextensionディレクトリにコピーするという荒業で回避はできる。(もっといい方法がありそうだが、これもウェブ系の開発知識不足でどうにもできない。)
おわりに
音のことをよくわかっていて、かつ、Web系の開発の知識も豊富で、かつ、Windows & Linux (& Mac) にある程度精通していて、かつ、サーバ管理もどんと来い、みたいな人材が世の中にもっと増えたらいいな、と思いました。
Acknoledgements
素晴らしい先人の開発者の皆様に感謝します。