# 人工素材で再現する

記事は既存の13試行からまとめている。この付録は、実行済みの条件・コマンドを持ち運べる形に整理したもの。配布用ラッパーは構文と設定の照合まで確認し、このパッケージで新しい変換・計測は実行していない。

以下はZIPを展開した `mediabunny-reader-package` ディレクトリで実行する。記事を読む場合は、パッケージ直下の [index.html](../index.html) をブラウザで開く。

## 必要な環境

- Python 3（標準ライブラリのみ）。
- `ffmpeg` と `ffprobe`。記録時はFFmpeg 9.0.1、libx264とAACエンコーダーを使用した。
- AVCの量子化指定とAACを扱えるChrome。記録時は155.0.8059.85のheadless、macOS 27.0.1、Apple M4・32 GiB。
- 同梱のMediabunny 1.52.0。外部CDNやnpm installは使わない。ライセンスは [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)。

ブラウザやOSが違えば、可否・容量・時間・同期差も変わり得る。通常のブラウザ画面は記事のheadless測定と同条件ではない。

## 1. 入力を生成する

```sh
python3 reproduce/make_fixture.py --output generated/input.mp4
```

[make_fixture.py](make_fixture.py) に全FFmpeg引数を記載した。付属の [fixture-overlay.png](fixture-overlay.png) は記録時の文字・縞パネルそのものなので、フォントを再描画する必要はない。

素材は1920×1080、30 fps、20秒。`testsrc2` と文字パネルを合成し、1・5・10・15・19秒に点滅と1 kHzのビープを置く。映像はlibx264 CRF16、maxrate 8M、bufsize 8M、Bフレームなし、2 threads。音声は48 kHz mono、AAC 128k指定。既存ファイルは上書きしない。

記録時の入力は20,428,131 bytes、SHA256は次の値だった。

```text
f9cadd3a4c0042ccd33754a6cdd9d20bed461c6e11cd75b0378761b080afaf22
```

生成スクリプトはハッシュ一致を表示する。FFmpegの版・ビルドが違って不一致なら、同一入力とは呼ばず、新しい入力として記録する。

## 2. ブラウザで変換する

```sh
python3 -m http.server 8765 --bind 127.0.0.1
```

ブラウザで `http://127.0.0.1:8765/reproduce/demo.html` を開く。人工素材 `generated/input.mp4` を選び、量子化値を指定して「変換する」を押す。出力MP4と測定JSONを、重ならない名前で保存する。

記事と同じ設定順は次の9回。音声の強制再エンコード欄はオフのままにする。

```text
q20-r1 → q26-r1 → q32-r1
q32-r2 → q20-r2 → q26-r2
q26-r3 → q32-r3 → q20-r3
```

[convert.mjs](convert.mjs) が変換本体。`Conversion.init()`の直前から`execute()`完了までを測る。入力読み込み、保存、解析は含めない。映像を1080p・30 fpsへ強制変換し、音声は `{}` とする。WebCodecsの設定と音声encode呼び出し回数も記録する。APIを一時的に計測するので、専用ページで一つずつ実行する。

## 3. 音声の対照条件を作る

元入力と同じ動画・音声パケットを保持し、先頭時刻を非負にした補助入力を作る。空の出力名を使うこと。次の `noclobber` は既存の出力を守るための設定で、FFmpegの引数は記録時と同じである。

```sh
(set -C
ffmpeg -hide_banner -nostdin -threads 2 -i generated/input.mp4 \
  -map 0:v:0 -map 0:a:0 -c copy -avoid_negative_ts make_zero \
  -movflags frag_keyframe+empty_moov -f mp4 pipe:1 > generated/normalized.mp4
)
```

記録時は20,419,307 bytes、SHA256：

```text
17160b06edf59ae34fc2c49b8fad65e4138324c67ed023141c54b0179015aa93
```

この処理は映像フレームの時刻・長さも変える。一般的な音ずれ修正の推奨手順ではない。

同じ補助入力を選び、q26で次の4回を行う。保存時に条件と回数をファイル名へ付ける。

- 音声強制オフ：`normalized-default-r1`、`normalized-default-r2`
- 音声強制オン：`normalized-force-r1`、`normalized-force-r2`

記事で実際に行った順は、先に通常処理1回、後から通常処理の2回目、強制処理の1回目と2回目。量子化値比較と同様、厳密なcold/warm統制ではない。

## 4. 画質と同期を解析する

保存したファイル名に合わせて引数を変える。

```sh
python3 reproduce/analyze_pair.py generated/input.mp4 generated/q32-r1.mp4
python3 reproduce/analyze_pair.py generated/normalized.mp4 generated/normalized-force-r1.mp4 \
  --quality-reference generated/input.mp4
```

[analyze_pair.py](analyze_pair.py) はJSONを標準出力へ返す。全画面と左上1120×320 pxのSSIM/PSNR、AACパケットの一致、同期差を計算する。画質はすでに圧縮された元入力との相対比較。補助入力の音声比較では、同期の基準は補助入力、画質の基準は元入力に分ける。

点滅は指定ROIの平均輝度が200を超えたフレームを検出し、`ffprobe`の実フレーム時刻を使う。フレーム番号を30で割らない。ビープは48 kHzへデコードし、5 ms窓・RMS 0.1超で立ち上がりを検出、音声ストリームの開始時刻を加える。人工素材に固有の検出法で、任意の動画の音ずれを測るものではない。

正の同期差は音声が映像より遅いことを示す。出力の差から、その変換で実際に使った入力の差を引く。記事の目安は追加差が1フレーム（33.333 ms）以内。音声終端差、主観的な可読性、容量は別に判断する。

補助入力は先頭フレームの長さが約54.688 msで、厳密なCFRではなかった。初期の解析にあった「番号÷30」は訂正済み。本文と [results.json](../data/results.json) は訂正値を使う。

記事のSSIM/PSNRは各量子化値の初回で測定し、残る2回は映像全パケットの順序付きハッシュ一致を確認して数値を再利用した。この簡易スクリプトは呼び出すたびに解析する。静止画を見ただけでは、ちらつきや12 px文字の正確な判読まで保証できない。

読み終えたらHTTPサーバーをCtrl+Cで終了する。出力MP4はパッケージには含めていない。全13試行の記録値は [trials.csv](../data/trials.csv) にある。
