24000/1001と23.976の違いは1時間あたり3.6msに過ぎないが、
このメモの本質的内容は、理論と経験が一致している。 ただし具体的な数値を導いている部分(ずれ発生確率など)は計算ミスしている可能性があるので、 暫定的な参考・速報値とみてください。
24fpsと呼ばれる動画には、実際には24000/1001 fps(NTSC定義値)のものと、 23.976 fps(割り切れるようにした近似値)のものがある。 これらの分子、分母は、AVIヘッダでは、 dwRate / dwScale として定義されている。(AVIFILEINFOヘッダは信頼性が低い。AVISTREAMINFOヘッダを使うべきである。) 後者の値としては、2997/125 や、それと等しい分数(23976/1000など)が用いられる。
前者は 23.976023976...fpsにあたり、23.976 fpsは切り捨て方向に丸められている。 言い換えれば、23.976の方がコマ送りが少し遅い。したがって、
前者のfpsは後者のfpsの
1.000001000001000001000001000001…
倍である。この係数は、
(24000/1001) / (2997/125) = (8000/1001) / (999/125) = 1000000/999999
である。なお
2997 = 34×37
である。
1フレームの継続時間は 41708375 vs. 41708333ns であり、 両者の差は、41.7ns/frame で、4時間たっても差の累積は0.01秒のオーダーに過ぎない。
しかし、このわずかな差が、字幕のタイミングに影響することは、現実に発生する。 一般に、いわゆるノーマライズ(フレームへのスナップ[吸着])を行っている場合、 すなわち「フレームチェンジ・イベントの発生時刻」と「字幕イベントのタイムスタンプ」が接近している場合、 後者は前者のゆらぎに巻き込まれやすい。 「理論上のフレームチェンジ時刻」と「字幕イベント時刻」が接近しているので、 実際のフレームチェンジ時刻がわずかにゆらいだだけで、 字幕イベントが前後どちらのフレーム上で発生するものなのかが変わってしまう可能性が高くなるのである。 この問題は、通常の(最大化方向の)スナップを行っているときには、 23.976でタイムしたものを24000/1001で再生された場合に発生し、 逆の場合には発生しない。また、もし通常と逆の(最小化方向の)スナップを行っているときは、 それと正反対になる。
最も典型的なケースとして、 最大化方向のスナップを行った字幕のタイミングについて、 23.976でタイムしたものを24000/1001で再生した場合、 フレームタイミングが意図より1フレーム遅延してしまうことがある。 23.976の方がコマ送りが少し遅いので、より大きな時刻にスナップしてしまうからである。
例えば、フレーム#21435上まで表示され#21436上では表示されない字幕を考えると、
フレーム#21436は、
14:54.060 727 394 @23.976fps (VirtualDubでの表示.061)
14:54.059 833 333 @24000/1001fps (VirtualDubでの表示.060)
であるから、23.976fpsを基準に、センチ秒の吸着を行えば14:54.06であり、
ミリ秒の吸着を行えば14:54.060である。この14:54.06というタイミングは、
上記の数値のように、23.976fpsではフレーム#21436より過去であるが、
24000/1001fpsではフレーム#21436より未来である。
だから、もし24000/1001fpsで再解釈すると、字幕消失イベントがフレームチェンジ・イベント#21436より未来になってしまい、
#21436上に字幕が表示されてしまう。これは当初の意図と異なる。
このように、1フレームあたり50ナノ秒にも満たない微細な違いが、 フレームタイミングの巨視的な差となることが現実にありうる。
上記は例外的ケースではなく、試算によると、 30分のクリップで、上記のような微妙な差の影響を受ける可能性のあるフレームは、約5%に及ぶ。
なお、SSAのタイムスタンプをフレームタイミングの約10~20ms前に「準吸着」させると、上記問題は、 おおざっぱに22万フレーム、ざっと2時間半までは、回避できる。
しかし、これはあくまで「現在のSSA処理系」に依存した場合で、 将来、サブフレーム処理(ビデオのfpsより速く、1フレーム内で字幕を更新する処理)が可能なレンダラーが出現すると、別の問題が生じる。また、USFのようにタイムスタンプが1ms単位の場合も、もっと細かく制御しないと、 問題が生じる。
(1) 23.976fpsのビデオでフレームタイミングを行うときには、 23.976fpsが本来近似値であり、いつなんどき24000/1001fpsにAssumeFPSされないとも限らないことを認識し、 可能最大スタンプに吸着するのでなく、 センチ秒精度での第二最大スタンプあたりに吸着(フレームタイミングへの「準吸着」)した方が安全である。 特に、最終出力が23.976fpsでも、VSFilterを通過するときは24000/1001fpsで、 そのあとの何かの処理で23.976に近似されている可能性がある。 この場合、「基準となるはずのrawがVSFilterの処理時と異なるfpsを示す」ことになり、 原因究明が難しいフレームタイミングの問題が発生する。 ただし、そのような変換がないことが分かっていて、23.976のままその場でただちにハードサブするのであれば、その限りでない。
(2) ノーマライズを行わないタイムスタンプは、たまたま吸着していたり、たまたま吸着していなかったりするので、 ノーマライズを行っているタイムスタンプより管理しにくい。基本的に何らかのノーマライズは行う。
(3) フレームごとに位置が変わるサイン(フライング・オブジェクト)を、 時間区分ごとに、いくつかのダイアログ行として分割して記述するとき、 念のため、各ダイアログごとにレイヤーを変える方が良い。 何らかの予期せぬ理由でフレームタイミングのゆらぎに巻き込まれたとき、 結果としてコリジョンが起き、 コリジョン処理が適用されると、いっそう事態が悪化してサインが1フレームだけ不連続に跳躍したりするからである。 ゆらぎに巻き込まれること自体、既に致命的であるが、もしレイヤーを変えてあれば、 コリジョン処理からは逃れられ、1フレーム内にサインが2重写しになってしまうものの片方は意図した位置、 他方は意図と少しだけ違う位置(1フレーム前の位置など)に来てくれる。 ぼけてしまうが、サインが不連続に跳躍するよりはまだましだ。 不連続跳躍は目立つが、一瞬の残像なら気づかれない可能性もある。
2006年2月のDGIndex 1.4.6以降では、正確なfps(4/5倍して24000/1001)を使うようになりました。 それ以前のDGIndex 1.4.5以前やDVD2AVIでは23.976fpsで処理されていました。 このため、1フレームの長さが4ns違うfps非互換の2種類のビデオがこれまで以上に混在するのが現状です。 VDではfps違いは連結できないので、連結する方法を追記します。
2997/125の clip1 と 24000/1001の clip2 を24000/1001に統一して連結するAVSの例:
a = AVISource( clip1 ).AssumeFPS( 24000, 1001, false )
b = AVISource( clip2 )
return a + b
AssumeFPS
の第3引数を true にすると、オーディオのサンプリングレートも合わせて変更しようとする。
しかし、サンプル/秒を単位として例えば clip1 のオーディオが48000のとき、
それを clip2 に合わせるときの補正後の理論値は約48000.048であって、
正常な処理が期待できない。
そもそも、23.976fps のクリップでは24000/1001に同期するタイミングの音声がmuxされている可能性が高い(もともと厳密には非同期)。
その場合、ビデオのfpsだけ変えて音声を補正しないことで、かえって厳密に同期するようになる。
仮に音声が23.976fpsに厳密に同期していたとして、
かつ、補正をしないとしても、音ずれ量は1時間あたり3.6msすなわち、
24~25分後に 1.4~1.5ms、3時間後に 10.8ms であり、無視できる。
よって false が正しい。
ちなみに、この場合の音声の同期ずれの蓄積が映像1フレーム分に達するのは11時間半後である。
Avisynth 2.55 以降では、
AssumeFPS ( clip1, clip2, false )
という書き方もできる。
この書き方は「どちらに合わせるのでもいいからとにかく連結したい」とき便利であり、
実際には clip1 が clip2 のfpsに補正される。
どちらのfpsに統一されるのか分かりにくいので(そして字幕作成では正確なfpsを知っている必要があるので)、
ミスを防ぐために、最初のように明示的に書いた方が良い。
「23.976」と直接書くのは危険
2種類の23.976については、既に詳述しました。 24000/1001 = 23.976023976... と、それを割り切れるようにした近似値 23.976 です。 ところが、 うっかりすると、もう一種類の23.976が入り込んで、事態がさらにややこしくなります。
23.97599983215332 fps
なんじゃこりゃ?
何とも妙な値ですが、勘のいいかたならピンと来たでしょう。 そうです、23.976が割り切れるのは十進法の話で、 二進数では23.976は割り切れない数なので、float の 23.976 は内部的に、 十進表記すると、上記のような値になっているのです。
実際、AviSynthに23.976というfloat値を与え、内部的に分数表現に変換させると、このような妙な値になります。 したがって、十進小数では割り切れる 23.976 を使いたい場合でも、 AssumeFPS(23.976) ではなく AssumeFPS(2997, 125) のように分数で書くべきなのです。 類例としては、BlankClip(fps=2997, fps_denominator=125...) があります。
VDMからOGMを書き出すと、24000/1001fpsが、10000000/417083fpsになるようだ。
23.9760431376968133441065687165... fps
OGMは、1rt (=100 ns)単位でのフレームの長さを保持しているのかも。
41708333 ns = 417083 rt
何となくMKVやMP4にも似た問題がありそうな予感がする。(追記: 確認したがMKVはかなりうまくやっていて、 fpsの揺らぎはあるもののそれを巨視的にするキラーサンプルを作れなかった。) 分数は分数で保持してほしいのに…
このずれ方は、本来の23.9760239760…より大きめ、つまりイベント進行が早めになるため、 ノーマライズされているタイムスタンプにとって、脅威となる。 「fpsのわずかな違いが巨視的問題を発生させるメカニズム」で具体的に説明する。
最初の30分間で、「可能最大スタンプ」を使った場合、3808フレームが問題になりうる。 msに丸めた最下位桁が0のとき10ms引くと、1971フレーム。 さらにmsに丸めた最下位桁が1のとき11ms引くと、93フレーム。 さらにmsに丸めた最下位桁が2のとき12ms引くと、0フレーム。
ms単位に丸めた最下位桁を切り捨て、10ms引く方法(暫定推奨)では、 最初の問題フレームは2:38:59後。 23.976fpsにて2:38:59.539539540(ms単位で2:38:59.540)のフレーム228720を2:38:59.53と表現すると、 24000/1001fpsではフレーム228721。
「次最大スタンプ」を使った場合、最初の問題フレームは2:47:20後。 23.976fpsにて2:47:20.040040040のフレーム240720を、2:47:20.03と表現すると、 24000/1001fpsではフレーム240721。 msに丸めた最下位桁が0のときさらに10ms引くと、最初の問題フレームは2:55:40後。
「第三スタンプ」を使った場合、最初の問題フレームは5:34:08後。 23.976fpsにて5:34:08.340006673のフレーム480679を、5:34:08.32と表現すると、 24000/1001fpsではフレーム480680。
字幕座標系の区別 ―― relative-to-screen(relative-to="Window")とrelative-to-frame(relative-to="Video") ―― は、2002年8月17日、Christophe PARISがUniversal Subtitle Format (USF) Specification DRAFT v0.02で導入した。 Gabestは初期USFをサポートしたが、現存する最古のSTS.h Revision 8(2003年5月)のSTSStyleクラスにも、relativeToメンバーがある。 しかし、MPCで座標系をユーザ設定できるようにしたのは2005年2月である。 VSFilterが常にrelative-to-frameであるのにそれまでMPCは常にrelative-to-screenであり、フルスクリーン表示にしたときソフトサブの表示位置の問題があった。
MPCの"Position subtitles relative to the video frame"オプションは2005年2月に加わった(MPC 6.4.8.4)。
このときSSAのスタイルごとに字幕座標系をユーザ側で設定できるようになったので、それをSSAデータ側から明示できるようにすることは自然な発想だろう。
ASS2形式は、ASSの拡張として、2005年11月、Gabestによって実装された(MPC 6.4.8.7、STS.cpp)。
このとき{\kt}
タグも追加された。
ASS2形式のデータを出力する例は、2005年11月のMP4Splitter.cppに見られる。
MEDIASUBTYPE_ASS2 {370689E7-B226-4f67-978D-F10BC1A9C6AE} は、2006年3月現在、/include/moreuuids.hで定義されている。 MEDIASUBTYPE_ASS2は抽象的なデータストリームで、テキストファイルとしてのASSではない。 MEDIASUBTYPE_ASS2のファイルは、ASSと同じ拡張子.assを使う(ConvertDlg.cpp)。
ASS2では、ScriptType: v4.00++、[V4++ Styles]とし、従来の
MarginV, Encoding の代わりに、MarginT, MarginB, Encoding, RelativeTo のように定義する。
イベントにもMarginが4つある。RelativeToを0にするとスクリーン座標、1にするとフレーム座標、2は未定義*1である(STS.h revision 281)。
外部ASS2ファイルを内蔵レンダラーで解釈した場合、有効になる。
VSFilterでは正しく解釈できない。
また、MPCのDSM MuxerでDSMに埋め込むことはできず、
GabestのMuxerとSubtitleSourceでMKVに埋め込むこともできなかったが、
mkvmergeでは透過的に(サポートしているわけではないのだが、結果的に)埋め込める。
外部ファイルのサンプル4KB、
ASS2_Test.7z
埋め込んだサンプル40KB、
ASS2_Test.mkv
*1 2004年12月の"Absolutely absolute" positioningでは座標の基準について「物理的スクリーン」「表示フレーム」「オリジナルのフレーム」の3種類を区別する必要があることが指摘されている。パン&スキャンやアスペクト比の変更があると「表示フレーム」と「オリジナルのフレーム」が一致しないからである。
SSA/ASSのScaledBorderAndShadowヘッダと、SSFについて。
ScaledBorderAndShadow(等比的な輪郭と影)というのは、 要するに「画面を200%に拡大して字幕の文字サイズが200%になったら、 字幕の文字の輪郭や影もそれに比例して拡大される」ということだ。 一見そうなるのが当たり前のようだが、実はSSA/ASSのデフォルトではそうなっていなかった。 CSSで「ピクセル単位で絶対的なサイズを指定するか%などの相対的なサイズを指定するか」の問題と似ている。 詳細はレンダラーの実装によるが、SSA/ASSのボーダーとシャドウ(BAS: Border And Shadow)は、 デフォルトではピクセル指定に近く、 画面全体を拡大して文字フェイスが大きくなっても、 BASはそれに比例しては拡大されなかった。
結果として、拡大率200%程度なら「影が軽く見えていい」くらいで済むが、 高解像度モニターの全画面表示などで表示上の不整合があり、特に、 ビデオフレームに対する字幕文字位置が、ビデオを拡大したときに微妙に相対変化してしまうなどの問題がある。
GabestはSSA/ASSでも独自にScaledBorderAndShadow
ヘッダ(値はYesか非Yes)を導入し、
CSimpleTextSubtitle
クラス(いわゆるSTS)の m_fScaledBAS によって挙動をスイッチさせる実装を行った。
SSA/ASSのScaledBASのデフォルトはfalseであり、
ほとんどのスクリプトは、ScaledBorderAndShadow
を明示しないから、ScaledBASが使われない。
SSAと違いASSとSSFではボーダーとシャドウは独立しているが、
さらに、ASSと違いSSFではデフォルトでScaledBASを使う。このため、SSFのBASは、ASSで
ScaledBorderAndShadow: Yes
を宣言した場合とほぼ互換になる。
サンプル: 320x240のスクリプトを640x480でソフトサブした場合のBAS(詳細)
ASSデフォルト(左)とScaledBorderAndShadow: Yesを宣言したASS(右)
SSF
ScaledBASを使うと、拡大された画面に対して、字幕のボーダー、シャドウについてもアンチエイリアスなどの計算が入るので、 描画品質が向上する。 SSFでは、さらに、SSA/ASSと違って、シャドウが落ちる角度も明示でき、シャドウの独立性・操作性が高まっている。
半面、精緻な処理のため再生時負荷が高くなることと、 SSA/ASSに慣れているソフトサバーにとっては「画面拡大時のBAS」に関するこれまでの経験と違う状態になることに注意する。
小数点以下6桁(7桁目で四捨五入)の数値で言うと、 フレーム16373は、24000/1001fpsでは682.890542秒、 10000000/417083fps(OGM)では682.889996秒。 この微視的な違いは「682.89秒とどちらが先か」が問題となるケースにおいて、 巨視化する。すなわち…
理論上は音声トラックとも同期ずれを起こすが、現象が「巨視化」しやすい字幕で説明しよう。
想定するfpsを24000/1001とすれば、
0ベースのフレーム16372は682.848833秒、
フレーム16373は682.890542秒に開始される。
今、フレーム16373から表示開始される字幕を考える(16372には乗ってなく、16373には乗っている)。
この字幕表示開始の実イベント時刻は、フレーム16373の開始時刻に等しい。
しかしSSAには0.01秒単位の時刻を記入するので、この実イベントに対応する論理タイムスタンプは、
0:11:22.85
0:11:22.86
0:11:22.87
0:11:22.88
0:11:22.89
の5種類が考えられ、どの論理タイムスタンプも、同じ実イベントタイミングに対応する。
なぜなら、ある論理タイムスタンプにおける字幕描画開始命令とは「次のフレームチェンジと同時に字幕をスタートさせよ」という命令だからであり、考えているfps(1フレームが約42msという時間解像度)とSSA(10msの時間解像度)では、
後者の方が粒度が4倍強、細かいので、前者の1イベントごとに後者では4~5個の論理タイミングが発生する。
考えられる論理タイムスタンプのうち最大のもの(可能最大)、
つまり実イベントのタイミングとの誤差最小のスタンプを使うことを、
「字幕のタイムスタンプのノーマライズ(最大化)」または「フレームへのスナップ(吸着)」という。
この場合、ノーマライズされたタイムスタンプは、0:11:22.89である。
0:11:22.89に「次にフレームが変わったらこの字幕を出せ」という命令を発行、
同22.890542にフレームチェンジ、よってそこに字幕が乗る。この順序が重大なのである。
ところが予定では24000/1001だったものが、少し速い10000000/417083fpsでイベント進行されると、
われわれが字幕を乗せたいと思っているフレーム16373は、同22.889996に描画されてしまう。
予定より早すぎるのである。このため、
0:11:22.89に「次にフレームが変わったらこの字幕を出せ」という命令を下しても、
既にフレーム16373は始まってしまっているので、字幕が出るのは一つ遅れた16374になってしまう。
この問題は全フレームで発生するわけではなく、 最大化のマージン (採用論理スタンプと、吸着したフレームのタイミングの差)がたまたま非常に小さいときに発生する。 上の例では、マージンは0.542msである。 「fps違い」といった根本的問題がない限り、0.5msは十分なマージンだが、 fpsのわずかな違いがあった場合、当然ながら、1時間、2時間…と長時間が経過するほど問題が少しずつ拡大し、 より大きなマージンでもなお危険になる。
以上が「字幕のタイムスタンプのノーマライズ」と「fpsが予定よりわずかに大きくなったときに発生する問題」である。
今の場合、fpsが間違っているという明らかなトラブルで、再現可能な現象となるが、 そうでなくても、何らかの原因で、偶発的に実イベントのタイミングがゆらぐことがありうる。 そのため、ノーマライズは、ぎりぎり最大までは行わず、多少ゆとりを持たせることが推奨される。 実イベントのタイミングつまりフレームの立ち上がりエッジに、 論理スタンプが接近し過ぎると、上記のように潜在的な危険が増す。 SSA入門中級編では、100%のノーマライズをせず、エッジとの距離が0.5ms離れられないときは、 一つ手前の論理スタンプ(「次最大スタンプ」)を使うようにしている。最低でも0.5msのマージンは保つ、ということだ。 最近、DGIndexがfpsの解釈を微妙に変えたことで、既存の字幕スクリプトと潜在的な非互換が発生している。 これを見ると、 ノーマライズのマージンは0.5ではなく約10にした方が安全なのではないか、とも思われる。 上の例ではマージンは0.542msであるが、 既に説明したようにfpsのわずかな変動によって、フレーム・アキュレートでなくなる。
なお、どの論理スタンプでも実イベントとの同期は同じになるというのは、あくまで同期のタイミングであり、 イベントのプロパティは保証されない。 アニメーションや色変化、アルファ変化などのミリ秒単位の動的エフェクトがかかっているダイアログでは、 論理スタンプをいじると、一般に、イベント発生時の字幕の状態が変化する。例えば、 50msでフェイドインしろ、という指示を書いたとき、スタンプが最大化されていれば、 字幕が出たときはほとんど透明だが、もし逆にスタンプが最小化されていると、 「50msでフェイドイン」と言っているのに、表示開始されたとたんにほとんど不透明になってしまっている。 このような問題があるから、 理論上は、 スタンプは最大化(ノーマライズ)しておく(要するに実イベントのタイミングと書いてあるタイミングのずれを最小化しておく)ほうが、 都合が良いのである。
理論的には劣るものの、 エッジに論理スタンプが接近することによる危険を嫌って、 あえて最大側にも最小側にも吸着せず、真ん中へんの論理スタンプを使うという考え方もある。 可能最大・次最大スタンプに対して中間的スタンプと呼ぶ。
以上のように、 (1) エッジとの距離の取り方、(2) フェイドを初めとするさまざまなエフェクトの完全なコントール、 の両方の観点から、 実務上、 画像の1フレームの時間よりさらに踏み込んだサブフレーム精度での字幕制御が必要である。 ここに至っては、 LAMEタグのようにまるまる1フレームも同期を混乱させる存在は「巨悪」としか言いようがないのである。