前回の記事からの続きです。
前回までで、RaspberryPiに接続して音源カードのメモリを読み書きするハードウェアの準備ができました。
これから、そこに書き込むためのメモリのファイルを準備します。
USBメモリや高機能なマイコンを搭載した機器、例えばmp3プレイヤーなどの場合、
Windowsなどで"ファイルシステム"、例えばFATなどでフォーマットし、
ファイル名を持ったファイルがおかれることが多いですが、
このような機器の場合、フラッシュメモリ内の領域に、ファイルシステムのない領域が広がっているだけ、ということが多いです。
初回のところで吸い出しのために構造体を作って確認していましたが、
書き込むとなるとさらにファイル構造の厳密な検定を行う必要があります。
なぜなら、我々はある仕様に従って作成されたファイルを持っているだけで、
このファイルがどういう仕様で作成されたか、ということを知ることができないため、
意味のあるデータのほかに、「不明」や「マジックナンバー」そして、「マジックナンバーとして扱う」と推測する、などといった部分が多く発生します。
そういった部分は、自分がこの機器を作るのであれば、どのようなデータ構造にするべきか、などといったことをよく考えながら検証する必要があります。
わからない部分は、「マジックナンバーとして固定」し、それでうまくいけば良し、とするしかないのです。
では、改めてファイル構造についてチェックしていきましょう。
デフォルトのカードから吸い出したメモリの先頭部分です。
このバイナリダンプを見ながら、どんな情報が埋まっているか考えていきます。
数値として意味を持つのは2バイトおよび4バイトからが多いので、そのぐらいのブロックに分けて読んでいきます。
ファイルの先頭に多いのは、そのファイルがなんであるかを表す文字が埋め込まれていることが多いです。例えば、ビットマップファイルなら"BM"、JPEGファイルなら"JFIF"などです。しかし、ここにはありません。
ただし、AA,55というバイト列は、ネットワーク通信やファイルシステムなどでは時々使われることがあります。0xAAは2進数にすると"10101010"、0x55は"01010101"と、それぞれビットパターンが交互に現れ、また、この2バイトをxorすると"0"になることから、同期信号や高速に通信経路のエラーが無いことを検出したりすることに使用できるからです。このことから、この先頭は、データ確認のためのマジックナンバーであると「仮置き」しておきます。続いての4バイトから、16バイト目まで。
なんとなく意味はありそうですが、この時点では不明ですのでやはり「仮置き」して次に行きます。
17バイト目からの4バイト"00 04 00 00”、また、その次の4バイトは"00 C4 15 00"はそれぞれリトルエンディアンで読むと"0x400","0x15C400"になります。何か意味がありそうですので、このアドレスにジャンプしてみましょう。
特徴的なデータ列が現れました。0x15C400も見てみると
こちらも同じ感じです。先頭はさっきとよく似た「55 AA 01 00」で始まります。
さて、ここでこの機器の仕様を思い出します。この機器は、チャンネル切り替えスイッチで2曲のうちどちらかを選んで再生するのです。
このアドレス二つに書かれたデータを曲として読み込んで再生する、という動作のために作られた、と矛盾なく推測できると思います。
ここで、なんとなく全体の構造を見ると
・先頭1kB→データ全体のヘッダ。曲情報1、曲情報2へのアドレステーブル
・曲情報1ヘッダ
・曲情報2ヘッダ
までがわかりました。
では、曲情報ヘッダを改めて見ます。2つを見比べるのがいいです。
先頭8バイト、4バイトのマジックナンバーに続き00 80 FF FF 。意味がありそうですが、二つとも同じ値ですのでとりあえず共通のマジックナンバーとしておきます。
1曲目は"00 80 00 00"(0x800) 、"FF BF 15 00"(0x15BFFF)が、
2曲目は"00 C8 15 00"(0x15C800)、"FF 57 27 FF"(0x2757FF)が意味のある数字として読み取れます。それぞれ、
FFが終わり実データが始まる部分、そして終わる部分であることがわかります。
つまり、このヘッダには曲の実データの最初のアドレスと最後のアドレスが書かれている、ということが確認できました。
今回はかなり簡単なデータ構造であったため(そもそも搭載しているマイコンにあまり複雑なことができないレベルのため、これ以上の情報も必要とされていません)、割と簡単に解析できました。本当に重要な、例えば課金情報などが保存されるようなストレージであれば、読み出せない領域があったり、それを使って暗号化されたりしている場合がありますが、そのようなことは無いようです。
さて、この情報をもとに、曲データを切り出します。
最初の検証の時点では、手作業で切り出しましたが、今後差し替えを行いますので
そのルーチンと組み合わせてテストに使用しますので、プログラムを書いていきます。
構造体のままのベタファイル入出力があるので、C++で書きたいところですが、
プロトタイピングとしてはやはりC#の駆け足が捨てきれませんので、C#で書き始めます。
解凍ルーチンはたったこれだけです。C#とGenericsすごいですね。
public void Unpack(string s)
{
byte bs = System.IO.File.ReadAllBytes(s);
UInt32 off1 = System.BitConverter.ToUInt32(bs.Skip(16).Take(4).ToArray());
UInt32 off2 = System.BitConverter.ToUInt32(bs.Skip(20).Take(4).ToArray());
byte song1 = bs.Skip*1.ToArray();
byte song2 = bs.Skip((int)off2).ToArray();
UInt32 start1 = System.BitConverter.ToUInt32(song1.Skip(8).Take(4).ToArray());
UInt32 stop1 = System.BitConverter.ToUInt32(song1.Skip(12).Take(4).ToArray());
song1 = song1.Skip(0x400).Take((int)(stop1 - start1 + 1)).ToArray();
UInt32 start2 = System.BitConverter.ToUInt32(song2.Skip(8).Take(4).ToArray());
UInt32 stop2 = System.BitConverter.ToUInt32(song2.Skip(12).Take(4).ToArray());
song2 = song2.Skip(0x400).Take((int)(stop2 - start2 + 1)).ToArray();
System.IO.File.WriteAllBytes(s + ".1.songblock", song1);
System.IO.File.WriteAllBytes(s + ".2.songblock", song2);
}
これで、吸い出したイメージから曲が2曲抽出できました。
改めて、この吸い出した曲のフォーマットを確認します。
前の記事でで、ADPCMっぽいということはわかっていましたが、Goldwaveで開いた波形を見るとちょっと変です。
先頭、約0.5秒は無音区間のはずですが、なぜか少しずつDC成分が詰みあがっていっています。ADPCMは初期データとその変化量を記録している方式ですが、実はいくつもの方式が開発されていて、それらは変化量などのテーブルが少しづつ違っているのです。おそらく、なんとなく音声としては再現されていますが、そのテーブルの違いのため正しいデータが再現されていません。これは困りました。
ADPCMを扱えるいろいろなツールを使い、正しくデコードできる形式を探していきます。
最終的に、Linuxで動作する"sox"というツールに、以下のようなコマンドラインを渡すと、どうやら正しいwaveファイルに変換できる、ということにたどり着きました。
sox -t ima -r 44100 -e oki-adpcm infile.bin outfile.wav
このコマンドで変換したファイルを開くと…
先頭の無音部分がきれいにフラットになり、曲全体のダイナミックレンジも問題なさそうです。
つまり、WAVEファイルをもとにこの逆変換を行い、Flashに書き込めば、きちんと再生できそう、という確信ができました。
逆変換のコマンドは、
になります。これで得られたファイルを書き込むためのルーチンを書いていきます。
public void Pack(string outfile)
{
try
{if(!System.BitConverter.IsLittleEndian)
{
throw new Exception("Environment exception(Endian)");
}
byte ch1 = System.IO.File.ReadAllBytes(textBoxSong1.Text);
byte ch2 = System.IO.File.ReadAllBytes(textBoxSong2.Text);
byte chs = new byte { ch1, ch2 };
System.IO.MemoryStream ms_gheader = new System.IO.MemoryStream();
ms_gheader.Write(new byte {
0xaa,0x55,0x01,0x00,
0x00,0x01,0x64,0x00,
0x00,0x00,0x00,0x00,
0x01,0x00,0x02,0x00
});
int arignment = 1024;
int ch1_length = (ch1.Length%arignment==0)?ch1.Length:*2;
ms_gheader.Write(System.BitConverter.GetBytes*3;ms_gheader.Write(pad);
for (int x = 0; x < 0x400-80; x++)
{
ms_gheader.WriteByte(0x00);
}
System.IO.MemoryStream song = new System.IO.MemoryStream();
for (int n = 0; n < 2; n++)
{
song.Write(songheaders[n].magic);
song.Write(songheaders[n].reserverd);
song.Write(System.BitConverter.GetBytes*4;
song.Write(System.BitConverter.GetBytes*5;for(int x = 0; x < 0x400 - 16; x++)
{
song.WriteByte(0xff);
}song.Write(chs[n]);
for(int p = 0; p < ch_padded_length[n] - chs[n].Length; p++)
{
song.WriteByte(0x80);
}
for (int x = 0; x < 0x400; x++)
{
song.WriteByte(0xff);
}
}ms_gheader.Write(song.ToArray());
while (ms_gheader.Length<1024*1024*4)
{
ms_gheader.WriteByte(0xff);
}
System.IO.File.WriteAllBytes(outfile, ms_gheader.ToArray());}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
こんな感じになります。
勢い重視コーディングのため、もっとちゃんとすればわかりやすくなると思いますが、めんどくさいのでこれからもたぶんこのままです。
先ほど推測したヘッダ情報を作成するほかに、おそらく必要であると思われる1kb単位へのデータの切り上げ、パディングなど、元のフォーマットと同じになるように細かな調整を行っています。なぜなら、仕様書が無いため、元のフォーマットとどれだけ変えてもきちんと通るかはまったくわからないからです。
Unpackで取り出した2曲をPack関数に入れ、元のファイルと全く同じファイルが出来上がることが確認できました。
さぁ、実際に入れる曲を作ります。
息子の希望は「ドン・キホーテの曲」。
曲名は「ミラクルショッピング」です。お店にCDが売っているらしいですよ。
ねぇ、知ってる…?(◎ё◎)ドンドンドン、ドン・キ~♪お店にいつも流れているこの曲は「ミラクルショッピング」ドンキのテーマソングなのダ。なんとドンキのCDコーナーで販売中♪ドンドンドン~(´・ё・`) pic.twitter.com/7cwlD2jJ
— 驚安の殿堂 ドン・キホーテ🐧 (@donki_donki) February 25, 2012
ドン・キホーテの曲を歌っている方本人出演のPVがありますので、今まで見たことない方はぜひ見てくださいねw
Youtubeで検索してmp3に変換して、もともとの音源カードと同じ範囲を切り出します。
切り出せたら先ほどのsoxコマンドで変換します。
チャンネル2には、いつものポポーポポポポを入れる、とことです。
自作プログラムにセットして「Pack」すると、4MBのバイナリファイルが出来上がります。
これを、前の記事でセットアップしたraspberry piに転送し、flashromコマンドで書き込むと…
エンコードパラメータ調整してボリュームバランス取れた pic.twitter.com/oWoxPCg4qF
— ひろみつ (@bakueikozo) December 8, 2022
できた!できたよ!ドン・キホーテの音源カードだよ!!!!
と息子に持っていくと…反応薄いなw
むしろ、作るって言ってから何日かかったんだよっていう顔してるけどww
とはいえ、できたからにはさんざん遊んでくれているようです。
今日も朝から呼び込み君自宅ガチ勢 pic.twitter.com/8PgqanUM6U
— ひろみつ (@bakueikozo) December 7, 2022
作っているところ、時々脇で見てましたので、メルカリで2600円で買ったものをほいって与えるよりは何か、与えられるものがあったとは思っています。
毎日うるせぇなあwwwwwwww
さて、とりあえず一つできましたが、例の謎の呼び込み君マニアのLINEグループからはこれをサッサとよこせとさんざん言われているため、もう少し手離れのいい形にしなければなりません。現状、PCとRaspberryPi,そして空中配線の書き込みスロットでは、ぶっちゃけ小中学生の取り扱いでは寿命は数時間でしょうw
せめて書き込み機はUSB-SPIかなんかでまとめて、PC単体で書けるぐらいにしておきたいなぁ。
*1:int)off1).Take((int)(off2-off1
*2:ch1.Length + arignment - 1) / arignment * arignment);
int ch2_length = (ch2.Length % arignment == 0) ? ch2.Length : ((ch2.Length + arignment -1 ) / arignment * arignment);
int ch_padded_length = { ch1_length, ch2_length };
int offsetof_ch1 = 0x400;
int offsetof_ch2 = 0x400 /* footer padding */ + 0x400 /* global header */ + (ch1_length + 0x400);
songheader songheaders = new songheader[2];
songheaders[0] = new songheader();
songheaders[0].start =(UInt32) offsetof_ch1+0x400;
songheaders[0].stop = (UInt32)(songheaders[0].start + ch_padded_length[0] -1);
songheaders[1] = new songheader();
songheaders[1].start = (UInt32)offsetof_ch2+0x400;
songheaders[1].stop = (UInt32)(songheaders[1].start + ch_padded_length[1] - 1);
byte[] pad = new byte[16 * 3 + 8];
Array.Fill<byte>(pad, 0xff);
ms_gheader.Write(System.BitConverter.GetBytes((int)offsetof_ch1
*3:int)offsetof_ch2
*4:int)songheaders[n].start
*5:int)songheaders[n].stop