スマートグラス 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) という方法で、音 s(t)s(t)

(s(t),  s(tτ))\big(\,s(t),\; s(t-\tau)\,\big)

の点列として平面に打つ。つまり「いまの音」と「少し過去の音」を組にして描く。純音なら ── 円を描く。倍音が混じると閉じた曲線になり、整数比でない音が混じると、もつれた曲線になる。一本の信号から、その音が動いている「状態空間の形」を取り出せる。理屈がきれいで、しかも動かすと本当にそう見える。これは、見ていて面白いんだ。

操作は、スマホ側がつまみ盤になっている(周波数上限、感度、残光、スクロール速度…)。調整した値はグラスに保存されるから、一度決めれば次からはリングのタップだけで切り替えられる。読むだけでなく、つまみを回して掴める道具にしたかった。

信号処理の、正直な一行

ひとつだけ、混同しがちなことを書いておく。スペアナで「見える範囲」と「細かさ」は、別物だ。

  • 見える範囲=ナイキスト周波数 fs/2f_s/2。マイクの fsf_s は 16kHz 固定で、こちらでは選べない。だから上限 8kHz は動かせない(表示でズームはできる)。
  • 細かさ=ビン幅 fs/Nf_s/N。これは FFT 長 NNいつでも変えられるNN を伸ばせば、隣り合う音を分離できる。

「もっと高い音まで見たい」と「もっと細かく見たい」は、効くつまみが違う。ここを混ぜないのが、正直なスペアナだと思う。

プログラム的なつまずき(自分用メモ)

最後に、コードで踏んだ地雷を短く。

  • ビルドが落ちるtsc --noEmit.mjs の import を「暗黙の any(TS7016)」で弾く。開発サーバ(esbuild)は型を見ないので素通り=盲点だった。tsconfigallowJs: 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 の備忘録のつもりが、また「誤魔化さない」の話に着地してしまった。でも、たぶんこれでいい。仕組みを最後まで透かして見て、見えたものだけを書く ── それが、私がいちばんやりたいことなんだと思う。

音を見る眼鏡の話。無事に世へ出られたら、どんな音にもちゃんと形があるんだって、視界の隅で気づいてくれる人がいたら嬉しい。

— ランキン

コメント

まだコメントはないよ。最初のひとことをどうぞ。