RustでFAMIC on USBにMMLを書き込む

RustでFAMIC on USBにMMLを書き込む

2026.03.06

Introduction

image.png

FAMIC on USBは、MML(Music Macro Language)という文字列で音楽を作り、
「1980年代のゲーム機でよく使われていた音色」を再生できるデバイスです。
※ファミコンのピコピコ音

公式サイトのMML Playground
MMLを記述し、実機を接続してUSB-Audioを利用して音楽を書き込むことができます。

1. 公式Playgroundを開く
2. MMLをch1などに書く
3. FAMICを接続して「転送(書き込み)」を実行する
4. 本体ボタンを押して再生する

以前FAMICを購入したのですが、Playgroundをつかっているとき、
ローカルでもできるんじゃないか?と思い、
実際に試したところ、ローカルのRustプログラムからMMLを実機に転送して
演奏させることができたので、その手法について紹介します。

リポジトリはここです。

Environment

  • MacBook Pro (14-inch, M3, 2023)
  • OS: macOS 15.7
  • Rust : 1.95

About Contents

目的

famic-server-2.png

FAMICはPlayground(ブラウザ)経由でしか曲を書き込めません。
MMLを入力してボタンを押すと、サーバーがWAVファイルを生成し、
ブラウザのWebAudio APIでデバイスに送信します。

今回の目的は、Playgroundを使わずローカルでMML→デバイス書き込むツールをRustで作ることです。
(完成したプログラムはこちら

課題

PlaygroundではMMLテキストをサーバに送信し、サーバはWAVファイルを返します。
ブラウザはそのWAVをWebAudio APIで再生し、USB接続された実機に音声入力に転送します。
デバイスはその音声信号をデコードし、内部の音源チップにMMLデータを書き込みます。
サーバが返すWAVファイルの中身は「音楽」ではなく、MMLデータを音声信号にエンコードしたものであり、
バイナリデータが音の波形に変換されています。

※実際FAMICに書き込むWAVをPCで再生すると、モデムの「ピー・ヒョロロロ・ガー」みたいな音が鳴ります

この処理をローカルプログラムで再現するには、サーバがWAVの中で何をやっているのか知る必要があります。
しかし、このエンコーディング仕様は公開されていません。
公式ドキュメントにはMMLの書き方は書かれていますが、MMLテキストがどのようなバイト列に変換され、
どのような変調方式で音声信号になるのかは不明です。

なので、ローカルツールを作るためには以下を解析する必要があります。

  • MMLテキストがどんなバイトコードにコンパイルされるか
  • バイトコードのパッケージング(ファームウェアとの結合方式)
  • データがどのように暗号化・スクランブルされるか
  • ビット列がどのようなフレーム構造で送られるか(UART仕様)
  • ビットがどのような音の波形に変換されるか(変調方式)
  • 信号全体の構造(同期信号、タイミング、無音区間の長さなど)

これらは全てサーバー内部の処理であり、手がかりはサーバーが返すWAVファイルしかありません。
MMLを入力してWAVを取得し、その波形データの差分を分析することで、
エンコーディングの規則を一つずつ逆算していく必要があります。

やったこと

  1. プロトコルのリバースエンジニアリング
    公式Playgroundが生成するWAVファイルを検証し、Pythonスクリプトで中身を分析しました。
    FSK変調・スクランブル・UARTフレーミング・MMLバイトコードのエンコード規則を特定していきました。

  2. RustでWAVエンコーダーをスクラッチ実装
    解析した仕様をもとに、MML文字列からデバイス書き込み用WAVを生成するプログラムを実装。
    音符・テンポ・音量・音色・ADSR・ループ・タイ・スラー・ポルタメント・連符など、
    実用上必要な主要MMLコマンドに対応(一部未実装あり)。

  3. Playgroundが生成するWAVとバイト単位一致テスト
    ローカル生成したWAVと公式API生成WAVをペイロードレベルでバイト比較する回帰テストを整備しました。
    204ケース中、既知の差異(2400ボー+テンポの2件)を除き全てバイト一致を確認しています(回帰テストスクリプト)。

  4. 実機テストで最終確認
    最後にFAMIC WRITER経由で実機に書き込み、実際に曲が再生されることを確認。

Claude Code / Codexの使用方法

本稿の検証や作成したプログラムは、全てClaude Code / Codexを使用しています。
具体的には以下のケースです。

  • 仮説の立案と検証設計
    実際に生成したwavのバイト列の差分パターンを与えて
    「このバイトは何を意味しているか?」
    「どんな計算式で導出されるか?」
    を確認・検証させた。

  • Pythonの検証スクリプト作成
    仮説を検証するための差分分析スクリプトを、Claude Codeに実装・実行させた。

  • Rust実装
    検証済みのアルゴリズムをRustに移植する作業。
    ユニットテスト作成やエッジケースの処理もお任せ。
    実装したらCodexにレビューさせて品質を上げる。

  • ドキュメント整備
    検証結果や解析したプロトコル仕様書もClaude Codeが記述。
    ドキュメントがのこっていれば引き継ぎできる。

実際に人がやったのは要所での方向性の判断&実機テストです。

  • 次はどのコマンドに対応するか指示
  • 仮説を立てて検証の方向性を決める
  • 実機での検証、結果をAIに通達

これらの作業は人間が行い、それ以外はClaude Code / Codexに任せる。

なお、このFAMICをつかったタスクは
2025年2月(Claude code&Sonnet 3.7がリリースされたころ)からはじめました。
このときのClaude Codeでは実機への書き込みもできませんでしたが、
Opus4がでて課題を明確化してWeb SearchでPlaygroundの調査を実施するようになり、
Claude Opus 4.5で実機への書き込みができるようになりました。
(この時点では、プロトコルは正しくないので音楽再生はできない)

そして先日Claude Opus 4.6がリリースされ、プロトコル解析を行って正しいフォーマットで
実機にデータを書き込んで再生できるようになりました。

Claude Code使用時に意識したとことは、詳細に調査ドキュメント、SoWを残しておくことです。
とくに調査ドキュメントについては、どんな仮説でどんな検証でどういう結果になったかを記録しておき、
後の調査の参考としました。
モデルがバージョンアップしたタイミングでいままでのドキュメントを読ませ、
そこからまた調査を継続するという形で作業をすすめていきました。

How to Analysis

このセクションでは次の3点に絞って解説します。

  1. MMLからどうやってwavを生成して実機に書き込むか
  2. リバースエンジニアリングで使った調査用Pythonスクリプト
  3. 検証・実装でClaude Code/Codexをどのように使ったか

先に結論

最終的に、次のコマンドでローカル生成WAVをFAMICに書き込んで再生できるようになりました。

% cargo run --bin famic-wav-encoder -- "O4CDE" --device "FAMIC"

全体フロー

このプロジェクトのパイプラインは次のような流れです。

1.MML文字列をコンパイル
2.ペイロード構築 (firmware + MMLバイトコード結合)
3.スクランブル (自己同期型スクランブラ)
4.UARTフレーム化 (バイト→ビット列)
5.FSK変調 → WAV再生 → デバイス書き込み

% cargo run --bin famic-wav-encoder -- "O4CDE" --device "FAMIC"

  │  [1] CLIパース

"O4CDE"                               ← MML文字列

  │  [2] MMLコンパイル                    compile_mml()
  │      音符・テンポ等を中間バイト列に変換

[0xE4, 0x31, 0x33, 0x35]             ← コンパイル形式

  │  [3] ユニバーサルエンコード           encode_compiled_to_bytecodes()
  │      XOR参加マップで最終バイトコード生成

[9 bytes]                             ← MMLバイトコード (6+k bytes)

  │  [4] ペイロード構築                   build_payload()
  │      firmware (2923B) + MMLバイトコード結合

[2932 bytes]                          ← ペイロード

  │  [5] スクランブル                     scramble()
  │      固定seed + 多項式で難読化

[2940 bytes]                          ← スクランブル済みデータ

  │  [6] UARTフレーム化                   encode_uart()
  │      1バイト → 11ビットのフレームに展開

[32,340 bits]                         ← UARTビット列

  │  [7] FSK変調                          fsk_modulate()
  │      bit 1→12kHz / bit 0→24kHz の矩形波に変換

[129,360 samples]                     ← FSKサンプル

  │  [8] WAV信号構築                      build_wav_signal()
  │      プリアンブル + 無音 + データ + 無音

[~370,000 samples ≈ 7.7秒]           ← 完成WAV (48kHz, 8bit PCM)

  │  [9] 出力

  ├─ --dry-run -o  → WAVファイル保存     write_wav()
  └─ --device      → FAMIC WRITERへ再生  play_wav_signal()

各ステップの詳細です。

STEP 1: CLIパース

コマンドラインからパラメータを受け取る。

# 基本(1トラック)
% cargo run --bin famic-wav-encoder -- "O4CDE"

# 2トラック
% cargo run --bin famic-wav-encoder -- "O4CDE" --ch2 "O3GA"

# WAVファイル出力のみ
% cargo run --bin famic-wav-encoder -- "O4CDE" -o output.wav --dry-run

# デバイス指定して直接書き込み
% cargo run --bin famic-wav-encoder -- "O4CDE" --device "FAMIC"

以下の引数をパース。

  • mml: 第1トラックのMML文字列(必須)
  • --ch2--ch5: 第2〜第5トラック(任意、連続指定必須)
  • -o: 出力WAVファイルパス
  • --dry-run: WAV保存のみ
  • --device: 再生先デバイス名

STEP 2: MMLコンパイル

以下のように人間が読める楽譜を中間バイト列に変換する。

入力: "O4CDE"
       |
       ├─ O4  → オクターブ4 → octave_byte = 0xE4
       ├─ C   → ド(デフォルト四分音符=480tick)→ 0x31 (length_code=3, note_id=1)
       ├─ D   → レ → 0x33 (length_code=3, note_id=3)
       └─ E   → ミ → 0x35 (length_code=3, note_id=5)

出力: [0xE4, 0x31, 0x33, 0x35]  ← コンパイル形式

compile_mml() 関数の中では、
MML文字列を1文字ずつパースしながら機械的に処理します。

※note_byte の計算方法などはドキュメント参照

STEP 3: ユニバーサルエンコード

STEP 2 で得た中間コードを、最終バイトコードに変換します。
k 個の音符を入力すると、出力は 6+k バイトになります。

アルゴリズム: XOR参加マップ

出力の各バイトには、あらかじめ「ベース値(定数)」が入っています。
ここに音符データを XOR で混ぜ込むのがこのステップです。

ポイントは1つの音符が出力の1箇所ではなく、決まった複数箇所に影響することです。
たとえば1番目の音符 C は、出力の pos0, pos2, pos5, pos6 に書き込まれます。
2番目の音符 D は pos1, pos3, pos6, pos7 に書き込まれます。

その結果、出力の1バイトには複数の音符が重なっていることになります。
たとえば pos6 の値は ベース値 XOR C XOR D と、2つの音符が混合されています。

3音 (C, D, E) → 出力 8バイト の場合:

pos0 pos1 pos2 pos3 pos4 pos5 pos6 pos7
ベース値
C が混入 C C C C
D が混入 D D D D
E が混入 E E E
結果 ●⊕C ●⊕D ●⊕C⊕E ●⊕D ●⊕E ●⊕C ●⊕C⊕D ●⊕D⊕E

元の音符を出力から直接読み取ることはできません。
この「1つの入力を複数箇所に散らして XOR で混ぜる」パターンは、
通信分野で誤り検出やスクランブルによく使われる手法とのこと。

Claude Code曰く、「この規則を見つけるのが最も苦労した」。

STEP 4: ペイロード構築

固定のファームウェアテンプレート(2923バイト)の末尾に、
STEP 3 で生成した MML バイトコードを結合します。

[firmware_template 2923B] + [MMLバイトコード] → ペイロード

ただし単純結合ではなく、テンプレート内のヘッダー領域(5箇所)を
MMLの長さに応じて書き換えるヘッダー補正が必要です。
この補正値の計算式もバイナリの解析で特定しました。

STEP 5: スクランブル

ペイロードのビットパターンを規則的に並べ替えて、受信側でのクロック同期を安定させる処理です。
暗号化とは違い、同じ規則を知っていれば誰でも元に戻せます。

[固定seed 8B] + [scramble(ペイロード)] → スクランブル済みデータ

理論上は seed(初期値)が異なっても復号できるはずですが、
実機ではAPI と同じ固定 seed でないと受け付けませんでした。
この固定値を API の WAV から抽出して合わせたことで、実機書き込みに成功しています。

STEP 6: UARTフレーム化

バイト列を、1ビットずつ順番に送れる形式に変換します。
1バイトに start/stop/idle ビットを付けて 11 ビットのフレームにします。

1バイト → 11ビット:
 [start=1] [d0] [d1] [d2] [d3] [d4] [d5] [d6] [d7] [stop=0] [idle=0]

※FAMIC の UART はアイドル=0(反転)という仕様だった

STEP 7: FSK変調

ビット列を音の波形に変換するエンコード処理です。
FSK(Frequency Shift Keying)は 0 と 1 を異なる周波数の音で表現する方式です。

bit 1 (mark)  → 12,000Hz の矩形波 → サンプル: [0, 0, 255, 255]
bit 0 (space) → 24,000Hz の矩形波 → サンプル: [0, 255, 0, 255]

1ビットあたり 4 サンプル、サンプリングレート 48kHz なので 9600 ボーになります。

STEP 8: WAV信号構築

STEP 7 で生成した FSK サンプルの前後に、同期信号と無音区間を付加して完成形にします。

[プリアンブル: 2秒のキャリア信号 + 同期バイト列]
[無音: 1秒]
[アイドルキャリア + データ区間]
[末尾無音: 1秒]

9600 ボーで O4C(1音)の場合、総サンプル数は約 370,000(約 7.7 秒)になります。

STEP 9: 出力 — 「WAV保存 or デバイスへ直接再生」

最後に、組み立てた波形データを出力します。

  • WAVファイル保存 (write_wav()): 48kHz, 8bit unsigned PCM, mono の標準WAV形式で保存
  • デバイス直接再生 (play_wav_signal()): cpal クレートで音声デバイスに出力

全体のデータ量の変化

O4CDE (3音) の場合のデータサイズ変化:

ステップ データ サイズ
MML文字列 "O4CDE" 5文字
コンパイル形式 [0xE4, 0x31, 0x33, 0x35] 4バイト
MMLバイトコード XORエンコード結果 9バイト (6+3)
ペイロード firmware + MMLバイトコード 2932バイト
スクランブル済み seed + scrambled 2940バイト
UARTビット列 2940 × 11ビット 32,340ビット
FSKサンプル 32,340 × 4サンプル 129,360サンプル
完成WAV プリアンブル + 無音 + データ + 無音 ~370,000サンプル (≈7.7秒)

5文字の入力が約37万サンプルの音声となる.
(ヘッダー、暗号化、フレーミングのオーバーヘッド)。

About Python Script

リバースエンジニアリングの各段階で専用のPythonスクリプトを作成しました。
各スクリプトは「仮説を立て、公式のサンプルファイルを使って仮説を検証する」
というサイクルで使っています。

スクリプトの役割

スクリプトは大きく3種類に分かれます。

  1. サンプル収集 — 公式 API に MML を送って正解 WAV を取得する
  2. 仮説検証 — 取得した WAV をデコードし、仮説どおりのバイト列か照合する
  3. 回帰テスト — Rust 実装の出力と正解 WAV をバイト単位で比較する

これらを組み合わせて、以下のサイクルを機能ごとに回しました。

サンプル収集  →  仮説検証  →  Rustに実装  →  回帰テスト
     ↑                                        |
     └──────── 不一致があれば戻る ───────────────┘

複数のスクリプトがあり、対象ごとに分かれています
(ファームウェア解析、エンコードアルゴリズム、テンポ/音色等の個別コマンド、マルチトラック、拡張音長・ループ)。

このあたりのスクリプト作成や検証サイクルについては、
こちらから具体的な指示は出してません。
「正式なwavとプログラムで生成したwavを単音で比較してみれば?」
とだけ私が指示したあと、Claude Codeが上記スクリプトを作成して検証しました。

Implementation in Rust

実機への書き込みフロー

FAMIC WRITER は PC に USB で接続しますが、
PCからは普通のスピーカーやヘッドホンと同じ「音声出力デバイス」として認識されます。
データの送り方は「ファイルをコピーする」ではなく、「音を鳴らす」です。

PC ──「音を再生」──→ FAMIC WRITER ──「音を解読」──→ 音源チップに書き込み

PC側では cpal という、Rustでオーディオ入出力を扱うためのcrateを使ってFAMIC WRITER に向けて「音」を再生します。

実際に再生される音は人間にはノイズに聞こえますが、
FAMIC WRITER はこの音の中からデータを読み取り、曲データとして保存します。

コード上の処理は3ステップです。

[1] MML → WAV データ生成
    "O4CDE" という文字列から、デバイスが解読できる
    「音のデータ」(WAV 形式)を作る

[2] 出力先デバイスを探す
    PC に接続されている音声出力デバイスの一覧から
    FAMICデバイスを探す

[3] 音声を再生する
    見つけたデバイスに向けてWAVを再生する
    → デバイスが音を解読して曲データを書き込む
    → 再生が終わるまで待機して完了

一般的な USB デバイス(USBメモリなど)のように
専用のデータ転送プロトコルを使っているわけではなく、
音声出力として音を流すだけで書き込みが完了します。

Summary

本稿ではFAMIC on USBに対してRustプログラムで書き込みをするための
調査過程や実装内容について紹介しました。
実質私がやったのは方針決めと途中の軌道修正やチェックだけで、
実作業はすべてClaude Code / Codexによる作業でした。

2025/2時点(Sonnet 3.7)で依頼してできなかったことが、
現時点(Opus 4.6)では数時間で実現でき、AIの進化に驚くばかりです。

用語説明

  • MML:
    音楽を文字列で書く記法。O4CDE のように音高・音長・テンポなどを指定する。
  • MMLバイトコード:
    MML文字列を、デバイスが解釈しやすいバイナリ形式に変換したもの。
  • ファームウェアテンプレート:
    書き込みデータの固定部分。ここにMML由来の可変データを埋め込む。
  • ペイロード:
    実際に送信したい本体データ(テンプレート + MMLデータ)。
  • ヘッダー補正:
    ペイロード長などに合わせて、ヘッダー内の特定バイトを計算し直す処理。
  • delta:
    ヘッダー補正に使う差分値。通常は MMLバイトコード長 - 基準長
  • スクランブル:
    送信データを規則的に混ぜる処理。受信側は同じ規則で元に戻せる。
  • デスクランブル:
    スクランブル済みデータを元のデータへ戻す処理。
  • seed(シード):
    スクランブル処理の初期値。値が違うと結果バイト列も変わる。
  • UART:
    1ビットずつ順番に送るシリアル通信方式。
  • UARTフレーム:
    1バイトを start/data/stop などのビットに分解した送信単位。
  • UARTビット列化:
    バイト列をUARTフレームの連続ビット列へ変換すること。
  • FSK:
    0/1を周波数の違いで表す変調方式。
  • FSK音声化:
    ビット列をFSKルールで波形サンプルに変換すること。
  • write_wav():
    サンプル配列をWAVファイルとして保存するRust関数。
  • play_wav_signal():
    生成したサンプルを指定デバイスへ直接再生するRust関数。
  • 矩形波:
    高い値/低い値を切り替える四角い波形。今回の信号は主に 0/255 を使う。
  • プリアンブル:
    本データ送信前に置く同期用の先頭信号。
  • 8bit unsigned PCM:
    WAVのサンプル形式。1サンプルが 0..255 の整数で表現される。
  • 48kHz:
    1秒あたり48,000サンプルで音声を記録・再生する設定。
  • SPB (samples per bit):
    1ビットを何サンプルで表すか。変調精度に直結する値。
  • baud(ボーレート):
    1秒あたりの通信シンボル数。今回の主な設定は9600。
  • fixture:
    テストで使う固定入力・固定期待値データ。
  • PPQN (Pulses Per Quarter Note):
    四分音符あたりのtick数。FAMIC では 480。音長の分解能を決める値。

References

この記事をシェアする

FacebookHatena blogX

関連記事