バイブコードing

https://github.com/shiomizu0620/vibe-receiver

Python

Flutter

Dart

バイブコードをingするプロダクトです。

しおみず

abe

1rou

推しアイデア

スマホの振動でURLを開けます。QRコードに変わる新たな1次元コード、バイブコードの誕生です。

作った背景

バイブコーディングって言葉は意味が分からない真のバイブコードingはこれだ。

推し技術

送信Flutter・受信Pythonで、振動を信号処理→復調→DB逆引きする自作プロトコルです。短/長で0/1、プリアンブルで同期をしてます。

プロジェクト詳細

プロダクト概要

スマホの「振動」でURLを開くプロダクトです。

ひとことで言うと、「QRコードを、カメラと光の代わりに"振動"でやる」ということです。

↓送信側のgithubリポジトリ https://github.com/shiomizu0620/vibe_code_sender

作成の理由

単純に振動でURL開いたら面白そうだなと思っていた、でも作るうちに『これQRの光を振動に置き換えてるだけだ』と気づいて、そこを証明する方向に振り切りました。

送信側機能

1,URL一覧・登録画面(演奏タブの入口)

  • URLを入力して 登録 すると id(0〜255)が発行される
  • 登録済みURLを 一覧表示(id付きアバター+URL)
  • 右上の 再読み込み ボタンで再取得
  • リストの項目をタップ → 演奏画面へ遷移
  • 登録/取得失敗時はSnackBar・定型文でエラー表示 image

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

  • 楽譜表示(ScoreView):パルスを ●/━ で並べ、現在位置をハイライト、ミス打を赤表示(score_view.dart)
  • 手動演奏(F5):「● 短」「━ 長」ボタンを人が叩く。叩いた長さが楽譜と違うとミス記録
  • 自動演奏(F6):プリアンブル+全パルスを機械精度で一括再生
  • プリアンブル送出フェーズ(演奏開始前の同期信号)
  • 連打防止ロック(_vibrating)で長振動の誤結合を防止
  • ステータス行:「○/○ 打」「演奏完了! ミス○打」など
  • リセット ボタン
  • 振動非対応端末(エミュレータ等)には警告表示 image

3,URL入力タブ = 登録なしで送信 URLを入力すると 符号化プレビュー(送信可否/スキーム http·https/本体文字数 最大63)を表示 「演奏画面を開く」で X1 固定の演奏画面へ(DBを介さずURLを直接符号化)

4,QRタブ カメラ/画像ファイルからQRを読み取る画面 image 5,音ゲー画面

  • 開始前: URL選択。「一覧」(登録URLをカルーセルで選ぶ→ID/X1エンコード選択+ノーツ速度調整)/「URL直接」(URL入力→読み込み)の2モード
  • ノーツ: 奥(上・幅狭)→手前(下・幅広)へパースで降下 短(tap)= アンバーの横長バー 長(hold)= シアン〜パープルのグラデ台形(先頭に明るいキャップ、尾を引く)
  • 判定ライン: 画面80%位置の白い発光ライン。その下に6レーンのタップパッド(叩くと発光)
  • 入力: 画面下部をタップ。叩いたレーンが対象ノーツと一致+タイミングが窓内ならヒット。違うレーンはMISS
  • 判定: Perfect ±150ms/Good ±400ms/それ以外 Miss。判定エフェクト(フラッシュ・リング・パーティクル・PERFECT/GOOD/MISS текスト)
  • 演出: 開始時に 3-2-1-GO カウントダウン、プレイ中は左上にスコア(7桁)・中央上に COMBO
  • 終了後: RESULT画面(ランク S/A/B/C/F、スコア、PERFECT/GOOD/MISS集計、「もう一度」) スコア=(Perfect×1.0+Good×0.5)÷データ音数×1,000,000
  • ボタン: 演奏開始/停止(カウントダウン中はキャンセル)、左上に「演奏」タブへ戻る image image

受信側機能

1,録音・信号処理:sounddeviceで録音→バンドパス→包絡線→ON/OFF検出 2,復号:パルスの長さでプリアンブル/short/longに振り分け→ビット列→URLまたはid 3,DB逆引き:Supabase でidからURL取得、オフラインフォールバックあり 4,ブラウザ演出:受信中の信号レベル(包絡線の振幅)をWebSocketでリアルタイムにブラウザへ送信し、Canvasでオシロスコープ風に可視化。振幅は実測値そのまま、色は短=アンバー/長=シアン/プリアンブル=グレーで切り替わる。URLの表示はタイプライター演出、復号の瞬間にフラッシュ。 image 5**,replayモード**:録音済み波形で再現できる保険

推し機能

振動版のQRコード

光で読むQRコードを振動で再現しました。 QRコードが持つ二つの形式を実装しています。

  1. URLの文字列を01に変換して、長短の振動として送信する形式:この形式だと文字列をそのまま変換しているのでサーバー要らずで本来のQRコードの形式に近い。しかし振動を送る回数が非常に多くなってしまい、300回以上振動を送ることもある。
  2. URLをDBに保存し、IDを発行して振動で送る形式:この形式は送信側でURLをDBに送りIDを貰い、その振動を受信側でIDからURLを参照。この形式は圧縮QRコードに近い、これはDBを通す代わりに振動の数は減り約10回程度の振動で済む。

QRコードを振動に翻訳 QRコードを読み取ってそれを上記の1の形式で振動に変換する機能です。一見QRコードを読み取っているので何の意味もない機能ですが、QRコードと「バイブコード」が同じデータを運ぶ証拠になってくれました。

音ゲーで振動を演奏する 振動を押して正確に送るのは人間には地味に難しい。そこで送信側を音ゲーにして、譜面どおり叩くと正しい長短の振動が出るようにしました。演奏のPerfectやgoodなどがそのままURL送信の成功率になる設計。 また、URL送信の正確さは、そのまま音ゲーのスコアという形表示されます。 振動の正確さを最優先に、判定の描画とは独立した固定長タイマで振動を出しています。

チェックサム失敗 → 運命のサイトへ 直接送信モードはCRC-8で誤りを検出します。誤り訂正はあえて入れず、合わなかったら「ミスったので運命のサイトへ」と出して安全な定番サイトをランダムで開く。完全ランダムにする方向性も考えましたが、そもそも開く可能性が低すぎると感じたので、「うん」要素をさらに強めるためにランダムな安全なサイトに飛ばされるという仕組みもできています。

全体構成

[送信側] スマホ(Flutter) 楽譜表示・手動/自動演奏・振動制御 ↓ 振動(短/長) [物理] ピエゾ(接触振動センサー) ↓ 音声入力 [受信側] PC(Python) 録音 → 信号処理 → 復調 → DB逆引き → 演出 ↕ [データ] Supabase(番号↔URL の対応表)

技術構成

フロントエンド:スマホアプリ(送信側image バックエンド:受信プログラム(受信側image データ管理 image 通信プロトコル(自作 v1.0) image 開発インフラ image

技術構成の詳細

アナログに見える「振動」を、デジタルの「QRコード」と同等の精度で通信させるため、以下のライブラリ群を用いて送受信の両面で**とても精度の高い厳密な信号処理(DSP)**を行っています。

  • 送信側(Flutter):UIとハードウェア制御の分離 使用パッケージ:vibration vibration パッケージの機能を用い、[150ms休止, 150ms振動(短), ...] という厳密な配列データを生成し、OSのバイブレーションAPIに直接流し込んでいます。これにより、音ゲーUIの描画ラグ等に一切影響されない、ミリ秒単位で正確な通信パルスを出力しています。 ↓参照 https://pub.dev/packages/vibration
  • 受信側(Python):ノイズの海から信号を拾うDSPパイプライン 使用パッケージ:sounddevice, numpy, scipy ピエゾマイクからの物理振動を正確なビット列に復元するため、以下のDSPパイプラインをリアルタイムに回しています。
  1. 極低遅延の集音sounddeviceでレイテンシを極限まで下げて生波形を取得。 ↓参照 https://zenn.dev/kun432/scraps/f56760d41fc5aa
  2. ノイズ除去scipy.signalのバンドパスフィルタで、スマホのバイブ特有の周波数帯のみを抽出。
  3. 包絡線(エンベロープ)抽出numpyscipyを駆使して細かい波形の輪郭を平滑化し、プログラムが扱いやすい「強さの推移」データに変換してエッジ検出へ繋げています。 ↓参照 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人開発でも破綻しないよう、最初に開発の仕組みそのものを整えました。

  • 詳細なロードマップ/開発マップ:データの流れや担当ごとの作業順・レーンをまたぐ「待ち合わせ(依存)」を1枚の図にまとめて、GitHub Pages で全員が見られるよう公開した。誰が・どの順で・何を作るかを常に共有できる状態にした。 ↓(URL) https://shiomizu0620.github.io/vibe_code_sender/vibecode_map.html
  • issue を細かく起票:各タスクを「単独で完了判定できる」粒度まで分解し、本文に作業内容・完了条件・依存・触ってよいファイルまで明記した。番号順に1つずつ消化すれば依存が壊れない設計にした。 ↓タスクボードURL https://github.com/users/shiomizu0620/projects/3
  • 担当ファイルの境界を明確化:ロジック担当・UI担当・受信担当で「触ってよいファイル」を分け、並行作業でもコンフリクトが起きにくいアーキテクチャにした。
  • CodeRabbit による自動コードレビュー:PR ごとに AI が自動レビュー。実際に複数の指摘(CIの収集エラー対策、スレッドの後始末、想定外エラーの握り潰し防止など)を受けて修正でき、レビュー負荷を下げつつ品質を保てた。今回初めて導入したけど入れて良かったなと感じた。 image
  • CI/CD・通知の整備:GitHub Actions でテスト自動化、PR を Discord に通知してレビュー待ちのラグを削減

いつもよりも事前開発を早めに行い、タスクの管理や全員での開発のしやすい体制作りなどをいつもよりも重視したため、本番では切羽詰まりすぎづ、スムーズな開発ができたなと感じました。

こだわった技術

  • なぜ振動の送信をモールス方式でなく,0,1ビット方式にしたのか

「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に詰めて送信回数を削っている。

苦労した点

今回苦労した点は

  • pythonを触るのが初めて
  • 振動も読み取り側も闘値が弱すぎて途中で詰んだ
  • 持っていたandroidの機種が古くてバイブレーションを大きくする機能に対応してなかった

その中でも一番苦労したのは

  • 振動も読み取り側も闘値が弱すぎて途中で詰んだ

です。  これに関してはそもそもの内臓マイクの問題だったりデバイスの問題だったりするので、めちゃくちゃ調べたりAIに聞いたりしても無理でした。 世にも珍しい「技術的に不可能です」という状態です。(もしかしたら調べ足りないだけかも)。

ということでこれに対応するために、ピエゾマイクというデバイスを買いました。 ↓購入したピエゾマイクのamazonURL https://x.gd/nfjFE

一言で言うと、空気の振動ではなく「物体そのものの振動」を拾って電気信号に変換するマイクです。 これを利用することでバイブをより感度高く感知することができるし、音ではなく振動を検知することになり、バイブのイメージに合わせることもできました。

しかし、ピエゾマイクがなぜか全然振動を読み取ってもらえないという問題が発覚し、ほかのものを買いに行く時間もなかったため、死ぬほど困りました。

↓下から二番目が振動の検知なのですが、死んでる様子です。 image が、奇跡が起こりなぜか端子を半刺しすると音声が読み取られるということに気づき一命をとりとめました。これに気づいたときは叫びました。

↓完璧に読み取れてる様子 image

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

しおみず

@shiomizu0620