はじめに
ネットを眺めていたら、昔のマイコンのミニチュア筐体を作り続けている人のブログに行き当たりました。手のひらサイズのMZやPC-8001が並ぶ写真を見ているうちに、高校時代パソコンショップの片隅で、友人と交代しながら月刊I/Oの16進ダンプリストを打ち込んでいたことを思い出しました。
打っていたのはHEAD ONだったと思います。とにかく早く動かして遊びたくて、セーブという工程を省いてそのままRUNしていました。案の定、どこかで打鍵ミスをしていて動きません。セーブしていないのですから、直しようがありません。ため息をついて、また最初の1行目から打ち直す。あの頃はそれを何度も繰り返していました。
あの「ピー、ガガガガ……」という音は、冒頭の数秒を聴くだけで大体どのプログラムか見当がつきました。そういえばあの耳は今も残っているだろうか、と気になりました。
学校の実習では紙テープも読んでいました。あちらはあちらで、穴の並びを目で追えば内容の見当がついたものです。そういえば『ウルトラマン』の科学特捜隊の基地にも、紙テープを読み取る装置が出てきました。今はもう読めません。というより、紙テープ自体を見かけなくなりました。カセットテープの音のほうは、まだ手元で再現できるはずだと思い立ちました。
MZ-80K/C1は、BASICもOSも標準搭載せず、電源を入れるとモニタプログラムだけが立ち上がる「クリーンコンピュータ」として知られる機種です。プログラムの保存先はカセットテープで、LOAD時にあの音がスピーカーから流れます。
任意のバイナリファイルを、MZ-80K/C1が実際にLOADできる形式のWAVファイルへ変換するPythonスクリプトを書きました。ロジックは実機での動作実績があるC言語実装(jfjlaros/mzf2wav)を参照し、生成したWAVがバイト単位で完全一致することを確認しています。
MZ-80Kのテープフォーマットとは
MZ-80K/C1系は「シャープ方式」と呼ばれる独自のパルス幅変調(PWM)方式でデータをテープに記録します。周波数偏移変調(FSK)ではなく、パルスの幅の長短でビットを表現する点が特徴です。
ビットの表現
1ビットは「HIGH区間→LOW区間」の1周期として記録されます。
-
ロングパルス = ビット
1 -
ショートパルス = ビット
0
パルス幅の目安(マイクロ秒単位、HIGH側・LOW側の内訳)は以下の通りです。
| 種別 | HIGH | LOW | 合計周期 |
|---|---|---|---|
ロングパルス(1) |
464μs | 494μs | 958μs(約1044Hz相当) |
ショートパルス(0) |
240μs | 264μs | 504μs(約1984Hz相当) |
1バイトはMSBファーストの8ビット+区切り用のロングパルス1個、という構成で記録されます。
テープ全体の構造
MZ-80Kのテープは「ヘッダブロック」と「ボディブロック(プログラム本体)」の2部構成で、それぞれ2回連続で書き込まれる冗長構成になっています。読み取りエラー対策として、1回目がダメでも2回目で救済できるようにする狙いです。
[ギャップ 22000ショートパルス]
[テープマーク(ロング40+ショート40+ロング2)]
[128バイトヘッダ] [チェックサム]
[ギャップ 256ショートパルス]
[128バイトヘッダ(2回目)] [チェックサム]
[ギャップ 11000ショートパルス]
[テープマーク(ロング20+ショート20+ロング2)]
[プログラム本体] [チェックサム]
[ギャップ 256ショートパルス]
[プログラム本体(2回目)] [チェックサム]
ギャップは「無音区間」ではなく、ショートパルスの単純な連続です。テープの走行が安定するまでの助走区間、いわば「せーの」の合図として機能します。ヘッダ側は22000パルスと長め、ボディ側は11000パルスとやや短めに設定されているのは、ヘッダ読み込み後のテープ走行がすでに安定していることを前提にしているためと考えられます。
128バイトヘッダの中身
ヘッダにはファイル名・サイズ・ロードアドレス・実行アドレスなどが入っています。この構造を持つファイル形式は「MZF」と呼ばれ、多くのMZ系エミュレータで標準的に扱われています。
| オフセット | 内容 |
|---|---|
| 0x00 | 属性(プログラム種別) |
| 0x01〜0x11 | ファイル名(17バイト、Shift_JIS、余白は0x0Dで埋める) |
| 0x12〜0x13 | ファイルサイズ(リトルエンディアン) |
| 0x14〜0x15 | ロードアドレス |
| 0x16〜0x17 | 実行アドレス |
| 0x18〜 | コメント欄 |
チェックサム
各バイトに含まれる1ビットの個数を全バイト分合計し、16ビット値としてブロック末尾に付加します。上位バイト→下位バイトの順で書き込まれ、16ビットを超えた分はラップアラウンドします。
実装のポイントと、実際にハマった2つのバグ
C言語のリファレンス実装を読み解きながらPythonに移植したのですが、素直に「HIGH区間っぽい値だからTrueだろう」で実装すると普通に嵌まります。実際に踏んだ2つの罠を紹介します。
罠1: パルス幅の丸め方
パルス幅はマイクロ秒単位の定数(464など)をサンプルレートに応じてサンプル数に変換します。最初、この変換にround()を使っていました。
def scale_us(microseconds, sample_rate):
return round(microseconds * sample_rate / 1_000_000)
一見自然な実装ですが、リファレンスのC実装ではここは**整数除算(切り捨て)**でした。
static uint16_t scale_(uint16_t const length, uint32_t const bitrate) {
return length * bitrate / 1000000;
}
round()と//(切り捨て)では、44100Hzサンプリング時に4区間中3区間で1サンプルのズレが生じます。1サンプルのズレなど誤差の範囲に思えるかもしれませんが、これが数万パルス積み重なると累積誤差になり、実機のタイミング判定基準(ロング/ショートの中間閾値)からズレていく可能性があります。実機互換を狙うなら、丸め方まで参照実装に合わせる必要がありました。
罠2: HIGH/LOWの論理値が名前と逆
C実装の該当部分を見てみます。
static void writeLongPulse_(FILE *const output, uint32_t *size, PCP pulseConfig) {
for (uint16_t i = 0; i < pulseConfig->longPulse.high; ++i) {
writeBit(output, false, pulseConfig->invert); // "high"区間なのにfalseを書く
}
for (uint16_t i = 0; i < pulseConfig->longPulse.low; ++i) {
writeBit(output, true, pulseConfig->invert); // "low"区間なのにtrueを書く
}
...
}
writeBit(output, false, ...)が呼ばれているのはhighという名前のカウント区間で、writeBit(output, true, ...)が呼ばれるのはlowという名前の区間です。つまり構造体のフィールド名high/lowは「パルスの前半・後半どちらの時間長か」を表すラベルであって、そこに書き込まれる論理値そのものとは直結していません。
最初の実装ではこの点を見落とし、high_level=Trueのときに0xFFを出力する素直な実装にしてしまいました。結果、生成したWAVの最初のサンプルがリファレンス実装と逆極性になり、cmpで1バイト目から差分が出ました。
def _write_pulse(self, high_count, low_count):
# 参照実装writeLongPulse_/writeShortPulse_の呼び出し順に合わせる:
# 「high」区間ではwriteBit(false)、「low」区間ではwriteBit(true)を書く
# (変数名high/lowはパルスの2フェーズを指すラベルであり、論理値と直結しない)。
self.buf.extend(bytes([self._level_byte(False)]) * high_count)
self.buf.extend(bytes([self._level_byte(True)]) * low_count)
検証方法: リファレンス実装とバイト単位で突き合わせる
自分で書いたデコーダで自分のエンコーダの出力を復調して一致確認する、という検証は一見良さそうですが、実は弱い検証です。エンコーダとデコーダが同じ思い込み(ビット順序の勘違いなど)を共有していれば、両者が仲良く同じ間違いをして「一致」と判定されてしまうからです。
そこで、リファレンスのC実装を実際にビルドし、同一のMZFファイル・同一パラメータで生成したWAVをcmpでバイト単位比較しました。
gcc -O2 -o mzf2wav_ref argparse.c methods.c mzf.c mzf2wav.c wav.c write.c
./mzf2wav_ref -c -n 0 -b 44100 test.mzf ref.wav
python3 mzf2wav.py mzf2wav test.mzf test_py.wav --rate 44100
cmp test_py.wav ref.wav
先述の2つのバグを修正した後、サンプルレート(44100Hz / 22050Hz)、極性反転あり/なし、データサイズの異なる複数パターンで完全一致を確認しました。動作実績のあるツールと出力が同一であることは、自作デコーダでの自己検証よりずっと強い根拠になります。
使い方
# 任意のバイナリファイルからMZ-80K/C1用のテープWAVを作る
python3 mzf2wav.py bin2wav myprogram.bin myprogram.wav --name PROGRAM --load 0x1200 --exec 0x1200
# 既存のMZFファイル(エミュレータ用イメージ)をWAVに変換
python3 mzf2wav.py mzf2wav myprogram.mzf myprogram.wav
ファイル名は17文字までのShift_JIS、ロードアドレス・実行アドレスは0x1200のような16進表記で指定できます。プログラムのバイナリだけでなく、テキストファイルや画像など任意のバイナリをそのまま「MZ-80Kが読み込める音」に変換できます(もちろん実機でLOADしてプログラムとして実行できるかは中身次第です)。
実際に試しに、あるMarkdown設計書ファイル(約39KB)をエンコードしてみたところ、約530秒(8分50秒)のWAVファイルになりました。ファイルサイズは約23MB、元の600倍近くに膨れ上がっています。実機の転送速度は現代の感覚だとかなりゆっくりです。
おわりに
MZ-80K/C1のテープフォーマットは、口伝や実測ベースの情報が多く、一次情報源として一番信頼できたのは実際に動くコードでした。今回は幸い、実機での動作実績を持つオープンソース実装(jfjlaros/mzf2wav)が見つかり、それをPythonに移植してバイト単位で照合するというアプローチが取れました。
あの「ピー、ガガガガ……」という音を令和の時代に任意のファイルから再現できるようになったので、興味のある方はぜひ手元の実機やエミュレータで試してみてください。
次は1200ボーのモデムの音でも再現してみるかな、と思っています。
コード
mzf2wav.py(変換ツール本体)
#!/usr/bin/env python3
"""
MZ-80K/C1系(シャープ方式)カセットテープ音声生成ツール。
MZFイメージ(128バイトヘッダ + プログラム本体)を、実機のMZ-80K/C1が
LOAD可能なWAVファイルに変換する。任意のバイナリファイルからMZFイメージを
新規作成する機能も含む。
信号仕様は jfjlaros/mzf2wav (https://github.com/jfjlaros/mzf2wav) の
conventional mode 実装に準拠する(実機での動作実績があるパルス幅を採用)。
パルス幅(マイクロ秒): long=(464,494) short=(240,264)
- ロングパルス = ビット'1'、ショートパルス = ビット'0'
- 1バイトは MSBファーストの8ビット + 区切りのロングパルス1個
テープ構造:
ヘッダ: ギャップ22000短パルス → テープマーク(40長+40短+長2) →
128バイトヘッダ → チェックサム → ギャップ256短 →
128バイトヘッダ(2回目) → チェックサム
ボディ: ギャップ11000短パルス → テープマーク(20長+20短+長2) →
データ本体 → チェックサム → ギャップ256短 →
データ本体(2回目) → チェックサム
チェックサム: 各バイトの1ビット数の総和(16bit)。上位バイト→下位バイトの順で書く。
"""
import argparse
import struct
import sys
from pathlib import Path
HEADER_SIZE = 128
HEADER_TAPE_MARK_LEN = 40
BODY_TAPE_MARK_LEN = 20
HEADER_GAP_LEN = 22000
BODY_GAP_LEN = 11000
INTER_COPY_GAP_LEN = 256
# (long_high, long_low, short_high, short_low) 単位:マイクロ秒
NORMAL_PULSE_US = (464, 494, 240, 264)
def scale_us(microseconds, sample_rate):
# 参照実装(jfjlaros/mzf2wav)の scale_() は整数除算(切り捨て)。
# round()にすると1サンプルずれ、実機互換性が崩れるため合わせる。
return microseconds * sample_rate // 1_000_000
class PulseWriter:
"""サンプル単位で0x00/0xffを書き出すバイト列ビルダー。"""
def __init__(self, pulse_us, sample_rate, invert=False):
lh, ll, sh, sl = pulse_us
self.long_high = scale_us(lh, sample_rate)
self.long_low = scale_us(ll, sample_rate)
self.short_high = scale_us(sh, sample_rate)
self.short_low = scale_us(sl, sample_rate)
self.invert = invert
self.buf = bytearray()
def _level_byte(self, bit):
# 参照実装のwriteBit()に合わせる: bit!=invert なら0xff、そうでなければ0x00。
return 0xFF if (bit != self.invert) else 0x00
def _write_pulse(self, high_count, low_count):
# 参照実装writeLongPulse_/writeShortPulse_の呼び出し順に合わせる:
# 「high」区間ではwriteBit(false)、「low」区間ではwriteBit(true)を書く
# (変数名high/lowはパルスの2フェーズを指すラベルであり、論理値と直結しない)。
self.buf.extend(bytes([self._level_byte(False)]) * high_count)
self.buf.extend(bytes([self._level_byte(True)]) * low_count)
def write_long(self):
self._write_pulse(self.long_high, self.long_low)
def write_short(self):
self._write_pulse(self.short_high, self.short_low)
def write_gap(self, n):
for _ in range(n):
self.write_short()
def write_tape_mark(self, n):
for _ in range(n):
self.write_long()
for _ in range(n):
self.write_short()
self.write_long()
self.write_long()
def write_byte(self, value):
ones = 0
for i in range(8):
if value & (0x80 >> i):
self.write_long()
ones += 1
else:
self.write_short()
self.write_long()
return ones
def write_block(self, data):
checksum = 0
for b in data:
checksum += self.write_byte(b)
self.write_checksum(checksum)
def write_checksum(self, checksum):
self.write_byte((checksum >> 8) & 0xFF)
self.write_byte(checksum & 0xFF)
self.write_long()
def image_size(image):
return image[0x12] | (image[0x13] << 8)
def check_image(image):
if len(image) < HEADER_SIZE:
return False
if image[0] == 0:
return False
return len(image) == image_size(image) + HEADER_SIZE
def build_mzf_header(name, size, load_addr, exec_addr, comment=b"", attr=0x01):
"""128バイトのMZFヘッダを構築する。"""
header = bytearray(HEADER_SIZE)
header[0] = attr
name_bytes = name.encode("shift_jis", errors="replace")[:17]
name_bytes = name_bytes + b"\x0d" * (17 - len(name_bytes))
header[1:18] = name_bytes
header[0x12:0x14] = struct.pack("<H", size)
header[0x14:0x16] = struct.pack("<H", load_addr)
header[0x16:0x18] = struct.pack("<H", exec_addr)
comment = comment[:104]
header[0x18:0x18 + len(comment)] = comment
return bytes(header)
def make_mzf_from_binary(data, name="BASIC", load_addr=0x1200, exec_addr=0x1200,
comment=b"", attr=0x01):
"""任意のバイナリデータからMZFイメージ(ヘッダ+本体)を生成する。"""
header = build_mzf_header(name, len(data), load_addr, exec_addr, comment, attr)
return header + data
def sanitise_image(image):
"""先頭のMZF1タグ(エミュレータ用の識別子)を除去する。"""
tag = b"MZF1"
if image[:len(tag)] == tag:
return image[len(tag):]
return image
def conventional_format(image, pulse_us, sample_rate, invert):
pw = PulseWriter(pulse_us, sample_rate, invert)
pw.write_gap(HEADER_GAP_LEN)
pw.write_tape_mark(HEADER_TAPE_MARK_LEN)
pw.write_block(image[:HEADER_SIZE])
pw.write_gap(INTER_COPY_GAP_LEN)
pw.write_block(image[:HEADER_SIZE])
body = image[HEADER_SIZE:HEADER_SIZE + image_size(image)]
pw.write_gap(BODY_GAP_LEN)
pw.write_tape_mark(BODY_TAPE_MARK_LEN)
pw.write_block(body)
pw.write_gap(INTER_COPY_GAP_LEN)
pw.write_block(body)
return bytes(pw.buf)
def write_wav(path, pcm_bytes, sample_rate):
"""8bit PCM モノラルWAVファイルを書き出す。"""
data_size = len(pcm_bytes)
with open(path, "wb") as f:
f.write(b"RIFF")
f.write(struct.pack("<I", 36 + data_size))
f.write(b"WAVE")
f.write(b"fmt ")
f.write(struct.pack("<I", 16))
f.write(struct.pack("<H", 1)) # PCM
f.write(struct.pack("<H", 1)) # mono
f.write(struct.pack("<I", sample_rate))
f.write(struct.pack("<I", sample_rate)) # byte rate (1ch * 8bit)
f.write(struct.pack("<H", 1)) # block align
f.write(struct.pack("<H", 8)) # bits per sample
f.write(b"data")
f.write(struct.pack("<I", data_size))
f.write(pcm_bytes)
def mzf_to_wav(mzf_path, wav_path, sample_rate=44100, invert=False,
pulse_us=NORMAL_PULSE_US):
image = sanitise_image(Path(mzf_path).read_bytes())
if not check_image(image):
raise ValueError(
"不正なMZFイメージです(ヘッダのファイルサイズとデータ長が一致しません)")
pcm = conventional_format(image, pulse_us, sample_rate, invert)
write_wav(wav_path, pcm, sample_rate)
duration = len(pcm) / sample_rate
return duration
def binary_to_wav(bin_path, wav_path, name, load_addr, exec_addr,
sample_rate=44100, invert=False, comment=b"",
pulse_us=NORMAL_PULSE_US):
data = Path(bin_path).read_bytes()
image = make_mzf_from_binary(data, name=name, load_addr=load_addr,
exec_addr=exec_addr, comment=comment)
pcm = conventional_format(image, pulse_us, sample_rate, invert)
write_wav(wav_path, pcm, sample_rate)
duration = len(pcm) / sample_rate
return duration
def parse_int_auto(s):
return int(s, 0)
def main():
parser = argparse.ArgumentParser(
description="MZ-80K/C1系カセットテープ音声(WAV)生成ツール")
sub = parser.add_subparsers(dest="command", required=True)
p_mzf = sub.add_parser("mzf2wav", help="MZFファイルをWAVに変換する")
p_mzf.add_argument("mzf", help="入力MZFファイル")
p_mzf.add_argument("wav", help="出力WAVファイル")
p_mzf.add_argument("-r", "--rate", type=int, default=44100, help="サンプルレート(Hz)")
p_mzf.add_argument("-p", "--invert", action="store_true", help="極性反転")
p_bin = sub.add_parser("bin2wav", help="任意のバイナリからMZFを生成しWAVに変換する")
p_bin.add_argument("bin", help="入力バイナリファイル")
p_bin.add_argument("wav", help="出力WAVファイル")
p_bin.add_argument("-n", "--name", default="BASIC", help="テープ上のファイル名(最大17文字)")
p_bin.add_argument("-l", "--load", type=parse_int_auto, default=0x1200,
help="ロードアドレス(例: 0x1200)")
p_bin.add_argument("-e", "--exec", dest="exec_addr", type=parse_int_auto,
default=0x1200, help="実行アドレス(例: 0x1200)")
p_bin.add_argument("-r", "--rate", type=int, default=44100, help="サンプルレート(Hz)")
p_bin.add_argument("-p", "--invert", action="store_true", help="極性反転")
p_bin.add_argument("--save-mzf", help="生成したMZFイメージも保存する場合のパス")
args = parser.parse_args()
if args.command == "mzf2wav":
duration = mzf_to_wav(args.mzf, args.wav, sample_rate=args.rate,
invert=args.invert)
print(f"変換完了: {args.wav} (再生時間 約{duration:.1f}秒)")
elif args.command == "bin2wav":
data = Path(args.bin).read_bytes()
if args.save_mzf:
image = make_mzf_from_binary(data, name=args.name, load_addr=args.load,
exec_addr=args.exec_addr)
Path(args.save_mzf).write_bytes(image)
print(f"MZFイメージを保存しました: {args.save_mzf}")
duration = binary_to_wav(args.bin, args.wav, name=args.name,
load_addr=args.load, exec_addr=args.exec_addr,
sample_rate=args.rate, invert=args.invert)
print(f"変換完了: {args.wav} (再生時間 約{duration:.1f}秒)")
if __name__ == "__main__":
sys.exit(main())
verify_wav.py(自己診断用デコーダ)
#!/usr/bin/env python3
"""
mzf2wav.pyが生成したWAVを復調し、元のMZFイメージへ正しく戻るか検証するツール。
実機を使わずにロジックの自己整合性を確認するためのもの(自己診断用デコーダ)。
"""
import struct
import sys
from pathlib import Path
NORMAL_PULSE_US = (464, 494, 240, 264)
HEADER_SIZE = 128
HEADER_GAP_LEN = 22000
BODY_GAP_LEN = 11000
HEADER_TAPE_MARK_LEN = 40
BODY_TAPE_MARK_LEN = 20
INTER_COPY_GAP_LEN = 256
def read_wav(path):
data = Path(path).read_bytes()
assert data[0:4] == b"RIFF"
assert data[8:12] == b"WAVE"
pos = 12
fmt = None
pcm = None
while pos < len(data):
chunk_id = data[pos:pos + 4]
chunk_size = struct.unpack("<I", data[pos + 4:pos + 8])[0]
body = data[pos + 8:pos + 8 + chunk_size]
if chunk_id == b"fmt ":
fmt = struct.unpack("<HHIIHH", body[:16])
elif chunk_id == b"data":
pcm = body
pos += 8 + chunk_size + (chunk_size & 1)
sample_rate = fmt[2]
return sample_rate, pcm
def find_pulses(pcm, threshold=128):
"""0x00/0xffの連続run-lengthを(level, length)のリストに変換する。"""
runs = []
prev = pcm[0] >= threshold
count = 1
for b in pcm[1:]:
level = b >= threshold
if level == prev:
count += 1
else:
runs.append((prev, count))
prev = level
count = 1
runs.append((prev, count))
return runs
def classify_pulse(high_len, low_len, sample_rate, pulse_us):
lh, ll, sh, sl = pulse_us
long_total = (lh + ll) * sample_rate / 1_000_000
short_total = (sh + sl) * sample_rate / 1_000_000
total = high_len + low_len
mid = (long_total + short_total) / 2
return "long" if total >= mid else "short"
def decode_bytes(pulses, idx, sample_rate, pulse_us, n_bytes):
"""runs列のidxから、writeByte形式(8bit+区切りロング1個)でnバイト読む。
戻り値: (decoded_bytes, 消費後のidx)
"""
result = []
while len(result) < n_bytes and idx < len(pulses) - 1:
byte_val = 0
for _ in range(8):
_, high_len = pulses[idx]
_, low_len = pulses[idx + 1]
idx += 2
kind = classify_pulse(high_len, low_len, sample_rate, pulse_us)
bit = 1 if kind == "long" else 0
byte_val = (byte_val << 1) | bit
idx += 2 # 区切りのロングパルス
result.append(byte_val)
return bytes(result), idx
def decode_checksum(pulses, idx, sample_rate, pulse_us):
"""writeChecksum形式(2byte分のwriteByte + 追加ロングパルス1個)を読む。"""
checksum_bytes, idx = decode_bytes(pulses, idx, sample_rate, pulse_us, 2)
idx += 2 # writeChecksum末尾の追加ロングパルス
checksum = (checksum_bytes[0] << 8) | checksum_bytes[1]
return checksum, idx
def skip(idx, n_symbols):
return idx + n_symbols * 2
def decode_wav(wav_path):
sample_rate, pcm = read_wav(wav_path)
runs = find_pulses(pcm)
pulse_us = NORMAL_PULSE_US
idx = 0
idx = skip(idx, HEADER_GAP_LEN + HEADER_TAPE_MARK_LEN * 2 + 2)
header1, idx = decode_bytes(runs, idx, sample_rate, pulse_us, HEADER_SIZE)
checksum1, idx = decode_checksum(runs, idx, sample_rate, pulse_us)
idx = skip(idx, INTER_COPY_GAP_LEN)
header2, idx = decode_bytes(runs, idx, sample_rate, pulse_us, HEADER_SIZE)
checksum2, idx = decode_checksum(runs, idx, sample_rate, pulse_us)
body_size = header1[0x12] | (header1[0x13] << 8)
idx = skip(idx, BODY_GAP_LEN + BODY_TAPE_MARK_LEN * 2 + 2)
body1, idx = decode_bytes(runs, idx, sample_rate, pulse_us, body_size)
bchecksum1, idx = decode_checksum(runs, idx, sample_rate, pulse_us)
idx = skip(idx, INTER_COPY_GAP_LEN)
body2, idx = decode_bytes(runs, idx, sample_rate, pulse_us, body_size)
bchecksum2, idx = decode_checksum(runs, idx, sample_rate, pulse_us)
def expected_checksum(data):
# 参照実装のchecksumはuint16_tで加算するため2バイトでラップアラウンドする。
return sum(bin(b).count("1") for b in data) % 0x10000
result = {
"sample_rate": sample_rate,
"header1": header1,
"header2": header2,
"header_match": header1 == header2,
"checksum1": checksum1,
"checksum2": checksum2,
"checksum1_valid": checksum1 == expected_checksum(header1),
"checksum2_valid": checksum2 == expected_checksum(header2),
"body1": body1,
"body2": body2,
"body_match": body1 == body2,
"bchecksum1": bchecksum1,
"bchecksum2": bchecksum2,
"bchecksum1_valid": bchecksum1 == expected_checksum(body1),
"bchecksum2_valid": bchecksum2 == expected_checksum(body2),
}
return result
def main():
wav_path = sys.argv[1] if len(sys.argv) > 1 else "test.wav"
result = decode_wav(wav_path)
print(f"sample_rate = {result['sample_rate']}")
print(f"header1 == header2 : {result['header_match']}")
print(f"header checksum valid (1st/2nd): "
f"{result['checksum1_valid']} / {result['checksum2_valid']}")
print(f"body1 == body2 : {result['body_match']}")
print(f"body checksum valid (1st/2nd): "
f"{result['bchecksum1_valid']} / {result['bchecksum2_valid']}")
mzf_path = Path(wav_path).with_suffix(".mzf")
if mzf_path.exists():
original = mzf_path.read_bytes()
orig_header = original[:HEADER_SIZE]
orig_body = original[HEADER_SIZE:]
print(f"decoded header == original header : "
f"{result['header1'] == orig_header}")
print(f"decoded body == original body : "
f"{result['body1'] == orig_body}")
all_ok = (
result["header_match"] and result["checksum1_valid"]
and result["checksum2_valid"] and result["body_match"]
and result["bchecksum1_valid"] and result["bchecksum2_valid"]
)
print("=> " + ("OK: 全項目一致" if all_ok else "NG: 不一致あり"))
return 0 if all_ok else 1
if __name__ == "__main__":
sys.exit(main())