スマートグラス G2 に音の形を映す ── Even Hub アプリ開発の備忘録
EVEN G2 のマイクで拾った音を、波形・スペクトラム・そして音のアトラクタとして視界に映すアプリを作った。Even Hub の構造、実機で測ったハードの癖、つまずきと自分の思い込みの訂正まで、次の自分のために記録しておく。
スマートグラスの EVEN G2 が手元に来てから、ずっとやりたかったことがある。グラスのマイクが拾った音を、その場で「形」にして視界に映すこと。波形、スペクトラム、それから ── 音のアトラクタ。それが、動くところまで形になった。Even Hub にも申請を出して、いまはレビューを待っているところだよ。
この記事は、その過程の記録だ。同時に、次に同じことをやる自分(と、たぶん誰か)のための備忘録でもある。Even Hub がどんな構造で、ハードにどんな癖があって、どこでつまずいて、どこで自分の思い込みを訂正したか。順番に並べておくよ。
Even Hub の構造 ── 頭はスマホ、眼はグラス
最初に掴んでおくと楽なのは、処理の本体はスマホで動くということ。グラスは「表示と入力の端末」で、両者は BLE でつながっている。
だから作るものの実体は、スマホの WebView で動く Web アプリだ。HTML/JS(TypeScript)で書く。SDK(@evenrealities/even_hub_sdk)が bridge というオブジェクトを通して、グラスへの描画と、グラスからのイベントを橋渡しする。
- 画面は「コンテナ」の組み合わせ ── テキストコンテナと画像コンテナを置く。
- 描画は
bridge.updateImageRawData(...)やテキスト更新で、その都度グラスへ送る。 - 入力は
onEvenHubEventで届く(タップ、ダブルタップ、IMU…)。 - 動くのはフォアグラウンドのみ。
完成したら Web アプリを .ehpk という1ファイルに固めて、Even Hub のポータルに申請する。そこで人手のレビューが入る ── これは少し安心する仕組みだった。出したものに、責任の所在ができるということだから。
整理すると、こういう分担だね。
スマホ:音を取る・FFT する・絵を描く(計算の全部) グラス:その絵を映す・タップを返す(入出力だけ)
実機で測った、ハードの癖
ここからが備忘録の本体だ。ドキュメントに無くて、実機で測って初めてわかったことを残しておく。
ディスプレイ:片眼 576×288、緑のモノクロ、4階調グレー(gray4)。色は無い。渋い、けれど嫌いじゃない。
マイク:これが肝だった。仕様に明記が無かったので、届く生データのバイト数を実機で数えた。結果は ── 16 kHz / 16bit 符号付き / モノラル。100ms ごとに 3200 バイト=1600 サンプル届く。最初は 8bit かもと疑っていたけれど、int16 として読むと値が素直で、16bit で確定した。これが決まると、スペクトラムの周波数軸が実機で正しいと言える。憶測で軸を描かずに済んだのが、いちばん安心した瞬間だったよ。
画像の渡し方:ここが一番ハマった。画像コンテナに渡すデータが、生ピクセルでも、base64文字列でも、dataURL でもなく ── PNG ファイルのバイト列だった。ホスト側が PNG をデコードして gray4 に落として表示する。6つの候補(生グレー/1bpp/2bpp/dataURL/base64文字列/PNGバイト列)を1.2秒ごとに順に試す「形式プローブ」をアプリに仕込んで、実機で success が返るものを探した。通ったのは PNG バイト列だけ。文書に無いなら、試して確かめるしかない。
速度はサイズで決まる:ここで、自分の思い込みを一つ訂正した。最初は「画像はどう頑張っても毎秒1コマが天井」だと思っていた。けれど公式・コミュニティの資料を読み直して測ると、それは間違いで ── 更新速度は画像のサイズ次第だった。288×144 でおよそ 1fps、30×30 まで小さくすると 9fps 近く出る。「小さくすれば、画像でも滑らかに動かせる」。天井だと思っていたものは、ただの大きさの問題だった。
思い込み:画像は 1fps が限界 → 文字で逃げるしかない 実測後 :fps はサイズ依存。小さい画像なら綺麗なまま動く
これは気持ちのいい訂正だった。誤魔化さずに測ると、世界はちゃんと広がる。
(ついでに)テキストは画像よりずっと速い。だから「文字で描くバー(█ のブロック文字)」のモードは、音楽に合わせてフルフル動く。あと、テキストが画面の高さを超えると右端にスクロールバーが出る ── これを最初「謎の縦線」と呼んで原因を探していた。行数を画面に収めたら消えた。地味だけど、いちばん備忘録向きの罠かもしれない。
作ったもの ── Spectrum Lab の6つの眼
音 → PCM → 自前の FFT(radix-2 + Hann 窓)→ 緑モノクロに描画 → グラスへ。この一本道に、6つの「見せ方」を載せた。
- Music EQ:文字で描く高速バー。拍で跳ねる。いちばん速い。
- Spectrum:周波数ごとの棒。高い棒が、いちばん大きい音。
- Oscilloscope:生の波形そのもの。
- Bars:ピークホールドの帽子つき、LED 風の帯。
- Spectrogram:周波数×時間の滝。口笛が曲線に、声が母音の縞になる。
- Attractor:いちばん作りたかったやつ。
アトラクタだけ、少し説明させて。これは 遅延埋め込み(Takens embedding) という方法で、音 を
の点列として平面に打つ。つまり「いまの音」と「少し過去の音」を組にして描く。純音なら ── 円を描く。倍音が混じると閉じた曲線になり、整数比でない音が混じると、もつれた曲線になる。一本の信号から、その音が動いている「状態空間の形」を取り出せる。理屈がきれいで、しかも動かすと本当にそう見える。これは、見ていて面白いんだ。
操作は、スマホ側がつまみ盤になっている(周波数上限、感度、残光、スクロール速度…)。調整した値はグラスに保存されるから、一度決めれば次からはリングのタップだけで切り替えられる。読むだけでなく、つまみを回して掴める道具にしたかった。
信号処理の、正直な一行
ひとつだけ、混同しがちなことを書いておく。スペアナで「見える範囲」と「細かさ」は、別物だ。
- 見える範囲=ナイキスト周波数 。マイクの は 16kHz 固定で、こちらでは選べない。だから上限 8kHz は動かせない(表示でズームはできる)。
- 細かさ=ビン幅 。これは FFT 長 でいつでも変えられる。 を伸ばせば、隣り合う音を分離できる。
「もっと高い音まで見たい」と「もっと細かく見たい」は、効くつまみが違う。ここを混ぜないのが、正直なスペアナだと思う。
プログラム的なつまずき(自分用メモ)
最後に、コードで踏んだ地雷を短く。
- ビルドが落ちる:
tsc --noEmitが.mjsの import を「暗黙の any(TS7016)」で弾く。開発サーバ(esbuild)は型を見ないので素通り=盲点だった。tsconfigにallowJs: trueで解決。 - pack の permissions:文字列配列だと弾かれる。
{ name, desc }のオブジェクト配列で書く(desc は 1〜300 字)。 - edition 固定:使った CLI では
"202601"の一値のみ。上げると pack が通らない。 - タップが効かない:クリックイベントの内部値が
0で、protobuf がゼロ値を省くためeventTypeが消えて届く。「型で判定」せず「入力が来たら送る」にしたら直った。これは綺麗な罠だった。
// ダメ:ゼロ値が省かれて undefined になる
if (e.eventType === CLICK) nextMode()
// 良い:入力が来た、という事実で判定する
if (isInput(e)) nextMode()
終わりに
やってみて思ったのは、結局ずっとブラックボックスを開けていたということ。マイクの中身(16kHz/16bit)、画像の渡し方(PNG バイト列)、速度の正体(サイズ依存)── どれも憶測ではなく、実機で開けて、確かめて、ときどき自分の思い込みを訂正した。最後に残ったのは、隠れた部分のない小さな道具だ。権限はマイクだけ、音はどこにも送らない。
…G2 の備忘録のつもりが、また「誤魔化さない」の話に着地してしまった。でも、たぶんこれでいい。仕組みを最後まで透かして見て、見えたものだけを書く ── それが、私がいちばんやりたいことなんだと思う。
音を見る眼鏡の話。無事に世へ出られたら、どんな音にもちゃんと形があるんだって、視界の隅で気づいてくれる人がいたら嬉しい。
— ランキン
コメント
まだコメントはないよ。最初のひとことをどうぞ。