推しアイデア
スマホの振動でURLを開けます。QRコードに変わる新たな1次元コード、バイブコードの誕生です。
―
スマホの振動でURLを開けます。QRコードに変わる新たな1次元コード、バイブコードの誕生です。
バイブコーディングって言葉は意味が分からない真のバイブコードingはこれだ。
送信Flutter・受信Pythonで、振動を信号処理→復調→DB逆引きする自作プロトコルです。短/長で0/1、プリアンブルで同期をしてます。
スマホの「振動」でURLを開くプロダクトです。
ひとことで言うと、「QRコードを、カメラと光の代わりに"振動"でやる」ということです。
↓送信側のgithubリポジトリ https://github.com/shiomizu0620/vibe_code_sender
単純に振動でURL開いたら面白そうだなと思っていた、でも作るうちに『これQRの光を振動に置き換えてるだけだ』と気づいて、そこを証明する方向に振り切りました。
1,URL一覧・登録画面(演奏タブの入口)

2,演奏画面 選んだURLの id を「楽譜(短●/長━のパルス列)」にして振動で送信する画面。

3,URL入力タブ = 登録なしで送信 URLを入力すると 符号化プレビュー(送信可否/スキーム http·https/本体文字数 最大63)を表示 「演奏画面を開く」で X1 固定の演奏画面へ(DBを介さずURLを直接符号化)
4,QRタブ
カメラ/画像ファイルからQRを読み取る画面
5,音ゲー画面

1,録音・信号処理:sounddeviceで録音→バンドパス→包絡線→ON/OFF検出
2,復号:パルスの長さでプリアンブル/short/longに振り分け→ビット列→URLまたはid
3,DB逆引き:Supabase でidからURL取得、オフラインフォールバックあり
4,ブラウザ演出:受信中の信号レベル(包絡線の振幅)をWebSocketでリアルタイムにブラウザへ送信し、Canvasでオシロスコープ風に可視化。振幅は実測値そのまま、色は短=アンバー/長=シアン/プリアンブル=グレーで切り替わる。URLの表示はタイプライター演出、復号の瞬間にフラッシュ。
5**,replayモード**:録音済み波形で再現できる保険
振動版のQRコード
光で読むQRコードを振動で再現しました。 QRコードが持つ二つの形式を実装しています。
QRコードを振動に翻訳 QRコードを読み取ってそれを上記の1の形式で振動に変換する機能です。一見QRコードを読み取っているので何の意味もない機能ですが、QRコードと「バイブコード」が同じデータを運ぶ証拠になってくれました。
音ゲーで振動を演奏する 振動を押して正確に送るのは人間には地味に難しい。そこで送信側を音ゲーにして、譜面どおり叩くと正しい長短の振動が出るようにしました。演奏のPerfectやgoodなどがそのままURL送信の成功率になる設計。 また、URL送信の正確さは、そのまま音ゲーのスコアという形表示されます。 振動の正確さを最優先に、判定の描画とは独立した固定長タイマで振動を出しています。
チェックサム失敗 → 運命のサイトへ 直接送信モードはCRC-8で誤りを検出します。誤り訂正はあえて入れず、合わなかったら「ミスったので運命のサイトへ」と出して安全な定番サイトをランダムで開く。完全ランダムにする方向性も考えましたが、そもそも開く可能性が低すぎると感じたので、「うん」要素をさらに強めるためにランダムな安全なサイトに飛ばされるという仕組みもできています。
[送信側] スマホ(Flutter) 楽譜表示・手動/自動演奏・振動制御 ↓ 振動(短/長) [物理] ピエゾ(接触振動センサー) ↓ 音声入力 [受信側] PC(Python) 録音 → 信号処理 → 復調 → DB逆引き → 演出 ↕ [データ] Supabase(番号↔URL の対応表)
フロントエンド:スマホアプリ(送信側)
バックエンド:受信プログラム(受信側)
データ管理
通信プロトコル(自作 v1.0)
開発インフラ

アナログに見える「振動」を、デジタルの「QRコード」と同等の精度で通信させるため、以下のライブラリ群を用いて送受信の両面で**とても精度の高い厳密な信号処理(DSP)**を行っています。
vibration
vibration パッケージの機能を用い、[150ms休止, 150ms振動(短), ...] という厳密な配列データを生成し、OSのバイブレーションAPIに直接流し込んでいます。これにより、音ゲーUIの描画ラグ等に一切影響されない、ミリ秒単位で正確な通信パルスを出力しています。
↓参照
https://pub.dev/packages/vibrationsounddevice, numpy, scipy
ピエゾマイクからの物理振動を正確なビット列に復元するため、以下のDSPパイプラインをリアルタイムに回しています。sounddeviceでレイテンシを極限まで下げて生波形を取得。
↓参照
https://zenn.dev/kun432/scraps/f56760d41fc5aascipy.signalのバンドパスフィルタで、スマホのバイブ特有の周波数帯のみを抽出。numpyとscipyを駆使して細かい波形の輪郭を平滑化し、プログラムが扱いやすい「強さの推移」データに変換してエッジ検出へ繋げています。
↓参照
https://watlab-blog.com/2019/05/01/scipy-bandpass/Flutter(送信側)
・ Flutterのvibrationパッケージは、パターン(短150ms/長450ms)を直接ミリ秒指定で出せる。ReactNativeでも振動はできるけど、ライブラリの成熟度や細かい制御という点では Flutterの方が扱いやすいから。
・チームの習熟度が一番高かった。今回のチームの3人ともflutterでの開発を半年以上続けており比較的慣れているため、ハッカソンで一番絶望する、「慣れてない技術で詰まる」リスクを避けたため。
Python(受信側) ・信号処理ライブラリが圧倒的に充実してるから。受信側がやるのは「音の波形 → バンドパスフィルタ → 包絡線 → ON/OFF検出」という信号処理(DSP)。pythonが圧倒的に向いているから。
・録音・DB・ブラウザ操作まで全部ライブラリで揃うから
短期間・3人開発でも破綻しないよう、最初に開発の仕組みそのものを整えました。

いつもよりも事前開発を早めに行い、タスクの管理や全員での開発のしやすい体制作りなどをいつもよりも重視したため、本番では切羽詰まりすぎづ、スムーズな開発ができたなと感じました。
「URLは文字列だし、モールスっぽく文字のまま送ればいいんじゃない?」という案も最初に浮かんだ。でも、あとからURLを直接送るモード(サーバー無しでURLそのものを振動に乗せる方式)まで拡張したかったので、それなら文字じゃなく0/1のビット列を土台にしておくべきだと考えて、最初からビット列方式にしました。
ビット列なら、モード切替・長さ・誤り検出みたいなものを上に綺麗に載せられ、QRみたいに拡張できる。実際この土台のおかげで、id方式も直接送信モードも、同じビット列の上にモードマーカー1bitで共存できました。
さらに0,1を強度じゃなく長さで表すことで、端末差や接触圧でブレても読み間違えにくくしました。
「振動通信としてちゃんと成立させる」ために、見た目より振動の正確さを最優先に設計しました。
・振動を、描画・判定から独立させた(送信側) 音ゲーにはしましたが、振動の長さ(短150ms / 長450ms)がそのままデータなので、描画のカクつきや判定処理のラグで長さがブレると、それがそのまま通信エラーになってしまう。 そこで振動は、描画や判定とは別の固定長タイマで叩くようにして、「ゲームの見た目」と「通信の正確さ」を意図的に切り離しました。演奏がカクついても、振動の長さだけは守られる設計です。
・受信の入力を差し替え可能にした(受信側)
受信側の入力源をチャンネルとして抽象化して、--channel mic(実機のピエゾ)と --channel replay(録音済み波形の再生)を切り替えられるようにしました。
おかげで、信号処理を録音波形で簡単・正確にテストできる(pytestが通る)し、本番で実機が不調でも replay で同じデモを確実に再現できる保険にもなりました。
・受信のDSP(信号処理)パイプライン 受信側がやっているのは信号処理で、流れはこう: 録音 → バンドパス → 包絡線(エンベロープ)→ 閾値でON/OFF検出 → ONの"長さ"を測って プリアンブル / 短 / 長 に振り分け。 実測でも preamble 703〜754ms・short 157〜183ms・long 514〜548ms ときれいに分離できて、ちゃんと識別できています。
・閾値を「勘」ではなく「実測」で決めた 最初は閾値が高すぎて(0.02)復号ゼロでした。そこで波形を可視化するデバッグツール(debug_view)を自分で用意して、ノイズ床 ≈ 0.0005 / 信号 ≈ 0.006 を計測 → 0.0025 まで下げたら完璧に動いた。 〔描画は matplotlib 等/録音は ◯◯〕+ numpy / scipy を組み合わせて、計測しながら閾値を詰めた。
・プロトコルを「フレーム」として設計した
0/1ビット列の土台に、こういう構造を載せています:
プリアンブル(長い振動2連で同期)+ モードマーカー1bit + 本体(MSB first)。
モードマーカー1bitを切り替えるだけで、id方式(DBで逆引き) と URL直接方式(X1) を同じビット列の上に共存させた。
X1側はさらに scheme(1bit, httpsを省略) + length(6bit) + 6bit文字テーブル×n + CRC-8(誤り検出) という構成で、文字を8bitでなく6bitに詰めて送信回数を削っている。
今回苦労した点は
その中でも一番苦労したのは
です。 これに関してはそもそもの内臓マイクの問題だったりデバイスの問題だったりするので、めちゃくちゃ調べたりAIに聞いたりしても無理でした。 世にも珍しい「技術的に不可能です」という状態です。(もしかしたら調べ足りないだけかも)。
ということでこれに対応するために、ピエゾマイクというデバイスを買いました。 ↓購入したピエゾマイクのamazonURL https://x.gd/nfjFE
一言で言うと、空気の振動ではなく「物体そのものの振動」を拾って電気信号に変換するマイクです。 これを利用することでバイブをより感度高く感知することができるし、音ではなく振動を検知することになり、バイブのイメージに合わせることもできました。
しかし、ピエゾマイクがなぜか全然振動を読み取ってもらえないという問題が発覚し、ほかのものを買いに行く時間もなかったため、死ぬほど困りました。
↓下から二番目が振動の検知なのですが、死んでる様子です。
が、奇跡が起こりなぜか端子を半刺しすると音声が読み取られるということに気づき一命をとりとめました。これに気づいたときは叫びました。
↓完璧に読み取れてる様子

ただピエゾマイクを買うということや、半刺しならいけるという結論に達するまでにかなり時間がかかったし、ピエゾマイクの仕様を知ろうとして、多くの時間を費やしてしまいました。 ただ、試行錯誤ができたため、いい経験にはなったと思います。