iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
AI 自動化

我以為我保留了五道閘門系列 第 19

Day 19|一條組裝鏈,只有一個參數從頭到尾沒漂過

  • 分享至 

  • xImage
  •  

模組四|剪輯管線(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

declick 那 0.03 秒,註解寫得比參數清楚

段落接合處各做 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,然後就一直沿用。

這是一個十分鐘可以修、而我沒修的東西。清單又多一項。

帶走什麼

把管線切成可以單獨聽、單獨重跑的段落。

代價是效能,收益是你能對每一步除錯。而在一條需要靠感官驗收的管線上(音訊、影像、文字),能不能單獨檢查每一步,決定了你調參數是在工程還是在猜。

第二條,關於參數會不會漂:

有外部標準當錨的參數不會漂,靠感官決定的參數一定會漂

  • 有錨的(響度、取樣率、編碼格式):抄標準,不用留理由
  • 沒錨的(去空拍門檻、BGM 音量、變速倍率):理由比數字重要,因為下次要改的時候你需要知道上次為什麼是這個值

而如果一個沒錨的參數其實可以從資料量出來(例如從靜音區量底噪),那就把它量出來。把一個要猜的東西變成一個可以算的東西,是最划算的一種自動化。

第三條:同一個概念不要有兩套單位。 一個是總量、一個是單側,這種差異不會報錯,只會在某天讓你做出一個安靜的錯誤比較。

明天講那個改了七次、中途還換過一套單位的參數。


上一篇
Day 18|逐字稿說那裡沒東西,於是我砍掉了六分半
系列文
我以為我保留了五道閘門19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言