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) を追加するにはこの自動生成の機能が欠かせません。