ジンジャー研究室

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

速度との戦い

オーディオプログラミングは速度との戦い。特にリアルタイム処理となると常にパフォーマンスを気にしていないとすぐに死んでしまう。 なぜなら、再生に間に合うようにデータを用意しないと音が途切れてしまうから。例えば、サンプリング周波数が 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 を使うべきだろうか?

センスの良い仕事

ふわっとつぶやいたんだけど、これって要するに何が言いたいんだっけとずっと考えてた。

センスの良い仕事とは一体なんだろう。 仕事というのは会社の業務に限らず、同人活動とか OSS とか割と色々当てはまると思うが、「センスがいいな〜」という仕事を見ると気持ちがいい。 じゃあどういう時に気持ち良さを感じるかというと、

「何も言わずに最適な答えを一発で出す」

のを見た時だと思う。センスというと先天的な才能みたいなイメージがあるが、おそらくそれまでの努力なり経験の蓄積が積み重なった結果、暗黙のうちに間違った答えが削ぎ落とされるのだろう。なんならちょっと気の利いたプラスアルファまで付いてくる。 阿吽の呼吸というか、信頼関係のある人と仕事をすると「そうそうそれそれ」っていうレスポンスが返ってきてやりやすい。

ところが、組織がスケールするとそういう再現性のない暗黙知のようなものは嫌われる。 彼はちゃんと関係者と合意も取らずに勝手に進めてしまうし、長期的な観点も足りていない。もっと決められたやり方を守って進めましょう、ということになる。

センスの対極にあるのは「正しいやり方」だと思う。

正しいやり方は正しいので厄介だ。何も進んでいなくてもそれを守っていれば仕事した気になるし、必要なコストとして許容されてしまう。今週は課題を適切に発見する方法を決めるための会議のための資料作りをやっていました、みたいな話になってくる。そして出てきた答えは必ずしも良いものとは限らないが、正しいプロセスを経たのでショボくても OK するしかなくなってしまう。

あとは単純にクールかどうかという問題があって、絵とか音楽にしても、下手な人ほど「ああでもないこうでもない、ここをああしてどう...」と言っていて一向に完成しないが、上手い人は第一声が「できた」なのは有名すぎる話。

もちろん黙ってやれば良いというわけではない。合意をとるべきところで勝手に下手なことをやってしまうのはセンスがない。 最初から正しい答えを出したから議論の必要性が生じなかった、あるいは合意形成に必要でない情報を切り捨てることが出来たというのが正確かもしれない。そう考えるとセンスというのはテクニックではなく実力そのものであるというなかなか厳しい話だ。

ふわっと始まったのでふわっと結論を言うと、クールでありたい。センスが欲しい。

Go 言語を始めた

1週間前くらいから Go 言語を触っている。

動機

オーディオ処理をやりたい。 今まで Web Audio API で色々やっていたが、細かいことをやろうとすると色々制約があってめんどくさい。

というわけで選択肢:

  • Node.js: パフォーマンスが不安
  • C++: 資産は多いけどそもそも C++ で安全に書くのが難しい
  • Rust: 安全で機能も充実してるけどコーディングで色々考えることが多くて面倒そう
  • Go: 並行処理が手軽に書けそうなので採用

(wasm ターゲットの場合 GC 不要な Rust が軽そうだけど)

最初の感想

1日目で本を読んで2日目に音が出た。簡単で最高。

現在の感想

ゴルーチンとチャンネルで手軽に色々できるんだけど、ちゃんと設計するのが難しい。

  • 関数内と呼び出し元のどちらに go を書くべきか
  • 関数内と呼び出し元のどちらでチャンネルを生成すべきか
  • WaitGroup で何をどこで待つべきか
  • どこで新しい Context を作るべきか
  • cancel()close(channel) のどちらでループを抜けるか
  • os.Signal をトラップしてループを止めるのが綺麗だけど失敗すると終了できない
  • ゴルーチンリークが不安で眠れない

可能性が無限大すぎる...色々試して確かめていくしかない。

読んだもの

TypeScript >= 4.1 の Template Literal Types を活用した引数パーサーを作ってみた

ヘルプっぽいものを書くと文字列をパースして型をつけてくれる。 デフォルト値を指定すると T | nullT になったりする。

f:id:jinjor:20201230222747p:plain

公開は今のところ GitHub Packages だけです(メンテしなくていい方法を考え中)

github.com

型レベルのパーサーはこちらの記事を大いに参考にしたというかパクりました。

(ネタ) TypeScript 型パズルで作るmini interpreter | by Yosuke Kurami | Medium

こんなに本格的じゃないけど。

難しいなと思ったのは、せっかく型レベルでパースしてもその結果を値レベルの実装に利用できない点。 なので、型は型、値は値でそれぞれパースして最終的に辻褄が合うように実装しました。

それでは皆さま良いお年を。

なるはや

なるはや(なるべく早く)」とはどういう意味なのだろう。

解釈1:優先度最高
なるべく早くなので、全リソースを注ぎ込んで考えうる最高の速度で終わらせるべき。

解釈2:優先度最低
なるべく早くなので、他の仕事の隙間時間に出来れば進める程度でいい。

分からないので、頼まれたら解釈2で進めることにしよう。