MPEG-4、ツリー探索、autovivification
近ごろ動画や音声データの読み書きを Scheme でしているんですが、MPEG-4 の処理が個人的にかなり楽しかったので少し書きます。
MPEG-4 のデータはこのようなツリー構造になっており、お馴染みのツリー探索のテクニックが活躍しそうな雰囲気です。
(define structure '((moov . (mvhd trak udta iods)) (trak . (mdia tkhd)) (mdia . (minf mdhd hdlr)) (minf . (stbl vmhd smhd dinf)) (dinf . (dref)) (stbl . (stsd stsc stts ctts stco co64 stss stsz)) (udta . (meta cprt)) (meta . (ilst id32))))
(全てを網羅したものではありません。例えば子ノード (box) を持たないトップレベル box はこの図には入っていません。)
MP4 のツリー構造にはルートノードというものは無く、トップレベルの box が複数並んでいる構成のため、box を構造体 (下記) で表し、それらをリストでまとめるという方法で扱っていきます。
(define-struct box (size type ext-size data))
size は 32 ビットの符号無し整数で、box のサイズがそれを超える場合は size を 1 とし、64 ビットの ext-size フィールドを使う仕様になっています。
data にはバイト列か、サブツリーとして box のリストを入れます。なお、メディア本体の場合はバイト列に変換せず、入力ポートをそのまま置くこととします。
ツリーの読み込みは前述のように、トップレベル box をリストで集めるだけです。
(fun (mp4-boxes in) (let loop ((t '())) (receive (x done) (read-box in) ((if done reverse loop) (cons x t)))))
動画サイトの MP4 を扱う前提なので、メディア本体のデータは最後に来ることを想定しています。この順序は必ず保たなければいけません。
個々の box は以下で読み込みます。
(fun (read-box in) (receive (size type ext-size) (box-head in) (if (eq? type 'mdat) (values (make-box size type ext-size in) #t) (values (let ((dsize (data-size size ext-size))) (make-box size type ext-size (if (eq? type 'meta) (meta-tag dsize in) (parent? type) (box-kids dsize in) (read-bytes dsize in)))) #f)))) (fun (box-kids size in) (let loop ((size~ 0) (kids '())) (if (= size~ size) (reverse kids) (receive (kid done) (read-box in) ;; `done' must not be #t here (loop (+ (box-size~ kid) size~) (cons kid kids))))))
parent? で子 box を持つタイプかどうかを判別します。最初の structure の左のコラムがそうです。
(define parent? [memq _ (map car structure)])
parent? は読み込みの際に必要ですが、ツリーの検索や更新をする場合には逆に、子からその親を調べる関数が必要になることに気づきました。
(fun (parent type) (ormap (fn ((cons par kids)) (and (memq type kids) par)) structure))
type を子に持つ親 box のシンボルを返します。#f が返った場合はトップレベル box だと判断します。
ここからが本題で、読み込んだツリー構造を更新する関数を作ります。タイトルを付けたりカバーアートを埋め込んだりする際に使うものです。
(fun (insert-box box boxes) (let ((par (parent (box-type box)))) (if par (if (find-tree (is-box? par) boxes) (map-tree (fn (x) (if (is-box? par x) (update-box x (append (filter (negate (is-box? (box-type box))) (box-data x)) (list box))))) boxes) (insert-box (new-box par (if (eq? par 'meta) (list null-space box) (list box))) boxes)) (cons box boxes)))) ;; Helper functions (fun (fold-tree f s t) (let loop ((t t) (s s)) (aif (f t s) it (box? t) (loop (box-data t) s) (pair? t) (loop (cdr t) (loop (car t) s)) s))) (fun (find-tree p? t) (prompt (fold-tree (lambda (x _) (if (p? x) (control k x))) #f t))) (define weight (fold-tree (lambda (x s) (if (box? x) (+ (box-head-size x) (weight (box-data x)) s) (meta-box? x) (+ (meta-box-size x) s) (meta-data? x) (+ (meta-data-size x) s) (bytes? x) (+ (bytes-length x) s))) 0)) (define ext-size? [>= _ (expt 2 32)]) (define (update-box abox data) (let* ((size (+ (weight data) 8)) (ext? (ext-size? size))) (struct-copy box abox (size (if ext? 1 size)) (ext-size (and ext? (+ size 8))) (data data)))) (fun (new-box type data) ;; using update-box to auto compute the size (update-box (make-box 0 type #f #f) data))
box をツリー内の狙った位置に投入したいということで、まず親を探して (find-tree)、もしあればその中に追加する (map-tree, update-box)、無ければ親 box を作る、という流れです。
妙なこだわりで、"!" の付く構文を一切使いたくないというのがあるせいで、find-tree と map-tree で同じ検索を2度してしまうことになるのが残念です。
ここで思い出しました。必要無い時はコンスしない map の定義方法です。
(fun (map-tree f t) (aif (f t) it (pair? t) ;; To cons less often. See: ;; http://okmij.org/ftp/Scheme/zipper-in-scheme.txt (let ((a (car t)) (d (cdr t))) (let ((a~ (map-tree f a)) (d~ (map-tree f d))) (if (and (eq? a a~) (eq? d d~)) t (cons a~ d~)))) (box? t) (let* ((x (box-data t)) (x~ (map-tree f x))) (if (eq? x x~) t (update-box t x~))) t))
これに基づいて変更したバージョンが以下です。
(fun (insert-box box boxes) (let ((par (parent (box-type box)))) (if par (let ((boxes~ (map-tree (fn (x) (if (is-box? par x) (update-box x (append (filter (negate (is-box? (box-type box))) (box-data x)) (list box))))) boxes))) (if (eq? boxes boxes~) ;not modified == par box not found (insert-box (new-box par (if (eq? par 'meta) (list null-space box) (list box))) boxes) boxes~)) (cons box boxes))))
find-tree を無くすことができました。
試しに使ってみましょう。空の box のリストに box を挿入しようとすると、
> (pp (insert-box (new-box 'ilst #f) '())) (#(struct:box 36 moov #f (#(struct:box 28 udta #f (#(struct:box 20 meta #f (#"\0\0\0\0" #(struct:box 0 ilst #f #f))))))))
となり、自動的にツリー構造が生成されていることが分かります。(サイズ計算もばっちり)
検索して挿入する場合もこの通り、
> (pp (insert-box (new-box 'ilst #f) (insert-box (new-box 'cprt #f) '()))) (#(struct:box 44 moov #f (#(struct:box 36 udta #f (#(struct:box 8 cprt #f #f) #(struct:box 20 meta #f (#"\0\0\0\0" #(struct:box 8 ilst #f #f))))))))
うまく行っています。
ニコニコ動画の MP4 には udta の下に cprt (コピーライト) の box しか無いので、メタ情報 (ilst box) を追加するにはこの自動生成の機能が欠かせません。