TS ファイルから分離した AAC が処理できなかった理由

d:id:RobinEgg:20090402:p1 で書いたようなワンセグに限らず、通常のフルセグでも TS ファイルを分割して取得した AAC ファイルを faad/ffmpeg で読み込ませようとするとコアダンプを吐いたりエラーが出て処理ができなかった理由について。

特に、 ffmpeg では

[libfaad @ 0x1538250]faac: frame decoding failed: Bitstream value not allowed by specification
[libfaad @ 0x1538250]faac: frame decoding failed: Invalid number of channels
Error while decoding stream #0.0
...

等というエラーが大量にはき出され、全く使い物になっていませんでした。

これは、放送波上の AAC データには、 raw data の前に ADTS ヘッダが付加されていることが多いのですが、この「ヘッダ・データ」の構造が正常に維持されず、ファイルがデータ部の途中から開始していると、ファイルの先頭に ADTS ヘッダが来ず、いきなりデータ部から始まるということになってしまい、 ADTS ヘッダのパースが正常にできないためにエラーが出力される、ということです。こうなると多くの再生・変換ソフトはお手上げで、軒並みエラーを吐いて死んでしまうため*1、ファイルの破損なのか、それともコーデック側の問題なのか、の切り分けが非常に困難なものとなってしまいます。一度 VLCWinamp を経由して wave ファイルに落とし込んでやれば特に問題ないのですが、如何せん面倒くさいのでどうにか自動的に処理をさせたいところ。

ちなみに、 AAC の ADTS ヘッダの構造は、

なんちゃって記 |FAAD2を使ってAAC再生ソフトを作る(その1)

というようになっているらしい。

手元にあるワンセグのTSファイルから抽出した AAC データの ADTS ヘッダは以下のようになっている:

FF F8 58 80 1F E2 24 0C AF A0
11111111 11111000 01011000 10000000 00011111 11100010 00100100 00001100 10101111 10100000

これを上記の表と比較すると、「MPEG2-LC/保護なし/2ch/24000Hz/オリジナル」であるということが判る。赤で示した部分が AAC のデータ部分。

同様に、フルセグ(2ch)では:

FF F8 4C 80 55 E1 3C F1 D5 21
11111111 11111000 01001100 10000000 01010101 11100001 00111100 11110001 11010101 00100001

「MPEG2-LC/保護なし/2ch/48000Hz/オリジナル」となっている。おそらく 5.1ch では

FF F8 4D 80...

となるものと思われます。ワンセグにしろ、フルセグにしろ、

  • MPEG2-LC
  • Layer 値が一定
  • 保護(DRM)はかかっていない
  • コピーフラグがない

なので、先頭は

FF F8

もしくは

FF F8 xx 80

で統一できるのではないかと。後者の場合、 aac_frame_length の値次第でビット値が変化しうるけれど、とりあえず手元にあるいくつかのデータでは全て一定値となっていたので、変化した場合はその時に考えるということで。なお、保存した AAC ファイルの先頭がこれらのビット値から始まっていなければ、その AAC ファイルは壊れたものと見なされていると思われます。

参考

519 :名無しさん◎書き込み中[sage]:2008/02/09(土) 16:56:03 ID: OhlfZXWj
    Mpeg2-AACのデータは現在サンプルをとっている限りでは
    2ch「FF F8 4C 80」 5.1ch「FF F8 4D A0」のようになっていて、1ブロックごとに同じヘッダが付いています。
    これが、1秒間に約46.88個並んでいます
    ※新たなヘッダ情報も出てくるかもしれないので、ヘッダ解析プログラムも作っておきます。
    擬似WAVE作成はこれを1ブロック毎にデータ内に時間軸に合わせて配置するだけです。
    ただし、ext_bsで抽出できる擬似WAVEとは異なるので、抽出プログラムも自分で作る必要があります。

    AviUtlで使用するならば、再圧縮無しWAVE出力はまったく化けないので、
    少々プログラムが書ける人ならば、一日で書けてしまうくらいの簡単なプログラムで終わります。

    MainConcept Mpeg HD PlugIn のPCM出力の場合は、おそらく1バイトおきに1ビットずつ化けています。
    これを考慮してプログラムを作るのが、なかなか厄介なのです。
【BDAV】BD Rip以降の行程を楽しむスレ Vol.2【AACS】

5.1ch「FF F8 4D A0」というのはコピーフラグが立っているから A0 になっているのか。

とりあえず書いてみた

非常に単純なコード。

上述の、

FF F8 xx 80

を検出し*2、検出点から終端までをそっくりそのままコピーしています。先頭2バイト FF F8 だけのチェックならもう少し簡単で済みますが、一応余白を取るような感じにしています。

AAC ファイルはそこまで大きくならない(100MB超えることは滅多にないと思う)ので、一気に読み出して変数に格納 → 一気に書き出し、をしようかと思ったけど、どうも某スレでは潤沢にメモリを積んだ環境のあるユーザーは少数なような気がするので、逐次 100KB ずつ読み込んで書き出し、を繰り返してます。50MB 位のファイルを指定して、メモリ増分は2,3MB程度かな。

特に理由はないけど Python で。やってることは非常に簡単なことなので、他言語への移植も簡単だと思う。移植はどうぞご自由に。なお、指定されたファイルが本当に AAC なのか、とかのチェックはしていません。その辺は臨機応変に。あまり綺麗なアルゴリズムじゃないのはご容赦。

#!/usr/bin/python
# -*- coding:utf-8 -*-
#
# ADTC ヘッダ開始の合図である FF F8 xx 80... を、壊れた AAC ファイルから探し出し、
# 以後のデータをそっくりそのまま書き出しファイルへコピーする
#
# 2009 (C) http://d.hatena.ne.jp/RobinEgg/
#
# Suppried with MIT License
#

import os, sys

if __name__ == '__main__':

  arg = sys.argv

  if (len(arg) != 2):
    print('Usage: $ python %s AACFile' % sys.argv[0])
    exit()

  # 読み出し AAC ファイル
  f = os.path.abspath(arg[1])
  splitExt = os.path.splitext(f)

  if (splitExt[1] != '.aac'):
    print("指定されたファイルは AAC ファイルではないようです。\nMPEG4 等のコンテナに含まれている場合は ffmpeg 等で\nraw AAC に変換してから処理を行ってください。\n念のため処理を中止します。")
    exit()
  
  # 出力ファイル
  outfile = splitExt[0] + '-truncated.aac'
  
  try:
    fr = open(f, 'rb')
  except IOError as erMsg:
    print("読み込みファイルを開けませんでした。\n" + str(erMsg))
    exit()

  # ファイルポインタ
  fp = -1
  
  # FF が見つかった時点で、配列上の位置情報を代入
  findPos = 0
  
  # ループフラッグ
  loop = True

  while (loop == True):
    # chunk に分けて読み出し
    x = fr.read(16)

    if (x == b''): break

    lg = len(x)

    # 0xFF = 255
    # 0xF8 = 248
    # 0x80 = 128

    for i, c in enumerate(x):
      if ( c == 0xFF ):
        if (i < lg - 3):
          if ( (x[i+1] == 0xF8) and (x[i+3] == 0x80) ):
            fp = fr.tell() - (lg - i)
            loop = False
            break
        elif ( (i == lg - 3) or (i == lg - 2) ):
          if (x[i+1] == 0xF8):
            findPos = i
            continue
        elif (i == lg - 1):
          findPos = i
          continue

      if (findPos != 0):
        if ( (findPos == lg - 3) and (x[0] == 0x80) ):
            fp = fr.tell() - (2*len(x) - findPos)
            loop = False
            break
        elif ( (findPos == lg - 2) and (x[1] == 0x80) ):
            fp = fr.tell() - (2*len(x) - findPos)
            loop = False
            break
        elif ( (findPos == lg - 1) and ( (x[0] == 0xF8) and (x[2] == 0x80) ) ):
            fp = fr.tell() - (2*len(x) - findPos)
            loop = False
            break

  if (fp == -1):
    fr.close()
    print("ADTS ヘッダが見つかりませんでした。指定されたファイルは本当に AAC ファイルですか?")

  elif (fp == 0):
    fr.close()
    print("既にヘッダが整えられています。これ以上作業する必要はありません。")

  else:

    fr.flush()
    fr.seek(fp)
    try:
      fw = open(outfile, 'wb')
    except IOError as erMsg:
      fr.close()
      print("書き込みファイルを開けませんでした。\n書き込み権限があるか、共有違反が起こっていないかを確認してください。\n" + str(erMsg))
      exit()


    while 1:
      # chunk に小分けして読み出して書き込み
      x = fr.read(1024 ^ 3)
      if (x == b''):
        break

      fw.write(x)

    fw.close()
    fr.close()

    print('Done!')

なお、

FF F8

のみのチェックにとどめたい場合は、 for ループ内を以下と入れ替えてください。

    for i, c in enumerate(x):
      if ( c == 0xFF ):
        if (i < lg - 2):
          if (x[i+1] == 0xF8):
            fp = fr.tell() - (lg - i)
            loop = False
            break
        elif (i == lg - 1):
          findPos = i
          continue

      if (findPos != 0):
        if ( (findPos == lg - 1) and (x[0] == 0xF8) ):
            fp = fr.tell() - (2*len(x) - findPos)
            loop = False
            break

どうぞご利用ください。

*1:試した中で正常再生できたのは VLC/Winamp くらい

*2:xx は任意の1バイト