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 を使うと標準規格からそれていきそうで危険だと思います