SAMD21 で各種ソフトを動かしてみたが性能不足で評価終了

前回は外部デバイスに対してでしたが内部の話です.

Wavpack Tiny Decoder (CPUの性能不足)

tiny decoder は Makefile を調整して ARM 用にしたぐらいですが、リンクも問題なく、動かすことができました.

ソースツリーに含まれている ARM 用のアセンブラソースは使えませんでした. これ ARM7TDMI 用で Cortex M0 用ではないし、自分は ARM のアセンブラが読めないのですすめられないです.

内部のタイマーを利用しながら 75 compact disc frames (=1秒間) を decode したところ 2.9 秒もかかりました. 1 秒間のデータを展開するのに 3 秒間かかるのでは使えません. 実は Cortex M0+ 48MHz ではだめかなと予想しててその通りでした.

一応なんですが hybrid mode で作ったデータでは 0.75 秒間程度の計測値がでてますので、ファイルシステムなしとか他に重い割り込み処理を入れないとか条件付きでは一応使えるという値です. アセンブラのソースをいれても4倍以上の処理速度がでるとは思えません.

LZ4 (decode できてない)

PC 側で encode するのはとても簡単だったのですが、decode のまともな C のコードを用意できてません.

LZ4 のサイトに多種の decoder への言及がありますが、ARM Cortex assembly decoder はリンクはできたけどちゃんと動きませんでした. C decoder (tiny) はどうも 80386 依存のコードになっていて実用的ではないです. LZ4 の公式の decoder の C ソースはちょっとみましたが、OS がある前提でかなり safety に作られてて MCU 用には各種手続きを削る手間があり、やってません.

LZF (実用可能)

こちらは無難な C の関数で fall-through の Warning を止めれば、コンパイル、リンク、Decode すべてできました.

計測方法は手抜きで 0x800 bytes (1/75秒)の data を decode して 75 倍した時間で 0.094 秒でした.

考察

CPU の性能不足なので SAMD21 での評価はこれで終了とし、SAMD51 など Cortex M4 あたりで評価をやり直す必要があります.

LZ4 は top page がぱっとみ素晴らしいんですが、 decode できないとか、アセンブラじゃ PC 上のテスト用環境でも動かせないとかで悪い方に予想外でした. msys で pacman からとってこれるとか PC 向けのパッケージ管理はよかったです.

LZF は LZ4 の性能比較対象にもありますけど、こっちのほうがいいかなと今のところは思います.

そもそも data sector を圧縮する意義はあるのかという話もあります.

SAMD21 のドライバを書いてみる

ASF (メーカー提供のライブラリ)なしでやってます.

I2S (断念)

master clock を入力もしくは内部の GCLK を利用し SCK (BCLK), FS (LRCK) を出すのはそんなに難しくありません. そこから data を出すのにつまってしまい、適切な解決方法が不明で断念となりました.

この確認はとりあえずソフトから I2S->DATA[x].reg = xx; とするのですが I2S->CLKCTRL[x] への I2S_CLKCTRL_SLOTSIZE_32 でのみ SD (DAC.DAT) が反応することまではわかりました. SLOTSIZE は 16 で使いたかったのですが...

よくわからないのは I2S->DATA[x].reg へ書き込む際の型も 16bit 単位でかけるのかもよくわからなく SLOTSIZE_32 で 32bit 単位で I2S->DATA[x].reg をかけば動くというものでした.

ここからうまく動けば DMA controller に RAM から I2S->DATA[x].reg を定期的に書いてもらうつもりでしたがうまくいかなそうだし、MCU から直接 I2S を出すのはここだけで試して今考えている今後の設計ではつかわないつもりでしたのでここでやめました.

WavPack の tiny decoder の出す decoded data が int32_t で bit15:0 のみが有効な配列のため I2S->DATA[x].reg に書く型は 16 bit で address increment を 4 にするつもりでやってたので無理そうです.

SERCOM.SPI.Master (使えた)

関数を抜粋します.

#define DECLARE_PORT(name) volatile SercomSpi *name = &SERCOM4->SPI
static inline void spi_write(uint8_t data)
{
	DECLARE_PORT(s);
	s->DATA.bit.DATA = data;
	while(s->INTFLAG.bit.TXC == 0);
}

この関数の前に s->CTRLB.bit.RXEN = 0; が必要です. この RXEN は書き換えはいつでもできて同期もいりません. 逆にいうとほかの設定向けレジスタは s->CTRLB.bit.ENABLE = 0; にした上で各種同期がいります.

TX の DMA (polling, 完了割り込みなし)です.

static void dma_write(const uint8_t *src, int src_increment, int length)
{
	uint16_t flag = DMAC_BTCTRL_STEPSIZE_X1 | DMAC_BTCTRL_BEATSIZE_BYTE;
	if(src_increment){
		flag |= DMAC_BTCTRL_SRCINC;
	}
	DECLARE_PORT(s);
	dma_descripter_set(DMACH_SDC_SPI_TX, src, (void *) &s->DATA, length, flag);
	dma_channel_enable_polling(DMACH_SDC_SPI_TX, SERCOM4_DMAC_ID_TX);
	while(s->INTFLAG.bit.TXC == 0);
}

DMACH_xxx は私が定義した DMA channel number のための enum です.

src_increment の切り替えは結構大切です. 意図的に CLOCK を動かして初期化する場面、memset のように一定データで埋めたい場面、RX のための CLOCK を動かす場面など用途が結構あります.
また dma controller における s->DATA の書き込みは SERCOM への同期が含まれていないので DMA controller としては最後のデータを書いた時点で完了となります. よって s->INTFLAG.bit.TXC の同期が必要です.

static inline uint8_t spi_read(void)
{
	DECLARE_PORT(s);
	s->DATA.bit.DATA = 0xff;
	while(s->INTFLAG.bit.RXC == 0);
	return s->DATA.bit.DATA;
}

この関数の前に s->CTRLB.bit.RXEN = 1 が必要です. CLOCK の動作は s->DATA への書き込みによって発生します.

RX の DMA (polling) で、これが曲者でした.

static void dma_read(uint8_t *dest, int length)
{
	const uint16_t flag = DMAC_BTCTRL_STEPSIZE_X1 | DMAC_BTCTRL_BEATSIZE_BYTE | DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_STEPSEL;
	DECLARE_PORT(s);
	dma_descripter_set(DMACH_SDC_SPI_RX, (const void *) &s->DATA, dest, length, flag);
	switch_rx();
	dma_channel_enable(DMACH_SDC_SPI_RX, SERCOM4_DMAC_ID_RX);
	
	const uint8_t ff = 0xff;
	dma_write(&ff, 0, length);
	dma_channel_disable(DMACH_SDC_SPI_RX);
}

SERCOMx_DMAC_ID_RX だけの DMA をいれても CLOCK が動かないので、TX も並列動作させます. TX の書き込みは increment なしで ff というローカル変数使ってますけど、割り込みでこの関数を抜ける場合(ffが無効になる場合)はいつでも無効にならない static 変数を利用する必要があります.
DMA channel number は TX < RX として, 転送量も同じにして、 RX を polling なしで動かしてから、 TX を polling します. また dma_write() のように同期がいります. (RXC の同期はいらない気がして抜きました)

dma_write() は 4 byte とか 8 byte でも使ってもいいんですが、 dma_read() は動作の波形をみると初期化に時間がかかるので 10 byte 以上に使ったほうがいいです. さらに転送量が多くなると当然割り込み待ちにもなるんですがまだそこまで試す段階にはなってません.

dma_read は forum の書き込みを参考に対処させてもらいました. DMA controller の使い方はよくわかってなかったのですが、channel 並列動作の場合は、channel を切り替えて順番にやることもここで理解しました.

*1

*1:dma_descripter_set(), dma_channel_enable_polling() は初期化をまとめたもので割愛します.

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 をねじ込んできそうで混乱はありそうです.

webブラウザの top へ戻るボタンを消すまどろこっしい手順

ブラウザの右下にでてくる top へ戻るボタンは PC で見るときには home キーを押せば代用できるので邪魔という理由でサンプルをいれたり都度消したりしてたが全部消すには程遠いのでスクリプトで網羅生成することにした.

簡単な解析

いまでの user style sheet をぱっと見で集計すると下記のようだった.

単語

  • id か class の記号
  • 動詞 back bk jump jmp scroll go rwd rewind return rtn
  • 助詞 to
  • 必須名詞 top pagetop ptop head
  • 補助名詞 button bt btn link lnk

区切り

  • (小文字なし) gototop
  • (小文字-) go-to-top
  • (小文字_) go_to_top
  • (小文字先頭大文字なし) goToTop
  • (先頭大文字なし) GoToTop

助詞と補助名詞はない場合もある.

正規表現

大文字小文字区別なしオプションをつけるとこうなる.

[\.#](back|bk|jump|jmp|scroll|go|rwd|rewind|return|rtn)[\-_]?(to)?[\-_]?(top|pagetop|ptop|head)[\-_]?(button|bt|btn|link|lnk)?

悲しいことに user style sheet には正規表現は使えるツールがない(気がする)

スクリプトで全通り網羅してだす

やり途中の時点で 4800 通りの名称がでてきた. これをはっつけた.

これでうまくいくのか

2週間ぐらい試して実用的か判断する. そもそもは名称に頼らず画面右下に停滞するスクロールしないリンクを消すのを、css を解析して消すのがいいのだろうけどやり方がわからない.

MAME から無劣化動画を作る (かなり面倒くさい)

MAME の AVI 作成機能ではなくて MNG 作成機能から lossless の動画を作りました. 10年前に試したところ AVI 作成機能は圧縮一切なしの超巨大ファイルを作るし、 MNG は対応しているソフトが当時も今も皆無です.

MAME で入力を記録する

mame [gamename] -record [recordname]

満足のいく内容が取れるまでゲームを遊びます. [] で囲ったところは任意です.

MAME で入力を再現させて、それを MNG file と wav file でだす

mame [gamename] -playback [recordname] -mngwrite [mngfilename] -wavwrite [wavfilename]

mng ファイルは、表示フレームをすべて png 画像として圧縮してそれを順番にいれているものです. 圧縮するので再現中はゲームのスピードがかなり遅くなるので気長に待ちます.

出力された mng と wav のファイルは間違って消さないように readonly 属性を付けておいたほうがいいでしょう. コマンドラインだと履歴巡って再実行でファイル上書きでやりなおしの事故がよくあります.

MNG file を連番の PNG file として大量に出力する

gitlab.com

対応しているソフトがないので分離します. やること自体は単純で IHDR と IEND の間の chunk を切り出して png のヘッダをつけてファイルに出します.

問題は1秒間で60個、1分間で3,600個、10分間で36,000個のファイルになるので2時間も録画したものを分離すると、ディスクの圧迫は当然としてファイルシステムやファイル管理に異常が出るらしくファイルの出力が何故か遅くなります.

この対策として 10,000 個ずつ png ファイルを出して、それを1つの mp4 ファイルに統合を繰り返し、複数の mp4 ファイルを1つに統合する、という流れであればマシになりました.

分離ソフトは自作です.先述のように1時間30分ほどの1つの mng ファイルを一度に分離したらファイル生成が妙に遅くなって出力に4時間かかりました. 10,000単位なら1分程度で終わります. 1分でも遅いのですがボトルネックはファイル出力にあるので、自分の力では改善できそうにありませんでした.

連番の PNG file 群を1つの mp4 ファイルに統合する

rm -f [09999.png]
ffmpeg [options] -r [fps] -i [%06d.png] -c:v libx264 -preset ultrafast -crf 0 -f mp4 [v%04d.mp4]
  • [09999.png] は分割生成で結合前に最終フレームのファイルを1つ消します. かなり重要で、結合の部分で説明します.
  • [options] は -y とか -loglevel とか好きなのを入れます
  • [fps] は MNG file の MHDR chunk 内部の Ticks_per_second から取得します. これは整数なのが気になりますがそれでうまくいきました. http://www.libpng.org/pub/mng/spec/mng-1.0-20010209-pdg.html#mng-MHDR
  • [%06d.png] は入力ファイル, [v%04d.mp4] は出力ファイル

これで 1 frame 欠けてますが、可逆圧縮の H264 でエンコードできます. MAME が出す AVI ファイルに比べるとかなり小さく、MNG ファイルより小さくなりました.
png への分離と H264 へのエンコードを繰り返します.

連番の映像ファイルを1つに統合する

ffmpeg [options] -f concat -safe 0 -i [list] -c copy video.mp4

このコマンドは web 上のffmpeg のマニュアルや野良情報と同じです.

この結合機能は入力ファイルごとになぜか 1 frame ずつ増えてしまい、音ズレの原因となっていました. 16個も分離した場合には後半の音のズレがひどいので使い物にならないので、入力側から 1 frame 削除しておくのが妥協案だと思います. (この原因解明に1日かかりました)

結合前に音をいれておくと音ズレは予防できたのですが、今度は結合フレームで音が切れてしまう問題があり、その点でもこれが一番マシだと思います.

統合した映像ファイルに音声を追加する

野良情報と同じなので省略します.

一連の処理をスクリプトで管理する

mng から png 分離プログラムとスクリプトはまだ未完成なのでできたら公開するつもりです.

かなり変数をとってくる必要があるので bat や bash では無理でした.
あとは中間ファイルの削除もスクリプトにいれる必要があります.

ようやく編集する

この面倒な作業を終えるとようやく編集できますのでアスペクト比とかfpsの変更、ステージごとの分割ということができます.

libFLAC 続き

malloc, calloc, realloc, fclose が残っていましたので除去しました.
一応PCで動いているみたいですが ROM と RAM の消費量は下記となりました.

0x3870 .text                    libflac.a(stream_decoder.o)
0x1158 .text                    libflac.a(bitreader.o)
0x0b24 .text                    libflac.a(format.o)
0x0a4c .text                    libflac.a(fixed.o)
0x09a4 .text.qsort              libg.a(lib_a-qsort.o)
0x0960 .text                    libflac.a(lpc.o)
0x0578 .text.__ieee754_log      libm.a(lib_a-e_log.o)
0x1100 .rodata                  libflac.a(crc.o)
0x063c .rodata.log.str1.4       libm.a(lib_a-w_log.o)
0x2be8 .bss                     libflac.a(stream_decoder.o)
0x102c .bss                     libflac.a(bitreader.o)
0x0428 .data.impure_data        libg.a(lib_a-impure.o)
0x0200 .bss                     libflac.a(format.o)

allocate_output_(), private_->output, private_->residual

output と residual は int32_t の配列で下記の RAM を必要とします.

sizeof(int32_t) * channels * blocksize * 2
  • sizeof(int32_t) は最終の decode された data が 16bits であっても decode の過程で上位 16bits は利用しているので int16_t に変更はできませんでした.
  • channels は CDDA であれば 2 固定です.
  • blocksize は encode 時のパラメータで変更可能です.
  • 最後の *2 は output と residual それぞれで2つです.

前回では根拠なく blocksize を 2352 にしましたが、これですと 0x9300 bytes も必要になりますので 2352 / 4 に変更しました. 588 を超える値で encode された flac data は利用できない制限ができてしまいました.

output は絶対にいるにしろ residual を 1 channel だけ使うようにできればもっと RAM の確保に余裕が持てそうです.

libFLAC/lpc.c

libFLAC/stream_decoder.c の read_subframe_lpc_() で decoder->private_->local_lpc_restore_signal(_16bit|_64bit)? がありますが x86_64 と i686 の CPU 向けの最適化で利用しているみたいなので関連する関数ポインタは使わずに、 FLAC__lpc_restore_signal() のみを利用します. FLAC__lpc_restore_signal_64bits() は CDDA ではいらないみたいなのでなくしました.

FLAC__lpc_restore_signal() は redisual と output を利用している部分なので residual が減らせるか調査した方がいいと思います.

metadata各種

METADATA_BLOCK_STREAMINFO は固定長、必須データですので問題ありません.
他の metadata は可変長データが含まれており、そのたびに malloc 系で RAM を確保します. METADATA_BLOCK_SEEKTABLE を除き、 可変長 metadata は音楽データの decode 前に動的確保され、 callback 関数の呼び出しの後に動的解放を行います.
先述の private_->output, private_->residual は排他の関係だと思うのでこの静的確保 RAM を必要量に応じて動的配分するということにしました.

METADATA_BLOCK_SEEKTABLE はずっと確保されているのですが、なくても動くと書かれているので利用しないようにソースを調整しました. 動かしてみて seek の時間が遅すぎるなら調整が必要です. 標準の encoder では単純に 10 秒間隔でいれているのでデータ量をこの用途ではもう少し減らした方がいいです.

残りは METADATA_BLOCK_PICTURE ですが、これは非常に難しいです. 問題を列挙します.

  • private_->output で確保した領域は画像を入れる場所としては小さい
  • png/jpeg などを使う場合は別途画像ライブラリで展開するため RAM が足りなすぎる
  • 別途の画像ライブラリはいまのところ何も考えていない
  • もし VRAM 用のベタデータを割り振るにしろ必ず RAM に展開する仕組みなのでやはり RAM が足りない
  • picture は UI でほしい

画像データの最小転送量は decode するなら block size だけ展開し、decode した data は VRAM 用の stream に流し込む仕組みでないと.. 難しいですね.

各種 metadata は利用できないように設定できますので今のところ画像は触れないということにしています.

FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size (長い)

safe_realloc_() を呼ぶ場所がありました. debug print をいれたら 0x100 * 2 bytes が必要で、呼ばれるのは1度だけでしたので、それだけを確保しました. それが適切なのかよくわかりません.

その他

libFLAC 側でファイルを管理してもらう FLAC__stream_decoder_init_FILE() と関連する関数や decoder->private_ の変数はすべてなくしました. ここに fclose が残っていてこれが malloc/free を利用しているようでした.

decoder->private_->last_frame.subframe は(今のところ)重要ではないのに消費量が多いのでなくして header と foot だけにしました.

今のところの評価

libflac に必要な RAM は 0x3e14 bytes なので、 MCU の SAMD20 の最低 1/4 を占めることになります. mruby/C も最低 1/4 なのでこの2つが最終的に2/3を使うのであればギリギリ大丈夫なのでは... というところです.

libFLAC の自作 PC 向けソフトへの利用、および MCU 向けの調査

説明の順番をすっとばしますが、 FLAC の一連のソースコードやドキュメントはとても丁寧です. いままでみた有名なプロジェクトのソースコードの中でもきれいで感動しました.

configure (automake) からのビルド

なにもよまずにやってみましたが、 memset_chk() がリンクできないというエラーがでてしまいました. make の途中では gcc のコマンドがでないようになっていてにコンパイル/リンクのオプションが不明な上、automake で生成された Makefile も私が解読できなかったのであきらめました.

automake の場合はこちらもとても丁寧な自動テストが入っているのでそれもつかうほうが品質のためにはよいです.

Makefile を作る

作ってから同じ目的の Makefile.lite というのがあることをしりましたのでここは飛ばしていいと思います.

とりあえず自分がいつも使うオプションでコンパイルを始めてとまったら分析してオプションをつけてやり直しという形にしました. src/libFLAC++, src/utils, テストスクリプトは使わなかったのでわかりません.

my.mk

ifeq ($(MSYSTEM_CARCH), i686)
	CC = gcc
else
	CC = clang
endif
CFLAGS = -O2 -Wall -Werror -I$(TOPDIR)/include
CFLAGS += -DHAVE_FSEEKO -DHAVE_LROUND -DPACKAGE_VERSION=\"1.3.3\"
CFLAGS += -DFLAC__HAS_OGG=0
CFLAGS += -D__STDC_FORMAT_MACROS -DFLAC__NO_ASM -DHAVE_STDINT_H

.PHONY: all clean

これは src/libFLAC, src/flac, src/metaflac で共用の設定で各ディレクトリの Makefile から include します. 必要なソースファイルは Makefile.am か Makefile.lite をみたほうがいいのでここでは記載を省略します.

この設定だけで(いままでみた著名なオープンソースプロジェクトと比較して)簡単に flac, metaflac, libflac.a がビルドできました. 試してみたところ flac, metaflac は私の目的では手を入れる必要がないとわかったので公式に配布されている物を利用した方がいいです.

SDL との連携

sound_flac.c, 大半の callback 関数は examples/c/decoder と libflac にのってたものをそのまま利用してます.

#include <inttypes.h>
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <FLAC/stream_decoder.h>
#define DP printf("%s:%d\n", __FUNCTION__, __LINE__); fflush(stdout)

struct sound_flac{
	FILE *flac_file;
	FLAC__StreamDecoder *flac_decoder;
	struct {
		uint8_t *buf;
		int len, pos;
	}src, dest;
};
//static 関数の prototype は省略
#define SRC_BUF_SIZE (2352*4*16)
struct sound_flac *flac_init(void)
{
	static struct sound_flac t;
	static uint8_t src_buf[SRC_BUF_SIZE];
	memset(&t, 0, sizeof(t));
	t.flac_file = NULL;
	t.flac_decoder = FLAC__stream_decoder_new();
	t.src.buf = src_buf;
	assert(t.flac_decoder != NULL);
	FLAC__StreamDecoderInitStatus r = 
	FLAC__stream_decoder_init_stream(t.flac_decoder, 
		flac_read_cb, flac_seek_cb,
		flac_tell_cb, flac_length_cb, flac_eof_cb,
		flac_write_cb, flac_metadata_cb, flac_error_cb, 
		&t
	);
	if(r != FLAC__STREAM_DECODER_INIT_STATUS_OK){
		printf("%s ERROR: initializing decoder: %s\n", __FUNCTION__, FLAC__StreamDecoderInitStatusString[r]);
		return NULL;
	}
	return &t;
}
void flac_file_load(struct sound_flac *t, const char *filename)
{
	t->flac_file = fopen(filename, "rb");
	assert(t->flac_file != NULL);
	FLAC__stream_decoder_process_until_end_of_metadata(t->flac_decoder);
}
void flac_delete(struct sound_flac *t)
{
	FLAC__stream_decoder_delete(t->flac_decoder);
	if(t->flac_file != NULL){
		fclose(t->flac_file);
	}
	t->flac_decoder = NULL;
	t->flac_file = NULL;
}
static FLAC__StreamDecoderReadStatus 
flac_read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data)
{
//サンプルと同じなので省略
}

static FLAC__StreamDecoderSeekStatus 
flac_seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, void *client_data)
{
//サンプルと同じなので省略
}
static FLAC__StreamDecoderTellStatus 
flac_tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, void *client_data)
{
//サンプルと同じなので省略
}
static FLAC__StreamDecoderLengthStatus 
flac_length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, void *client_data)
{
//サンプルと同じなので省略
}
static FLAC__bool 
flac_eof_cb(const FLAC__StreamDecoder *decoder, void *client_data)
{
//サンプルと同じなので省略
}
static void flac_metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data)
{
//サンプルとほぼ同じなので省略
}

static void flac_error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data)
{
//サンプルと同じなので省略
}
void flac_seek(struct sound_flac *t, uint64_t pos)
{
//サンプルと同じなので省略
}
void flac_decode(struct sound_flac *t, uint8_t *buf, int len)
{
	if(t->src.len != 0){
		int l = t->src.len;
		if(l > len){
			l = len;
		}
		memcpy(buf, t->src.buf + t->src.pos, l);
		buf += l;
		t->src.pos += l;
		t->src.len -= l;
		len -= l;
	}
	//printf("dest.len:%d, src.len:%d\n", t->dest.len, t->src.len); fflush(stdout);
	if(len == 0){
		return;
	}
	t->dest.buf = buf;
	t->dest.len = len;
	assert(len % 4 == 0);
	while(t->dest.len != 0){
		FLAC__bool r = FLAC__stream_decoder_process_single(t->flac_decoder);
		assert(r == true);
	}
}
static FLAC__StreamDecoderWriteStatus 
flac_write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data)
{
	assert(frame->header.channels == 2);
	assert(buffer[0] != NULL);
	assert(buffer[1] != NULL);

	if(frame->header.number.sample_number == 0) {
	}
	/* write decoded PCM samples */
	struct sound_flac *t = client_data;
	if(t->dest.buf == NULL){
		return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
	}
	//printf("dest.len %d\n", t->dest.len); fflush(stdout);
	size_t i = 0;
	assert(t->dest.buf != NULL);
	assert(t->dest.len % 4 == 0);
	for(; t->dest.len != 0 && i < frame->header.blocksize; i++) {
		int16_t v = buffer[0][i];
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		v >>= 8;
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		
		v = buffer[1][i];
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		v >>= 8;
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		t->dest.len -= 4;
	}
	uint8_t *src = t->src.buf;
	t->src.pos = 0;
	for(; i < frame->header.blocksize; i++) {
		int16_t v = buffer[0][i];
		*src++ = v & 0xff;
		v >>= 8;
		*src++ = v & 0xff;

		v = buffer[1][i];
		*src++ = v & 0xff;
		v >>= 8;
		*src++ = v & 0xff;
		t->src.len += 4;
		assert(t->src.len <= SRC_BUF_SIZE);
	}
	//printf("dest.len:%d, src.len:%d\n", t->dest.len, t->src.len); fflush(stdout);

	return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}

sound_sdl.c

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <SDL_audio.h>
#include "sound_flac.h"

static void sdl_stream_cb(void *user, Uint8 *stream, int len);
struct sound_flac;
struct sound_sdl{
	SDL_AudioDeviceID stream;
	struct sound_flac *sf;
} *sound_sdl_init(struct sound_flac *sf)
{
	static struct sound_sdl t;
	SDL_AudioSpec w, h;
	w.freq = 44100;
	w.format = AUDIO_S16LSB;
	w.channels = 2;
	w.samples = 2352;
	w.callback = sdl_stream_cb;
	w.userdata = &t;
	t.stream = SDL_OpenAudioDevice(NULL, 0, &w, &h, 0);
	t.sf = sf;
	if(t.stream != 0){
		puts(SDL_GetError());
	}
	assert(t.stream != 0);
	assert(w.format == h.format);
	SDL_PauseAudioDevice(t.stream, 0);
	return &t;
}
void sound_sdl_delete(struct sound_sdl *t)
{
	SDL_CloseAudioDevice(t->stream);
}
static void sdl_stream_cb(void *user, Uint8 *stream, int len)
{
	struct sound_sdl *t = user;
	flac_decode(t->sf, stream, len);
}

#ifdef MAIN
#include <SDL.h>
#undef main
int main(int c, const char **v)
{
	if(c < 2){
		printf("%s [flacfile]\n", v[0]);
		return 1;
	}
	SDL_Init(SDL_INIT_AUDIO);
	struct sound_flac *sf = flac_init();
	flac_file_load(sf, v[1]);
	flac_seek(sf, 142812264 / 2 + 142812264 / 4);
	struct sound_sdl *sd = sound_sdl_init(sf);
	SDL_Delay(100*1000);
	sound_sdl_delete(sd);
	flac_delete(sf);
	SDL_Quit();
	return 0;
}
#endif

ファイルの読み込み手順に困りましたがこれだけで flac ファイルの音が再生できました. 絵や操作性はまったくありませんが目的としてはこれで十分です. あと定数の2352はなんとなくつけてるので別の値でもかまいません. (2352 samples であるので、2532 * 4 bytes -> 4 cd frames です)

libFLAC の軽量化

ここからは arm の mcu に ROM として入れるために libflac で使う関数は sound_flac.c で使っている物だけで encoder はいらないということにします.

今回の目的で libFLAC で本当に必要なソースファイルは下記だけでした.

libFLAC_sources = \
	bitmath.c bitreader.c crc.c fixed.c \
	lpc.c memory.c format.c stream_decoder.c

cpu.c, md5.c はそのままだといりますので #ifdef/#ifndef で使わないようにしました.
cpu.c は intel/AMD 向けの i686 か x64 かというものなので省略, md5.c は正当性チェックのようでテスト環境でエラーを出せばいいので省略. crc.c は演算に時間がかからなそうだと思って入れました.

次に printf 系/ malloc 系/ free を排除できるかやってみます. 自分が作った flac_init() をみればわかるんですが struct メンバの隠蔽化のために malloc 系をつかっている箇所がありましたのでこれを static 化します.

bitreader.c

FLAC__BitReader *FLAC__bitreader_new(void)
{
#ifndef SHRINK
	FLAC__BitReader *br = calloc(1, sizeof(FLAC__BitReader));
	/* calloc() implies:
		memset(br, 0, sizeof(FLAC__BitReader));
		br->buffer = 0;
		br->capacity = 0;
		br->words = br->bytes = 0;
		br->consumed_words = br->consumed_bits = 0;
		br->read_callback = 0;
		br->client_data = 0;
	*/
	return br;
#else
	static FLAC__BitReader br;
	memset(&br, 0, sizeof(br));
	return &br;
#endif
}

stream_decoder.c

FLAC_API FLAC__StreamDecoder *FLAC__stream_decoder_new(void)
{
	FLAC__StreamDecoder *decoder;
	uint32_t i;

	FLAC__ASSERT(sizeof(int) >= 4); /* we want to die right away if this is not true */
#ifndef SHRINK
	decoder = calloc(1, sizeof(FLAC__StreamDecoder));
	if(decoder == 0) {
		return 0;
	}

	decoder->protected_ = calloc(1, sizeof(FLAC__StreamDecoderProtected));
	if(decoder->protected_ == 0) {
		free(decoder);
		return 0;
	}

	decoder->private_ = calloc(1, sizeof(FLAC__StreamDecoderPrivate));
	if(decoder->private_ == 0) {
		free(decoder->protected_);
		free(decoder);
		return 0;
	}

	decoder->private_->input = FLAC__bitreader_new();
	if(decoder->private_->input == 0) {
		free(decoder->private_);
		free(decoder->protected_);
		free(decoder);
		return 0;
	}
#else
#define STATIC_CALLOC(TYPE,NAME) static TYPE NAME; memset(&NAME, 0, sizeof(NAME))
	STATIC_CALLOC(FLAC__StreamDecoder, dd);
	decoder = &dd;
	STATIC_CALLOC(FLAC__StreamDecoderProtected, dp);
	decoder->protected_ = &dp;
	STATIC_CALLOC(FLAC__StreamDecoderPrivate, dv);
	decoder->private_ = &dv;
	decoder->private_->input = FLAC__bitreader_new();
#ifdef WIN32
	printf("dd %u, dp %u, dv %u dv.rice %u\n", sizeof(dd), sizeof(dp), sizeof(dv), sizeof(dv.partitioned_rice_contents));
	printf("dv.frame %u dv.frame.header %u dv.frame.subframes %u dv.frame.footer %u \n", sizeof(dv.frame), sizeof(dv.frame.header), sizeof(dv.frame.subframes), sizeof(dv.frame.footer));
	fflush(stdout);
#endif
#endif

これでは malloc を使わないので複数の struct が管理できませんが、複数を使うことはないと判断しております. これの利点は free() がいらなくなります.
ほかの細かい malloc/calloc/free は簡単に判断できなさそうなので後でやることにしました.

arm 向けの ROM としてリンクする

必要な関数だけを書いてビルドします. libflac を作る過程で標準ライブラリのヘッダが間違っているとか、関数の引数と呼び出しで int * と int32_t * が一致しないとかありましたが全て浅い問題ですぐに直せます.

diff --git a/flac-1.3.3/src/libFLAC/include/private/memory.h b/flac-1.3.3/src/libFLAC/include/private/memory.h
index a6d3faf..2298a81 100644
--- a/flac-1.3.3/src/libFLAC/include/private/memory.h
+++ b/flac-1.3.3/src/libFLAC/include/private/memory.h
@@ -37,7 +37,8 @@
 #include <config.h>
 #endif

-#include <stdlib.h> /* for size_t */
+//#include <stdlib.h> /* for size_t */
+#include <stddef.h>

main.c

static void flac_call(void)
{
	FLAC__stream_decoder_new();
	FLAC__stream_decoder_init_stream(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
	FLAC__stream_decoder_process_until_end_of_metadata(NULL);
	FLAC__stream_decoder_delete(NULL);
	FLAC__stream_decoder_seek_absolute(NULL, 0);
	FLAC__stream_decoder_process_single(NULL);
}
int main(void)
{
	//関係のないものは略
	flac_call();
	while(1) __WFI();
}

いまのところ動かせませんのでリンカが出す map file をみて .text (ROM), .rodata(arm では ROM), .data(RAM), .bss(RAM) の消費量をみます. .map ファイルを整形した物がこれです.

0x43f4 .text                    libflac.a(stream_decoder.o)
0x356c .text                    libflac.a(lpc.o)
0x1158 .text                    libflac.a(bitreader.o)
0x0bc0 .text                    libflac.a(format.o)
0x0a4c .text                    libflac.a(fixed.o)
0x09a4 .text.qsort              libg.a(lib_a-qsort.o)
0x07d4 .text._malloc_r          libg.a(lib_a-mallocr.o)
0x1100 .rodata                  libflac.a(crc.o)
0x063c .rodata.log.str1.4       libm.a(lib_a-w_log.o)
0x802c .bss                     libflac.a(bitreader.o)
0x1838 .bss                     libflac.a(stream_decoder.o)
0x0428 .data.impure_data        libg.a(lib_a-impure.o)
0x0408 .data.__malloc_av_       libg.a(lib_a-mallocr.o)
0x0028 _malloc_current_mallinfo libg.a(lib_a-mallocr.o)

libFLAC/bitreader.o

これは丁寧なコメントがあります. また malloc 排除の過程で定数からマクロに変えました.

/*
 * This should be at least twice as large as the largest number of words
 * required to represent any 'number' (in any encoding) you are going to
 * read.  With FLAC this is on the order of maybe a few hundred bits.
 * If the buffer is smaller than that, the decoder won't be able to read
 * in a whole number that is in a variable length encoding (e.g. Rice).
 * But to be practical it should be at least 1K bytes.
 *
 * Increase this number to decrease the number of read callbacks, at the
 * expense of using more memory.  Or decrease for the reverse effect,
 * keeping in mind the limit from the first paragraph.  The optimal size
 * also depends on the CPU cache size and other factors; some twiddling
 * may be necessary to squeeze out the best performance.
 */
#ifndef SHRINK
static const uint32_t FLAC__BITREADER_DEFAULT_CAPACITY = 
	65536u / FLAC__BITS_PER_WORD; /* in words */
#else
#define FLAC__BITREADER_DEFAULT_CAPACITY (0x2000)
#endif

FLAC__bool FLAC__bitreader_init(FLAC__BitReader *br, FLAC__BitReaderReadCallback rcb, void *cd)
{
	FLAC__ASSERT(0 != br);

	br->words = br->bytes = 0;
	br->consumed_words = br->consumed_bits = 0;
	br->capacity = FLAC__BITREADER_DEFAULT_CAPACITY;
#ifdef SHRINK
	static brword buf[FLAC__BITREADER_DEFAULT_CAPACITY];
	br->buffer = buf;
#else
	br->buffer = malloc(sizeof(brword) * br->capacity);
#endif
	if(br->buffer == 0)
		return false;
	br->read_callback = rcb;
	br->client_data = cd;

	return true;
}

0x2000 を 0x0400 に変えると 0x1000 bytes -> 4K bytes の消費になります.
この値をかえても PC で動かしてもいまのところ差はわかりません.

libFLAC/stream_decoder.c

libflac.a からの .text は当然ですが 0x1838 .bss は使いすぎなのでこれを調査しました. 原因はすでに stream_decoder.c に追加した printf の部分で FLAC__StreamDecoderPrivate\.(last)?frame\.subframes が原因でした.

libFLAC のライブラリを利用する側の include/FLAC/format.h に記述がありました.

(中略)
#ifndef SHRINK
#define FLAC__MAX_CHANNELS (8u)
#else
#define FLAC__MAX_CHANNELS (2u) //追記分
#endif
(中略)
typedef struct {
	FLAC__FrameHeader header;
	FLAC__Subframe subframes[FLAC__MAX_CHANNELS];
	FLAC__FrameFooter footer;
} FLAC__Frame;
(以下略)

FLAC__MAX_CHANNELS は私の目的では 2 つ必要で6個は未使用ですので削ります.*1それと lastframe は seek に必要と書いてあるのですが、削ってしまいました. こうすると消費量は大体 2/16 になります.

flac 側もsubframesの定義はポインタに変えてもらって動的確保していただくと静的化の記述も楽で助かります...

対応後

0x43b4 .text                    libflac.a(stream_decoder.o)
0x356c .text                    libflac.a(lpc.o)
0x1158 .text                    libflac.a(bitreader.o)
0x0bc0 .text                    libflac.a(format.o)
0x0a4c .text                    libflac.a(fixed.o)
0x09a4 .text.qsort              libg.a(lib_a-qsort.o)
0x07d4 .text._malloc_r          libg.a(lib_a-mallocr.o)
0x1100 .rodata                  libflac.a(crc.o)
0x063c .rodata.log.str1.4       libm.a(lib_a-w_log.o)
0x102c .bss                     libflac.a(bitreader.o)
0x0738 .bss                     libflac.a(stream_decoder.o)
0x0428 .data.impure_data        libg.a(lib_a-impure.o)
0x0408 .data.__malloc_av_       libg.a(lib_a-mallocr.o)
0x0028 _malloc_current_mallinfo libg.a(lib_a-mallocr.o)

ここまでやりますとメモリ確保はいけそうな気がしてきました. 動かすまではわからないのが処理速度で、これでいけると楽しくなる気がします.

何で flac を使おうかと決めたのは可逆圧縮は最優先ではなくて metadata として cuesheet, picture, tag が1つのファイルで管理できるのが魅力的だなと思ってます.

*1:ただし、これ前提で encoder を使うと標準規格からそれていきそうで危険だと思います