圧倒的インフレゲームの億千万タイピングを作りました。
タイピングは色々なものを作っていますが、タイピングを最も学ぶのは小3〜小4なので、
その年代でより面白いものが作れればと前々から思っていました。
という訳で作ったのがこれで、学習指導要領に合わせて万・億・兆などの桁を学びながらタイピングができます。
さらに京・垓・秭・穣・・・無量大数まで対応しているので、圧倒的インフレのタイピングを楽しめます。
タイピングとしてはすこし難しいけど、面白いかもなという感じです。
私は小さな頃、家に転がっていた参考書を読んでいたら無量大数まで桁があることを知ったのですが、
今の子はどうやって覚えるんでしょう。兆の先を知る機会ってあんまりないんじゃないかな。
こんな感じの神ゲー(笑) で遊んでみるとすぐに覚えられます。
いわゆるインフレゲーはたくさんありますが、無量大数がすぐに出てくるゲームはなかったと思う。
ゲームの終盤になってやっと出てくるのが限度でしょう。学習用途で使うのはなかなか難しい。
その点、億千万タイピングはスタート時点から無量大数に親しめるので、効率的に勉強できます。
億の桁を超えると、ローマ字を表示しきれなくなるのでどうするかで悩みましたが、
表示しきれないものは見えないようにしてしまって、
桁が変わるごとにテキストを削減することで対応しました。
何回か遊べば、その後は違和感なくプレイできると思います。
神ゲーというかネタゲーですが、たまにはこういうのも良いと思う。
無量大数の得点をゲットできるので、友達と争うのにいいかも。
打鍵速度が十分に早くなってくると引き運ゲー感があるけど、そこはまあ仕方ない。
marmooo's blog
2025年7月6日日曜日
2025年6月15日日曜日
シームレスなエフェクトを適用するアプリ CV-Masker を作った
シームレスなエフェクトを適用するアプリ CV-Masker を作りました。
マスクを手書きで設定して、そのマスクに対してグラデーションをかけつつシームレスなエフェクトを適用できます。
色々なエフェクトを用意しているので画像はあくまでイメージですが、こんな感じの画像をサクッと作れます。
最初は cv.colorChange, cv.illuminationChange, textureFlattening などの シームレスなエフェクトを与える関数の確認のために作っていたのですが、 いくつかボツ案があったので、マスクにエフェクトを掛けて遊ぶアプリに変えました。
当初の目的のシームレスなエフェクトの関数は、良い感じの画像を作れるのですが、 へぼい CPU だと処理速度がちょっと遅かったです。 コードを見ると SIMD + Threads の最適化は甘そうなので、将来の改善に期待です。 やはり OpenCV と言えども、コア部分以外は遅い時もあるとわかりました。 あと textureFlattening は実行するたびに結果が異なるので元の実装がバグってると思う。
OpenCV の cv.colorChange などの関数は高度なシームレス処理を行っていますが、 たいていそこまで精度は必要ありません。 そこで自作の軽量な汎用局所シームレス関数を作って遊べるアプリにしました。 0/255 で書いたマスクを boxFilter でグラデーション化して、 グラデーション化したマスクに任意のエフェクトを掛け合わせます。 処理は非常に軽いですが、十分なシームレス感があります。 どんなエフェクトでもシームレスに適用できますが、 一般的かつシンプルなエフェクトをいくつか利用できるようにしておきました。 局所モザイク、局所シャープ化、局所色調補正などが利用できます。 いざ作り始めたら追加したいエフェクトが多すぎて困ってきましたが、 応用的なエフェクトは別のアプリで作ります。 アルゴリズム多すぎのものはすぐに組み込むのが難しいです。
シームレスなエフェクトを加える関数は、メモリリークが直らず苦労しました。 やはり C++ は行数が増えてくると、ハマったときになかなか厳しい。 結局少し前のバージョンでは直らず、すべて書き直したら直りました。 メモリリークそのものは input event で大量に処理させるとすぐに見つかります。 たぶん MatVector の扱いが一番難しいのですが、 push_back() はコピーらしいので、すぐに delete() するのが綺麗だと思う。 参照がどうなってるか熟知してないとできないのが厳しい。
色々なエフェクトを用意しているので画像はあくまでイメージですが、こんな感じの画像をサクッと作れます。
最初は cv.colorChange, cv.illuminationChange, textureFlattening などの シームレスなエフェクトを与える関数の確認のために作っていたのですが、 いくつかボツ案があったので、マスクにエフェクトを掛けて遊ぶアプリに変えました。
当初の目的のシームレスなエフェクトの関数は、良い感じの画像を作れるのですが、 へぼい CPU だと処理速度がちょっと遅かったです。 コードを見ると SIMD + Threads の最適化は甘そうなので、将来の改善に期待です。 やはり OpenCV と言えども、コア部分以外は遅い時もあるとわかりました。 あと textureFlattening は実行するたびに結果が異なるので元の実装がバグってると思う。
OpenCV の cv.colorChange などの関数は高度なシームレス処理を行っていますが、 たいていそこまで精度は必要ありません。 そこで自作の軽量な汎用局所シームレス関数を作って遊べるアプリにしました。 0/255 で書いたマスクを boxFilter でグラデーション化して、 グラデーション化したマスクに任意のエフェクトを掛け合わせます。 処理は非常に軽いですが、十分なシームレス感があります。 どんなエフェクトでもシームレスに適用できますが、 一般的かつシンプルなエフェクトをいくつか利用できるようにしておきました。 局所モザイク、局所シャープ化、局所色調補正などが利用できます。 いざ作り始めたら追加したいエフェクトが多すぎて困ってきましたが、 応用的なエフェクトは別のアプリで作ります。 アルゴリズム多すぎのものはすぐに組み込むのが難しいです。
シームレスなエフェクトを加える関数は、メモリリークが直らず苦労しました。 やはり C++ は行数が増えてくると、ハマったときになかなか厳しい。 結局少し前のバージョンでは直らず、すべて書き直したら直りました。 メモリリークそのものは input event で大量に処理させるとすぐに見つかります。 たぶん MatVector の扱いが一番難しいのですが、 push_back() はコピーらしいので、すぐに delete() するのが綺麗だと思う。 参照がどうなってるか熟知してないとできないのが厳しい。
const resultVec = new cv.MatVector();
for (let i = 0; i < 4; i++) {
const ch = srcChannels.get(i);
resultVec.push_back(ch);
ch.delete();
}
手軽にいい感じのエフェクトが作れるので、結構使いやすい気がします。
2025年5月12日月曜日
様々な非写実的レンダリングを適用するアプリ CV-NPR を作った
様々な非写実的レンダリング (Non-Photorealistic Rendering) を実現するアプリ CV-NPR を作りました。
最近は生成 AI で画像がすごく簡単に作れるようになっているので、そちらとは技術がズレている感はありますが、
最近 Web 上で手軽に使えるようになった技術を地道にアプリ化しています。
色々なエフェクトを用意しているので画像はあくまでイメージですが、こんな感じの画像をサクッと作れます。
このアプリでは OpenCV の cv.detailEnhance, cv.edgePreservingFilter, cv.pencilSketch, cv.stylization、cv.oilPainting などの面白エフェクトが利用できます。 上記はそれなりに有名でいろいろな記事がネットでも見つかりますが、もっと色々なエフェクトが欲しかったので、 モザイク、色鉛筆化、cv.applyColorMap, cv.anisotropicDiffusion などをさらにサポートしました。 色鉛筆化は Lineart Converter を作ったときにできた副産物です。 cv::anisotropicDiffusion はうまく使うと迷路画像や、味のあるスムージングができます。 たいして設定項目がなく使えるエフェクトはこれくらいでした。 他に何かあるかな?
GIMP や Photoshop で使うようなエフェクトが Web 上で実現できれば、割と便利かもなと個人的には思っています。 とはいえまだまだエフェクトが足りないのが現状です。 ただあまり時間は掛けたくないし、今回は OpenCV だけを使って実現できることを実装しています。 OpenCV を使えば 1日で根幹部分は作れるので…。 なんらか実装が必要なものは、他のアプリで作る予定です。
実装はしないように心掛けたアプリなので、正直このアプリでは開発期間の大半はビルド時間だったりします。 公式にはまだサポートされていないように見える機能を色々使っているので、チェックするたびにビルド時間が掛かりました。 色々なモジュールを触り始めたことによってビルドが苦痛でした。 wasm を作るだけで 1時間近く掛かります。 またアプリごと・ビルド種類にビルド用のディレクトリを持ってキャッシュすると 1GB 以上容量がいるし、 キャッシュしてもオプションを少し変えただけで無意味化する問題などがあります。 ESM 並の tree shaking ができるなら依存関係をモリモリにしてビルドできるのですが、 依存関係をモリモリにすると不要な定数を大量に登録される問題もあります。 このままアプリを増やしていくとビルドだけで 1日掛かりそうな気がしたので、 依存関係を真面目に処理して簡単にビルドできるスクリプトを作りました。 ビルド時間が 1/100 になるので本家にも反映してほしい機能ですが、 デフォルトの Python 設定ファイルだとモジュールの情報がなくてできません。 OpenCV には裏コマンドとして JSON 形式の設定ファイルがあるのですが、そちらならできます。 というか Python 設定ファイル、JavaScript で言うところの eval 使っていて、 危ないしやめたほうが良い気がするけどなあ。 本体の改良もできるようになってきて、OpenCV のこともちょっとわかってきた気がします。
色々なエフェクトを用意しているので画像はあくまでイメージですが、こんな感じの画像をサクッと作れます。
このアプリでは OpenCV の cv.detailEnhance, cv.edgePreservingFilter, cv.pencilSketch, cv.stylization、cv.oilPainting などの面白エフェクトが利用できます。 上記はそれなりに有名でいろいろな記事がネットでも見つかりますが、もっと色々なエフェクトが欲しかったので、 モザイク、色鉛筆化、cv.applyColorMap, cv.anisotropicDiffusion などをさらにサポートしました。 色鉛筆化は Lineart Converter を作ったときにできた副産物です。 cv::anisotropicDiffusion はうまく使うと迷路画像や、味のあるスムージングができます。 たいして設定項目がなく使えるエフェクトはこれくらいでした。 他に何かあるかな?
GIMP や Photoshop で使うようなエフェクトが Web 上で実現できれば、割と便利かもなと個人的には思っています。 とはいえまだまだエフェクトが足りないのが現状です。 ただあまり時間は掛けたくないし、今回は OpenCV だけを使って実現できることを実装しています。 OpenCV を使えば 1日で根幹部分は作れるので…。 なんらか実装が必要なものは、他のアプリで作る予定です。
実装はしないように心掛けたアプリなので、正直このアプリでは開発期間の大半はビルド時間だったりします。 公式にはまだサポートされていないように見える機能を色々使っているので、チェックするたびにビルド時間が掛かりました。 色々なモジュールを触り始めたことによってビルドが苦痛でした。 wasm を作るだけで 1時間近く掛かります。 またアプリごと・ビルド種類にビルド用のディレクトリを持ってキャッシュすると 1GB 以上容量がいるし、 キャッシュしてもオプションを少し変えただけで無意味化する問題などがあります。 ESM 並の tree shaking ができるなら依存関係をモリモリにしてビルドできるのですが、 依存関係をモリモリにすると不要な定数を大量に登録される問題もあります。 このままアプリを増やしていくとビルドだけで 1日掛かりそうな気がしたので、 依存関係を真面目に処理して簡単にビルドできるスクリプトを作りました。 ビルド時間が 1/100 になるので本家にも反映してほしい機能ですが、 デフォルトの Python 設定ファイルだとモジュールの情報がなくてできません。 OpenCV には裏コマンドとして JSON 形式の設定ファイルがあるのですが、そちらならできます。 というか Python 設定ファイル、JavaScript で言うところの eval 使っていて、 危ないしやめたほうが良い気がするけどなあ。 本体の改良もできるようになってきて、OpenCV のこともちょっとわかってきた気がします。
2025年4月23日水曜日
画像の部分修正アプリ Inpainter を作った
画像の部分修正アプリ Inpainter を作りました。
名前の通り Inpaint アルゴリズムを使っています (安直)。
OpenCV と opencv.js の勉強、AI を使わないアルゴリズムの性能確認のために作りました。
不要オブジェクトを削除したこんな画像がサクッと作れます。 注意点としては、分布を調整するだけのアルゴリズムなので、消しゴムマジックのようには使えません。 消しゴムマジックのアプリだと思って使うと、ただの雑コラになるでしょう。
画像にちょっとしたノイズが走っているときに、それをいい感じに消してくれるアプリと思えば、なかなかの精度です。 ただアルゴリズム的には周囲の分布を見ながら消すので、周囲の分布が安定していないとうまく行かない訳です。 周囲の分布をどれくらい考慮するかは radius パラメータで調整できる訳ですが、 普通に考えれば周囲の分布が安定しているかどうかのほうがよほど重要だとわかります。
とはいえ分布ガチャをうまく引けば良い訳ですから、消したい箇所を何度か指定しながら分布を安定化させると、割といい感じの画像になります。 機械学習を用いた場合は分布を既存の知識を使って綺麗にごまかすイメージですが、inpaint アルゴリズムはガチャで分布を綺麗にしてごまかすものだと開き直ると、割と使いやすい気はします。
inpaint のアルゴリズムは cv::photo と cv::xphoto に実装されていますが、 cv::xphoto の inpaint はまだ Wasm ビルドができない感じです。 ちょろっと定義を変えれば動く気はするんですが、こういうときなかなかツライ。 動くようになったら追加予定です。
不要オブジェクトを削除したこんな画像がサクッと作れます。 注意点としては、分布を調整するだけのアルゴリズムなので、消しゴムマジックのようには使えません。 消しゴムマジックのアプリだと思って使うと、ただの雑コラになるでしょう。
画像にちょっとしたノイズが走っているときに、それをいい感じに消してくれるアプリと思えば、なかなかの精度です。 ただアルゴリズム的には周囲の分布を見ながら消すので、周囲の分布が安定していないとうまく行かない訳です。 周囲の分布をどれくらい考慮するかは radius パラメータで調整できる訳ですが、 普通に考えれば周囲の分布が安定しているかどうかのほうがよほど重要だとわかります。
とはいえ分布ガチャをうまく引けば良い訳ですから、消したい箇所を何度か指定しながら分布を安定化させると、割といい感じの画像になります。 機械学習を用いた場合は分布を既存の知識を使って綺麗にごまかすイメージですが、inpaint アルゴリズムはガチャで分布を綺麗にしてごまかすものだと開き直ると、割と使いやすい気はします。
inpaint のアルゴリズムは cv::photo と cv::xphoto に実装されていますが、 cv::xphoto の inpaint はまだ Wasm ビルドができない感じです。 ちょろっと定義を変えれば動く気はするんですが、こういうときなかなかツライ。 動くようになったら追加予定です。
2025年3月19日水曜日
画像の背景を削除する GrabCutter を作った
画像の背景を削除する GrabCutter を作りました。
名前の通り GrabCut アルゴリズムを使っています (安直)。
前景と後景をアノテーションできるようにしておいたので、
削除と復元の微調整しやすいのが利点です。
こんな感じの背景透過画像がサクッと作れます。
割と大きめの画像でも 初回実行は 1秒以内、微調整は一瞬という感じです。 巨大な画像に適用すると abort するのが課題そうです。 ROI を作って部分適用したほうが良いかも知れません。 他にも共有メモリで分散処理はどんなアルゴリズムでも検討したいところですが、今回はそこまで作ってないです。 AI を使わない時にどれくらい精度が出るのかの勉強用で作りましたが、 AI なしでも割と良い精度です。せいぜい 2-3回の微調整で十分な結果が得られるので、 これはこれでアリじゃないかなあ。
グラフカットの試行回数がパラメータで設定できるのですが、まったく必要ない気がします。 試行回数が増えても遅くなるだけなので、1回で固定して微調整で修正したほうが良いです。 alphamat をさらに考慮する方法などもあるっぽいことに後から気付きましたが、 ものすごい時間が掛かるみたいなので、現状はこれでいいかなと。
こんな感じの背景透過画像がサクッと作れます。
割と大きめの画像でも 初回実行は 1秒以内、微調整は一瞬という感じです。 巨大な画像に適用すると abort するのが課題そうです。 ROI を作って部分適用したほうが良いかも知れません。 他にも共有メモリで分散処理はどんなアルゴリズムでも検討したいところですが、今回はそこまで作ってないです。 AI を使わない時にどれくらい精度が出るのかの勉強用で作りましたが、 AI なしでも割と良い精度です。せいぜい 2-3回の微調整で十分な結果が得られるので、 これはこれでアリじゃないかなあ。
グラフカットの試行回数がパラメータで設定できるのですが、まったく必要ない気がします。 試行回数が増えても遅くなるだけなので、1回で固定して微調整で修正したほうが良いです。 alphamat をさらに考慮する方法などもあるっぽいことに後から気付きましたが、 ものすごい時間が掛かるみたいなので、現状はこれでいいかなと。
2025年2月11日火曜日
MIDI 再生ライブラリ Midy を作った
MIDI 再生ライブラリを前々から欲しかったので、Midy というライブラリを作りました。
ひとまず GM1 の再生に必要な機能は実装しているつもりです。
このライブラリを作る前は FluidSynth を wasm にするのが無難ではあったと思いますが、
wasm サイズが大き過ぎたり、確認が甘いだけかも知れませんが動作に納得がいかないところがありました。
うまく使いこなせなかったので、再生負荷が低く、ライブラリのサイズが小さく、拡張性の高い実装が欲しかったです。
Web での利用を想定して SF3 形式に対応していることは大前提です。
実際にこの構成で基本的な部分はあっさり完成しました。 しかしメインスレッドで実行すると再生負荷が高い問題にすぐ直面しました。 何も考えずにスケジューリングするだけではすぐにラグが発生します。 再生負荷が高いのは短時間にイベント処理を大量に行うからと予想できたので、 setInterval() を使って対象となる音符を探しながら、 AudioWorklet 上でサンプリング単位で処理を行うことで、負荷を低減する実装をしました。 まあまあ動くようになったところで他のライブラリを改めて見てみると、 AudioWorklet を使う実装はほとんどないことを知りました。 AudioWorklet を使う方法もアリとは思いますが、 たいていは AudioBufferSourceNode で十分のようです。
AudioBufferSourceNode を使った MIDI 再生は以下が参考にはなるのですが、 GUI の実装と基盤ライブラリの実装が絡み合っていたり、バグがあったり、中身がよくわからないところが多いので、やはり自作することにしました。 あとゼロから実装していると「その実装じゃ動かないんだわ」にたくさん気付かされたので、やはり採用はできなかったです。 でもまあ私の実装もまだまだ間違いがたくさんあると思うので、じょじょに直していきます。 私が欲しいのはきちんとした再生ができるライブラリです。 他にも色々な実装や fork がありますが、中身がよくわからないところが多いので参考にしてないです。 ちなみに再生負荷を下げるだけなら js-synthesizer が安定しています。 自作はより良いものを作るためです。 他には FluidSynth や Timidity のコードは当然ながら参考にはなるのでしょうが、苦手過ぎてあまり読めていません。 やっぱ JavaScript のコードってめちゃくちゃ読みやすいし、メンテしやすいんだよなあ。
サウンドフォントで一番謎だったのは音源のサンプリングデータの扱いです。 SoundFont 2.04 の 24bit とかどうやって扱うんでしょうかねこれは…。 そんな型はないので 32bit にするのか、専用の型を用意するのか。 サンプリングデータは通常は 16bit で扱うんだと思いますが、 SF3 サポートや 24bit 対応を考えると Uint8Array が正解かな。 ただこれだとサウンドフォントの仕様を知ってないと扱えなくなる欠点はあります。 とはいえどうせ以下のサウンドフォントの仕様をよく読んで作らないと動かないので、あまり差はない気もします。 しかし読んでもわからないことのほうが多いのがまた問題ですが…。
タイマー周りは検証が面倒臭かったですが、実装面で苦労したのは Promse 周りです。 Promise は本当に難しくて、たとえば以下の 1行だけでバグります。
何日か格闘して再生処理をフルスクラッチで書いたら、JavaScript でも十分早いことがわかりました。 途中で気付きましたが、音声処理は大部分がブラウザ上でネイティブ動作するので、 wasm にしたからといって速度が上がる訳ではないと思います。 速度が上がるのはせいぜいバッファー生成だけですが、MIDI では事前生成がほとんどです。 再生はネイティブのタイマーで処理できるので、 JavaScript 側では負荷の小さいスケジューリングで渡せば十分と思います。 より高速な実装を目指す場合、AudioWorklet + wasm でバッファー処理すれば、 リアルタイム性では勝てる可能性があるかも知れませんが、wasm よりネイティブのほうが早いので期待はできないと思います。
ちなみに Tone.js のコードもかなり眺めたのですが、私の実装と基本的には一緒だと思います。 ただ核心部分では演奏位置のメモリ管理、タイムラインの検索処理、 タイムラインのループ処理あたりの実装が非効率です。 エフェクトとかもチェインを細かく設定しないといけないので、 なんやかんや自作が最強な気がしています。
現状の課題としてはリバーブが重いことを認識しています。 複雑な実装はしていないので、単純にリアルタイム処理が重いということだと思います。 MIDI はリアルタイム処理すると、構想段階の実装でもノート再生でさえ遅いことがわかっているので、いかにリアルタイム処理をなくすかが肝となりました。 リバーブの重さも、回避するにはなるべく様々な処理をオフラインでレンダリングしておいて、リアルタイム処理を減らす必要があると思います。 いかにリアルタイム処理をなくすかが肝だからこそ、AudioWorklet を使うのはあまり良い手段ではなさそう。 MIDI はノート・チャネル・マスターと 3層に分かれて音声処理をしていているので、層ごとに最適化が必要そうです。 GM2 だと初期状態でリバーブが掛かっていることを想定しているように見える のですが、これは現状だとちょっと厳しい。 今のところ GM1 対応しかしてないので、リバーブはオフにしています。 リバーブがなくても大量に音符があると稀に重いので、最適化はまだまだ必要です。
あとエフェクトが掛かったときの細かなサウンドフォントの扱いなどもまだまだ完全には実装していません。 やることが山ほどあり過ぎるので、ひとまず基本部分が再生できるようになったところでリリースしたという状態です。 完全に動くまで待つとエターなりそうだし。 まずは GM2 をフルサポートし、将来的には GS/XG の一部にも対応しようとは思っています。 実は GM2 をフルサポートしているライブラリってないんじゃないかな。 FluidSynth とかも対応してないです (SoftPedal を実装しているときに気付いた)。 GS/XG に対応し始めると 1流シンセサイザーという感じで、特にエフェクトのチェインが難しいです。 まずはそこを作る前にバグを潰していく必要があります。
Midy をリリースするためは、大量のライブラリに手を入れる必要があって、割と時間が掛かりました。 まだ色々と実装が足りないところがあると思いますが、ボチボチ直していきます。 だいたい動くものとして first commit をするまでが、検証が多くて一番大変。 ここからは平凡に実装とバグ潰しです。 現状はデモで動くものを頑張って探すレベルなので、もっと実装の精度を高めていく必要があります。
構想段階の話
上記の条件を満たすライブラリの開発は前々から検討していて、 最初はサウンドフォントのパーサを実装してサウンドフォントを読み込み、 @tonejs/midi で MIDI をパースし、Tone.js で再生処理を実装することを考えていました。 Tone.js を使うとコントロールチェンジの実装が簡単かなと最初は思っていました。実際にこの構成で基本的な部分はあっさり完成しました。 しかしメインスレッドで実行すると再生負荷が高い問題にすぐ直面しました。 何も考えずにスケジューリングするだけではすぐにラグが発生します。 再生負荷が高いのは短時間にイベント処理を大量に行うからと予想できたので、 setInterval() を使って対象となる音符を探しながら、 AudioWorklet 上でサンプリング単位で処理を行うことで、負荷を低減する実装をしました。 まあまあ動くようになったところで他のライブラリを改めて見てみると、 AudioWorklet を使う実装はほとんどないことを知りました。 AudioWorklet を使う方法もアリとは思いますが、 たいていは AudioBufferSourceNode で十分のようです。
AudioBufferSourceNode を使った MIDI 再生は以下が参考にはなるのですが、 GUI の実装と基盤ライブラリの実装が絡み合っていたり、バグがあったり、中身がよくわからないところが多いので、やはり自作することにしました。 あとゼロから実装していると「その実装じゃ動かないんだわ」にたくさん気付かされたので、やはり採用はできなかったです。 でもまあ私の実装もまだまだ間違いがたくさんあると思うので、じょじょに直していきます。 私が欲しいのはきちんとした再生ができるライブラリです。 他にも色々な実装や fork がありますが、中身がよくわからないところが多いので参考にしてないです。 ちなみに再生負荷を下げるだけなら js-synthesizer が安定しています。 自作はより良いものを作るためです。 他には FluidSynth や Timidity のコードは当然ながら参考にはなるのでしょうが、苦手過ぎてあまり読めていません。 やっぱ JavaScript のコードってめちゃくちゃ読みやすいし、メンテしやすいんだよなあ。
前準備 (@marmooo/soundfont-parser を作った)
きちんとした再生ライブラリを作るためには色々前準備も必要でした。 まず @tonejs/midi だと SysEx 命令に対応できないことに気付いたので midi-file を使って MIDI ファイルをパースするようにしました。 サウンドフォントのパーサは ryohey/sf2synth.js のコードが綺麗だったのでベースとしながら、SF3 サポートを加えました。 名前はせっかくなので @marmooo/soundfont-parser にしました。 SF3 サポート以外にも、実装途中で気付いた問題として、パーカッション対応や、軽微な処理速度の向上、規格準拠率の向上、strict mode への対応などもしています。サウンドフォントで一番謎だったのは音源のサンプリングデータの扱いです。 SoundFont 2.04 の 24bit とかどうやって扱うんでしょうかねこれは…。 そんな型はないので 32bit にするのか、専用の型を用意するのか。 サンプリングデータは通常は 16bit で扱うんだと思いますが、 SF3 サポートや 24bit 対応を考えると Uint8Array が正解かな。 ただこれだとサウンドフォントの仕様を知ってないと扱えなくなる欠点はあります。 とはいえどうせ以下のサウンドフォントの仕様をよく読んで作らないと動かないので、あまり差はない気もします。 しかし読んでもわからないことのほうが多いのがまた問題ですが…。
再生処理の実装
SF3 形式が読み込めるようになった後は、再生処理を作りました。 最初は setInterval() で作っていたのですが、処理が被ると酷いことになることがわかったので、setTimeout() にしました。 しかし setTimeout() もバックグラウンド再生の課題があるとわかったので AudioBufferSourceNode を使ったタイマーを作っています。 このへんの話は、JavaScript で使えるタイマーのベンチマークを作った が関連します。タイマー周りは検証が面倒臭かったですが、実装面で苦労したのは Promse 周りです。 Promise は本当に難しくて、たとえば以下の 1行だけでバグります。
const promise = new Promise(...);
return promise;
これは以下のように書かないといけません。
return new Promise(...);
音声処理みたいにタイミングがシビアだと、非同期でない部分をすべて Promise で囲わないとバグります。
1ms くらいのタイムラグの話ですが、音声処理だとこれが致命的な問題になります。
他のアプリで山のように間違ってそうだから直していかないとなあ。
何日か格闘して再生処理をフルスクラッチで書いたら、JavaScript でも十分早いことがわかりました。 途中で気付きましたが、音声処理は大部分がブラウザ上でネイティブ動作するので、 wasm にしたからといって速度が上がる訳ではないと思います。 速度が上がるのはせいぜいバッファー生成だけですが、MIDI では事前生成がほとんどです。 再生はネイティブのタイマーで処理できるので、 JavaScript 側では負荷の小さいスケジューリングで渡せば十分と思います。 より高速な実装を目指す場合、AudioWorklet + wasm でバッファー処理すれば、 リアルタイム性では勝てる可能性があるかも知れませんが、wasm よりネイティブのほうが早いので期待はできないと思います。
ちなみに Tone.js のコードもかなり眺めたのですが、私の実装と基本的には一緒だと思います。 ただ核心部分では演奏位置のメモリ管理、タイムラインの検索処理、 タイムラインのループ処理あたりの実装が非効率です。 エフェクトとかもチェインを細かく設定しないといけないので、 なんやかんや自作が最強な気がしています。
MIDI Event の実装
根幹部分が完成したらあとはひたすら細かな実装です。 コントロールチェンジや SysEx 命令などの MIDI イベントを地道に実装します。 実装しなければいけないことを、以下で確認しながら作りました。 ただ非常にドキュメントが長く真面目に読んでいるといつまで経っても実装が終わらないため、正直あまりきちんとは読んでない気がします…。- General MIDI - Wikipedia (ja)
- General MIDI - Wikipedia (en)
- General MIDI Level 2 - Wikipedia (en)
- MIDI 1.0 Control Change Messages (Data Bytes) – MIDI.org
- GM2(GENERAL MIDI Level 2) | DTM Solutions
- MIDI:コントロール番号一覧 | DTM Solutions
- ソフトウェアプログラマーのためのMIDI CCリスト - Fwmiの日記 (良い)
- MIDI Registered Parameter Number (RPN) | RecordingBlogs
- RPN/NRPN 一覧表
- MIDI:RPNとNRPN | DTM Solutions
- サクラの為のMIDI規格 -System Exclusive Message
- MIDI 1.0 Universal System Exclusive Messages – MIDI.org
- Global Parameter Control – MIDI.org
- RP/CA (すべての規格書)
- MIDI 1.0 規格書 (300ページあって熟読は無理→152ページが重要)
- General MIDI Lite
- GENERAL MIDI Level 2 Recommended Practice (RP024)
TODO
先に述べたエフェクトは GM2 の話で、既に結構実装はできているのですが、完璧ではない状態です。 GM2 をフルサポートをしようとすると、Global Parameter Control でリバーブやコーラスの推奨要件が細かく規定されているので、実装の精度が上がっていくと思います。 たぶん一番よくわからなかったコーラス周りの仕様が安定するはずです。現状の課題としてはリバーブが重いことを認識しています。 複雑な実装はしていないので、単純にリアルタイム処理が重いということだと思います。 MIDI はリアルタイム処理すると、構想段階の実装でもノート再生でさえ遅いことがわかっているので、いかにリアルタイム処理をなくすかが肝となりました。 リバーブの重さも、回避するにはなるべく様々な処理をオフラインでレンダリングしておいて、リアルタイム処理を減らす必要があると思います。 いかにリアルタイム処理をなくすかが肝だからこそ、AudioWorklet を使うのはあまり良い手段ではなさそう。 MIDI はノート・チャネル・マスターと 3層に分かれて音声処理をしていているので、層ごとに最適化が必要そうです。 GM2 だと初期状態でリバーブが掛かっていることを想定しているように見える のですが、これは現状だとちょっと厳しい。 今のところ GM1 対応しかしてないので、リバーブはオフにしています。 リバーブがなくても大量に音符があると稀に重いので、最適化はまだまだ必要です。
あとエフェクトが掛かったときの細かなサウンドフォントの扱いなどもまだまだ完全には実装していません。 やることが山ほどあり過ぎるので、ひとまず基本部分が再生できるようになったところでリリースしたという状態です。 完全に動くまで待つとエターなりそうだし。 まずは GM2 をフルサポートし、将来的には GS/XG の一部にも対応しようとは思っています。 実は GM2 をフルサポートしているライブラリってないんじゃないかな。 FluidSynth とかも対応してないです (SoftPedal を実装しているときに気付いた)。 GS/XG に対応し始めると 1流シンセサイザーという感じで、特にエフェクトのチェインが難しいです。 まずはそこを作る前にバグを潰していく必要があります。
GUI
本格的な GUI に関してはこれから作りたい…と言いたいのは山々ですが、現実的にはそうも言ってられません。 なぜかというと WebAudio は現状ブラウザ上でしか確認できないので、テストをするためには GUI が必要だからです。 仕方ないので簡易的な再生用 UI を作ってデバッグすることにしました。 どうせそのうち作ることになる MIDI 再生 GUI を簡単に作れる @marmooo/midi-player を作りました。 @marmooo/midi-player を作る時に基本的な操作で必要な API の確認やテストをしているので、 そんなにバグはないんじゃないかと思います。 やはり GUI 化すると細かな API のズレが出がちですが、自分で作っているからこそバグが起きないようにできたのが大きいです。 基本的な再生部分が完成すれば、もう Promise などでハマる可能性がないので、あとはひたすら実装するだけです。 @marmooo/midi-player はもっと高機能にしていくつもりですが、 あまり高機能にし過ぎるよりはライブラリを分けたほうが良いかも知れません。Midy をリリースするためは、大量のライブラリに手を入れる必要があって、割と時間が掛かりました。 まだ色々と実装が足りないところがあると思いますが、ボチボチ直していきます。 だいたい動くものとして first commit をするまでが、検証が多くて一番大変。 ここからは平凡に実装とバグ潰しです。 現状はデモで動くものを頑張って探すレベルなので、もっと実装の精度を高めていく必要があります。
2025年1月29日水曜日
JavaScript で使えるタイマーのベンチマークを作った
JavaScript で使えるタイマーのベンチマーク js-timer-benchmark を作りました。
タイマー関数として setTimeout, setInterval がよく知られていて、
画像処理などをやっている人はこれに加えて requestAnimationFrame も知っていることでしょう。
ただ音声処理の実装をしていると、これらではうまくいかないことに気付いたので、
あまり知られていない他のタイマー機能を紹介します。
それは AudioBufferSourceNode, OscillatorNode, ConstantSourceNode を使う方法です。 これらは音声データや正弦波、定数値を再生するためのノードですが、 これらには s秒後に再生を開始し、t秒後に再生を止める機能が備わっています。
上記を用いてタイマー機能を作成すると、方法によって様々な精度の違いが生まれます。 精度を検証するためのアプリ js-timer-benchmark はこちら。 Audio 関連のコードは Node/Deno では動かないのでブラウザ上で確認するしかありません。 まずはフォアグラウンドで初回起動時の結果がこちら。
フォアグラウンドで2回目以降時の結果がこちら。
バッググラウンドで初回起動時の結果がこちら。
バックグラウンドで2回目以降時の結果がこちら。
まとめると、requestAnimationFrame はバックグラウンドでは停止しているようなもので使いものになりません。 setTimeout, setInterval はバックグラウンドでは非常に遅くなります。 AudioBufferSourceNode, OscillatorNode, ConstantSourceNode は同じ速度で実行できます。 音楽などはバックグラウンドで再生速度が変わったら困るので、それを考慮しているのだと思います。 AudioBufferSourceNode は初回起動時になぜか遅いですが、それ以降は高速です。 つまり、音声処理のタイマーや、バックグラウンドでミリ秒単位の高精度タイマーが必要なら、 AudioBufferSourceNode を使いましょうという結論になりそうです。 とはいえ setInterval より精度が低いのは意外でした。
ちなみにウィンドウが非アクティブのときにどのように動作するかは、Window: setTimeout() メソッド - Web API | MDN にまとまっています。 setTimeout/setInterval はベンチマークの結果通り、1秒単位で処理するようです。 記述を読む限り AudioContext を使った手法はおそらく処理速度が落ちたりはしないですが、特にそれが定まっている訳でもないみたいです。
それは AudioBufferSourceNode, OscillatorNode, ConstantSourceNode を使う方法です。 これらは音声データや正弦波、定数値を再生するためのノードですが、 これらには s秒後に再生を開始し、t秒後に再生を止める機能が備わっています。
上記を用いてタイマー機能を作成すると、方法によって様々な精度の違いが生まれます。 精度を検証するためのアプリ js-timer-benchmark はこちら。 Audio 関連のコードは Node/Deno では動かないのでブラウザ上で確認するしかありません。 まずはフォアグラウンドで初回起動時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 0.9ms | 0.6ms |
setInterval (10) | 0.8ms | 0.4ms |
setInterval (100) | 0.6ms | 0.4ms |
AudioBufferSourceNode | 106.1ms | 2.2ms |
OscillatorNode | 6.2ms | 14.3ms |
ConstantSourceNode | 6.6ms | 20.2ms |
requestAnimationFrame | 9.7ms | 8.8ms |
フォアグラウンドで2回目以降時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 0.7ms | 0.4ms |
setInterval (10) | 0.6ms | 0.4ms |
setInterval (100) | 0.4ms | 0.5ms |
AudioBufferSourceNode | 8.6ms | 5.8ms |
OscillatorNode | 9.3ms | 19.1ms |
ConstantSourceNode | 10.1ms | 20.4ms |
requestAnimationFrame | 1.0ms | 0.1ms |
バッググラウンドで初回起動時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 998.9ms | 900.6ms |
setInterval (10) | 0.5ms | 999.7ms |
setInterval (100) | 998.8ms | 999.5ms |
AudioBufferSourceNode | 89.9ms | 5.8ms |
OscillatorNode | 10.7ms | 19.9ms |
ConstantSourceNode | 3.9ms | 20.2ms |
requestAnimationFrame | 34985.8ms | 30565.4ms |
バックグラウンドで2回目以降時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 998.4ms | 390.6ms |
setInterval (10) | 1000.2ms | 0.4ms |
setInterval (100) | 999.5ms | 999.1ms |
AudioBufferSourceNode | 8.0ms | 1.3ms |
OscillatorNode | 9.7ms | 19.6ms |
ConstantSourceNode | 10.1ms | 19.5ms |
requestAnimationFrame | 14580.2ms | 2245.5ms |
まとめると、requestAnimationFrame はバックグラウンドでは停止しているようなもので使いものになりません。 setTimeout, setInterval はバックグラウンドでは非常に遅くなります。 AudioBufferSourceNode, OscillatorNode, ConstantSourceNode は同じ速度で実行できます。 音楽などはバックグラウンドで再生速度が変わったら困るので、それを考慮しているのだと思います。 AudioBufferSourceNode は初回起動時になぜか遅いですが、それ以降は高速です。 つまり、音声処理のタイマーや、バックグラウンドでミリ秒単位の高精度タイマーが必要なら、 AudioBufferSourceNode を使いましょうという結論になりそうです。 とはいえ setInterval より精度が低いのは意外でした。
ちなみにウィンドウが非アクティブのときにどのように動作するかは、Window: setTimeout() メソッド - Web API | MDN にまとまっています。 setTimeout/setInterval はベンチマークの結果通り、1秒単位で処理するようです。 記述を読む限り AudioContext を使った手法はおそらく処理速度が落ちたりはしないですが、特にそれが定まっている訳でもないみたいです。
登録:
投稿 (Atom)