OBONO’s Diary

へっぽこプログラマの戯言

PC-E550 音声データ解析大作戦

序章

かつて、我が家に一本のカセットテープがあった。

子供の頃、サンタさんからもらった SHARPポケコン PC-E550。そのプログラムのデータが保存されていたメディアである。
当時、お小遣いを叩いてポケコンカセットデッキを繋ぐための周辺機器を購入し、プログラムを音声データに変換してセーブしたりロードしたりしていた。あの頃が懐かしい…

大人になって、ポケコンRS-232C 通信用変換回路を自作したのを機に、カセットテープの中のプログラム達をパソコンに引き上げたのだが、残念ながら、いくつかのプログラムは読み込む事ができなかった。
ひとつは明らかに音声が途中で一瞬途切れており、ポケコンはその時点で "I/O Error" と表示して諦めてしまう。その他のいくつかは、セーブした時のカセットデッキの特性の違いからか、そもそもポケコン側が音声をプログラムとして認識してくれない。
でも、いつかはこれらのプログラムを救い出す事ができたら……そんな事を思いつつ、大事に取っておいたカセットテープ。

ある時、カミさんが家の整理をしていて、カミさん保有のカセットテープが大量に出てきた。このまま処分するには忍びないと、約一年前に購入したのがコレ。

カミさんのカセットテープを mp3 化した際、ついでに自分のカセットテープも mp3 化しておいたんだよね。
mp3 化する事で波形の劣化が心配されたが、ロードが成功する実績があったプログラムはパソコン経由でも問題なくロードできたので、気にするほどの影響はないだろう。ということで、思い出のカセットテープはゴミ箱の中に。


これから話すのは、その音声データから数々の思い出が甦った、そんなお話。

それは突然やってきた

何かの流れで、ポケコンについての話題に触れる機会があったので、なんとなく、

とツイートしてみたところ、とのリプライが。

早速ウェブサイトをチェックして、PDF ファイルに目を通したところ、以下のような変調ルールという事が判明。なんだ、意外とシンプルじゃん。
f:id:OBONO:20190412235433g:plain
その知識を踏まえて、音声ファイルの内容を眺めてみたところ、

という事で、これはイケそうだと確信し、本腰を入れて音声データの解析をする決意したのであった。

解析ツールの設計

解析ツールの基本的な流れはこんな感じ。


f:id:OBONO:20190411233604p:plain

ただ、各種変換処理でデータが壊れている可能性を考慮し、それぞれの中間生成物をファイルに出力したり、修復したデータを途中の処理から再開できるようにした。フローチャートにするとこんな感じ。


f:id:OBONO:20190411234351p:plain

音声データを01データに変換

仕様に基づき、以下のようなルールで '0' と '1' に復調するようにした。

  • 音声データを順次チェックし、中央より上のサンプル、中央より下のサンプルの数をそれぞれカウントしていく
  • 波形が、中央より上から下にまたがったら、
    • 2つのカウンタが両方とも 1~10 の範囲内 → '0'
    • 2つのカウンタが両方とも 10~30 の範囲内 → '1'
    • カウンタをリセットする

※サンプリングレート 44100 Hz、8 ビットモノラル前提

まぁ、ポケコンでも復調できるんだから、単純で然るべきだよね。

ただ、一部の音声は波形がだいぶ歪んでしまっていて、このルールでは太刀打ちできない事が判明。

幸い、'1' を示す波形はしっかりしているので、

  • 音声データを順次チェックし、波形の極小値と極大値の位置とサンプル値を控えておく
  • 極小値を検出したら
    • 直前の極大値からの時間差が 18±2 サンプル、かつサンプル値の差分が 80 以上であれば '1' とみなす
    • その際、前回検出した '1' との時間差から 36 を引いた値を 16 で割った値の回数だけ '0' が連続していたとみなす

※サンプリングレート 44100 Hz、8 ビットモノラル前提

というルールで復調するモードも用意した。

この処理では、音声データから0と1の羅列のテキストデータを出力する。見易いように、72文字ごとに改行するようにしておいた。

01データからバイト配列に変換

基本的には単純な変換処理なのだが、前の処理がうまくいかなかった場合を考慮しなければならない。
例えば、復調したデータの途中がこんな感じだったとする。

000010000000010000000010000000010010100110010110110010100010101100010011
001010010101010001110110000000010000000010011000010000000010000000010000
000010000000010010101110001110110000000010000000111001010110000000010000
000010000000010000000010010110010101100110011001010010101010001110110000
000010000000010010000010000000010000000010000000010000000010010101110001
110110000000010000000110001001010000000010000000100000000100000000100101
001100001101100000110101001010100100010101011000100110001100111101101011
000100110010100111010101011001100110001100111101101011001100110010100111

仕様書によると、「'1' が1つ、その後に '0'/'1' が8つで1バイト」となっているので、9個おきにに必ず '1' が来るはず。この事から「6行目がちょっとおかしいぞ」というのが分かる。ツールの方でも「何文字目に想定外の '0' が来ている」というのが分かるようにした上で、01データを眺めてちょっとずつ辻褄を合わせ、データを修復していく。大抵の場合、連続して '0' が表れるところで、'0' が1つ足りないことが多かった。

データが問題なければ、この0と1の羅列から、ルールに基づいてバイナリ配列を出力する。

バイト配列からプログラムコードに変換

実際に得られるバイト配列は、単純にプログラムの ASCII データが格納されているわけではなく、データ長を短くする等の諸々の事情でちょっと複雑になっている。詳細は割愛するが、以下の表を見れば大体想像はつくと思う。
f:id:OBONO:20190413000445g:plain
各行ごとにデータのバイト数が格納されているおかげで、データ欠けに対して「この行は何バイト欠けている」というのが分かる。これは非常に有用な情報だったりする。

例えば、こんな感じで 10 行目の … の部分が欠けている場合。

10 CLEAR :RANDOMIZE :DIM B(1),N(9),S(4),B$(3),C$( … ),D$(3)
20 INPUT "BEEP OFF(Y/N)?";K$:P=(K$="Y")+1:N(0)=10:N(9)=10
30 FOR I=0 TO 3:READ C$(I):FOR J=0 TO 3:B$(I)=B$(I)+C$(I):NEXT :NEXT

ここには数値が入るのは間違いないし、それ以降の処理を見れば、値はいくつが適切なのかが分かる。

10 CLEAR :RANDOMIZE :DIM B(1),N(9),S(4),B$(3),C$(3),D$(3)


また他の例として、以下のように 180 行目の末尾近く、結構重要そうな処理が派手に欠けてしまっている場合。

180 N(Y)=N(Y)+B(1-(A XOR B)):N(Y+1)=N(Y+1)+B(A XOR B):A=-(N(Y)<N( … -A
190 FOR I=1 TO N(B):GCURSOR (162-I*6,Y*4+3):GPRINT C$(3):NEXT :IF N(B)=N(Y+A) THEN 210
200 FOR I=N(B) TO N(Y+A)-1:GCURSOR (156-I*6,Y*4+3):GPRINT C$(A):NEXT
210 IF N(Y+A)>9 THEN 280

これも、プログラムの内容や以降の処理、そして欠けているバイト数から、こんな式が入るんだろうなというのを推察する。まるでパズルを解くような感覚。

180 N(Y)=N(Y)+B(1-(A XOR B)):N(Y+1)=N(Y+1)+B(A XOR B):A=-(N(Y)<N(Y+1)):B=Y+1-A


こんな感じでプログラムデータを修復しながら、最終的に ASCII なプログラムコードが得られた。

結末

このような地道な作業を繰り返した結果、最終的に12個のプログラム、合計約31KBのプログラムを復元することができた。
早速、ポケコン実機にシリアルケーブル経由で転送し動かしてみる。おおおお、ちゃんと動くではないか。四半世紀ぶりに実行したことになるのかな?
f:id:OBONO:20190413003207j:plain
f:id:OBONO:20190413003332j:plain
f:id:OBONO:20190413003856j:plain
んで、一通り動かしてみて感慨に浸った後は、時間をかけた割には大した成果は無かったなという虚無感だけが残った。

ちなみに、今回の解析ツールは Go 言語で書いた。
以前、Go 言語で何か作った際は、Go 言語とは関係ないところに労力を割いてしまい、イマイチ Go 言語の練習にはならなかったのだが、今回の活動では割と Go 言語の練習になったと思う。せっかく慣れ始めてきた感じなのだが、今後も Go 言語で何かやり続けないと、すぐに忘れてしまいそうだ。


おしまい。