逆向网易云音乐 NCAE 音效格式

背景
网易云音乐 App 里有两百多个「音效」:超重低音、演唱会、迷幻电音这类场景音效,以及按耳机品牌型号适配的「适配耳机」。这些音效在客户端里不是明文下发的,而是一种私有格式 NCAE(NetEase Cloud Audio Effect)——加密压缩的二进制文件,App 内置解密逻辑后加载生效。
本文以 迷幻电音.ncae(125 字节)为例,把整条逆向链路拆开看:文件结构 → 密钥提取 → 流密码 → 解压 → 参数解读。用到的开源实现见文末参考。
先说结论:NCAE 解密后的明文有两种形态——
- JSON 参数型:一段 JSON,声明 eq(均衡器)、bt(低音/高音)、rvb(混响)、se(空间音效)四个模块的开关和参数,场景音效基本都是这种;
- WAV 卷积型:解密后直接是一个 WAV 脉冲响应(IR),用卷积套用,耳机适配类音效是这种。
迷幻电音属于前者。
文件结构
迷幻电音.ncae 全文 125 字节,头部按大端序解读:
| 偏移 | 长度 | 值 | 含义 |
|---|---|---|---|
| 0 | 4 | 4E 43 41 45 | 魔数,ASCII 即 NCAE |
| 4 | 4 | 5F 00 00 00 | 密文数据长度(大端),0x5F = 95 字节 |
| 8 | 8 | 全零 | 未使用 |
| 16 | 1 | 0D | 密钥块长度 + 1,即 key0 共 13 字节 |
| 17 | 13 | … | 密钥块 key0 |
| 30 | 95 | … | 密文(zlib raw deflate 压缩后再流加密) |
对照验证:4 + 4 + 8 + 1 + 13 + 95 = 125,和文件大小严丝合缝。
结构本身没什么花活,有意思的都在后面两步。
密钥提取
key0 是 13 字节,但真正参与解密的 key 只有 12 字节,提取方式是把 key0 的第 5 个字节抽出来当异或掩码,剩下的字节逐个与它异或:
key = np.array(key0[:4] + key0[5:]) ^ key0[4]注意切片是 key0[:4] + key0[5:]——去掉的正是下标 4(第 5 字节)。也就是说头部里存的密钥自带一层轻度混淆:单看 key0 猜不出实际密钥,必须知道「去掉第 5 字节再异或」这条规则。迷幻电音.ncae 提取出的 12 字节密钥实际是 [100, 75, 57, 80, 69, 124, 218, 113, 101, 98, 136, 82]。
这层混淆挡不住有心人(逆向 App 就能拿到规则),但对格式分析者是一块绊脚石——你直接拿 key0 去跑标准解密,得到的是乱码。
流密码:一个不换牌的 RC4
解密器是一个 RC4 变体。熟悉 RC4 的知道它分两阶段:
KSA(密钥调度)——这部分和标准 RC4 完全一致:初始化 0~255 的 S 盒,然后用密钥反复打乱:
table = np.arange(0x100, dtype=np.uint8)byte = 0for i in range(0x100): byte = (table[i] + key[i % len(key)] + byte) & 0xFF table[i], table[byte] = table[byte], table[i]PRGA(密钥流生成)——这里和标准 RC4 不一样了。标准 RC4 每生成一个字节都要交换 S 盒里的两个元素,S 盒持续自演化;而这个实现完全不交换,S 盒经 KSA 打乱后就冻结,密钥流直接从静态表里双索引导出:
for j in range(len(data)): byte = table[(j + 1) & 0xFF] data[j] ^= table[(table[(byte + j + 1) & 0xFF] + byte) & 0xFF]逐项解释关键变量:
j是密文字节下标,直接充当序号;byte = table[(j+1) & 0xFF]取第一层索引值;- 密钥流字节为
table[(table[(byte+j+1) & 0xFF] + byte) & 0xFF]——用第一层的值再查一次表,双重索引; - 明文 = 密文 XOR 密钥流,逐字节异或。
「无置换」意味着这个变体的密钥流周期性和统计特性都比标准 RC4 更弱,安全性进一步下降——不过对音效参数这种低价值数据,设计目标本来也只是「格式不透明」,不是真正的安全防线。
解压
流密码解出的 95 字节还不是明文,而是 zlib raw deflate 流(无 zlib 头、无 gzip 头的纯 deflate 数据),对应参数 -zlib.MAX_WBITS:
zlib.decompress(data, -zlib.MAX_WBITS)解压后得到 136 字节的 UTF-8 JSON。136 字节的明文被压到 95 字节密文,压缩率约 70%。
参数解读
解密出的完整 JSON:
{ "eq": { "on": true, "eqs": [5, 6.09, 2.2, 0, -1.6, 1.5, 0, 1, 2.57, 3.77] }, "bt": { "on": true, "bass": 3, "treble": 2 }, "rvb": { "on": false }, "se": { "on": false }}四个模块,迷幻电音开了两个:EQ 和低音/高音,混响(rvb)与空间音效(se)关闭。
eq.eqs 是 10 段图形 EQ 的各段增益(dB),频点为标准倍频程分布:
| 频点 | 31Hz | 62Hz | 125Hz | 250Hz | 500Hz | 1kHz | 2kHz | 4kHz | 8kHz | 16kHz |
|---|---|---|---|---|---|---|---|---|---|---|
| 增益 | +5 | +6.09 | +2.2 | 0 | -1.6 | +1.5 | 0 | +1 | +2.57 | +3.77 |
这里有一点 NCAE 格式本身没有交代:Q 值在协议里没有定义,完全由客户端实现决定,一般取的是固定值 Q ≈ 1.41(即 √2)。这个数不是随便挑的——对图形 EQ 里常见的 peak(钟形)滤波器,Q 与带宽(octave,倍频程)近似满足
代入 BW = 1(即每段控制 1 个倍频程,也正好对应上表里相邻频点之间的间隔)算出来 Q ≈ 1.414,正好是 √2。也就是说客户端选 √2 不是巧合,而是「让每一段 EQ 的作用范围大致覆盖一个倍频程、彼此邻段之间平滑衔接、既不过窄产生突兀的尖峰也不过宽互相严重重叠」这个工程目标下的标准解,10 段图形 EQ、吉他效果器等场景里 Q=1.41 是很常见的经验值/近似值。
一眼就是「V 型曲线加超低频强化」:62Hz 顶到 +6.09dB、31Hz +5dB 拉满低频下盘,8k/16kHz 分别 +2.57/+3.77 提亮高频,只在 500Hz 轻微 -1.6dB 收一下中低频的浑浊感——典型的电子乐调音取向。
实现上每段是一个 peak 滤波器(scipy SOS 级联),增益非零的段才参与滤波;此外还有一个防削波细节:preamp = -max(最大正增益, 0),迷幻电音最大增益是 6.09dB,所以整体先预放 -6.09dB,保证 EQ 抬升后不会爆音。
bt 是低架/高架两个 shelf 滤波器,数值直接就是 dB 增益:
bass: 3→ 120Hz 低架 +3dB(RBJ shelf,S=1)treble: 2→ 8kHz 高架 +2dB
同样有 preamp = -max(bass, treble, 0) = -3dB 的防削波预放。也就是说迷幻电音的最终效果是「10 段 EQ 曲线 + 两端 shelf 再叠一层」,低频实际叠加到了 +9dB 上下的量级(62Hz 段 +6.09 与 120Hz 低架 +3 在频响上部分叠加)。
rvb 与 se 虽然在这例里关闭,参数结构也已被还原:rvb 有 er/il/ol/rl/rvb/tc 六组(早期反射、各级延迟、房间列表、衰减时间等),se 有 ambience/presence/sshapper/stereoizer 四组(氛围、现场感、瞬态塑形、立体声扩展)——开了这两个模块的音效(如「演唱会」「livehouse」)参数量会大得多。
复现
脚本见 Github:
python main.py 歌曲.flac -e bass # 用仓库自带预设python main.py 歌曲.flac -e 迷幻电音 # 用自己解密出的预设(需重命名为 .ncae 放入 presets/)-n 开启输出响度归一化,-o 指定输出路径。也可以直接用仓库里的 _decrypt_all.py 批量把所有 .ncae 解成可读的 JSON / WAV。
整条链路总结:魔数校验 → 头部取密钥块 → 去位异或提 key → RC4 变体(标准 KSA + 无置换 PRGA)流解密 → raw deflate 解压 → JSON 参数(或 WAV 脉冲响应)。加密强度不高,但「私有格式 + 客户端解密」这一层窗户纸,对于防止普通用户直接拿走参数文件来说,够用了。
相关资料
| 文件 | 说明 |
|---|---|
| ncae_raw.7z | 原始 .ncae 文件集合,未经处理 |
| ncae_processed.7z | 解密后的原始输出(JSON / WAV,未格式化) |
| ncae_processed_fmt.7z | 格式化后的 JSON(缩进美化,便于阅读) |
| ncae_processed_fmt_wav2flac.7z | WAV 卷积型音效转 flac 后的版本 |
频域脉冲响应


部分参数表
| EQ | Bass | Treble | ||
|---|---|---|---|---|
| genre_electronic | 迷幻电音 | [5, 6.09, 2.2, 0, -1.6, 1.5, 0, 1, 2.57, 3.77] | 3 | 2 |
| genre_electronic+ | 动感电音 | [4, 4, 3, 1, -1, -2, -1, 2, 4, 4] | ||
| genre_rock | 摇滚经典 | [-5.7, 0, 1.5, -3.81, -0.2, -1.1, 0.6, 1.5, 1.91, 1.91] | ||
| genre_hiphop | 嘻哈音效 | [5.6, 5.09, 2, 0.5, -2, 1.2, 3.79, 0.5, 1.5, 4] | 2 | 2 |
| genre_acg | 纯净 ACG | [4, 6.19, 1.5, -0.5, -0.8, 2, 5.19, 1, -1.5, 4] | 2 | 3 |
| genre_folk | 民谣音效 | [0, 3, 0, -0.8, 1.5, 4.8, 5.8, 3.2, -0.5, 2] | 0 | 1 |
| genre_classical | 婉约古风 | [4, 2, 0, -0.5, -1, 3.5, 4.5, 1, -1, 3] | 2 | 2 |
| eq_bass | 极重低音 | [6, 4.5, 3.2, 2.09, 0, 0, 0, 0, 0, 0] | 3 | 0 |
| eq_vocal | 清澈人声 | [-1.5, -1, 0, 1.79, 4, 3.79, 1.79, 0.5, -1, 1] | ||
| spatial_surr | 3D 环绕 | |||
| spatial_stereo | 独享立体声 | |||
| spatial_stereo_surr | 环绕立体声 | |||
| spatial_stereo_crystal | 水晶立体声 | |||
| scene_concert | 音乐厅 | |||
| scene_church | 教堂混响 | |||
| scene_live | 演唱会现场 | [5.59, 5, 3, 0, -1, 0, 3.09, 4.5, 4, 5] | 3 | 2 |
| scene_live_surr | 狂嗨 LIVE | [-6, -6.5, -3.7, -9.93, -12, -12, -12, -12, -12, -12] | 12 | -12 |
| scene_livehouse | LiveHouse 现场 |
只列了部分数据,SE(Presence / Stereoizer / SShaper / Ambience)、RVB(OL / ER / RVB / TC)等其余场景与全部参数详见:param_table.html
参考
版权声明
本文仅用于技术研究与学习交流,禁止用于商业用途,也不鼓励绕开客户端限制批量获取或分发音效文件。文中涉及的密钥提取与解密方式仅为格式分析所需的最小复现,版权属于网易公司(NetEase, Inc.)。














