CD image を可逆/非可逆圧縮して保存する

前回 libFLAC を組み込めるか検討しましたが大変そうなのでやめました. そのあと... とても長い間やる気をなくしてしまいました.

WavPack を利用する

別途同目的の規格を探していたところ、 WavPack のダウンロードページに https://www.wavpack.com/downloads.html Tiny Decoder (Tiny Encoder もある)があるのを見つけたので利用してみました.

Tiny Decoder は10年以上更新されていないソースでしたが、現在のコンパイラ(x64 の clang)でも特に問題なく通り、最新バージョンの encoder で生成したファイルも問題なく decode できました. またドキュメントには当時の MCU で利用していることが限られた性能で十分に動いているし、 malloc を使っていないことを書いてあるのでこれを深く試すことにしました.

Tiny Decoder のソースを触る

これは本当に再生だけで seek 機能がないので、wavpack 規格を知ることも兼ねてをソースに手をいれました. 弱いと感じたのは順番にデータを読むので時刻とファイルオフセットを1度すべて読んでキャッシュを作る必要がある点でした.

ほかのソースも作りましたがこれが一番時間がかかりました.(3日間)

wavpack ですべての PCE の CD をエンコードする

data track も無関係にエンコードしたところ、平均圧縮率(decoded file size / original file size, 小さいほど優秀) は 61% でした. audio data だけでみるとおおよそ 1/2 ぐらいで、 flac とも同じに見えます.

平均ですので、最悪値もあり圧縮率 101% もありました. これは 1 disc で data track の割合が 100% のソフトがあったからで、それ以外にも data track が多いソフトは圧縮率 70% もあります.

PC 上の .wv file を再生すると当然ながら data track を audio track して再生したことになり不快な音がでてしまいます.

dll を利用して、専用 wavpack encoder を作ってみる

WavPack5FileFormat.pdf 内 2.0 Block Header に下記の記述があります.

typedef struct { 
    char ckID [4];          // "wvpk"
    uint32_t ckSize;        // size of entire block (minus 8)
    uint16_t version;       // 0x402 to 0x410 are valid for decode
    uchar block_index_u8;   // upper 8 bits of 40-bit block_index
    uchar total_samples_u8; // upper 8 bits of 40-bit total_samples
    uint32_t total_samples; // lower 32 bits of total samples for
                            // entire file, but this is only valid
                            // if block_index == 0 and a value of -1
                            // indicates an unknown length
    uint32_t block_index;   // lower 32 bit index of the first sample
                            // in the block relative to file start,
                            // normally this is zero in first block
    uint32_t block_samples; // number of samples in this block, 0 =
                            // non-audio block
    uint32_t flags;         // various flags for id and decoding
    uint32_t crc;           // crc for actual decoded data 
} WavpackHeader;

block_samples = 0 とした場合は non-audio block にできるとあるので、実際に生成されたファイルのヘッダを分析し、手を加えることにしました. block_samples = 0 として、こちらで data sector (中身だけ, 0x800 bytes) をいれて、audio data は 0 だけの無音をいれました.

重要なソースは下記です. ソース全体は https://pastebin.com/T3Hj2HMc でみてください.

	const uint8_t compactdisc_data_header[] = {
		0, 0xff, 0xff, 0xff, 0xff, 0xff, 
		0xff, 0xff, 0xff, 0xff, 0xff, 0
	};
	WavpackHeader h = {"wvpk", 0, 0x410, 0, 0, src_sector_num * config.block_samples, 0, 0, 0x14801821, 0};
	assert(sizeof(h) == 0x20);

	(中略)
		int32_t *audio_src = b;
		if(memcmp(compactdisc_data_header, fb, sizeof(compactdisc_data_header)) == 0){
			uint8_t compressed_data[0x800 * 4];
			uint32_t compressed_size = (*packer)(fb + 0x10, 0x800, compressed_data, 0x800);
			if(compressed_size == 0){
				compressed_size = 0x800;
				memcpy(compressed_data, fb+10, 0x800);
			}
			h.ckSize = 0x20 - 8 + compressed_size;
			h.crc = crc32(0, fb + 0x10, 0x800);
			fwrite(&h, 1, 0x20, d);
			fwrite(compressed_data, 1, compressed_size, d);

			audio_src = audio_silent;
		}
		r = WavpackPackSamples(w, audio_src, config.block_samples);
		assert(r == TRUE);
		r = WavpackFlushSamples(w);
	(以下略)

いまは .img のファイルだけみてるので data/audio 振り分けはヘッダの有無というとても雑なものですが、実験ということで細かい部分は後回しにします.

*packer に関しては data 用の可逆圧縮ルーチンで非力な MCU でも利用できる lz4 か lzf を選んでいれてみました. ただこれでも圧縮できない場合は、もとのデータをそのままいれることにしました.

エンコードしたデータは PC 用の player (foobar2000, MPC-HC)で問題なく再生できます. ソース全体見ればわかりますがこちらが作成したのは1つのCファイルでこれも1日間で作れました.

非可逆オーディオもいれられる

data track は可逆圧縮(または無圧縮)は必須ですが、オーディオは非可逆でもなんとかなる気がしたので、 wavpack の hybrid mode を利用してみました. これだと audio data が 1/4 程度になります.
音質に関しては詳しくないのですが、違和感はありませんでした.

これも同様に PC 用の player でも再生できますし、 tiny decoder でも問題なく decode できました.

ソフト別で圧縮率を比較する

全部を見たい方は http://dev.upergrafx.com/download/cdimage_fraction_ratio.csv .

0.82	0.18	0.61	0.578	0.577	0.27	0.51	(average)
audio	data	wv/img	lz4/img	lzf/img	lossy/img	chd/img	name
0.00	1.00	1.01	0.84	0.84	0.85	0.61	jccd3012_sherlock_holmes_no_tantei_kouza
0.00	1.00	1.00	0.86	0.85	0.86	0.61	jccd1004_sherlock_holmes_no_tantei_kouza
0.02	0.98	0.95	0.66	0.63	0.66	0.42	nipr1002_de-ja
0.12	0.88	0.89	0.73	0.72	0.69	0.54	kmcd4007_tokimeki_memorial
(中略)
0.97	0.03	0.60	0.61	0.61	0.21	0.57	nxcd3019_downtown_nekketsu_monogatari
0.97	0.03	0.60	0.60	0.60	0.21	0.56	napr1025_forgotten_worlds
0.93	0.07	0.60	0.61	0.61	0.23	0.56	nxcd2010_double_dragon2the_revenge
0.97	0.03	0.60	0.62	0.62	0.21	0.57	fccd4001_shin_nihon_pro_wrestling_kounin
(中略)
0.90	0.11	0.43	0.39	0.39	0.24	0.35	hcd4064_deden_no_den
0.86	0.15	0.41	0.40	0.40	0.19	0.35	tjcd1015_cosmic_fantasy2bouken_shounen_b
0.93	0.08	0.40	0.39	0.39	0.22	0.34	nscd0002_sol_bianca
0.96	0.04	0.37	0.38	0.38	0.20	0.33	nxcd2011_wizardry5heart_of_the_maelstrom

値の説明

  • audio, data: 1 disc の audio/data の割合. 2つを足すと1になる.
  • wv/img: 全部audioとして encode した .wv file size / original file size
  • lz4/img: audio は可逆 wavpack, data は lz4 + 無音で encode した圧縮率
  • lzf/img: 上記から lz4 を lzf にかえたもの
  • lossy/img: lz4/img から audio は hybrid (非可逆) wavpack にかえたもの
  • chd/img: mame の chdman で encode した chd file size / original file size
  • 平均圧縮率は lz4:0.578, lz4:0.577 でほんのわずか lzf が優秀

考察

  • data sector 可逆圧縮は意外と効果が少ない. 0x800 bytes 単位では小さいのでオーバーヘッドが大きいと思われる.
  • data track が多い場合は可逆圧縮がそれなりに効いている, audio track が多い場合は data 圧縮が効果が少ない
  • 平均をみると chd にわずかに多い程度でしょぼい MCU でもそんなに悪くない
  • hybrid にするなら今回の data sector + 無音挿入は結構よさそう.

付加データ

f:id:na6ko:20210211161140p:plain
.wv ファイルに cuesheet とジャケット画像をいれられるので、ぱっとみが非常によくできます. 画像は web 上から勝手にとってきました.

ちょっと気になるのは cuesheet や tag の文字コードが混沌としていて、palyer 側は UTF-16LE で非ASCII文字がでる可能性が高いです. 規格上は UTF-8 と書いてるんですが大丈夫なのでしょうか. また古参ユーザが無意識に Windows-31J をねじ込んできそうで混乱はありそうです.