惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Secure Thoughts
罗磊的独立博客
T
The Blog of Author Tim Ferriss
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
Last Week in AI
Last Week in AI
美团技术团队
Google Online Security Blog
Google Online Security Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
D
Docker
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
酷 壳 – CoolShell
酷 壳 – CoolShell
小众软件
小众软件
月光博客
月光博客
L
LINUX DO - 最新话题
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
W
WeLiveSecurity
H
Heimdal Security Blog
Vercel News
Vercel News
SecWiki News
SecWiki News
Forbes - Security
Forbes - Security
Blog — PlanetScale
Blog — PlanetScale
Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
TaoSecurity Blog
TaoSecurity Blog
T
Troy Hunt's Blog
A
About on SuperTechFans
C
Check Point Blog
S
Security Affairs
Hacker News - Newest:
Hacker News - Newest: "LLM"
AI
AI
WordPress大学
WordPress大学
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Help Net Security
Help Net Security
博客园_首页
The Last Watchdog
The Last Watchdog
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Engineering at Meta
Engineering at Meta
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
I
Intezer
K
Kaspersky official blog
M
MIT News - Artificial intelligence
J
Java Code Geeks
G
GRAHAM CLULEY
P
Palo Alto Networks Blog

依云's Blog

通过字幕总结YouTube视频内容 - 依云's Blog 自定义系统默认中文字体 - 依云's Blog Wayfire支持不缩放Xwayland啦 - 依云's Blog 使用wayvnc远程访问无头Wayfire会话 - 依云's Blog 给论坛用上了文本嵌入模型 - 依云's Blog 使用 Restic 备份数据 - 依云's Blog Arch Linux 中文论坛迁移杂记 - 依云's Blog pacfiles: 高速的 pacman -F 替代品 用 Android 手机当电脑的话筒 - 依云's Blog 为团队部署邮件服务 - 依云's Blog 使用 nftables 屏蔽大量 IP - 依云's Blog YubiKey 初体验 - 依云's Blog fcitx5 码表同步方案 - 依云's Blog 我正在使用的火狐扩展(2024年版) - 依云's Blog
使用 ffmpeg 对音频文件进行响度归一化 - 依云's Blog
依云 · 2024-12-11 · via 依云's Blog

我喜欢用本地文件听歌:没有广告、没有延迟、没有厂商锁定。但是有个问题:有的歌曲文件音量挺大的,比如 GARNiDELiA 和桃色幸运草Z的都感觉特别吵,需要调小音量,但有的音量又特别小,以至于我时常怀疑音频输出是不是出了问题。

这时候就要用到响度归一化了。响度衡量的是人的主观感知的音量大小,和声强——也就是声波的振幅大小——并不一样。ffmpeg 自带了一个 loudnorm 过滤器,用来按 EBU R128 标准对音频做响度归一化。于是调整好参数,用它对所有文件跑一遍就好了——我最初是这么想的,也是这么做的。

以下是我最初使用的脚本的最终改进版。是的,改进过好多次。小的改进如排除软链接、反复执行时不重做以前完成的工作;大的改进如使用 sem 并行化、把测量和调整两个步骤分开。之所以有两个步骤,是因为我要线性地调整响度——不要让同一个音频不同部分受到不同程度的调整。第一遍是测量出几个参数,这样第二遍才知道怎么调整。只过一遍的是动态调整,会导致调整程度不一,尤其是开头。

至于参数的选择,整体响度 I=-14 听说是 YouTube 它们用的,而真峰值 TP=0 和响度范围 LRA=50 是因为我不想给太多限制。

#!/bin/zsh -e

for f in **/*.{flac,m4a,mp3,ogg,opus,wma}(.); do
  json=$f:r.json
  if [[ -s $json || $f == *_loudnorm.* ]]; then
    continue
  fi
  echo "Processing $f"
  export f json
  sem -j+0 'ffmpeg -i $f -af loudnorm=print_format=json -f null /dev/null </dev/null |& sed -n ''/^{$/,/^}$/p'' > $json; echo "Done with $f"'
done

sem --wait

for f in **/*.{flac,m4a,mp3,ogg,opus,wma}(.); do
  json=$f:r.json
  output=$f:r_loudnorm.$f:e
  if [[ ! -f $json || -s $output || $f == *_loudnorm.* ]]; then
    continue
  fi
  echo "Processing $f"
  export f json output
  sem -j+0 'ffmpeg -loglevel error -i $f -af loudnorm=linear=true:I=-14:TP=0:LRA=50:measured_I=$(jq -r .input_i $json):measured_TP=$(jq -r .input_tp $json):measured_LRA=$(jq -r .input_lra $json):measured_thresh=$(jq -r .input_thresh $json) -vcodec copy $output </dev/null; echo "Done with $f"'
done

sem --wait

不得不说 zsh 的路径处理是真方便。相对地,sem 就没那么好用了。一开始我没加 </dev/null,结果 sem 起的进程全部 T 在那里不动,strace 还告诉我是 SIGTTOU 导致的——我一直是 -tostop 的啊,也没见着别的时候收到 SIGTTOU。后来尝试了重定向 stdin,才发现其实是 SIGTTIN——也不知道 ffmpeg 读终端干什么。另外,给 sem 的命令传数据也挺不方便的:直接嵌在命令里,空格啥的会出问题,最后只好用环境变量了。

等全部处理完毕,for f in **/*_loudnorm.*; do ll -tr $f:r:s/_loudnorm//.$f:e $f; done | vim - 看了一眼,然后就发现问题了:有的文件变大了好多,有的文件变小了好多!检查之后发现是编码参数变了:mp3 文件全部变成 128kbps 了,而 flac 的采样格式从 s16 变成了 s32。

于是又写了个脚本带上参数重新处理。这次考虑到以后我还需要对单个新加的歌曲文件处理,所以要处理的文件通过命令行传递。

#!/bin/zsh -e

doit () {
  local f=$1
  local json=$f:r.json
  local output=$f:r_loudnorm.$f:e

  echo "Processing $f"

  if [[ -s $json || $f == *_loudnorm.* ]]; then
  else
    ffmpeg -i $f -af loudnorm=print_format=json -f null /dev/null </dev/null |& sed -n '/^{$/,/^}$/p' > $json
  fi

  if [[ ! -f $json || -s $output || $f == *_loudnorm.* ]]; then
  else
    local args=()
    if [[ $f == *.mp3 || $f == *.m4a || $f == *.wma ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of json $f | jq -r '.streams[0].bit_rate')
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.m4a ]]; then
      local src_profile=$(ffprobe -v error -select_streams a:0 -show_entries stream=profile -of json $f | jq -r '.streams[0].profile')
      if [[ $src_profile == HE-AAC ]]; then
        args=($args -acodec libfdk_aac -profile:a aac_he)
      fi
    fi
    if [[ $f == *.opus ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries format=bit_rate -of json $f | jq -r '.format.bit_rate')
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.ogg ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of json $f | jq -r '.streams[0].bit_rate')
      if [[ $src_bitrate == null ]]; then
        src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries format=bit_rate -of json $f | jq -r '.format.bit_rate')
      fi
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.flac ]]; then
      local src_sample_fmt=$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_fmt -of json $f | jq -r '.streams[0].sample_fmt')
      args=($args -sample_fmt:a $src_sample_fmt)
    fi
    ffmpeg -loglevel error -i $f -af loudnorm=linear=true:I=-14:TP=0:LRA=50:measured_I=$(jq -r .input_i $json):measured_TP=$(jq -r .input_tp $json):measured_LRA=$(jq -r .input_lra $json):measured_thresh=$(jq -r .input_thresh $json) $args -vcodec copy $output </dev/null
    touch -r $f $output
  fi

}

for f in "$@"; do
  doit $f
done

然后我就神奇地发现,sem 不好用的问题突然没有了——我直接 parallel loudnorm ::: 文件们 就好了嘛……