ジンジャー研究室

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

賞味期限が1年

このブログもまあ長いことやってるんだけど、これだけダラダラやっててもちょいちょい「○○さんが『△△』をブックマークしました」って通知が来たりする。でも、正直古い記事だと「それもう賞味期限切れてるよなー」って思うことがあって苦い気持ちになったりする。

自分の感覚としては1年経ったら大体も主張も色あせてきて、振り返ると「何々はなんとかすべきである」とか何を偉そうにのたまっとんじゃ〜という気持ちになる。いやもちろん正しいことも言ってるし、将来考えが変わることを予想して予防線を張っていたりはするんだけど。何かを主張している系の記事はどんどん気が変わってしまう。

CSS はもう全部インラインでいいだろ」 「いや、なんやかんや言って普通に CSS ファイル使うのがやりやすいわ」 「CSS ファイル膨れ上がりすぎるし書く場所を移動するのめんどい。やっぱインラインで書けばいいんじゃないか」 「CSS フレームワーク要らね」 「要らないとは言ったけど、結局は代わりに自前で似たような仕組み作らざるを得ないわ」 「View は Model の関数だよね、状態持つとか以ての外だよね」 「あーバケツリレーめんどい、状態持たせろ」 「人間は DOM から離れては生きていけないのじゃ」 「関数型最高、イミュータブル最高、手続きなんて捨ててしまえばいいね!」 「どーでもいいわ。こんなの手続きでちょろっと書けば終わりなのに何でわざわざ難しいことしてるの」 「class 普通に使うでしょ、わざわざ用意されてる便利な道具を使わない理由なんてある?」 「ボイラープレートは必要悪だ、同じことを何度も書いてもシンプルで見通しが良けりゃいいんだ」 「そこ共通化できないの最早プログラミングじゃないだろ、マクロ使わせろ」 「これが世間のベストプラクティス、みんな従うべき!」 「世間のベスプラが正解とは限らないし、守る必要なんてないでしょ」

こういうのを何度も何度も繰り返していくうちに、段々めんどくさくなってきて「適当にみんな好きなようにやればいいんじゃないの、知らんけど」になったり、「要はバランス」とでも言いたくなったりする。その場で思いついた適当なことを周囲に撒き散らしながら、自分だけは「こういう場合はこうだよね」という言語化されないバランス感覚が身につくので、まあ迷惑なやつだ。

適当なことを言ってるので適当に聞いてもらえれば良いんだけど、中には目を輝かせて聞いてくれる人が一定の割合でいて、「いやこんなの信用しなくて良いし1年後には全然違うこと言ってるよ?」という気分になってしまう。自分も web 界隈の強そうな人がなんか言うと「そうなんだ〜」ってなるけど、逆に「うるせー好きにやらせろ」って思うことも多々ある。そもそも web 界隈の平均的なプロジェクトの事情と自分の目の前にあるプロジェクトの抱える事情は違うわけで、「参考にはするけど内心は疑ってますよ」というスタンスで聞いてたりする。でも、自分がそうだからといって読者もそうとは限らないなと。ここのところはそういう主張モノを書くのはやめてしまって、なんか試しにやってみたっていう日記か、ただただ技術的に正しい事実を淡々と書いていく感じ。「あのブログにこれが良いって書いてあった」と言われると面倒だし。

じゃあ、そういう主張モノが害しかないかというと、そうは思っていなくて、みんなが色んな意見を言ってるのを聞いて上のように行ったり来たりしてるうちによりアイデアが洗練されてくるということはあるだろうし、言ってることも大体「一理ある」とは思う。誰も何も言わなくなったら、少なくともそういう考えを持った人がいるということを知る機会もなくなるし、それはそれで面白くない。

プロジェクトに採用するライブラリやアーキテクチャにしても「あの時はそれが良いと思ってたけど、試した結果イロイロ問題があることが分かったわ」みたいのがあって、「なんでこんな風になってるの?」と言っても本人も「なんでだろね、当時はそれがベストな方法と信じていたんだ」となったりする。

このアイデアの賞味期限というのは人によって違って、中には自ら 10 年前の主張をピックアップする人もいる。それだけ長い期間ブレない考察が得られるのは素直に尊敬する。真似できない。経験なのか知識なのか性格なのか、それは分からない。自分はコーディングも1分前にしていたと全く違う方法で書いてしまったりする。「さっきは関数を下にまとめるのがみやすいと思った。今は上かなと思ったから上に書いてる。」気分屋にもほどがある。でもブレないスタイルでずーっと書き続けられる人もいる。多分あれは「考えずに書くだけ」モードに入ってる。すごい。

世の中にはすごい人もいるけど自分には無理なので諦める。読者も諦めて欲しい。 ここに書くアイデアの賞味期限は1年です。

wasm ターゲットな言語を作って音を鳴らしてみた

前回、 Faust という言語を使って音を鳴らしてみたわけですが、AudioParam を使ったオートメーションに対応していないのでちょっと微妙だなと思いました。まあ Web が唯一のターゲットではないので仕方ない気はしますが。多分これができないと楽譜データからミックスダウンとかもできないはず。

じゃあということで次に試したのが EmscriptenC++ から出力するやつで、まあ動いたんですが、サイズがでかいとか、余計な JS コードがついてくるとか、バインディングを書くのが面倒くさいとか細かいところが色々気になったりしました。他にも Rust とか色々選択肢はあるんですが、どれにしても結局 AudioWorkletProcessor との接続部分は書かないといけないのが面倒で、ああ本当に面倒なのでそれ用の言語が作りたくなりました。

AssemblyScript を調べてたら binaryen.js という Binaryen のラッパーを使ってたので採用。この時点で LLVM が消えてターゲットが wasm のみに。脳みそが狐さん以下なので仕方ないですね。

作ったもの

名前はまだない。 github.com

こういう感じのコードを書くと、wasm モジュールとそれをラップした AudioWorkletProcessor が生成されます。デモはこちら

言語を作ったりするのは初心者もいいところなので、コスト削減のために大幅な割り切りをしています。

  • 動的なメモリの確保はできない
  • 型推論のような贅沢なものはもちろんない
  • 配列とループの長さは 128 決め打ち
  • 新しい型も作れない
  • if もねえ

逆に、あるもの

  • AudioParam 定義用の構文
  • VSCode 拡張

f:id:jinjor:20200415023948p:plain

エディタに色がつくと急にそれっぽい!

感想

wasm の特性なのかは不明ですが、 C っぽい言語が圧倒的に作りやすいというか、普通に普通の文法を動かすのが普通に大変で、それ以上の気の利いた文法とかはもう「動くんだから愚直に書けばいいでしょ」という気分になってきますね。 あと binaryen のレイヤーが高いのか、全然アセンブリ書いてる気がしないというか、結局アセンブリのこと何も分かってないかもしれない。うーん。

反省点としては、自作のパーサーコンビネーターがトークナイズしてないせいでホワイトスペースが至る所に出現してノイズが多いのと、VSCode 拡張(というか Language Server Protocol)で補間とか出そうとするとマメにコンテクストを管理しなきゃいけないなーっていうのに後で気づきました。直すの辛いけどやった方が良さそう。

まあとりあえず動いたので満足です。メンテナンスしたくないので広く公開することは多分ないけど。 早く FM 音源とか作りたい〜!

関数型言語 Faust で wasm な AudioWorkletNode を一瞬で作るサンプル

Faust って何

faust.grame.fr

音声信号処理に特化した言語というか DSL です。次のような短い記述でサクッとシンセ が作れます。

import("stdfaust.lib");
freq = hslider("freq",200,50,1000,0.01);
gain = hslider("gain",0.5,0,0.5,0.01);
gate = button("gate");
process = os.sawtooth(freq) * gain * gate;

簡潔、素晴らしい。 UI も定義できますが十分に抽象的なので見た目は完全にカスタマイズできます。

Faust については次の記事が詳しいです。 qiita.com

AudioNode を吐き出す

コンパイルターゲットの一つに wasm があり Web Audio API の AudioNode を作ってくれます。 例えば Hello.dsp というファイルを作って faust2wasm というコマンド(実体は shell )を叩くと、次の3点セットが出てきます。

  • Hello.js: AudioWokletNode ( main thread 側)
  • Hello-processor.js: AudioWorkletProcessor ( audio thread 側)
  • Hello-processor.wasm: 信号処理部分

ミニマルなサンプルを作った

言語そのものに関しては公式サイトがかなり詳しく解説しているので、上から順にやっていくと一通り作れるようになります。 が、個別のターゲットに関する情報は少ないので、 wasm 向けにすぐに音を出せるサンプルを作りました。

動作確認は2020/3/4 時点( FAUST Version 2.23.1

github.com

Web Audio API との相性

AudioWorklet の想定としては「ノード毎に processor を実装して組み合わせてね」ということなんですが、 Faust でなんでも出来てしまうので、ぶっちゃけ1ノードで済みます。 1ノードになんでもやらせるか、多数のノードを個別に作って組み合わせるのは Web Audio API でやるかは悩みどころ。

ちなみに現時点で吐き出されるコードは AudioParam のオートメーションが実装されていないので setTargetAt() などが効かないはず。ということは OfflineContext を使ったミックスダウンも上手く動かないんじゃないかと予想。気軽にコントリビュートするには重い課題な気がしますが、どうなんでしょうね。

github.com

VSCode 拡張を作って公開せずに使う

VSCode 拡張を自作して使いたいけど、全世界に公開するほど意識が高まらない時とか、特定のプロジェクトに特化したものを作りたい時。ググったら公開前提の解説が多いけど、その必要はない。

ここに大体書いてある。 code.visualstudio.com

最短の手順はおよそ次の通り。

npm install -g yo generator-code # yeoman と vscode 用のジェネレータ
yo code # 雛形生成(色々聞かれるので答える)
cd helloworld # 生成したプロジェクトへ
# README を書き換える
# package.json に "publisher" と "repository" を入れる
npm i -D vsce # VS Code Extension Manager を入れる
npx vsce package # パッケージを作る(prepublish のチェックが走るが publish はしない)
code --install-extension helloworld-0.0.1.vsix # パッケージをインストール

途中で README とか package.json を書き換えているのは、vsce package 時の怒られを回避するため。

これで捗る。あとは進捗するのみ。

Custom Elements を正しく実装するのはとても難しい

f:id:jinjor:20191221055521p:plain

React みたいなコンポーネント作る系フレームワークだと思って Custom Elements を使おうとすると、たちまち死んでしまう。まだ色々試している最中なのでアウトプットはないんだけど、とりあえず今考えてることを書いておく。役立たないし刺されたら困るからポエム宣言しとこうか。ポエムです。

Custom Elements やっていきたい

Custom Elements の良さは特定のフレームワークに依存しないところだと思う。例えば React とか Vue とかだとそれぞれのフレームワークの世界にどっぷり浸かってしまい互換性がないが、 Custom Elements ならば普通の要素の延長線上でどこに持って行っても使える。 npm とか使わなくても script タグで CDN とかから持ってくればすぐに動く。夢のよう。もちろん、データフローはアプリケーション固有のものになるだろうから Custom Elements が力を発揮するのはせいぜい末端要素だろうとは思う。それでもよく使いそうな末端要素を埋めてくれるのは便利。

だけど、実装を始めると途端に難易度が高いことに気づく。もちろん、チュートリアル通りにやればすぐに動くものはできるのだが、「ちゃんと」動くものを作るのはめちゃくちゃハードルが高い。「ちゃんと」というのは言い換えると「普通の DOM らしく動作する」という意味。別に使い方は自由だと思うけど、変な動きをしたら驚くし、せっかくだからちゃんとしたものを作りたいという気持ちがある。

属性とプロパティの二重管理

色々難しい点はあるが、その中でも特に、難易度を劇的に高めているのは「属性とプロパティの二重管理」だと思う。

まず、属性に関しては静的に検査されている訳でもなんでもないので、適当な文字列がどんどん入ってくる。省略される可能性があるので、デフォルト値の定義も必須だ。おかしな値が入っていても例外を投げることもできない。例えば <input type="number" min="10" max="0" value="5"> などと書くことも可能だ。この場合どうなるかというと、最初は 5 が入っているがインクリメントすると値が 10 に飛び、デクリメントすると値が 0 に飛ぶ。0 のまま送信しようとすると「値は 10 以上にする必要があります」と怒られ、10 で送信しようとすると「値は 0 以下にする必要があります」と怒られる。おかしいのだが無難な動きにはなっている。 また、min="hoge" のような明らかに意味のない値を入れると無視される。無視されるが属性が消える訳ではなく、"hoge" という属性はセットされたままになる。

対してプロパティは JS から操作されるためにあり、文字列以外に数値やブール値を許容している。とは言っても、max なら数値かと思えば文字列が返ってくる。MDN によると date-time が入りうるからということらしい(たとえ type="number" でも)。対して maxLength はどうかというと、こちらは数値が返ってくる。属性を指定しない場合は、それぞれ ""-1 が返る。 null ではない。値をセットした時の挙動も属性とは微妙に違って、例えば minLength = 10 の時に maxLength = 0 という値を入れようとすると「 10 より小さくするな」と例外を投げてくる。文字列で "100"を入れると 100 と解釈される。"hoge" を入れると 0true1 。というわけで、型に関しては指定されているものの違っても怒られずなんらかの解釈をするようだ。

次に属性とプロパティの関係について。最初「属性を変更したらプロパティも変更される」という一方通行だと思っていたが、どうも双方向らしい。つまり、どちらかを変更した時にもう片方を呼ぶような実装にしていると無限ループになる。共通部分を切り出す必要がある。だいたい思うように動くが、変な値を入れると妙な動きをする。属性 maxlength"hoge" とすると、属性値は "hoge" に、プロパティは -1 になる。今度はプロパティを "hoge" にすると、属性値は "0" に、プロパティは 0 になる。別にこの通りの動きにする必要はないと思うが。

また、ややこしいことに、value のような属性は双方向ではなく属性には反映されない。頻繁に変更されるものに関しては逐一属性値を書き換えたくないようだ。その辺の細かい作法みたいなのが、以下の "Custom Element Best Practices" に書いてある。

developers.google.com

正直「ベストプラクティス」と書いてはいるが、「お前らちゃんと正しく実装しろよ」ということだと思う。いや、できることならそうしたいけど難しくないっすか。他には例えば「プロパティに値をセットした時にイベントを発火するのは避けるべし」とある。理由は「プログラムから値を変更したんだからどんな値が入ってるかは知ってるでしょ」とのこと。うーん...そう言われればそうな気もするし、そうじゃないような気もする。

感想

とりあえず、「 Custom Elements を使えばアプリがスイスイ作れるぜ」みたいな生易しいものじゃないので万人に勧められないし、開発チームに丸投げしたらコンポーネントがホイホイ上がってくるようなものでもなさそう。いや、上がってくるかもしれないが。本当に一部の実力者みたいな人がちまちま作るか、上のベストプラクティスをうまいことまとめたツールキットなりテストなり、なんらかの仕組みが必要そう。 Custom Elements のチュートリアルに「ユーザーはどんな使い方をするか分からないからね!」とあるんだけど、ほとんどの開発現場では世界中の人に使ってもらうわけではなく、限られた使い方だけを網羅すればいいはずなので。そして限られた使い方しかしないのであれば、まあ React とかでいいよねという話に戻ってしまう。その辺はなんか立場の違いというか、今まで実現できなかったことをボトムアップに構築していく集団と、今までできてたことをいかに高効率でやってくかっていう集団があって、それは両方あっていいと思う。ただ、効率厨が軽い気持ちで触ると難しさに音を上げるという話。覚悟を持って挑もう。覚悟と気合い。

よいお年を。

.ogg ファイルのメタデータを読んでみた

.ogg ファイルのメタデータを確認する作業が発生したので、ブラウザでディレクトリ内の .ogg ファイルのリストを読めるようにしてみた(TypeScript)。

背景

友達のゲームに BGM を提供しました。

ゲームに使用する ogg ファイルにループ位置指定のためのメタデータを埋め込むんだけど、どのファイルにメタデータを入れたのかが分からなくなるのと、変数名のタイポが不安だったので一覧でさっと確認したくなった。というのが経緯。

ちなみに、仕様は RPG ツクールのを踏襲しているらしいので、多分同じことで困っている人は多いはず。まあ欲を言えば音量レベルが揃ってるか等も知りたいけど、時間がなかったので今回はメタデータのみ読めれば OK ということに。

作ったもの

コード

GitHub - jinjor/ogg-vorbis-experiment

こんな風に読める。

f:id:jinjor:20191215230025p:plain

ディレクトリを読みたい

Native File System API が必要で Chromium でフラグを立てたりしないといけないんだけど、今回の趣旨ではないので省略。

Ogg Vorbis を読みたい

公式に vorbis-tools というツール(コマンドは vorbiscomment)が提供されているので、 CLI で済ませたい場合はこれで出来る(Mac なら brew インストールできる)。

GitHub - xiph/vorbis-tools: Command-line tools for creating and playing Ogg Vorbis files.

npm にも .ogg 読むパッケージあるよね絶対。うん、あった。 でもそれじゃつまらないので、折角だから仕様を理解してバイナリを読むことにする。

Xiph.org: Ogg

仕様はここから読めるので、サクッと Google 翻訳して読む。最近は翻訳の精度が良いので下手に自力で読むよりも母国語の方が速く読める(英語が得意なら別)。たまにテクニカルタームが翻訳されてしまうので、そこだけ原文を確認する。

Ogg コンテナを剥がしたい

実は Ogg はパケットを伝送するためのコンテナなので、Vorbis がコーデック本体。なので Vorbis 以外にも Opus とか別のフォーマットのデータが入ってるかもしれないし、音声じゃなくてもストリーミングしたいものならなんでも可能とのこと。

というわけで、 Vorbis を読むにはまず Ogg コンテナを剥がす必要がある。

以下に現時点での浅い理解を図示してみる。(間違ってるかもしれないけど、何も情報ないよりは取っ掛かりとして良いはず。ちゃんと知りたい人は公式ドキュメントを読んでください。思想とかも書いてあるので。)

f:id:jinjor:20191220004908p:plain

Page 単位でパケット列が送られる。パケットは内部的にいくつかのセグメントに分けられる(255 バイトずつ+最後は余り)。1つのパケットは複数のページにまたがることもある。多重化も考慮されていてページごとにストリームの番号が振られている。

コンテナを剥がすと上のレイヤー(上の図の2番目の帯)では、単にパケット列として読めばいい。カプセル化されていて、ちょうど TCP と HTTP みたいな関係。

Vorbis を読みたい

1~3 パケット目がヘッダーで、 1 パケット目にチャンネル数やビットレート、 2 パケット目にコメントの類が入ってる。今回読みたかったのは、 2 パケット目にある任意のユーザーコメント(LOOPSTART, LOOPLENGTH)です。まあここは単にバイナリを読むだけなので、特に図とか解説とかは必要なさそう。

今回の要件はここまでで十分なので、 3 パケット目以降はまだ読んでません。

感想など

バイナリを読むのは楽しい。

そのうちコーデックにも手を出したい。ツールを使うとクオリティ 0 ~ 1 みたいなざっくりした表示しかないので何をしてるのか良く分からないし、再エンコードした時に無駄に音質劣化していないか不安で眠れない。あと Vorbis よりも Opus の方が音質が良さそうなので、そっちも使う機会があるといいなぁという感じ。

Nim v1.0 で簡単なツールを作ってみた

Nim

今朝たまたまこんなツイートが流れてきて、聞いたことある言語だったので気になって触ってみた。 見たところ、静的型付けで文法も結構親しみやすい感じ。その上パフォーマンスが良いとか。ひょっとしてこれから来る言語なのでは。知らんけど。解説は先駆者の記事とか読んでください。最近の言語かと思ったら初登場が 2008 とからしくて意外と歴史を積み重ねてる。

お題を考える

やるからには何か意味のあるものを作らないとつまらないので、お題を考える。

良さそうな題材が浮かんだのでこれで。

インストール

Mac なので brew install nim でサクッとインストール。パッケージマネージャの nimble も付いてくる。

チュートリアル

ここで簡単な文法を学びつつ、コンパイルの方法 num c -r greetings.nim を覚えた。

Part I から Part III まであるので一通り目を通した。と言っても3つ目は主にマクロの話なので、1〜2を読んでおけば大体なんとかなる。とは言え想像よりもボリュームがあるので頑張りが必要。なんかもっとチャラい言語を想像してたんだけど、結構ガチ系っぽい。

言語仕様は結構センス良い感じだと思った。普段やってる TS とかと比べると若干コンパイル時の処理とかメモリの場所とかを気にするようになっているけど、普通のプログラムを書いている分には明示的なメモリ確保とかポインタ操作とかはしなくて良さそう。

早速プログラムを作り始める

チュートリアルは基礎文法中心でアプリの作り方という感じではなかったので、まず何から取り掛かればいいのか分からなかったのだが、まあ nimble init とかしたらプロジェクトの雛形が出来るんでしょと思ったらそうだった。ただプロジェクトの中にもう一個ディレクトリが出来るという謎の動きをしたので、一枚皮を剥がした。プロジェクトのタイプとしては、ライブラリとバイナリとハイブリッドの3つから選べるようだ。

nimble run で確認するサイクルを回そうと思ったけど、なんか上手く動かなかったので nimble build して出てきたものを実行した。

ひたすら API ドキュメントを読みながら進める

ここからはとにかく API ドキュメントとにらめっこしながら進めていくしかない。 JS とかと違って StackOverflow とかによくある書き方みたいな情報が全然ないし。だけど標準ライブラリはかなり充実していて、不親切ということはない。自力で進むコツを掴みさえばなんとかなる(はず)。

まず grep を実行するために child process の実行の仕方(osproc)と正規表現の使い方(re)を覚える。次に HTTP (httpclient)の使い方と JSON のパースの仕方(json)を覚える。アウトプットに色もつける(terminal)。最後に、引数もちゃんとパース(parseopt)して CLI として使えるようにする。色々と一通り触れる良いお題だったっぽい。

ちょっと詰まったのが HTTPS を実行しようとすると実行時に SSL support is not available. Cannot connect over SSL. [HttpRequestError] と怒られる。コンパイル時に -d:ssl を指定しないといけないらしい。nim コマンドはそれでいいんだけど nimble 経由で渡すにはどうすればいいのよって探し回ったら、 nimble build にそのままフラグを渡せるよって閉じられた issue に書いてあった。

あとは stdout.write の後に flushFile し忘れると何も出ないとか、 seq に sort がないとか、seq から配列に変換する簡単な方法がないとか、正規表現バグってないかとか、細かいつまづきポイントはあったものの、全体としては「こんな感じかな、えい」ってやると大体動く。文法が直感的で良い。あ、でも全体的にかなり手続き的な書き方をするので、関数型フリークは満足しないかも。

完成・感想

というわけで完成しました。

標準ライブラリが充実しているおかげでプロジェクトがかなりスッキリしてますね。 TypeScript だと tscargvchalknode-fetch を入れて tsconfig も書いて...となることを考えると、最初から揃ってるのはかなり嬉しい。あとはもう少し情報とコミュニティ規模が〜という感じはするけど、時間が解決してくれることを期待してます。

おしまい。