Skip to content

Latest commit

 

History

History
768 lines (625 loc) · 57.7 KB

joining.ja.md

File metadata and controls

768 lines (625 loc) · 57.7 KB

複数ファイルの結合・基準クリップ・トランジション

← ドキュメント ・ ← SmartCut ・ English

ここまでの機能は、どれも 1 本の録画から必要な部分を書き出すものである。 この文書が扱うのは、複数の録画を 1 つのファイルに書くときに初めて生じる 3 つの問題である。

結合 複数の録画を順に並べて 1 ファイルにする
基準クリップ 仕様の違う録画を、基準にしたクリップの形に合わせる
トランジション クリップとクリップの継ぎ目に効果を入れる

3 つが同時に入ったのは、同じ問題の言い換えだからである。出力ファイルは映像の 仕様を 1 つしか持てないが、録画のリストは本数ぶんの仕様を持っている。

1 本のカットは、もともと「リール 1 本の結合」だった

必要な仕組みの大半は既にあった。新しく書いた部分を説明する前に、その理由を 先に述べる。

1 本の録画のカットは、残す区間を出力タイムライン上に順に並べたものである。 各区間は自分の最初のピクチャが出る瞬間を基準に置かれるので、ある継ぎ目 での誤差が次の継ぎ目へ積み上がらない。音声も区間ごとにその基準へ対応づけ、 前の区間から持ち越さない。セグメントは毎回自分で入力ファイルを開き直す。

どれも結合のために書いたものではない。5 区間に切った録画には継ぎ目が 4 か所 あり、その 1 つ 1 つが結合と同じ問題だからそうなっていた。

つまり今回の変更は連結処理を新設することではなく、区間のリストが複数の録画 から来られるようにすることだった。

pub struct Reel<'a> {
    pub src: &'a Source,
    pub plans: &'a [RangePlan],
    pub after: Transition,
}

pub fn join(reels: &[Reel], master: usize, output: &str, opts: &CutOptions) -> Result<()>

cut はリール 1 本の join であり、同じ経路を通る。これは主張ではなく検査 してある。tests/run_join_tests.sh が同じ単体カットを 2 回書き出して バイト単位で比較する。

出力が 1 つしか持てないものは、すべて基準クリップから取る

ストリームは最初のパケットを書く前に宣言され、後から取り消せない。したがって 出力は次の項目をそれぞれ 1 つだけ持ち、その供給元は基準クリップ 1 本である。

映像 コーデック・解像度・ピクセルフォーマット・フレームレート・走査方式・画素アスペクト・色域
タイムライン 目盛りの細かさ。基準クリップのレートから決まる(Grid)
トラック構成 音声トラックの本数と各トラックの宣言内容、字幕ストリーム
テーブル 放送の SDT・EIT・TOT と、各ストリームを載せる PID
ディスクが描く字幕 基準クリップのものだけ(後述)

基準クリップを先頭固定にせず呼び出し側に選ばせているのは、順番と基準が別の 問題だからである。12 本の本編と 1 本の予告から成るリストで欲しいのは本編の 仕様であり、予告がリストの先頭にあることは珍しくない。

画素アスペクトは、基準クリップを表示するときの比を使う。MP4 や Matroska のコンテ ナがシーケンスヘッダとは別に比を書いていればその値、書いていなければビットスト リームの値である。.mp4 と .mkv には、これをストリームの値として書く。.ts・ .m2ts・ディスク・.mpg・.vob にはその置き場所が無い。そこで、基準クリップの コンテナとシーケンスヘッダが食い違うときは、ファイル中のコピーしたシーケンスヘッ ダをすべて基準クリップの形に書き換え、継ぎ目も同じ比で符号化する(Rust コ アを参照)。 1 本の .ts にコピーで結合しても、途中で形が変わることは無い。

基準以外のクリップが供給するもの

映像と音声、それに字幕である。どのストリームが出力のどのトラックになるかは 順番で決める。 1 本目の音声は出力の 1 本目へ、2 本目は 2 本目へ入る。

他に手がかりがない。PID は放送ごとに意味が変わり、ストリーム番号は libavformat がファイルごとに付ける番号であり、言語は記載されていないことの ほうが多い。同じ放送を録画した 2 本はトラックの並び順が同じなので、想定して いる用途では順番が最も確実である。

字幕も順番で決めるが、数えるのは種類と形式が同じものどうしの中だけで ある。放送は字幕と文字スーパーをどちらの順にも並べる。まとめて数えていた ときは、2 本目の録画の字幕が基準クリップの文字スーパーに書かれたり、ARIB の トラックが TTML のトラックへ渡されたりしていた。

基準よりトラックが少ないクリップは、足りないトラックの区間が無音になる。 多いクリップは余ったトラックを出力しない。どちらも書き出す前に 1 度だけ 知らせる。

字幕も、種類と形式が同じものどうしで基準クリップより多い分は出力しない。 字幕の無い録画を基準にして字幕付きの録画を結合したときがこれにあたる。この 場合も書き出す前に note: <クリップ> carries N more caption stream(s) than the master does を 1 度だけ表示する。0.8.36 までは何も表示せずに外していた。

ディスクが描く字幕は基準クリップのものだけを運ぶ。 手間を省くための制限 ではなく、正しい答えを保つための制限である。グラフィックストラックが運ぶのは パケットではなく画面の状態である。表示セットが何かを画面に出し、後のセットが それを消す。カットは区間の先頭で表示中だったものを出し直し、区間の末尾で 消す。DVD の字幕は元のディスクから読んだパレットに対して変換する。これらは ファイルをまたいで合成できない。2 本のリールは 2 つの画面状態と 2 つの パレットを 1 つのトラックに書き込むことになる。

基準クリップ: 何が一致すれば良く、一致しないと何が起きるか

判定は crate::conform だけが行う。リストの表示に使う答えと、実際のカットが 使う答えが食い違わないようにするためである。

一致の条件は、映像についてはコーデック・解像度・ピクセルフォーマット・ フレームレート・走査方式・画素アスペクト・色の 3 項目が基準と同じであること。 音声トラックについてはコーデック・サンプリングレート・チャンネル数が同じで あること。それ以外は不一致として、項目名を挙げて報告する。

note: other.ts is not the shape of one.ts, so its pictures and its sound are
written afresh to fit it -- frame size: 640x360 vs 320x240; frame rate: 29.97
vs 25; sample rate: 48000 Hz vs 44100 Hz; channels: 2 vs 1.

形が違っても書き直さない音声トラックは、この表示に含めない。--drop-stream で 除いたトラックはそもそも書かない。基準と同じ TrueHD・MLP・DTS で形だけが違う クリップの音声は、フレーム単位でそのまま運ぶ (後述)。 以前は、TrueHD 5.1 の基準に TrueHD 2.0 のクリップを結合すると、フレームを コピーしているのに全体をデコードしてエンコードし直すと表示していた。 同じように、音声だけを書き直すクリップでは、映像の違いも、映像の画質についての 一文も表示しない。音声だけが違うスマートフォンのクリップ 2 本を結合したとき、 映像はコピーしているのに、1 回のエンコードの画質にとどまると表示していた。

ウィンドウからも同じ判定を使える。join_fit コマンドは、リストのパスと基準 クリップの位置を受け取り、クリップごとに映像・音声を書き直すかどうかと、その 理由を返す。出力画面の再エンコード表示では、継ぎ目のプランより先にこの答えを 求めている。順序が逆になると、全編書き直されるクリップが「無劣化コピー」と 表示されてしまう。

記載のない項目は不一致として扱わない

コンテナが述べていない項目は、クリップ間の違いではない。判定の材料はすべて コンテナから読んだものであり、トランスポートストリームは自分の形式を述べる 義務を負わない。放送の MPEG-2 は色をシーケンス表示拡張で述べるが、番組の 途中から始まった録画にはそれが 1 つも入っていないことがある。走査方式は libavformat がプローブしたピクチャから割り出すので、判断が付かなければ unknown になる。画素アスペクトを一度も述べないストリームもある。

これらは記述の欠落であって、映像の違いではない。同じ番組を 2 週続けて録画した 2 本のうち、片方にだけ欠落が出る。それを違いとして読むと、そのクリップは 1 時間かけて再エンコードされる。同じ局の同じ番組は同じ形式の同じ映像なので、 片方が自分について多く語ったというだけで書き直すのは、誰も頼まない作業である。

そこで、どちらか一方が述べていない項目は比較しない。出力が述べるのは基準 クリップの内容であり、それが唯一の記載なので、述べていないクリップと矛盾する ことはない。色は 3 項目を別々に見るので、原色か伝達特性が unspecified の クリップも、3 項目すべてを述べるクリップと同じ色として扱う。

ただし YUV の変換行列だけは、書かれていなくても読んで比べる。 プレーヤーは 書かれていない行列をピクチャの大きさで決め、カットがリールを変換するときも 同じ規則を使う(後述)。しかもデコーダーは、一度読んだ行列を、行列の書かれて いない後続のピクチャにも使い続ける。比べずに済ませていたころは、BT.601 と 書いた 720p の録画(画面録画ソフトを 601 の色空間に設定すると、こう書かれる) と、何も書いていない 720p のクリップが同じ色と判定されていた。クリップは コピーされ、BT.709 のサンプルが BT.601 で表示された。.ts・.mp4・.mkv の どれでも同じである。いまは書かれていない行列を大きさで読んでから比べる (conform::read_matrix)。RGB や YCgCo のように変換の対象にならない行列は、 これまでどおり、書かれていない行列と一致するものとして扱う。

失うのは、片方が「実際とは違う内容」を述べている場合だけである。記載と中身が 食い違う録画は、記載のない録画と区別が付かないので、もともと得られていた答えは 無い。判定は conform::stated と conform::same_colour にある。

レンジの違いは色の違いとして扱う

0.8.11 から。

色の 3 項目に加えて、サンプルのレンジ(フルレンジか、16〜235 のスタジオレンジか) も比べる。yuvj 系のピクセルフォーマットはフルレンジであり、それ以外は color_range が pc と述べたときだけフルレンジとみなす。何も述べていなければ スタジオレンジである。放送はすべてスタジオレンジで、デコーダもそう仮定する。

0.8.10 までは、ピクセルフォーマットが同じならレンジが違ってもコピーしていた。 たとえば 10 ビットのフルレンジのクリップを、10 ビットのスタジオレンジの基準に 結合した場合である。その区間は黒が 0 のまま出力され、基準の宣言(黒は 64)で 読まれるので、黒がつぶれ白が飛んでいた。いまはレンジが違えば書き直す。

書き直すときは swscale にレンジを変換させる。ただし swscale はレンジを ピクセルフォーマットからしか読まない(yuvj ならフル、それ以外はスタジオ)。 しかも大きさと形式が同じなら、変換せずにそのまま写す経路を選ぶ。そのため レンジが違うときだけ、SwsContext を手で作り、src_range と dst_range を 初期化の前に渡している(-vf scale=in_range=..:out_range=.. と同じやり方で ある)。レンジが同じでも、片方が yuvj 系でもう片方がフルレンジの通常形式 (yuv420p10le の pc など)なら、形式から読むレンジが実際と食い違うので、 同じく手で作る。トランジションに重ねる静止画も、基準のレンジへ変換してから 重ねる。blend::Scaler を参照。

実測では、フルレンジの黒(0)を 8 ビットのスタジオレンジの基準へ入れると 16 に、 10 ビットでは 64 になった。逆向きでは、スタジオレンジのテストパターン(40〜888) が、フルレンジの基準の中で 0〜965 に広がった。

YUV の変換行列も、同じように変換する。大きさを変えてもサンプルは元の行列で 作られたままであり、swscale は変換元と変換先の両方の行列を渡されたときしか 行列を変換しない。以前は、SD のリール(BT.601)を HD の基準(BT.709)へ書くと サンプルがそのまま出力され、別の行列で表示されていた。テストカードの緑は 7 分の 1 ほど暗くなり、赤は橙色になる。DVD や SD の放送を HD の録画に結合すると、 必ずこうなっていた。

いまは cut_conform::matrices がピクチャと基準の行列を読み、違っていれば convert_matrices が両方の行列とレンジをスケーラに渡す (-vf scale=in_color_matrix=..:out_color_matrix=.. と同じやり方である)。 行列が書かれていない場合は、mpv と同じく大きさで決める。

大きさ 行列
幅 1280 以上、または高さ 576 超 BT.709
それ以外 BT.601

1280x536 に切り抜いた 720p の映画は HD として扱われる。行列が基準と同じ リールの出力は以前と変わらない。

RGB には行列が無いが、その変換先や変換元の YUV には行列がある。 QuickTime Animation や PNG の .mov(RGB)のリールを HD の基準クリップに結合す ると、libswscale の既定の BT.601 で YUV にされ、BT.709 で表示されていた。純粋な 緑は 6 分の 1 ほど暗くなり、赤は 255,25,0 になる。HD のリールを RGB の基準クリッ プに結合したときも、同じく BT.601 で RGB にされていた。0.8.46 まではどちらもその まま書き出していた。いまはこの変換の両側に YUV 側の行列を渡す。RGB のリールなら 基準クリップの行列、RGB の基準クリップならリール自身の行列である。それが BT.601 なら出力は変わらない。

パレットのリール(256 色の QuickTime Animation、パレットの PNG の .mov、 8 ビットの .avi)は、パレット画像と同じく、まずパレットを引いて RGBA にする (blend::palette_as_rgba)。そのあとは RGB のリールと同じ経路を通る。 libswscale はどの行列を指定してもパレットを BT.601 の係数で YUV にするので、 0.8.46 までは、上の RGB の修正のあとも、こうしたリールを HD の基準クリップに 結合すると赤が 255,25,0 になっていた。いまは出力が RGB のリールのときと バイト単位で一致する。SD の基準クリップでは丸めの分だけ値が変わる。 パレットの基準クリップには、どのリールも書き直して入れられない(libswscale は パレットを作れない)。その場合は専用のメッセージで知らせて止める。

実際に食い違うのは走査方式である(実測)

1 か月ぶんの放送録画 1186 本・32 局を conformdiag --group で読んだ。 12 局に「同じ局なのに形式がひとつに揃わない」録画があり、そのすべてが 走査方式だけの違いだった。ほかに 3 局は本当に違うもの(1440x1080 の局に 720x480 の番組が 1 本、開けないファイルが 1 本)である。全体では インターレース(トップフィールド優先)が 1088 本、プログレッシブが 76 本、 インターレース(ボトムフィールド優先)が 22 本で、そのうち 40 本は、 同じ局のほかの録画とだけ食い違っている。

走査方式はストリームが明示している値ではなく、libavformat がプローブした ピクチャから割り出した読みである。日本の放送は 1080i のストリームの中に プログレッシブ符号化のフレームを日常的に入れる(アニメはほとんどがそれである) ので、プローブが見たフレームがプログレッシブかどうかは、録画が本編から 始まったか直前の CM から始まったかで変わる。同じ番組を 2 週続けて録画すると、 片方がプログレッシブ、片方がインターレースになる。

これは違いではなく読みのために 1 本まるごと再エンコードしていることになる。 同じシリーズの 2 話をつなぐときに実際に踏むのはこれである。

決め手はピクチャ自身が何と言っているかである。同じ局の 1 日違いの 2 本、 片方はインターレース、片方はプログレッシブと読まれたもので、それぞれ中ほどの 60 秒を数えた:

コンテナの答え プログレッシブ符号化のピクチャ インターレース符号化
インターレース(トップフィールド優先) 507 1138
プログレッシブ 1090 427

どちらの録画にも両方入っている。 コンテナの 1 つの答えは、プローブが見た フレームがどちらに寄っていたかでしかない。ここには、片方をもう片方に合わせる べき性質が存在しない。

そこで走査方式は映像の書き直しの理由にしない。What::costs_pictures は is_video から Scan を除いたもので、fit_of はこちらを見る。ピクチャは 1 枚ずつ自分のフラグを持っている ―― トラックが何と宣言されていようと、 フレームとして符号化されているか 2 フィールドとして符号化されているかは ピクチャ自身の情報である ―― ので、コピーしたものも録画そのものと同じように 読まれる。参照ツールもこれでは Recue しない。

比較も報告もそのまま行う。この組み合わせをコピーした実行は 1 度だけこう言う: 「were read as different shapes -- scan: ... That is a reading rather than a difference.」見たうえで判断した違いは 1 行出すに足り、何も言わずに捨てる道具は 誰にも検算できない。

29.97 と 30 は同じレートではない。 許容誤差は相対で 1 万分の 1 である。 これは小数で書かれたレートの末尾の桁を吸収するためのもので、30000/1001 と 30 を分ける 1000 分の 1 より十分に小さくなければならない。どちらかを前提に 組んだタイムラインでは、もう一方が 33 秒で 1 フレームずれていく。

基準クリップが可変レートなら、宣言されたレートどうしで比べる。 可変レートの 録画の平均は、その録画が持つレートではない。同じスマートフォンで撮った 2 本は どちらも 60 を宣言していたが、明るさなどで平均が 56.59 と 58.26 になり、平均で 比べると別のレートと判定されて、2 本目は丸ごと書き直されていた。可変レートの 基準クリップでは、出力のタイムラインを細かく刻み、コピーする各ピクチャを 自分の時刻に置くので、平均を使う場面が無い。そこで基準クリップが可変レートで、 宣言されたレートが両者で同じなら、クリップをコピーする (conform::same_authored_rate)。基準クリップが固定レートなら、可変レートの クリップはそのフレームに丸められるので、これまでどおり平均で比べる。Matroska の時計である 1000 という宣言は、レートとして扱わない。

一致しないクリップは全フレームを書き直す

中間の答えは存在しない。解像度の違うクリップには、そのまま出力へ運べる ピクチャが 1 枚もない。コピーできる範囲を探し、その両端の部分 GOP だけを 再エンコードするというプランナーの仕事は、分割する対象を持たない。 plan::reencode_range が区間あたり 1 セグメントを返し、conform_segment が それを復号して基準クリップの形に書き直す。

ここで 2 点を明記しておく。

ピクチャは元のレートではなく基準クリップの格子に置く。 継ぎ目の 再エンコードは復号したピクチャを元の時刻に置く。周囲のピクチャが同じレートで 来ている以上それが正しい。しかしここでは同じレートではない。25fps のクリップ を元の時刻のまま 29.97 のファイルへ置くと、2 枚に 1 枚がフレームとフレームの 間に落ちる。出来上がるのはどちらのレートでもないストリームである。

そこで、出力側のフレーム時刻を区間の先頭から数え、その瞬間に表示されていた ピクチャを割り当てる。入力が遅ければ同じピクチャを 2 回書き、速ければ 1 枚 飛ばす。実測では 8.02 秒の 25fps クリップが 241 フレームになった。29.97 で 8.02 秒ぶんである。

これがレート変換であり、スマートレンダラーが誠実に提供できる唯一の形である。 撮られていないピクチャをここで作り出すことはしない。

例外は、基準クリップのピクチャが平均どおりに並ばない場合である (cut_conform::own_instants。中身は VideoInfo::off_the_average で、 Rust コアを参照)。 画面録画は、画面が動くあいだ毎秒 30 枚を送り、動かないあいだは 1 枚を数秒 保持するので、平均は 9 前後になる。その平均で数えると、結合した放送の クリップは毎秒 9 枚で書かれ、682 枚のうち 207 枚しか残らなかった。2 本目の 画面録画を結合すると、画面が動いた区間のピクチャが失われた。

この場合の出力タイムラインは細かく刻んであるので、各ピクチャを自分の時刻に 1 回だけ書き、次のピクチャまで出しておく。継ぎ目の再エンコードと同じ置き方で ある。1 秒より長く保持するピクチャは、1 秒ごとに書き直す。MP4 は各差分を 32 ビットで書き、目盛りは基準クリップの長さに収まる分しか確保していない からである。タイムラインで区別できないほど近い 2 枚は、先のほうを残す。

エンコーダには、基準クリップの平均ではなくクリップ自身のレートを伝える。 エンコーダはビットレートを、1 秒に来ると伝えられた枚数で割って使うからで ある。自分の 30 で書いた放送のクリップは、こうした基準クリップの 1.01 Mbit/s に対して 3.98 Mbit/s になっていた。

次のものは対象外で、これまでどおり基準クリップのフレームで数える。

対象外 理由
MPEG-1・MPEG-2 シーケンスヘッダごとにレートを書く
トランジションの中 進み具合を、前後とも基準クリップのフレームで数える
平均が宣言どおりの可変レートの基準クリップ ときどきフレームを落とすスマートフォンの録画など
フィールドを宣言するインターレースの基準クリップ ピクチャは平均どおりに並んでいる

キーピクチャは基準クリップと同じ間隔で入れる。 継ぎ目は前後の GOP より 短いので、先頭にキーピクチャを 1 枚だけ置いて書く。丸ごと書き直すリールも 以前は同じ書き方で、キーピクチャは最大 600 枚に 1 枚だった。スマートフォンの クリップをカメラの 720p の基準クリップに結合して .ts にすると、IDR 1 枚の あとに 199 枚が続き、リールの途中へシークすると何も復号できなかった。DVD を DVD の基準クリップに合わせて 1 時間書き直すと、キーピクチャは 20 秒に 1 枚に なった。いまは丸ごと書き直すリールに、基準クリップのエントリポイントの間隔の 中央値を使う(cut::master_gop)。これで、リールの途中からも基準クリップと 同じ細かさで再生を始められる。

スケーラーはデコーダーの出力形状が変わるたびに作り直す。 過剰ではない。 放送の録画は局が回線を切り替えた場所で解像度が変わることがあり、最初の形状で 作ったスケーラーは次の形状を警告なく受け付けない。

いずれかのリールが一致しない音声トラックは、ファイル全体で書き直す

リールごとではなく、トラックごとにファイル全体で 1 つの答えを決める。 トラックの宣言は 1 回きりであり、ストリームの自己記述はそのトラックの全 フレームを説明できなければならない。前半は基準クリップのパケットをそのまま 使い、後半はエンコーダーの出力を使うトラックは、片方にしか当てはまらない ヘッダーで両方を説明することになる。

リールの境界をまたぐのはエンコーダーであり、入れ替わるのはその手前の デコーダーである。audio::Reencoder::retune が入れ替えるのは、デコーダー、 入力サンプリングレート、入出力レート間のリサンプラー、最後に見たタイムスタンプ の 4 つ。保持するのは、エンコーダー、投入済みサンプル数、復号済みでまだ フレーム化していないサンプルである。継ぎ目で 2 つ目のエンコーダーを起こすと、 フレーム番号が 0 から振り直され、残りのファイルを先に書いたものの上に 重ねてしまう。

基準クリップの音声が TrueHD・MLP・DTS のときは、この方法が使えない。どれも 書き直さずにフレームのまま運ぶ音声で(音声)、トラックは基準 クリップのフレームでできている。コーデックの違うリールの音声には行き場が無い。 0.8.27 までは警告なく書き進めていた。TrueHD の録画に AAC の録画を結合すると、 AAC の側の 100 秒が無音のまま、終了コード 0 で終わった。DTS の基準クリップに AAC や E-AC-3 のリールを結合すると、そのバイトが DTS のトラックに入り、リール 全体が復号できなかった。いまはこの組み合わせを、何も書かないうちにエラーにする (cut::lossless_master_refusal)。 メッセージは、そのリールと食い違う点を挙げ、書ける方法を示す。そのリールを基準 クリップにする方法(--master N、GUI では基準クリップの選択)と、トラック全体の コーデックを指定する方法(--audio-codec)である。リール自身の音声も TrueHD・ MLP・DTS なら、基準を入れ替えても同じことになるので、前者は示さない。 音声だけを書き出すとき(--sound-only)も、同じ文でエラーにする。

コーデックが同じでチャンネル数などが違うリール、たとえば 5.1 の TrueHD に 2.0 の TrueHD を結合する場合は、これまでどおり運ぶ。フレームが自分の形を 書いているので、デコーダはそれに従う。 音声だけを書き出すときも、いまは同じように運ぶ。0.8.27 までは、この形の DTS の 結合は全体を再エンコードし、TrueHD の結合はエラーで止まっていた。

トランジション

トランジションは、SmartCut でただ 1 つスマートレンダリングできない機能で ある。 他のすべては録画のピクチャに手を触れないために存在する。 トランジションが求めるのは、どちらの録画にも存在しないピクチャである。片方が 半分ずつ混ざったフレーム、黒へ 4 分の 1 進んだフレーム。したがって範囲内の 全フレームをここで作る。2 秒を指定すれば、継ぎ目ごとに 2 秒ぶんの エンコードが発生し、それ以外の場所は一切変わらない。

トランジションは手前のクリップに属する。参照ツールと同じ置き方であり、 ファイルが書かれる順にリストを読める唯一の置き方でもある。最後のクリップには 次が無いので、フェード以外を指定した場合もファイル末尾の黒へのフェードになる。

時間の扱いが 2 通りある理由

種類 扱い
フェード(黒・白) 手前のクリップが半分の時間で色へ落ち、次のクリップが残り半分で色から戻る。両者がそれぞれ自分の担当部分を提供するので、出力の長さは変わらない
ディゾルブ・ワイプ・スライド 2 本が指定時間のあいだ同時に映る。両者が指定時間ぶんを提供するので、出力はその時間だけ短くなる

これは選択肢ではなく、2 種類の効果がそういうものだからである。黒へのフェード は 2 本を同時に映さないので、両方から指定時間を取るとどちらかを捨てることに なる。ディゾルブは 2 本同時以外に映すものを持たない。

可変レートのリールにかかるフェードだけは、基準クリップのフレームを丸ごと 並べない。フェードの区間は、手前の本体が終わるピクチャから、後ろの本体が 始まるピクチャまでである。どちらもリール自身のピクチャなので、可変レートの リールでは基準クリップのフレームの整数倍にならない。以前は丸ごと並べていたため、 最後のフレームが区間を最大 1 フレーム越え、その後のピクチャがすべて遅れていた。

向き 起きていたこと
黒から入る 以後のピクチャが音声より遅れる。揺れを付けた 30 fps の試験用クリップで 30〜38 ms、平均毎秒 1.2 フレームの試験用の画面録画で 575 ms
黒へ出る 次のリールが音声ごと後ろへずれ、その音声が範囲より前から始まる

いまは、基準クリップのタイムラインが十分細かく分かれていれば(Grid)、 フェードのフレーム数と各フレームの画像はそのままに、リール自身の区間へ均等に 割り振る(cut_conform::pictures_afresh)。一定レートのリールのフェードと、 ディゾルブ・ワイプ・スライドは、従来どおり基準クリップのフレーム単位で並べる。

トランジションは区間ではなく区間内のセグメントである

音声は区間単位で切る。両端それぞれに 1 回の境界判定があり、区間ごとに 1 つの 基準時刻がある。フェードのために区間を 2 つに割ると、映像には無い継ぎ目が 音声にだけできてしまう。そこで区間は自分の範囲を保ったままセグメントを 1 つ 増やし、Segment::retouch がそのセグメントの内容を指定する。

例外は重なり型の両端 2 か所で、ここは実際に区間が短くなる。

  • 後ろのクリップは先頭の指定秒数を手放す。その部分は重なりの最中に 映っており、重なりの処理が書き出す。区間は音声ごとその秒数だけ遅く始まる。
  • 手前のクリップは何も手放さない。映像は重なりに使われるが音声は重なりの あいだも鳴り続ける。これが前後の映像と音声の対応を保つ。

トランジションのセグメントはピクチャの境目から始める。区間の本体が書くのは、 その中で始まるピクチャである。後に続くトランジションは、開始時点で画面に出ている ピクチャから始める。トランジションの長さがフレームの整数倍でないと(29.97 fps で 既定の 1 秒は 29.97 フレーム)、境目がピクチャの途中に来て、そのピクチャを両方が 書いていた。トランジションのピクチャがすべて 1 フレーム遅れ、クリップの最後の ピクチャが抜けた。いまは本体を丸ごと書き直す場合を除き、計画の前に本体の終わりを その時点で画面に出ているピクチャの先頭へ動かす(plan::on_screen_at)。

後ろのクリップは、重なりが最後に映したピクチャの次から再開する。 重なる トランジションは、後ろのクリップのピクチャを整数枚書く。以前は、後ろの区間を 「クリップの先頭+トランジションの秒数」から再開していた。29.97 fps の 1 秒 なら、この時刻は重なりが書いた最後のピクチャの途中に来る。そのピクチャが 2 回書かれるか、次のピクチャが抜けていた。

いまは重なりが実際に書いたピクチャの枚数を数えておく。コピーで続く場合は、 その最後のピクチャより後に始まる最初のピクチャから再開する。位置は パケットから読む(plan::picture_after)。基準クリップの格子で書き直す クリップなら、その枚数ぶん先のフレームから再開する。

このパケットの読み取りでは、最初のキーピクチャより前にあるものを読み飛ばす 必要がある。放送の録画を ffmpeg -c copy で .mkv にすると、オープン GOP の リーディングピクチャがキーピクチャの前に残る。Matroska は 0 より前の時刻を ブロックに付けられないので、libavformat はこれらを時刻なしで渡す。以前はこれを 読めなかったピクチャとみなし、そうしたファイルの先頭からの読み取りはすべて 結果が得られなかった。すると後ろのクリップは、重なりの秒数から再開位置を計算した。 0.8.29 までは、H.264 のリマックス録画を半秒のスライドのあとに丸ごとつなぐと、 再開が 1 ピクチャ遅れた。そのピクチャは一度も映らず、以降は音声と 1 フレーム ずれた。いまはこのリーディングピクチャを読み飛ばす。どの区間も映さない ピクチャだからである。それでも読めない場合(DVD の参照ピクチャは表示時刻を 持たない)は、秒数ではなく、重なりが数えた枚数のフレームから再開する。

格子から境目を計算できない録画もある。 3 フィールド表示されるピクチャが あると、その後のピクチャはすべて半フレームずれる。ソフトテレシネ、映画の DVD、ときどきの放送がこれにあたり、リマックスした録画ではそのことが記録 されていないこともある。フレームレートが一定でない録画には、そもそも格子が 無い。これらの録画と、MPEG-1・MPEG-2・VC-1 はつねに (plan::pictures_off_the_grid)、重なりの手前で本体が終わる位置を パケットから読む(plan::picture_on_screen)。フィールド単位で符号化された ピクチャは、2 つのフィールドを 1 枚として数える。ソフトテレシネのクリップを それ自身と 1 秒のディゾルブで結合すると、出力の 350 枚目と 351 枚目がどちらも 元の 84 枚目になっていた。

クリップを自分の時刻で置く基準クリップ(前述)に結合するときも、クリップの 種類に関わらず同じように読む。出力のフレームで本体を終えると、ピクチャの 途中で止まり、そのピクチャがほんの一瞬だけ出て、重なりがもう一度始まって いた。

2026-10-09 に測った結果は次のとおりである。

素材 修正前 修正後
ソフトテレシネ(コマンドライン) 12 件中 8 件で不一致 67 件すべて一致
可変フレームレート 6 件すべて不一致 10 件すべて一致

同時に、ピクチャが 2 回出る別の 3 つの場合も直った。基準の格子で書き直した クリップではトランジションのたびに、両端に重なりがあるほど短い区間では その両方で、1 枚が 2 回出ていた。フィールド単位で符号化された録画では、 2 枚目のフィールドをピクチャの先頭と取り違えていた。トランジションの無い 出力は、バイト単位で以前と同一である。

したがって音声はミックスしない。 手前のクリップの音が重なりを通して鳴り、 後ろのクリップの音は重なりが終わったところから始まる。これは実装漏れではなく 判断である。2 系統をミックスするには両方を復号して結果をエンコードする必要が あり、1 時間の番組のうち 1 秒のクロスフェードのために、ファイル内のすべての 音声トラックがコピーではなく書き直しに変わる。 段差はフェードで和らげる。追加の費用は無い。

重なりの字幕は後ろのクリップのもの

字幕は音声と同じ場所では入れ替わらない。字幕の画面に出せるのは 1 本の録画の 行だけで、ディゾルブ・ワイプ・スライドが目を導く先は後ろのクリップである。 そこで、重なりが始まった瞬間、つまり後ろのクリップの映像が見え始めた瞬間 から、字幕は後ろのクリップのものとする。 放送の ARIB 字幕も 4K 録画の TTML 字幕も同じ規則で扱う(cut::take_caption、cut::read_ttml_crossing)。

  • 後ろのクリップの映像が見え始める時点で出ている行は、重なりの最初のフレーム に出す。重なりの中で後ろのクリップが出す行・消す行は、それぞれ元の時刻に 書く。
  • 手前のクリップの字幕は、重なりの中で後ろのクリップが最初に何かを送るところ までしか運ばない。放送なら最初の字幕文(行を出す文でも消す文でもよい)、 4K なら最初の文書である。その時点で出ている手前の行はそこで置き換わり (4K の文書はその時刻で打ち切る)、それ以降に手前のクリップが送るものは 書かない。後ろのクリップが頭から行を出していれば、それは重なりの最初の フレームである。
  • 後ろのクリップが重なりの中で何も出していなければ、手前のクリップは範囲の 終わりまでほかの範囲と同じように運ぶ。重なりの中の消去も元の時刻に書き、 範囲の終わりで出ている行は後ろのクリップの最初の行まで残る。後ろのクリップ のその位置に字幕トラックが無いときも同じである。

0.8.35 までは、後ろのクリップの字幕を自分の範囲の頭、つまり重なりの終わりから しか読み返していなかった。頭の行は重なりの長さだけ遅れて出て、重なりの中で 出て消えた行は失われ、手前のクリップの最後の行が重なりのあいだずっと後ろの クリップの映像の上に残っていた。字幕付きの録画を、それ自身から切り出したもの と 1 秒のディゾルブで結合すると、後ろのクリップの頭の行は 10.00 秒ではなく 11.02 秒に出ていた。3 秒にすると後ろの行 2 つと消去 1 つが失われ、手前の行が 9.58 秒から 14.22 秒まで出ていた。4K のタイトルを同じように 3 秒のディゾルブで 結合すると、後ろのクリップの最初の行が丸ごと抜けていた。

ディスクが描く字幕(PGS と DVD の字幕)も映像に合わせて切り替わる。ただし 運ぶのは基準クリップの字幕だけなので、重なりで変わるのは後ろのクリップが 基準のときに限られる。このとき、後ろのクリップの映像が見え始める時点で 出ている字幕は、重なりの最初のフレームに出す。DVD の字幕は、録画の中で 消えていた時刻に消える。重なりの中で出る字幕は、それぞれ元の時刻に出す (cut::read_graphics_before、cut::read_subpictures_before を範囲の頭まで 読み進める)。手前のクリップの字幕は出力に入っていないので、入れ替えるものは無い。 基準が手前のクリップなら、その字幕は以前と同じく重なりを含む範囲の終わりまで 出る。0.8.36 までは、重なりのあとの基準クリップの字幕を重なりの終わりから しか読み返していなかった。ディスクのタイトルを、それ自身から切り出したものと 3 秒のディゾルブで結合すると、重なりの中で 2.1 秒出ていた字幕が抜け、次の 字幕は 0.78 秒遅れて出ていた。1 秒のワイプでは最初の字幕が 1 秒遅れていた。 .sup と .idx/.sub に書き出す場合も同じである。

フェードは重なりではない。色をはさんで 2 本の映像が順に出るので、字幕も順に 出る。これは以前から変わらない。トランジションの無い結合とフェードの結合は、 以前とバイト単位で同じ出力になる。ディゾルブとワイプで変わるのは、字幕の ストリームと、後ろのクリップが基準のときのディスクの字幕のストリームだけで ある。継ぎ目の設定ウィンドウのプレビュー(crossview.rs)は、 字幕をまったく描かない。

継ぎ目のフェードは前後で別々に指定する

1 本の録画の中のカット点に入るフェードは CutOptions::audio_fade であり、 実行に 1 つの値である。クリップの継ぎ目は違う。終わるほうの番組をどう 終わらせるかと、始まるほうをどう始めるかは別の問いであり、1 つの数では 表せない。自分のテーマ曲で終わる番組は長く落としたいが、次の番組は頭から 普通に始めたいことがある。

そこで Transition が 2 つの秒数を持つ。fade_out は手前のクリップの音が 消えるまで、fade_in は次のクリップの音が戻るまでである。映像の効果とは 独立で、kind が None の継ぎ目にもフェードは入る。効果は前後から素材を 取って成立する(Transition::takes)が、フェードは残る素材の上に書くだけ なので、出力の長さは変わらない。

フェードアウトの終点は、区間の終わりではなくその時点で音が残っている最後の サンプルである。放送の録画は音声トラックが映像より 0.5 秒ほど短いことがあり、 区間の終わりを目指すと、音が尽きた時点でランプはまだ 4 分の 1 ほどの高さに いる。フェードアウトが 4 分の 1 の音量で途切れることになるので、boundary_patches は手元のフレームの最後と区間の終わりの小さいほうを終点にする。

なお、その 0.5 秒の隙間は次のクリップの音で埋まる(min_start)。これは フェードの有無にかかわらず起きる。

cut::fade_lengths が、書き出す順の全区間について「頭のフェード」と 「尻のフェード」の対を作る。1 本の録画の中の継ぎ目には audio_fade が入り、 クリップの継ぎ目にはその継ぎ目の 2 つの値が入る。出力の先頭と末尾は継ぎ目では ないので 0 になるが、これは表ではなく audio::fades_for が区間の位置から 決める。

各種類と、その演算

どの種類も演算としては同じである。出力の各サンプルについて、2 つの数のどちら かを選ぶか、両者を混ぜる。違いは「サンプルの位置」と「進行度」の関数だけで ある。crate::blend がこの演算を書いてある唯一の場所である。4 か所に分けて 書けば、色差の間引きを間違える場所が 4 か所になる。

種類 演算
フェード 各サンプルを指定色へ寄せる。0 と最大値ではなく 16 と 235(スタジオ基準の黒と白)。周囲のピクチャが同じレンジで符号化されており、そこを超えるフレームは画面で光って見える
ディゾルブ 一方のサンプルと他方の同じ位置のサンプルを混ぜる
ワイプ 境界線が画面を横切る。2 枚の画像は動かない
スライド 同じ境界線で 2 枚とも動く。手前のクリップは次のクリップが入ってくる側へ押し出される

境界線の位置は各プレーンのサンプル数に対して切り捨てではなく四捨五入で 求める。輝度の境界と色差の境界が常に画面上の同じ位置に来るようにするため である。半サンプルずれると、色の付いた縁が画面を横切ることになる。

演算はすべて整数である。4K のフレームでサンプルごとに浮動小数の乗算を行うかどう かは、書き込み速度でエンコードできるかどうかの分かれ目になる。受け付ける形式は次 のとおりである。

形式 扱い
プレーナー YUV(8 / 10 / 12 ビット) そのまま混ぜる。ほぼすべての録画はこれに復号される
プレーナー RGB(gbrp など) そのまま混ぜる
パックド RGB(サンプルが 8 ビットか 16 ビット) 効果ごとにプレーナー RGB へ並べ替え、終わったら戻す
パレット、5・6 ビットの RGB(rgb555、rgb565)、x2rgb10 など 受け付けない

パックド RGB は、QuickTime Animation の rgb24 や argb、PNG の .mov の rgb48、FFV1 が返す bgr0 などである。並べ替えはサンプルの位置を変えるだけで、 値は変えない。ただし libswscale はアルファを 1 ずらすことがあるので、アルファだ けは自前で写す。5・6 ビットの RGB と x2rgb10 は往復で値が変わるので対象外とし た。RGB のフェードは 0 と最大値へ向かう。RGB ではそれが黒と白である。

4 枚目のプレーンにアルファを持つ形式(ProRes 4444、透過付きの PNG の .mov)で は、ディゾルブとワイプでアルファも混ぜる。0.8.46 までは、トランジション中のアル ファに新しいフレームのメモリの中身が残っていた。また RGB の基準クリップは、継ぎ 目の設定ウィンドウではトランジションが表示されるのに、書き出しは継ぎ目でエラーに なっていた。

イージング

名称は参照ツールのものをそのまま使う。同種のツールを使ったことのある人が 探す名前だからである。なし、Back、Bounce、Circle、Elastic、Exponential、 Power、Sine、Quadratic、Cubic、Quartic、Quintic の 12 種に、イン/アウト/ イン-アウト/アウト-イン の 4 モードを組み合わせる。中身は各名称の標準的な 曲線そのもので、独自のものは無い。

Back と Elastic は設計上 0〜1 の外へ出る。これを定義側で丸めることはしない。 ディゾルブの混合比は使用箇所で丸め、ワイプの境界線は丸めない。画面の外へ少し 出た境界線は、単に画面の外の境界線だからである。全曲線・全モードが 0 から 始まり 1 で終わることは検査してある。そうでなければ、トランジションが取り除く はずの段差が端に現れる。

継ぎ目に画像を重ねる

タイトルやカードなどの静止画を、トランジションの範囲にだけ重ねる。前 4 分の 1 で現れ、後ろ 4 分の 1 で消えるので、唐突に出入りすることはない。

範囲を限っていることが、この機能を提供できる理由そのものである。 番組の 上に画像を重ねるとは、重なる全フレームを再エンコードするということである。 字幕を 1 つ入れただけで 1 時間を何も言わずに再エンコードするスマートレンダラー は、スマートレンダラーではない。トランジションの範囲は元から書き直している。

画像は他と同じく libavcodec で読む。PNG、JPEG、BMP など、デコーダーのある 形式であれば何でもよい。拡大は 2 回行う。色のために出力のピクセルフォーマット へ、アルファチャンネルのために RGBA へ。アルファを持たない画像は不透明として 扱う。隅に置かずに画面いっぱいへ引き伸ばすのは、小さい画像をどこに置くかが 万人向けの答えを持たない問いであり、アルファチャンネルのある画像はその問いに 自分で答えているからである。

画像は RGB なので、重ねるフレームの行列で YUV に直す。 libswscale は、 指定がなければ BT.601 の行列で RGB を YUV にする。そのため HD の基準クリップに 重ねたタイトルは違う行列を通り、元の画像とは違う色になっていた(純粋な赤で 8 % ほど暗い)。いまは基準クリップに書かれた行列を渡し、書かれていなければ 大きさで決める(blend::read_laid_in、conform::read_matrix)。BT.601 と 書かれた HD なら BT.601、BT.709 と書かれた SD なら BT.709 になる。0.8.45 までの カットは、書かれた行列を見ずに大きさだけで決めていた。行列の書かれていない SD の 基準クリップでは何も変わらない。

パレット画像(インデックスカラーの PNG、8 ビットの BMP)も RGB である。しかし libswscale は、どの行列を指定してもパレットを BT.601 の係数で YUV にする。そのた め 0.8.46 までは、こうした画像を HD の基準クリップに重ねると赤が 255,24,0 になっ ていた。同じ画像を RGB で保存すれば正しかった。いまはまず画像の大きさのままパレ ットを引いて RGBA にし、そのあとは RGB の画像と同じ経路を通す。不透明な画像なら、 出力は RGB で保存した画像のときとバイト単位で一致する。GIF は最初から RGBA に復 号される。

設定画面のプレビュー(crossview.rs)

継ぎ目の設定は別ウィンドウで行い、そこには継ぎ目の前後が合成された状態で映る。 そのための経路が crossview.rs である。

演算はここに一切書かない。 出力する 1 サンプルも、プレビューの 1 サンプル も、通るのは同じ blend.rs であり、進行度は同じ transition.rs から取る。 プレビューと出力が食い違わないための唯一の方法は、2 つ用意しないことである。

音声のフェードも同じ扱いである。プレビューは Heard::fades として秒数を 受け取り、audio::fade_shape を掛ける。カットが書き込むときのランプ (audio::fade_run)と同じ曲線であり、単位だけが違う ―― あちらは書く対象が サンプルなのでサンプルで数え、こちらは再生なので時計で数える。何秒が適当かは 聞いてみないと決められないので、映像の効果と同じく、このウィンドウで判断 できなければならない。

2 度書いてあるのはスケジュール、つまり出力のどの瞬間に何が映るかのほうで ある。カッター側ではこれが編集全体に対するプランのセグメントとして組み立て られる(ranges_with_transitions)。プレビューには編集が無く、あるのは録画 2 本と設定 1 つだけなので、継ぎ目 1 か所を中心に組み立て直している。両者が ずれていないことは crossview.rs 末尾のテストが受け持つ。

プレビュー 出力
合成する形 プレビューの表示サイズ・yuv420p 基準クリップの形
スケーラ FAST_BILINEAR(1 枚は見たら捨てる) BICUBIC(リールの長さぶん効く)
音声 2 本を順に 1 つの出力デバイスへ(play_audio_across)。どちらも、基準クリップで残す先頭のトラックと同じ位置のトラック(heard_nth) 書き出し時に同じ順・同じトラックで(Threads)

行列の違うリールは、出力と同じ cut_conform::matrices と convert_matrices で、 基準の行列に変換してからプレビューの大きさに縮める。そのため SD と HD を結合した ときも、ウィンドウには出力と同じ色で映る。合成したフレームには、JPEG にする前に その行列を書き込む。プレビューの大きさで新しく作るフレームには行列が書かれて おらず、JPEG 化は書かれていない行列を大きさで決めるので、幅数百画素では BT.601 になる。0.8.44 までは、HD の基準クリップで組んだ結合がすべて、このウィンドウ でだけ違う行列で表示されていた。出力のほうは正しかった。

トランジションの画像も、同じ行列で合成に重ねる(crossview::overlay_of)。 0.8.45 までは画像をプレビューの大きさで BT.601 として YUV にし、HD の基準 クリップの BT.709 で表示していた。そのため赤いタイトルが、出力では赤なのに このウィンドウではオレンジに見えた。どのリールも変換の対象にしない行列 (YCgCo や予約値)が書かれた基準クリップは、編集画面の静止画と同じく大きさで 決める。

基準クリップのピクチャが RGB やパレットのときは、行列が書かれていてもいなくても BT.601 で合成し、合成したフレームにも BT.601 と書き込む。基準クリップのピクチャ と RGB のリールはスケーラが BT.601 で YUV にし、YUV のリールはリール自身の行列か ら BT.601 に変換する。0.8.45 では、QuickTime Animation の基準クリップを HD の大 きさで読んでいたので、BT.601 のサンプルを BT.709 で表示していた。0.8.46 では合成 に行列を書き込まなかったので、幅 1280 画素の画像が出るほどウィンドウが広いと、合 成が HD として読まれて同じことが起きていた。

合成はスタジオレンジの yuv420p で行い、JPEG 化はそのあとである。順序を逆に すると、Shade::yuv が返すスタジオ黒がフルレンジの yuvj420p 上では違う明る さになってしまう。

計測結果

tests/run_join_tests.sh の 17 項目。素材はその場で生成するので、ffmpeg 以外 に必要なものは無い。

項目 確認内容
同じ仕様 2 本 長さが 2 本の合計、ピクチャが 2 本ぶん、音声が末尾まで届く、復号時に警告が出ない
3 本 指定した順に並ぶ
仕様の違う 1 本 全ピクチャが基準の幅、長さが 2 本ぶん、不一致を書き出し前に報告
フェード 出力の長さが変わらず、中間点が黒になる
ディゾルブ 出力が指定秒数だけ短くなる
ワイプ 書き出せて復号できる
短い区間への効果 両端のうち短いほうに合わせる。後ろのクリップは、効果に映らなかった分を失わない
単体カット バイト単位で従来と同一、結合に関する出力を一切しない

実素材での計測: 放送録画の 20 秒 2 本を結合して 39.91 秒・1,194 ピクチャ。 個別に書いたときの 598 + 596 と一致し、音声の末尾は映像から 1 フレーム以内、 デコーダーからの警告は無し。720x480 のクリップを 1440x1080 の基準へ結合した 場合は全編 1440x1080 で 22.02 秒(想定 22.04)、2 本目に焼き込まれた タイムコードは 6 秒地点で 6.039 を示した。