我用 Python 脚本扫了 3000 首 FLAC,四分之一是假无损

你收藏的”无损”FLAC 文件,可能只是 MP3 换了一件马甲。马甲穿得敷衍——一条水平线就暴露了。
起因:一张频谱图让我决定翻遍整个音乐库
Section titled “起因:一张频谱图让我决定翻遍整个音乐库”今年 2 月,少数派作者 keeping 发了一篇文章,叫《用频谱分析找出我存的那些假无损,假高解析度音乐》。文章不复杂——用一张频谱图(横轴是时间、纵轴是频率、颜色深浅代表音量),真假无损一目了然:
真的无损文件,频率像火焰一样自然延伸到顶部(44.1kHz 采样率下到 22kHz)。假的?16kHz 或 20kHz 处一条笔直的水平线,上面一片死黑。像被人用剪刀咔嚓剪断了一样。
这玩意儿太好认了。认到我不需要 AI、不需要训练模型,只需要看一眼。

但我的音乐库有将近 3000 个 FLAC 文件——从 CD 抓轨的、从各大平台下载的、朋友分享的合集。一个个拖进频谱工具看?那是人干的事吗?
我是工程师。遇到重复劳动,第一反应不是忍,是写脚本。
第一版脚本:几乎全是假无损
Section titled “第一版脚本:几乎全是假无损”第一版逻辑很直接:把 FLAC 文件用 ffmpeg 解码成原始 PCM 数据,跑个 FFT 频谱分析,从最高频往低频扫,找到最后一个能量高于阈值的频率点——这就是”截断频率”。低于 17kHz?假无损。高于 20.5kHz?真的。
跑完一看,几乎全是假无损。
Beyond《海阔天空》?假无损。光良《童话》?假无损。这不对。
排查后发现,第一版有个低级 bug:扫描的起始频率设成了 21550Hz,但 FFT 的频率 bin 总共只有 2049 个。扫描只覆盖了 21550Hz 到 22050Hz 之间那 47 个 bin——而所有这些 bin 的能量本来就低于阈值。真正的截断点在 16-20kHz 范围,扫描根本没到那儿。
就像拿着手电筒只照天花板角落,然后说屋里没东西。
为什么 MP3 转码会在频谱上留下”一刀切”
Section titled “为什么 MP3 转码会在频谱上留下”一刀切””要理解这个”一刀切”,得从 MP3 编码机制说起。
MP3 不是把整个音频信号均匀压缩。它先把 PCM 信号切成 32 个等宽的频率子带——每个子带大约 689Hz 宽。然后用一个”心理声学模型”判断哪些声音人耳听不到(被强声音掩盖了),直接丢掉。最后量化、Huffman 编码压缩。
128kbps 的 MP3,比特率太低了——32 个子带里高频子带分到的比特极少。编码器做了一个干脆的决定:直接不要 16kHz 以上的子带。与其用极低比特编码出更差的伪影,不如一刀切掉。
这就是频谱图上那条水平线的物理来源。不是玄学,是比特不够用了。

320kbps 的 MP3 好一些,20kHz 以上才切。但仍然是硬截断。CD 标准采样率 44.1kHz,Nyquist 频率是 22.05kHz——理论上能到 22kHz。MP3 128k 砍到 16kHz,丢掉了近 30% 的频率范围。
AAC 编码稍微体面一点,不硬切,用感知噪声替换技术处理高频。但在频谱图上会留下毛刺和黑色断裂带——像一把钝刀子割的。
第二版:梯度分析,又被骗了
Section titled “第二版:梯度分析,又被骗了”修了扫描范围后,我换了梯度分析法——计算频谱在每个频率点的能量变化率,找最陡的那个点,那就是截断位置。MP3 截断是”悬崖”,梯度一定陡。真无损的高频衰减是渐进的,梯度平缓。
结果 Beyond《海阔天空》在 20.5kHz 处有一个陡梯度,被判为假无损。但用 ffmpeg 生成高分辨率频谱图一看——频谱自然延伸到了 22kHz,这是真的。那是 Nyquist 频率附近的自然滚降,不是 MP3 截断。
区分方法很简单:陡梯度出现在 19.5kHz 以下,那是 MP3 截断。出现在 21kHz 以上,是物理现象(采样率限制了最高频率),正常的。
第三版:MP3 128k 的梯度不够”悬崖”
Section titled “第三版:MP3 128k 的梯度不够”悬崖””更大的问题出来了。陈鸿宇的《理想三旬》,频谱图上 16kHz 处一条清晰的砖墙截断线——铁证如山的假无损。但梯度分析给出的最大负梯度只有 -5.93 dB/kHz,远低于我设的 -12 dB/kHz 检测阈值。
为什么?MP3 128k 的截断不是”悬崖”,是”缓坡的尽头突然消失”。 在截断点之前(14kHz 处)能量已经很低了(约 -56dB)。从 14kHz 到 16kHz 的能量下降只有约 10dB,分布在约 2kHz 的范围内——梯度自然平缓。
梯度分析适合检测 MP3 320k(截断点附近能量还高,梯度陡峭)。对 MP3 128k,它瞎了。
突破:不看”平均”,看”每一帧”
Section titled “突破:不看”平均”,看”每一帧””我换了一个思路。不再看”整首歌的平均频谱”——那是把所有时刻的频率混在一起。看每一帧 FFT 中最高能跑到多少频率。
具体做法:对每一帧 FFT,找到该帧中能量高于 -70dB 的最高频率。然后对所有帧的最高频率取中位数。
这个指标干净利落。真无损在任何时刻都有频率成分延伸到接近 Nyquist 频率。MP3 转码的文件,每一帧的最高频率都被截断限制住了——中位数直接暴露。
测试结果:
| 文件 | 逐帧最大频率中位数 | 判定 |
|---|---|---|
| Beyond - 海阔天空 | 20720Hz | 真无损 |
| 光良 - 童话 | 20543Hz | 真无损 |
| 陈鸿宇 - 理想三旬 | 15035Hz | 假无损 |
| 莫扎特 - 第40号交响曲 | 19816Hz | 可疑 |
《童话》之前被梯度分析误判,这次对了——频谱确实自然延伸到了 22kHz。低分辨率频谱图看着像截断的特征,高分辨率下其实是自然衰减。
《理想三旬》的 15035Hz,说明每一帧的最高频率都没超过 16kHz——铁证。
一个有意思的发现:古典乐不会”骗人”
Section titled “一个有意思的发现:古典乐不会”骗人””莫扎特《第40号交响曲》的中位数 19816Hz,被标为”可疑”。直觉上,古典乐自然高频少——8kHz 以上能量已经很低了,会不会误判?
不会。用高分辨率频谱图验证后,这个文件确实在 16kHz 处有砖墙截断。它就是假无损。
古典乐的高频成分确实少,但这不意味着它应该在 16kHz 处截断。真无损的古典乐,逐帧最大频率中位数仍然 ≥ 20kHz。16kHz 截断只有一种解释——原始音源就是 128kbps MP3 级别的,被转成了 FLAC。
不要因为”古典乐听起来高频少”就给 16kHz 截断找借口。信号就是信号,不听主观判断。
全量扫描结果:四分之一不干净
Section titled “全量扫描结果:四分之一不干净”最终脚本跑完了 2985 个 FLAC 文件(4 workers,ARM64,约 30 分钟):
| 类别 | 数量 | 占比 |
|---|---|---|
| 真无损 | 2211 | 74.1% |
| 可疑 | 626 | 21.0% |
| 假无损 | 108 | 3.6% |
| 假 Hi-Res | 40 | 1.3% |
加起来,约四分之一的文件有问题。

假无损集中在流行歌扁平目录和古典乐合集。假 Hi-Res 更离谱——有些文件标称 192kHz/24bit,但逐帧最大频率中位数只有 16kHz。什么意思?原始音源是 MP3 128k 级别的,被上采样到了 192kHz。就像把一张 480p 的图片插值到 4K,然后告诉你这是原生 4K。
这不是个别现象。多个音乐平台曾被曝销售”假 Hi-Res”音乐。流媒体平台 Deezer 在 2025 年对超过 1340 万个 AI 生成音轨打了标签,每天处理 6 万多个上传——音频质量检测的需求正在扩大。
如果你也想检查自己的音乐库
Section titled “如果你也想检查自己的音乐库”不需要写 Python 脚本。两个选择:
普通用户:下载 Spek(开源免费,拖拽即用)。把 FLAC 文件拖进去,5 秒出频谱图。16kHz 或 20kHz 处有一条水平截断线?假的。频率自然延伸到顶部?真的。
批量检测:AuCDtect 用神经网络自动检测,输出 CDDA/MPEG 概率百分比,不需要人工判断频谱。foobar2000 用户装 fooCDtect 插件,批量处理整个音乐库。ffmpeg 一行命令也能批量生成频谱图:
ffmpeg -i file.flac -lavfi showspectrumpic=s=1200x600:mode=combined:color=viridis output.png如果你想写自己的检测脚本,核心就一条:逐帧最大频率中位数。不要算平均频谱,不要搞复杂的梯度分析——对每一帧找到最高频率成分,取中位数。低于 17kHz 是假无损,17-20kHz 可疑,20kHz 以上大概率是真的。

频谱图不会说谎。FFT 把信号拆开,每个频率分量摆在那里。截断就是截断,滚降就是滚降,不需要专家鉴定,不需要主观判断。
你看到的,就是信号本身。
你本地音乐库里有多少 FLAC 文件?用 Spek 拖几个看看,16kHz 那条线出现了吗?
keeping. 用频谱分析找出我存的那些假无损,假高解析度音乐. 少数派 (sspai). https://sspai.com/post/106454