ジンジャー研究室

長めのつぶやき。難しいことは書きません。

自分のやりたいことを正確に把握するのは難しい

キャリアの話になると、学生に向けて「自分の本当にやりたいことに向き合いましょう」というメッセージが発せられることが多い。それに対する学生側のよくある悩みは「やりたいことが見つかりません」「やりたいことがよく分かりません」というものだ。その様子を見ていて「やりたいことなんて考えなくても常にあるし簡単じゃん」と思ってたんだけど、最近は「本当にやりたいことを把握するのはめちゃくちゃ難しい」と思っている。

以下は個人的な話で、キャリアというよりは趣味レベルの話だけど、それでも難しいと感じている。

他人のやりたいことの影響を受ける

例えば SNS に、個人で Web サービスを作っている人とか、 OSS に貢献している人とかがいると、なんとなく自分もそれがやりたいような気持ちになってしまう。実際に Web サービスを作り始めてみると、ある程度動くようになったところでふと「よく考えたら別に Web サービスを運営したいわけでもないしお金を稼ぐモチベーションもないな」と気づいてしまう。

OSS も自作ライブラリは作るけど、他人のライブラリを一緒に良くしていこうとはあまり思わないので「貢献する」というモチベーションがない。パッケージとして公開するけど、ユーザーから issue や PR が来ると面倒臭いので最近は公開したくないほどだ。

フォローしている人の中には自作のプログラミング言語を作る人も多い。自分も面白そうだなと思ってちょっと作ってみて、ある程度動いたところで「こんなもんか」と思って満足した。でも好きな人は理論を体系的に学んでいるし、何より継続している。やはり冷静になると彼等ほどは好きでないことに気づいてしまう。

仕事の影響を受ける

仕事でなんらかのテーマを扱っていたとして、それが趣味に波及するパターン。例えば、 Swagger が面倒臭いからシュッとしたツールが欲しいみたいな場合に、プライベートでもそういうライブラリを作ったり色々調べ物をしていたりする。でも良く考えたら Swagger はほとんど仕事でしか使わないので、それに時間をかけるのは勿体無い。

あとは繁盛しているサービスでもないのに無駄にスケーラブルなアーキテクチャを考えてしまったりする。もっと手軽な方法もあるが、仕事と全然違うことをすると仕事に活かせなくてコスパが悪いと考えてしまう。

スキルの影響を受ける

仕事で使っているのが Web 関連の技術なので、なんとなく新しいアプリを作り始めると Web アプリになる。本当は VST プラグインが作りたいが C++ が分からないので WebAudio API で作ろうとしたり、その延長で OAuth とか SNS でシェアする機能とかを付けたりしてしまう。しかし WebAudio API をいくらこねくり回しても VST プラグインは作れない。

流行りに乗ってしまう

「これからはこの技術が来る!」とか言われると、「今これに乗っておけば将来それが流行った時に楽できるのではないか」と思って飛びついてしまう。かつては Kubernates であり、今は Rust であったりする。アーリーアダプターはエコシステムが貧弱だろうがものすごい労力をかけて進んでいけるので「将来」を「現在」にすることができるが、ただ「楽したい」程度のモチベーションでは将来は永遠に将来のままだ。ぶっちゃけ既存の道具を使う方が楽。

それから、こういうフロンティアを目の前にすると「画期的なシステムを発明したら一発当てられるのでは?」などと妄想してしまう。それは数年かけて会社を作ってやるくらいの覚悟が必要な話なのだが、 PoC とか言ってそれっぽいものを作って時間を無駄にしたりする。そりゃ PoC は簡単だよ。

ただ手を動かすのが楽しいだけ

プログラミングは楽しいから、ただ手を動かして何かを作って満足したいだけという場合がある。そう自覚しているのならそれでいいのだが、実際には「なんらかの目的のため」にやっていると思い込んで手を動かしていることが多く、ある程度手を動かして満足すると「実はそんな目的はなかったな、俺は一体なんのために...」となったりする。もちろんその過程で得られるものは多いので全く無駄な時間ではないのだが、何かもっと別の目的があるのであれば遠回りだ。

興味の対象が移り変わる

何かに夢中になっていたら、その過程で別の何かを発見してそちらに興味が移るパターン。例えば WebAudio API を触っていたら限界が見えてきたので wasm をやり始めて、 wasm をやっているうちに自作言語を作りたくなったりする。

連想式にやりたいことが増えていくので、優先順位をつけるべきだ。オーディオをやりたいのなら自作言語は我慢して適当な言語を wasm にコンパイルするのが一番早いはずだ。ただ問題は「やりたい順」に優先順位をつけると、その時点では自作言語が一番やりたいことなのでトップに来てしまう。ここで「いや、言語は今は別にいい」と言えなければならない。

短期的な報酬を求めてしまう

これが一番本質的で、ぶっちゃけここまでの話をなくしてこれだけでも話が成り立つのだが、具体的な話があった方が面白いかと思ってここまでダラダラ書いた。

ここでの報酬はほぼ承認欲求のことで、要するに「 Twitter で活動報告するといいねを押してもらえること」だったり「仕事で同僚に褒めてもらえること」だ。まあそれが長期的な目標に向けた取り組みの一環だったりすれば良いのだが、そこで報酬を得るために何かして後に何も残らないのは虚しい。このエントリも正直いいねが欲しくて書いているのでまあ虚しいが、こういう積み重ねが将来的に転職やら何かしら良いことに繋がると信じて書いている。

これが行き過ぎると本当に一発芸しかできなくなる危惧がある。あれをするのもこれをするのも、結局は SNS にアップするためなのだ...そして小さな満足を繰り返して消費される人生。

成果が出ないと面白くなくなる

上と言っていることは同じなのだが、長期的な報酬を得る前のフェーズでコストに対するメリットが伸び悩むと「これは自分のやりたいことじゃなかったな」と思ってしまう。途中で投げ出したことを認めないための防衛本能でもある。 ここまで書いてきたのは「大してやりたくないことに時間をかけてしまう」ことだが、下手をすると「実は本当にやりたかったことだが努力したくないのでやりたくなかったことにした」だけという可能性もある。

今まで「大してやりたかったことじゃなかった」と言って切り捨ててきたものの中に、実は本当にやりたかったことがあっただろうか?と自問するのが良いかもしれない。まあキャリアレベルの話をすると人生やり直さないと無理というのはあるだろうけど。

どうすればいいのか

今まで「気の向くままに色々やってきたが実はあんまりやりたくもないことに時間を割いていた」という話が多かったが、では自分の本当にやりたいことに時間を割くにはどうすればいいのか。ここまで書いてきてなんだが、実はまだ答えが出ていない。おそらく「他人は他人だから振り回されない」のも一つの答えだし、「自分のルーツを探る」も一つの答えだと思う。あるいは「そんなに難しく考えなくても総合的に自分の最もパフォーマンスの出るやり方を本能が選んでいるのだ」と楽観的に考えていいのかもしれない。あと、旅に出るとか。

ただ、一つ確かなのは冒頭の「自分の本当にやりたいことに向き合いましょう」はそんなに簡単ではないということ。 この言葉を聞いて勘違いしていたのは「やりたいことがあっても周りがそうさせてくれないから必死に抵抗しろ」というメッセージだと思ってたんだけど、実際には周りに何をされなくても自分自身をうまくコントロールできないということだった。

ああ、朝がきた。

Electron と Go で音を鳴らしてみた話

SoundEngine とか Audacity みたいな波形編集ソフトを作りたいと思った時に、本当はデスクトップアプリが作りたいんだけど Web 技術の方が慣れてるから(ここでの Web 技術は HTML や JavaScript など)という理由で、わざわざローカルでサーバーを起動してブラウザで WebAudio API を利用するアプリを作ったりしてしていた。でもユーザー体験は確実に悪いので、まずそれが嫌になった。面倒がらずに Electron を使おう。

もう一つ、 WebAudio API はとても良く出来ているのだがレイヤーが高すぎて細かいことができなかったり、パフォーマンスを求めるときは AudioWorklet を使わないといけないのが面倒だった。 AudioWorklet は既存のノードの拡張として使うのが行儀が良いのだろうけど、最近は他の言語で書かれたアプリケーションを丸ごとドーンと wasm に変換して、 WebAudio API は単にオーディオバッファをやり取りするインターフェースとして使う事例が増えていそう。その流れなら WebAudio API は一旦脇に置いてパフォーマンスの良い言語で自由に書いてみようかと。

というわけで、使い慣れた Web 技術で GUI を書きつつ音声処理はパフォーマンスを出すために Electron + Go という組み合わせを採用するに至った。特に Web サービスを作るという動機はないので AudioWorklet や wasm も使わずに両者は IPC で繋ぐことにする。

┌───────────── Electron ─────────────┐
┌────────────┐           ┌───────────┐           ┌───────┐
│ ui         │<-- IPC -->│ core      │<-- IPC -->│ audio │<---- MIDI
│ (TS/React) │           │ (TS/Node) │           │ (Go)  │<---> File
└────────────┘           └───────────┘           └───────┘

サーバーを立てて WebSocket 使うよりパフォーマンス出てるんじゃないかな、多分。プロトコルはシンプルにスペース区切りの文字列を双方向にやり取りすることにする。

{url-encoded} {url-encoded} {url-encoded} ...
{url-encoded} {url-encoded} {url-encoded} ...
...

ここで一番検証したかったのは UI 操作や描画がスムーズかということだったんだけど、ここは何事もなく普通にサクサク動いた。トラブルのひとつやふたつあるかと思ったけど。ボタンを押したらすぐに音が鳴るし、波形も 60fps で描画できた。ということは、これから任意のデスクトップアプリの GUI を Web 技術で作りたくなった場合はこの構成が使えるということなので嬉しい。

今回は上の検証が主な目的でアプリとして作るものはあまり決めていなかったので、最初は波形編集ソフトを作る予定だったのが気づいたらシンセを作っていた。でもシンセを作るなら VST とかで作らないと実際に DAW で使えないよね、ということで一旦この構成での制作は打ち切りにして C++ (JUCE) に移行中。

途中まで作ったもの。 github.com

ちなみに Go のライブラリは hajimehoshi/oto を使わせてもらってます。BGM 制作で参加させてもらっている Odencat 製のゲームなどで実績があります。 github.com

C++ 初心者がハマったこと

普段 Web のぬるい言語しか触っていない C++ 初心者がハマったこと。

(なぜ C++ をやっているかというと VST プラグインを作成するのに C++ がほぼ必須だから。この話はまた改めて書く予定。)

配列がポインタ

関数に配列を渡そうと思ったところ、受け側の引数をどう書けばいいのか分からなかった。 が、どうやらポインタで書くらしい。 ということは、関数側では渡されたポインタの指す先が配列かどうかを知る術はない。 また、長さも分からない。そういう用途では vec を使うっぽい。

C がそうなのは知っていたが、 C++ もそういう所は C の親戚らしい。 「配列かどうかが区別できないとかそんな訳ないだろ」と思って検索しまくって時間が溶けた。

初期値が 0 ではない

宣言だけして初期化していない変数は 0 ではなく適当な値が入っている。

という話は聞いたことはあったものの、 0 で初期化される言語に親しんだ時間が長すぎて無意識下に刷り込まれていたようだ。 enum の初期値がセットされている前提でロジックを組んでいたので意味不明な挙動になった。 (ここからはオーディオ系のプログラミングに特有なのだが、ロジックが間違っていてもノイズが入る程度で致命的な挙動の変化にはならない。「気のせいかな」と思って放置していて間違いに気づくのに3日かかった。)

インデックス例外が発生しない

配列の範囲外にアクセスした時、特に何も言われず何かしらを壊しながら処理が進んでしまう。 そして全然関係ないところで起こるアクセス違反。 どこかでポインタの指す先を誤って解放してしまったか?と思い込んで2時間ほど消えた。

「インデックスが範囲を超えたら例外が発生してプログラムが止まる」というのが、これまた無意識下に刷り込まれているため、「なんか間違ってたら実行時に気づくでしょ」という安心感(?)からロジックをあまり確認せずに軽率に実行する癖がついてしまっているようだ。 C++ でそれをやると間違えていても普通に気づかないので、何かあった時の時間の溶け方が半端ない。

思ったよりポインタでは事故らない

フレームワーク(JUCE)のサンプルに倣っているので、今のところポインタ関連の事故はほぼ起きていない。 まあオーディオ系のプログラミングなので、短いライフサイクルでオブジェクトを生成・破棄していないというのはありそう。ライフサイクルの長いオブジェクトが所有しているオブジェクトへの参照を渡していってるだけ。

事故り始めたらスマートポインタに乗り換えようかな。

時間を溶かさないための対策

とにかくアサーションを入れる。 他の言語に比べて実行時例外が全然出ないので、不安なところにアサーションをどんどん入れていく。間違っていたら IDE が動作を一時停止してくれる。 パフォーマンスが不安な場所は条件コンパイルデバッグ用の処理を差し込んでいく。

ユニットテストでも良さそうだけど C++ でどう書くのかはまだ調べてない。

得意なこと

「あの人はなんでこんなに簡単なことができないんだろう」

と思った時、それは自分が得意なことであり自分の強みであると思った方がいい。

「やるだけなのに、やってない」

とかではないのだ。

速度との戦い

オーディオプログラミングは速度との戦い。特にリアルタイム処理となると常にパフォーマンスを気にしていないとすぐに死んでしまう。 なぜなら、再生に間に合うようにデータを用意しないと音が途切れてしまうから。例えば、サンプリング周波数が 48000Hz だとすると1サンプルあたり 0.02ms で処理しないと再生速度に負ける。ステレオにして沢山エフェクトをくっつけて同時発音数を増やしてとやっているとすぐに超えてしまう。

このパフォーマンスチューニングが個人的には結構面白い。 お仕事は Web の主にフロントエンドなので、パフォーマンスを気にせず富豪的に処理しても大体なんとかなってしまう。例えば React とかは Virtual DOM を immutable に作っては捨ててというのを繰り返しているので、まあ GC が頑張っているはず(もちろんメモ化はできるけど)。 しかし話がオーディオになると真逆のプログラミングスタイルになる。とにかくループ中にメモリを新たに確保しないようにあらかじめ用意した領域を使い回す。 immutable とか何それ美味しいの。GC だけではなく、ちょっとした処理が意外と速度に影響するのが新鮮で驚く。

まず文字列結合が遅い。例えば、"lfo" + index + "_freq" などというキーワードを作るのをループの中でやっていたのでかなり影響が出ていた。パターンがあらかじめ決まっているので ["lfo0_freq", "lfo1_freq", "lfo2_freq"] という配列を作ってインデックスアクセスすることで改善した。

さらに文字列の比較も遅い。上のようなキーワードは enum で実体を int にしておくだけで改善が見られる。

周波数をノート番号に戻すのに math.Log2 を使っていたが、この計算も遅かったので予めノートから周波数を計算しておいて2分探索したら速くなった。

その他、無駄な計算をすると当たり前だがその分遅くなる。

こういう一つ一つの処理・改善がダイレクトに響く。普段は IO が支配的すぎて計算処理のコストはゼロとして考えていたり、考えたとしてもオーダーが爆発しないかを気にするくらいだったり、あとはブラウザだとレイアウト計算をいかに減らすかを気にするくらいだけど、文字列処理が遅いとかは「昔そういう話を聞いたことがあるけど本当だったんだ!」という感じで純粋に面白かった。貧民の精神で行こう。

綺麗な矩形波を作る

オーディオプログラミングをしていて、よくある矩形波の作り方は 1-1 を周期的に繰り返すというもの。 で、最近までなんの疑問も持たずにそれで実装していたんだけど、 FFT周波数スペクトルを描いてみて気づいた。

なんか汚い。

f:id:jinjor:20210225143730p:plain

いやいやまさかね、何かの間違いでしょと思ってよく耳を澄ますと、わずかに高音にブジジジィィィ...というノイズ混じりの音が聴こえる。気がする。耳より目を信じるミュージシャンなので、見た目がそうならきっとそう。 おかしいなと思って検索しまくったら、同じことを Stackoverflow で質問して自己解決している人がいて "Band-limited waveform" を使えば良さそうと書いてあった。

自分なりの解釈だと、真の矩形波フーリエ級数展開すると無限の周波数までの足しあわせになるんだけど、実際のデジタル波形はナイキスト周波数(サンプリング周波数の半分)までしか出ないので、それを超えた分は低い周波数に折り返されてノイズになるということだと思う。たぶん。

というわけで、ナイキスト周波数を超えない範囲で正弦波を足しあわせて矩形波を再現することにする。具体的な方法は以下が参考になる。リアルタイムに計算するのはコストが馬鹿にならないので Wavetable をあらかじめ作っておく。

https://www.musicdsp.org/en/latest/Synthesis/17-bandlimited-waveform-generation.html

面倒なのが、周波数の上限が決まっているので低い音は倍音が沢山出せるが高い音だと少ない倍音で打ち切るしかない。なので、ノート毎に 128 個の配列を用意する(glide する時は線形補完)。 という感じで Wavetable を用意したら、40MB にもなってしまった。うーん。

ともあれ、結果こうなった。

f:id:jinjor:20210225151548p:plain

正弦波を足しているのだからそれはそう、という感じ。でもちゃんと矩形波の音が聴こえるし、このスペクトルを見ながら聴くとよりクリーンに聴こえるぞ(いいのか)。ノコギリ波も同じ要領で出来た。

以下、課題。

  • ギブス効果を考慮していないのでそのうち対応したい(上の資料に方法が書いてある)
  • LFO とかでも同じ Wavetable を使うべきだろうか?