ffprobe Duration and Sample-Rate Verification Matrix for Highlight Reel Audio - No More Drift 2026
Your VO consent metadata passed V4 on one facilitator line. The July highlight reel still shipped with facilitator_vo_03 ending 0.18 seconds early—enough to desync disclaimer captions on reel safe-zones and enough for legal to reject the cut because the waveform strip no longer matched the spoken consent_record_id window.
June–July 2026 teams learned that metadata survival (mux challenge) and duration truth are different gates. A single ffprobe one-liner per export is fine for your first line; seven VO assets, three sample-rate conversions, and two “small” ffmpeg trims need a verification matrix—one CSV row per file, one policy column per tolerance, one reason code when drift is real.
This Programming & Technical article is the batch QA layer for Lesson 250 gates V4/V5 and receipt #5 in the top 20 evidence hub. It does not re-teach Audacity export—that is the VO evening tutorial. It does give audio leads a scriptable matrix they can run in CI or before every mux night.
Non-repetition note: Mux challenge M3 is a 45-minute pre/post mux sprint on one asset. Case study recovery covers strip points, not systematic drift tables. This URL owns ffprobe duration + sample-rate matrix as primary keyword.
Who this is for and what you get
| Audience | You will be able to… |
|---|---|
| Audio programmer | Batch-export ffprobe rows for every WAV in reel_manifest.csv |
| Reel editor | Compare measured duration vs clip_plan.csv with team tolerances |
| Producer | Block mux when any row is FAIL or WARN_ESCALATE |
Time: ~90 minutes first matrix setup; ~8 minutes per reel refresh once templates exist.
Prerequisites: ffprobe on PATH, reel_manifest.csv + clip_plan.csv, cousin receipts #4–#5 from the July VO trend playbook.
Why this matters now (June–July 2026)
- Playback drift reports — Discord and facilitator feedback now cite “audio ends before caption” after 0.1 s trims—audible on mobile reels, invisible in waveform-only spot checks.
- Sample-rate churn — Teams bounce 44.1 kHz OBS extracts through 48 kHz VO masters and 44.1 kHz social exports; duration math changes when resample is implicit.
- Automation — BUILD_RECEIPT expects
ffprobe_duration_matrix_okbesidewav_consent_metadata_ok; spreadsheets without ffprobe columns fail Thursday row review. - Multi-line reels — One bad line poisons concat with OBS MKV gap preflight symptoms that look like video bugs.
- Post-mux regression — “Small”
-shortestor AAC encode shifts end padding; matrix must run pre and post mux on the audio path legal reads.
Direct answer: Maintain clip_plan.csv expected durations → run batch ffprobe → join into duration_matrix.csv → apply tolerance policy → file ffprobe_duration_matrix_receipt_v1.json → only then promote mux or public reel.
Two-lane model (do not merge with consent fields)
| Lane | Tooling | This article |
|---|---|---|
| Consent metadata | LIST/iXML inspectors, vo_consent_metadata_receipt_v1 |
Cross-check only—matrix does not replace field proof |
| Clock truth | ffprobe format=duration, stream=sample_rate |
Primary owner |
Whisper concat decision tree cares about concat_ok on MKV batches; highlight reels care about per-line VO clocks aligned to design receipt R2 waveform strip.
Source files (beginner setup)
Create under release-evidence/audio/ffprobe-matrix/:
reel_manifest.csv
| column | example | notes |
|---|---|---|
asset_id |
facilitator_vo_03 |
stable key |
wav_path |
art/voice/facilitator/vo_line_03.wav |
repo-relative |
role |
vo_line |
or bed_music, sfx_sting |
mux_stage |
pre_mux |
pre_mux / post_mux |
clip_plan.csv
| column | example | notes |
|---|---|---|
asset_id |
facilitator_vo_03 |
joins manifest |
vo_duration_expected_sec |
3.450 |
from script timecode |
tolerance_class |
vo_single |
maps policy table below |
sample_rate_expected |
48000 |
Hz integer |
channels_expected |
1 |
mono VO typical |
Beginner mistake: Copying After Effects duration into clip_plan while the shipped asset is WAV from Audacity—always measure the file you mux, not the NLE comp.
Gate overview (P1–P8)
| Gate | Minutes | Pass when |
|---|---|---|
| P1 Manifest lock | 10 | Every reel WAV listed with mux_stage |
| P2 Batch ffprobe export | 15 | ffprobe_raw.csv generated |
| P3 Matrix join | 10 | duration_matrix.csv has delta columns |
| P4 Sample-rate policy | 10 | All sample_rate_hz match expected or documented exception |
| P5 Duration policy | 15 | All rows PASS or approved WARN |
| P6 Post-mux repeat | 15 | post_mux rows exist for legal audio path |
| P7 Reason-code triage | 10 | Every FAIL has drift_reason_code |
| P8 Receipt + BUILD_RECEIPT | 15 | JSON filed; row reviewed |
P2 — Batch ffprobe export (developer path)
Bash loop (Linux/macOS/Git Bash)
#!/usr/bin/env bash
set -euo pipefail
out="release-evidence/audio/ffprobe-matrix/ffprobe_raw.csv"
echo "asset_id,wav_path,duration_sec,sample_rate_hz,channels,codec_name" > "$out"
tail -n +2 reel_manifest.csv | while IFS=, read -r asset_id wav_path role mux_stage; do
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$wav_path")
sr=$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of csv=p=0 "$wav_path")
ch=$(ffprobe -v error -select_streams a:0 -show_entries stream=channels -of csv=p=0 "$wav_path")
codec=$(ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of csv=p=0 "$wav_path")
echo "$asset_id,$wav_path,$dur,$sr,$ch,$codec" >> "$out"
done
PowerShell variant (Windows-friendly)
$manifest = Import-Csv reel_manifest.csv
$rows = foreach ($m in $manifest) {
$dur = ffprobe -v error -show_entries format=duration -of csv=p=0 $m.wav_path
$sr = ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of csv=p=0 $m.wav_path
$ch = ffprobe -v error -select_streams a:0 -show_entries stream=channels -of csv=p=0 $m.wav_path
$codec = ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of csv=p=0 $m.wav_path
[pscustomobject]@{
asset_id = $m.asset_id
wav_path = $m.wav_path
duration_sec = [double]$dur
sample_rate_hz = [int]$sr
channels = [int]$ch
codec_name = $codec
}
}
$rows | Export-Csv release-evidence/audio/ffprobe-matrix/ffprobe_raw.csv -NoTypeInformation
Developer note: format=duration is container-level; for weird WAV subformats, add -show_entries stream=duration as secondary signal and log both in notes column when they disagree by >1 frame.
P3 — Matrix join and delta columns
Python join script (paste into scripts/join_duration_matrix.py)
#!/usr/bin/env python3
"""Join ffprobe_raw.csv + clip_plan.csv -> duration_matrix.csv"""
import csv
from pathlib import Path
RAW = Path("release-evidence/audio/ffprobe-matrix/ffprobe_raw.csv")
PLAN = Path("release-evidence/audio/ffprobe-matrix/clip_plan.csv")
OUT = Path("release-evidence/audio/ffprobe-matrix/duration_matrix.csv")
TOLERANCE = {
"vo_single": 0.05,
"vo_padded": 0.15,
"bed_music": 0.25,
"sfx_sting": 0.10,
"post_mux_extract": 0.05,
}
def row_status(delta: float, tol_class: str, sr_ok: bool, ch_ok: bool) -> str:
if not sr_ok or not ch_ok:
return "FAIL"
limit = TOLERANCE.get(tol_class, 0.05)
ad = abs(delta)
if ad <= limit:
return "PASS"
if ad <= 2 * limit:
return "WARN"
return "FAIL"
plan = {r["asset_id"]: r for r in csv.DictReader(PLAN.open(encoding="utf-8"))}
out_fields = [
"asset_id", "wav_path", "duration_sec", "vo_duration_expected_sec",
"duration_delta_sec", "duration_abs_delta", "sample_rate_hz",
"sample_rate_expected", "sample_rate_match", "channels", "channels_expected",
"channels_match", "tolerance_class", "row_status", "drift_reason_code",
]
with OUT.open("w", newline="", encoding="utf-8") as fout:
w = csv.DictWriter(fout, fieldnames=out_fields)
w.writeheader()
for raw in csv.DictReader(RAW.open(encoding="utf-8")):
p = plan.get(raw["asset_id"])
if not p:
continue
dur = float(raw["duration_sec"])
exp = float(p["vo_duration_expected_sec"])
delta = dur - exp
sr_ok = int(raw["sample_rate_hz"]) == int(p["sample_rate_expected"])
ch_ok = int(raw["channels"]) == int(p["channels_expected"])
status = row_status(delta, p["tolerance_class"], sr_ok, ch_ok)
w.writerow({
"asset_id": raw["asset_id"],
"wav_path": raw["wav_path"],
"duration_sec": f"{dur:.3f}",
"vo_duration_expected_sec": f"{exp:.3f}",
"duration_delta_sec": f"{delta:.3f}",
"duration_abs_delta": f"{abs(delta):.3f}",
"sample_rate_hz": raw["sample_rate_hz"],
"sample_rate_expected": p["sample_rate_expected"],
"sample_rate_match": str(sr_ok).lower(),
"channels": raw["channels"],
"channels_expected": p["channels_expected"],
"channels_match": str(ch_ok).lower(),
"tolerance_class": p["tolerance_class"],
"row_status": status,
"drift_reason_code": "" if status == "PASS" else "",
})
Run after every P2 export: python scripts/join_duration_matrix.py. Commit the script beside release-evidence/ so facilitators on Windows and macOS share one truth.
Join ffprobe_raw.csv to clip_plan.csv on asset_id. Add computed columns:
| column | formula / rule |
|---|---|
duration_delta_sec |
duration_sec - vo_duration_expected_sec |
duration_abs_delta |
abs(duration_delta_sec) |
sample_rate_match |
sample_rate_hz == sample_rate_expected |
channels_match |
channels == channels_expected |
row_status |
see policy table |
Tolerance policy table
tolerance_class |
max duration_abs_delta |
typical use |
|---|---|---|
vo_single |
0.05 s | one facilitator line |
vo_padded |
0.15 s | intentional tail silence |
bed_music |
0.25 s | looped bed under VO |
sfx_sting |
0.10 s | short hit |
post_mux_extract |
0.05 s | extracted AAC→PCM path |
row_status |
Meaning |
|---|---|
PASS |
Within tolerance; sample rate + channels match |
WARN |
Within 2× tolerance—requires human note in receipt |
FAIL |
Outside tolerance or sample-rate mismatch |
SKIP |
opt_out or internal-only line not in public reel |
Working dev rule: Never auto-PASS a row with sample_rate_match=false even if duration looks fine—resample hidden in DaVinci can shift lip-sync on the next export.
P4 — Sample-rate verification matrix (expanded)
| Scenario | ffprobe reads | Action |
|---|---|---|
| VO master 48 kHz | 48000 |
Expected for July reel spine |
| OBS extract 44.1 kHz | 44100 |
Resample explicitly; log drift_reason_code=SR_OBS_SOURCE |
| Accidental 32 kHz | 32000 |
FAIL; block mux—likely wrong export preset |
| Post-mux AAC extract | 48000 |
Compare to pre-mux; delta duration not sample rate |
| Stereo VO | channels=2 |
Only pass if channels_expected=2 documented |
Document team house sample rate in audio_policy.md (one paragraph): public facilitator reels = 48 kHz mono PCM unless legal approves otherwise.
P5 — Duration drift reason codes (P7 triage)
When row_status=FAIL, assign exactly one primary code:
| Code | Meaning | First fix |
|---|---|---|
D1_TRIM |
ffmpeg -ss/-t removed tail |
Re-export without silent trim |
D2_SHORTEST |
-shortest ended VO early |
Lengthen video or pad audio |
D3_RESAMPLE |
implicit resample changed length | Force aresample=async=1 policy or fix source |
D4_NLE_ROUNDTRIP |
editor re-encoded with different tail | Re-spot from Audacity master |
D5_WRONG_MASTER |
wav_path not the file in clip_plan |
Fix manifest paths |
D6_PLAN_STALE |
script changed; plan not updated | Update clip_plan.csv deliberately |
Escalate to iXML recovery case study when duration passes but consent fields vanished—different failure family.
P6 — Post-mux matrix row (legal audio path)
Repeat P2–P5 for mux_stage=post_mux on the WAV your legal tooling reads (Lane A extract, Lane B parallel master, or Lane C Audacity re-export per mux challenge).
| Check | Pre-mux | Post-mux |
|---|---|---|
duration_sec |
baseline | must stay within post_mux_extract tolerance |
sample_rate_hz |
48000 | must not drop to 44100 silently |
consent fields |
not this article | cousin VO receipt |
Fail P6 when post-mux duration drifts but pre-mux passed—open mux ticket before updating design templates.
Example duration_matrix.csv (three rows)
asset_id,duration_sec,vo_duration_expected_sec,duration_delta_sec,duration_abs_delta,sample_rate_hz,sample_rate_match,row_status,drift_reason_code
facilitator_vo_01,3.21,3.21,0.00,0.00,48000,true,PASS,
facilitator_vo_02,3.08,3.25,-0.17,0.17,48000,true,FAIL,D2_SHORTEST
facilitator_vo_03,3.45,3.45,0.00,0.00,44100,false,FAIL,SR_OBS_SOURCE
ffprobe_duration_matrix_receipt_v1.json
{
"schema": "ffprobe_duration_matrix_receipt_v1",
"build_label": "playtest-july-2027-rc1",
"matrix_path": "release-evidence/audio/ffprobe-matrix/duration_matrix.csv",
"raw_probe_path": "release-evidence/audio/ffprobe-matrix/ffprobe_raw.csv",
"policy_version": "2026-05-vo-tolerance-v1",
"rows_total": 7,
"rows_pass": 5,
"rows_warn": 0,
"rows_fail": 2,
"sample_rate_mismatches": 1,
"gates": {
"P1_manifest": "pass",
"P2_ffprobe_batch": "pass",
"P3_join": "pass",
"P4_sample_rate": "fail",
"P5_duration": "fail",
"P6_post_mux": "pass",
"P7_reason_codes": "pass",
"P8_receipt_filed": "pass"
},
"ffprobe_duration_matrix_ok": false,
"cousin_receipts": {
"vo_consent_metadata": "release-evidence/audio/VO_CONSENT_METADATA_RECEIPT.json",
"metadata_survives_mux": "release-evidence/audio/mux-challenge/2026-05-26-rc1/metadata_survives_mux_receipt_v1.json"
},
"public_reel_blocked": true,
"notes": "Resolve facilitator_vo_02 shortest trim and resample vo_03 before mux."
}
Pass P8 only when rows_fail=0 or producer signs warn_accepted with ticket id—never silent override.
BUILD_RECEIPT column suggestion
Add boolean row:
"ffprobe_duration_matrix_ok": false,
"ffprobe_matrix_receipt": "release-evidence/audio/ffprobe-matrix/ffprobe_duration_matrix_receipt_v1.json"
Review on Thursday row review beside wav_consent_metadata_ok.
DaVinci Resolve and NLE roundtrip notes (developer)
Editors often “fix” VO in Resolve while programmers certify WAV masters. Document two paths:
| Path | Matrix behavior |
|---|---|
| WAV master authoritative | Matrix runs on art/voice/... only; NLE is preview |
| NLE export authoritative | Add wav_path pointing to exports/reel_vo_03.wav and re-run P2 after every export |
Fail pattern: Resolve timeline shows 3.25 s but exported WAV is 3.08 s—matrix catches D4_NLE_ROUNDTRIP while editors insist “timeline is correct.”
Fix discipline: Spot from Audacity master into Resolve; never re-record consent lines only in NLE without re-running VO consent V1–V3.
AAC and MP4 intermediate traps
Teams sometimes ffprobe AAC inside MP4 instead of PCM WAV:
ffprobe -v error -select_streams a:0 -show_entries stream=codec_name,sample_rate,duration -of json clip.mp4
| codec_name | Risk |
|---|---|
aac |
encoder padding changes end silence vs PCM master |
pcm_s16le |
preferred for matrix baseline on VO masters |
When legal reads extracted PCM from deliverable MP4, set mux_stage=post_mux and compare against pre_mux PCM row—do not compare AAC bitstream duration to WAV plan without extraction step documented in receipt.
Multi-line reel manifest example (seven assets)
| asset_id | role | vo_duration_expected_sec | tolerance_class |
|---|---|---|---|
| facilitator_vo_01 | vo_line | 3.21 | vo_single |
| facilitator_vo_02 | vo_line | 3.25 | vo_single |
| facilitator_vo_03 | vo_line | 3.45 | vo_padded |
| facilitator_vo_04 | vo_line | 2.90 | vo_single |
| bed_loop_01 | bed_music | 12.00 | bed_music |
| sting_hit_01 | sfx_sting | 0.35 | sfx_sting |
| disclaimer_bed | bed_music | 4.00 | bed_music |
Producer rule: Any row with opt_out=true in consent log gets row_status=SKIP in matrix—remove from reel_manifest.csv for public cuts, do not widen tolerance to absorb illegal lines.
jq one-liners for standup (working dev)
Count failures without opening Excel:
jq -r 'select(.row_status=="FAIL") | .asset_id' duration_matrix.json
Convert CSV to JSON once for jq:
import csv, json, sys
rows = list(csv.DictReader(open("duration_matrix.csv", encoding="utf-8")))
json.dump(rows, sys.stdout, indent=2)
Paste failing asset_id list into standup—faster than scrolling ffprobe stdout.
Workshop — 90-minute team install
| Minute block | Activity |
|---|---|
| 0–15 | Agree audio_policy.md sample rate + tolerance table |
| 15–30 | Author clip_plan.csv from locked script |
| 30–45 | Run P2 bash or PowerShell on all pre_mux paths |
| 45–60 | Run join script; review first FAIL row live |
| 60–75 | Dry-run mux; run post_mux rows |
| 75–90 | File receipt; update BUILD_RECEIPT booleans |
Exit criterion: Everyone on call can explain difference between D2_SHORTEST and D3_RESAMPLE without opening ffmpeg docs.
Integration with BUILD_RECEIPT beginner pipeline
If your studio just adopted first BUILD_RECEIPT evening, add matrix columns in week two—not day one. Order of operations:
playtest_clip_consent_ok(video)wav_consent_metadata_ok(embedded fields)ffprobe_duration_matrix_ok(clocks)metadata_survives_mux_ok(post-mux fields)vo_reel_design_ok(layout R1–R6)
Skipping step 3 while step 2 is GREEN still ships reels that sound compliant but end early.
Evidence for facilitators (plain language)
Tell facilitators: “We are not judging your performance—we are measuring file length against the script so captions and consent disclaimers line up.” Share only asset_id + row_status columns in standup, not raw UUIDs from cousin consent receipts.
Regression log template (MATRIX_LOG.md)
## 2026-05-26 playtest-july-2027-rc1
- vo_02 FAIL D2_SHORTEST: removed -shortest on QA mux
- vo_03 FAIL SR_OBS_SOURCE: resampled 44100 -> 48000
- post_mux vo_02 PASS after fix
Link each bullet to ticket id—auditors read logs months later.
CI hook sketch (optional)
- name: ffprobe duration matrix
run: |
bash scripts/run_ffprobe_matrix.sh
python scripts/assert_matrix_pass.py release-evidence/audio/ffprobe-matrix/duration_matrix.csv
assert_matrix_pass.py should exit 1 on any FAIL row—do not parse stderr from ffmpeg in the same step; keep probe and assert separate for clearer logs.
Facilitator multi-laptop handoff
| Role | Owns |
|---|---|
| VO editor | P1–P5 on pre_mux |
| Reel lead | P6 on post_mux |
| Producer | P8 + BUILD_RECEIPT |
Sync via shared drive path—attach duration_matrix.csv, not screenshots of ffprobe stdout.
Pairing with OBS concat receipts
When highlight reel pulls MKV fragments + VO lines, run:
- OBS highlight concat preflight for video consent.
- This matrix for VO clocks.
- Mux challenge for metadata survival.
Skipping step 2 produces GREEN video receipts with RED reel playback.
Three scenarios (beginner walkthrough)
Scenario A — One line fails shortest
Symptom: facilitator_vo_02 D2_SHORTEST.
Fix: Remove -shortest for QA export or extend picture by 0.2 s; re-run matrix.
Proof: duration_delta_sec moves inside vo_single tolerance.
Scenario B — 44.1 kHz source
Symptom: SR_OBS_SOURCE on line recorded from OBS extract.
Fix: ffmpeg -i in.wav -ar 48000 -ac 1 out.wav with documented command in receipt resample_command field.
Proof: sample_rate_match=true and duration re-spotted.
Scenario C — Post-mux only drift
Symptom: pre-mux PASS, post-mux FAIL D1_TRIM.
Fix: Change AAC extract to PCM -acodec pcm_s16le or keep parallel WAV master (Lane B).
Proof: post-mux row matches pre within post_mux_extract.
Proof folder layout
release-evidence/audio/ffprobe-matrix/
reel_manifest.csv
clip_plan.csv
ffprobe_raw.csv
duration_matrix.csv
ffprobe_duration_matrix_receipt_v1.json
MATRIX_LOG.md # human notes for WARN rows
Key takeaways
- One ffprobe line is not a matrix—batch every VO asset in the reel manifest.
- Duration and sample rate are separate FAIL conditions—do not mask SR mismatch with duration luck.
- Tolerance classes belong in
clip_plan.csv, not in engineers’ heads. - Post-mux rows are mandatory on the audio path legal certifies.
- Reason codes turn drift reports into fix tickets (
D1–D6,SR_OBS_SOURCE). ffprobe_duration_matrix_okcomplementswav_consent_metadata_okon BUILD_RECEIPT.- Pair with VO tutorial V4 for first-line setup, then this matrix for reel-scale QA.
- Block public reel when receipt shows
rows_fail > 0without signed warn. - Re-run matrix after any “small” ffmpeg edit—even 0.1 s.
- Pair with #20 consent governance before public reel; forward #23 ten-minute ritual (planned).
- 16-tool concat listicle includes ffprobe—this article is the highlight-reel matrix pattern, not general VOD triage.
- Audacity VO preflight stays ninety-second; matrix is pre-mux night batch.
Channel layout and loudness (adjacent checks)
Duration matrix does not replace LUFS metering—pair with 14 free LUFS tools for trailer lanes. Still document channel layout in ffprobe because mono/stereo mismatch breaks some mux maps:
| channels | Typical VO | Matrix rule |
|---|---|---|
| 1 | facilitator mono | default expected |
| 2 | stereo ambience bed | set channels_expected=2 |
| 6 | accidental surround export | FAIL until downmixed |
ffprobe -v error -select_streams a:0 -show_entries stream=channel_layout -of csv=p=0 vo_line_01.wav
If channel_layout reads stereo but you expected mono, downmix with explicit ffmpeg -ac 1 and log command in receipt—do not silently accept stereo VO over mono disclaimer timing.
Comparison — matrix vs cousin articles
| Question | Answer on this URL | Answer elsewhere |
|---|---|---|
| How do I export WAV with consent fields? | cross-link only | VO tutorial |
| Does metadata survive mux? | post-mux duration rows | Mux challenge |
| Where do captions go? | timing must match matrix | Reel design R4 |
| What receipts exist? | ffprobe_duration_matrix_receipt_v1 |
Top 20 hub |
| Why July urgency? | drift after small edits | July trend playbook |
assert_matrix_pass.py (minimal gate)
#!/usr/bin/env python3
import csv, sys
from pathlib import Path
path = Path(sys.argv[1])
rows = list(csv.DictReader(path.open(encoding="utf-8")))
fails = [r for r in rows if r.get("row_status") == "FAIL"]
if fails:
for r in fails:
print(f"FAIL {r['asset_id']} delta={r.get('duration_delta_sec')} code={r.get('drift_reason_code')}")
sys.exit(1)
print(f"OK {len(rows)} rows")
Wire into CI after join script—exit code 1 blocks merge to fest-reel branch.
Floating-point and frame-rate pitfalls
Beginner trap: Comparing 3.21 s plan to 3.209999 s ffprobe output—use rounded milliseconds in matrix display (3.210) but compute delta on floats.
Developer trap: 29.97 fps video with 48 kHz audio—matrix is on audio files, not video avg_frame_rate; do not divide video frames to guess VO duration.
When script timecode is frames, convert once in clip_plan.csv to seconds with documented fps—do not maintain two competing expected columns.
Stereo VO with mono disclaimer bed
Some teams record VO stereo for spatial feel but mux mono disclaimer beds:
| asset | channels_expected | note |
|---|---|---|
| vo_line | 2 | document in MATRIX_LOG |
| disclaimer_bed | 1 | do not downmix VO without re-spot |
Mismatch between VO stereo and mono waveform strip in design template is a design problem—matrix still reports truth; open ticket to reel design R2.
Batch size and performance
Seven VO lines × two mux stages = 14 ffprobe calls—under one second on SSD. Scale to 40 lines before optimizing; ffprobe is not your bottleneck—human interpretation of FAIL is.
For hundred-line archives, cache ffprobe_raw.csv hash in receipt when manifest_sha256 unchanged.
Legal readout packet (what to send)
Zip for legal review without game repo access:
duration_matrix.csv(redactwav_pathif needed)ffprobe_duration_matrix_receipt_v1.jsonMATRIX_LOG.md- Cousin
VO_CONSENT_METADATA_RECEIPT.json(separate lane)
Legal cares that clocks match script and sample rate is consistent—not ffmpeg brand.
FAQ
Is ffprobe enough or do we need soxi / mediainfo
ffprobe is sufficient for duration, sample rate, and channels on WAV/PCM paths teams use for July reels. Add MediaInfo only when you need container tags outside ffprobe’s stream table.
What tolerance should we use for caption sync
Start ±0.05 s on vo_single lines tied to design R4 disclaimer; tighten if legal requests frame-accurate consent windows.
Does the matrix replace the mux challenge
No—challenge proves metadata survives mux; matrix proves clocks and sample rates across all lines before and after mux.
Can we run matrix only on post-mux files
You may, but you lose the ability to blame mux vs export—always keep pre_mux rows for facilitator VO masters.
How does this relate to Lesson 250 V4 and V5
V4 is duration spot on export; V5 is post-mux survival—this matrix operationalizes both at reel scale with CSV artifacts.
What if clip_plan and script disagree
Update clip_plan.csv deliberately with producer sign-off—do not “fix” by widening tolerance without documenting D6_PLAN_STALE.
Should bed music use the same sample rate as VO
Yes for July spine—48 kHz everywhere in public reel manifest unless legal approves split-rate deliverables.
Can WARN rows ship
Only with warn_accepted ticket in receipt notes—default policy is block.
Where does AI-generated VO fit
Run matrix on exported WAV files regardless of generator; pair with planned #22 AI voice QA reason-codes when tags are wrong but clocks look fine.
How often to re-run
Every manifest change, every mux command change, every “small” trim—treat like Wednesday smoke for audio clocks.
What file naming helps automation
Use facilitator_vo_## matching asset_id in CSV—avoid spaces and locale-specific decimal commas in duration columns.
Can we store matrix in Google Sheets
Export from Sheets to clip_plan.csv for git; do not treat Sheets as source of truth without version column.
Does bitrate affect duration
Codec bitrate does not change duration on PCM WAV; AAC VBR in MP4 can—always extract PCM for post-mux matrix rows when legal requires it.
Who owns P8 sign-off
Producer when rows_fail=0; audio lead co-sign when WARN rows ship with tickets.
Relationship to playtest facilitator contract
Multi-channel facilitator contract requires audio gate evidence—attach matrix CSV path in daily notes template.
What if ffprobe returns N/A
Usually corrupt path or zero-byte file—FAIL with D5_WRONG_MASTER; do not coerce to PASS.
Should we version tolerance policy
Yes—bump policy_version in receipt when legal changes caption window requirements.
Can interns run the matrix
Yes—give them P2 scripts only; producer signs P8 after reviewing FAIL/WARN rows.
Highlight reels need a verification matrix, not a single ffprobe one-liner—batch duration and sample-rate truth before July mux, file ffprobe_duration_matrix_receipt_v1.json, then promote BUILD_RECEIPT.