模組四|剪輯管線(Day 17–22)
前兩天講逐字稿。今天講剪點決定之後,音訊實際上經過了什麼。
這是整條產線裡最像工程的一段:全部是確定性的、可重跑的、輸出可量測的。也是我最放心讓它全自動的一段。
而它有一個很誠實的地方:七個參數,六個漂過,一個沒有。

先講一個實作決定,它比參數本身重要。
整條鏈不是一個 filter_complex。它是七八個獨立的 ffmpeg 程序,每一個讀上一步的輸出、寫一個中間檔:
切段 → 串接 → 去空拍 → 變速 → declick → crossfade → 響度 → 輸出
↓ ↓ ↓ ↓ ↓ ↓ ↓
mp3 mp3 mp3 mp3 mp3 mp3 mp3
寫成一條 filter_complex 會快很多(少了七次編解碼)。而我選了慢的那個,理由有兩個:
一、每一步都可以單獨聽。 去空拍切壞了?去聽那一步的輸出。這在調參數的時候是決定性的。你不能對一個黑盒調參數。
二、每一步都可以單獨重跑。 只有響度不對,就從響度那步開始跑,前面的中間檔還在。
代價是多次 mp3 編解碼會累積失真。Day 20 會講一個實際的後果:BGM 在人聲全靜的瞬間被量化掉了 9 段,合計 2.73 秒。
我知道這個代價,而且我接受它,因為可除錯性比那 0.06% 的瑕疵重要。
# 切段
ffmpeg -ss {start} -to {end} -ar 44100 -ac 2 -c:a libmp3lame -b:a 192k
# 串接(每軌先重設時間戳)
asetpts=PTS-STARTPTS → concat=n={N}:v=0:a=1
# 去空拍
silenceremove=stop_periods=-1:stop_duration=0.7:stop_threshold=-30dB:stop_silence=0.35
# 變速
atempo=1.1
# declick(段落接合處消喀聲)
afade=t=in:st=0:d=0.03 + afade=t=out:st={dur-0.03}:d=0.03
# 片頭 crossfade
acrossfade=d=1.5:c1=exp:c2=exp
# 響度(雙 pass)
# pass 1 量測
loudnorm=I=-16.0:TP=-1.5:LRA=11:print_format=json
# pass 2 套用(帶入量測值,linear 模式)
loudnorm=I=-16.0:TP=-1.5:LRA=11:measured_I=...:linear=true
# 最終輸出
-ar 44100 -ac 2 -c:a libmp3lame -b:a 320k
段落接合處各做 0.03 秒的淡入淡出。而註解寫著:
消硬切喀聲;刻意不做真 crossfade,避免人聲疊字
這一句解釋了為什麼是 0.03 而不是 0.3。
真正的 crossfade 會讓前後兩段重疊,而在人聲上重疊 = 兩個人同時講話。所以這裡要的不是「平順過渡」,是「不要有喀聲」。那個喀聲來自波形在切點的不連續,只要幾十毫秒的淡入淡出就消掉了。
段落接合用 0.03 秒,片頭那種音樂接人聲的邊界用 0.08 秒,因為那裡沒有疊字風險,可以做長一點。
同一個技術手段,在不同位置用不同的值,理由是內容性質不同。 這種參數如果只寫數字不寫理由,三個月後就是天書。

響度目標值:
I = -16.0 LUFS
TP = -1.5 dBTP
LRA = 11
這組數字,在我查到的每一支剪輯腳本裡完全一致。零漂移。
而其他每一個參數都漂過。為什麼只有它沒有?
因為它有一個外部標準當錨。
-16 LUFS 是串流平台對單聲道/立體聲節目的常見目標值,TP -1.5 是為了避免轉檔削波留的餘裕。這些數字不是我決定的,是我抄的。
當一個參數有外部標準,它就不會漂,因為沒有可以爭論的空間。 而當它只能靠耳朵決定(BGM 多大聲、去空拍多敏感),它一定會漂。
這給了一條很實用的判準:看一個參數會不會漂,先問它有沒有外部錨。 有錨的參數不用留理由;沒錨的參數,理由比數字重要得多。
現在講反面。
silenceremove 的門檻,我查到的值是這樣的:
-38dB → -36dB → -40dB → -36dB → -30dB → -30dB → -40dB
四個不同的值,而且來回擺盪。
看起來像沒有標準。但每一次變動都留了理由,而理由全部是內容性的:
-40dB 抓不到本集底噪
本集底噪偏高,-30dB 量測到 8 段長靜音,不切語音
也就是說:這個參數依賴每一集的錄音條件,本來就不該固定。
漂移不是問題,沒有寫明「這一集為什麼是這個值」才是問題。而我的紀錄有寫,所以這些漂移是可讀的。
真正該修的是別的:這個值應該從音訊量出來,不是每集用猜的。 錄完先量一段靜音區的平均音量,門檻設在它上面幾 dB,這是十行程式碼的事,而我每一集還在手動試。
早期我用一支包裝過的工具,它的 CLI 有個 --keep 參數,意思是「保留多少靜音」。
而它的實作是這樣:
half_keep = keep_silence / 2.0
# 然後分別填進 start_silence 與 stop_silence
--keep 是總量,會被除以二分給頭尾。
所以 --keep 0.45 實際上是每側保留 0.225 秒。
而後來直寫 filter 的時候,stop_silence=0.35 是單側的值。
兩種寫法的數字看起來可以比較,實際上不能:0.45 跟 0.35,前者比後者小。
我在整理這些參數的時候,一開始就比錯了。
同一個概念在兩個地方用不同的單位,是一種很難發現的技術債。 它不會報錯,只會讓你在某一天做出一個錯誤的比較。
還有一個更直接的:某一份剪輯記錄裡,第 50 行寫 -30dB,第 107 行寫 -40dB。
同一集、同一個參數、同一份文件。
成因大概是後面那段是從上一集的記錄複製過來的(Day 27 會講這個複製機制),而前面那段是這一集真的改過的值。
以腳本為準。 而這件事本身說明了:記錄跟程式碼分開存放,記錄就會漂。
這條組裝鏈最實際的代價是慢。
七八次獨立的編解碼,一集要跑好幾分鐘。如果寫成單一 filter_complex,大概快五倍。
我接受這個,因為那幾分鐘是機器的時間(Day 17 講過這條)。
第二個代價比較實際:多次 mp3 編解碼會累積失真。 Day 20 會給你看它造成的一個可量測的瑕疵。而正確的做法是中間檔用無損格式(wav 或 flac),只有最終輸出才編 mp3。我沒有這樣做,純粹是因為一開始隨手寫了 mp3,然後就一直沿用。
這是一個十分鐘可以修、而我沒修的東西。清單又多一項。
把管線切成可以單獨聽、單獨重跑的段落。
代價是效能,收益是你能對每一步除錯。而在一條需要靠感官驗收的管線上(音訊、影像、文字),能不能單獨檢查每一步,決定了你調參數是在工程還是在猜。
第二條,關於參數會不會漂:
有外部標準當錨的參數不會漂,靠感官決定的參數一定會漂。
而如果一個沒錨的參數其實可以從資料量出來(例如從靜音區量底噪),那就把它量出來。把一個要猜的東西變成一個可以算的東西,是最划算的一種自動化。
第三條:同一個概念不要有兩套單位。 一個是總量、一個是單側,這種差異不會報錯,只會在某天讓你做出一個安靜的錯誤比較。
明天講那個改了七次、中途還換過一套單位的參數。