某游戏的音频和图像格式解析

前言

我只是想听歌和找张壁纸而已。

准备

游戏目录结构如下:

1
2
3
4
5
6
7
8
.
├── D3D12
├── GameAssembly.dll
├── ******.exe
├── ******_Data
├── UnityCrashHandler64.exe
├── UnityPlayer.dll
└── baselib.dll

典型的使用 Unity 框架的游戏。

好消息:针对 Unity 框架,我们已经拥有相当多的成熟工具可用。

坏消息:下一级目录表明,该游戏使用了 Il2Cpp 技术,这意味着,我们大概率不太可能获取到 C# 源码。

1
2
3
4
5
6
7
8
9
10
******_Data
├── Plugins
├── Resources
├── RuntimeInitializeOnLoads.json
├── ScriptingAssemblies.json
├── StreamingAssets
├── app.info
├── boot.config
├── il2cpp_data
└── ... (一堆资源文件)

不过我们总是可以看看这些资源文件里面存储了些什么东西。

使用 AssetRipper 解包资源,发现这里面并没有以典型音频格式(.mp3.m4a 等)格式存储的文件,搜索曲目名称发现这些资源的类型是 MonoBehavior

某个音频的详细信息如下:

这表明,这些音频直接以 MuaAudio 对象的形式存储为二进制。

查看数据开头:

1
0000000100000000001ab2bb0000bb800000000200...

发现这的确不是一个常见音频文件的格式,应当是游戏自定义的一种音频格式,不妨称其为 mua 格式。

mua 格式的解析只能尝试阅读游戏源码了。

Unity 层

使用 Il2CppDumper 恢复各种符号的定义。

在生成的 dump.cs 中查找 MuaAudio 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Namespace: Milody.Unity
[NullableContext(1)]
[Nullable(0)]
public class MuaAudio : ScriptableObject // TypeDefIndex: 14270
{
// Fields
[SerializeField]
[HideInInspector]
private MuaAudioClip clip; // 0x18

// Methods

// RVA: 0x3375E0 Offset: 0x335FE0 VA: 0x1803375E0 Slot: 4
public virtual MuaAudioClip Get() { }

// RVA: 0x3375E0 Offset: 0x335FE0 VA: 0x1803375E0 Slot: 5
public virtual MuaAudioClip GetNormalClip() { }

// RVA: 0x1505010 Offset: 0x1503A10 VA: 0x181505010 Slot: 6
public virtual string GetState() { }

// RVA: 0x32ED00 Offset: 0x32D700 VA: 0x18032ED00
public void Import(MuaAudioClip audioClip) { }

// RVA: 0x4C8430 Offset: 0x4C6E30 VA: 0x1804C8430
public void .ctor() { }
}

发现 MuaAudio 内仅仅存储了一个 MuaAudioClip 的对象。

MuaAudioClip 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Namespace: Milody.Unity
[NullableContext(1)]
[Nullable(0)]
[Serializable]
public class MuaAudioClip // TypeDefIndex: 14269
{
// Fields
[SerializeField]
[HideInInspector]
private byte[] data; // 0x10

// Methods

// RVA: 0x3277F0 Offset: 0x3261F0 VA: 0x1803277F0
public byte[] GetRawBytes() { }

// RVA: 0x1504FA0 Offset: 0x15039A0 VA: 0x181504FA0
public static MuaAudioClip FromRawBytes(byte[] bytes) { }

// RVA: 0x327080 Offset: 0x325A80 VA: 0x180327080
public void .ctor() { }
}

只是简单的把这种文件的二进制以 byte[] 形式存储起来而已。

这说明,MuaAudio 不负责处理 mua 格式,只保存原始二进制形式。

不过,在 dump.cs 中搜索 mua 的时候,发现了一个非常可疑的类 JuceMUASourcePlayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// Namespace: Milody.Audio
[NullableContext(1)]
[Nullable(0)]
public class JuceMUASourcePlayer : DisposableNativeObject, IJuceAudioPlayer, INativePtrHolder // TypeDefIndex: 14294
{
// Fields
[CompilerGenerated]
private AudioTag <Tag>k__BackingField; // 0x38
private readonly ReaderWriterLockSlim Lock; // 0x40
private readonly PlayerManager _owner; // 0x48
[Range(0, 1)]
private float _volume; // 0x50
private float _volumeWeight; // 0x54
private bool _mute; // 0x58
private VolumePreset _volumePreset; // 0x5C

// Properties
public AudioTag Tag { get; set; }
public bool PreviewDecoded { get; }
public bool FullSongDecoded { get; }
public long PlaybackIndex { get; }
public long SampleLength { get; }
public long PreviewSampleLength { get; }
public double Length { get; }
public double PreviewLength { get; }
public float Volume { get; set; }
public float VolumeWeight { get; set; }
public bool Mute { get; set; }
public VolumePreset VolumePreset { get; set; }
public double SampleRate { get; }
public uint Channels { get; }

// Methods

[CompilerGenerated]
// RVA: 0x32EB50 Offset: 0x32D550 VA: 0x18032EB50 Slot: 17
public AudioTag get_Tag() { }

[CompilerGenerated]
// RVA: 0x32EC60 Offset: 0x32D660 VA: 0x18032EC60 Slot: 18
public void set_Tag(AudioTag value) { }

// RVA: 0x1503AA0 Offset: 0x15024A0 VA: 0x181503AA0
private void .ctor(IntPtr ptr, PlayerManager owner) { }

// RVA: 0x1503610 Offset: 0x1502010 VA: 0x181503610
public void Dispose() { }

// RVA: 0x1503540 Offset: 0x1501F40 VA: 0x181503540 Slot: 7
protected override void DisposeUnmanaged() { }

// RVA: 0x1502CA0 Offset: 0x15016A0 VA: 0x181502CA0
internal static JuceMUASourcePlayer CreateFromBytes(PlayerManager owner, byte[] data, float initialAudioSourcePlayerGain) { }

// RVA: 0x15030A0 Offset: 0x1501AA0 VA: 0x1815030A0
public void DecodePreview(CancellationToken ctx) { }

// RVA: 0x1504000 Offset: 0x1502A00 VA: 0x181504000
public bool get_PreviewDecoded() { }

// RVA: 0x1502E90 Offset: 0x1501890 VA: 0x181502E90
public void DecodeFullSong(CancellationToken ctx) { }

// RVA: 0x1503770 Offset: 0x1502170 VA: 0x181503770
public void ReleaseFullSong() { }

// RVA: 0x1503C80 Offset: 0x1502680 VA: 0x181503C80
public bool get_FullSongDecoded() { }

// RVA: 0x1503EE0 Offset: 0x15028E0 VA: 0x181503EE0
public long get_PlaybackIndex() { }

// RVA: 0x1504380 Offset: 0x1502D80 VA: 0x181504380
public long get_SampleLength() { }

// RVA: 0x1504260 Offset: 0x1502C60 VA: 0x181504260
public long get_PreviewSampleLength() { }

// RVA: 0x1503DA0 Offset: 0x15027A0 VA: 0x181503DA0
public double get_Length() { }

// RVA: 0x1504120 Offset: 0x1502B20 VA: 0x181504120
public double get_PreviewLength() { }

// RVA: 0x1503660 Offset: 0x1502060 VA: 0x181503660
public void Play(bool doInitialize) { }

// RVA: 0x1503990 Offset: 0x1502390 VA: 0x181503990
public void Start() { }

// RVA: 0x1503980 Offset: 0x1502380 VA: 0x181503980
public void Resume() { }

// RVA: 0x15039A0 Offset: 0x15023A0 VA: 0x1815039A0
public void Stop() { }

// RVA: 0x1503870 Offset: 0x1502270 VA: 0x181503870
public void ResumeAt(double position) { }

// RVA: 0x15045C0 Offset: 0x1502FC0 VA: 0x1815045C0 Slot: 9
public float get_Volume() { }

// RVA: 0x1504630 Offset: 0x1503030 VA: 0x181504630 Slot: 10
public void set_Volume(float value) { }

// RVA: 0x4CE1B0 Offset: 0x4CCBB0 VA: 0x1804CE1B0 Slot: 11
public float get_VolumeWeight() { }

// RVA: 0x1504610 Offset: 0x1503010 VA: 0x181504610 Slot: 12
public void set_VolumeWeight(float value) { }

// RVA: 0x128AB10 Offset: 0x1289510 VA: 0x18128AB10 Slot: 13
public bool get_Mute() { }

// RVA: 0x15045D0 Offset: 0x1502FD0 VA: 0x1815045D0 Slot: 14
public void set_Mute(bool value) { }

// RVA: 0x32EB30 Offset: 0x32D530 VA: 0x18032EB30 Slot: 15
public VolumePreset get_VolumePreset() { }

// RVA: 0x15045E0 Offset: 0x1502FE0 VA: 0x1815045E0 Slot: 16
public void set_VolumePreset(VolumePreset value) { }

// RVA: 0x15044A0 Offset: 0x1502EA0 VA: 0x1815044A0
public double get_SampleRate() { }

// RVA: 0x1503B70 Offset: 0x1502570 VA: 0x181503B70
public uint get_Channels() { }

// RVA: 0x15032B0 Offset: 0x1501CB0 VA: 0x1815032B0
public BufferPlayerPair Detach() { }

[NullableContext(2)]
// RVA: 0x1502B50 Offset: 0x1501550 VA: 0x181502B50
public void ApplyIIRFilterState(float[] c, uint durationInSamples) { }
}

该类提供了 CreateFromBytesDecodeFullSong 等方法,很难不让人怀疑这是和音频解码高度相关的类。

该类继承了 DisposableNativeObject,预示着这些方法可能会以 native 代码的形式存在,而非由 C# 实现。

使用 IDA 打开 GameAssembly.dll,使用 Il2CppDumper 提供的脚本恢复符号。

其中 CreateFromBytes 反编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
Milody_Audio_JuceMUASourcePlayer_o *Milody_Audio_JuceMUASourcePlayer__CreateFromBytes(
Milody_Service_PlayerManager_o *owner,
System_Byte_array *data,
float initialAudioSourcePlayerGain,
const MethodInfo *method)
{
__int64 (__fastcall *v6)(intptr_t *, uint8_t *, __int64, const MethodInfo *); // rax
__int64 max_length_low; // rdi
intptr_t v8; // rsi
__int64 v9; // rdi
System_Threading_ReaderWriterLockSlim_o *v10; // rbx
Milody_Service_PlayerManager_o *v11; // rcx
__int64 v13; // rax
System_Exception_o *v14; // rbx
System_String_o *v15; // rax
__int64 v16; // rax
_QWORD v17[4]; // [rsp+20h] [rbp-48h] BYREF
int v18; // [rsp+40h] [rbp-28h]
int v19; // [rsp+44h] [rbp-24h]
int v20; // [rsp+48h] [rbp-20h]
char v21; // [rsp+4Ch] [rbp-1Ch]
intptr_t ptr; // [rsp+78h] [rbp+10h] BYREF

if ( !byte_1832B3434 )
{
sub_180260980(&Milody_Audio_JuceMUASourcePlayer_TypeInfo);
byte_1832B3434 = 1;
}
ptr = 0;
if ( !data )
goto LABEL_11;
v6 = (__int64 (__fastcall *)(intptr_t *, uint8_t *, __int64, const MethodInfo *))qword_1832B32E0;
max_length_low = SLODWORD(data->max_length);
if ( !qword_1832B32E0 )
{
v18 = 0;
v17[0] = L"libMilody";
v21 = 0;
v17[2] = "MilodyAudioJuceMUASourcePlayerCreate";
v17[1] = 9;
v17[3] = 36;
v19 = 2;
v20 = 28;
v6 = (__int64 (__fastcall *)(intptr_t *, uint8_t *, __int64, const MethodInfo *))sub_180260C20((__int64)v17);
qword_1832B32E0 = (__int64)v6;
}
if ( v6(&ptr, data->m_Items, max_length_low, method) < 0 )
{
v13 = sub_1802609A0(&System_Exception_TypeInfo);
v14 = (System_Exception_o *)sub_180260B80(v13);
v15 = (System_String_o *)sub_1802609A0(&StringLiteral_7979);
System_Exception___ctor_6468750720(v14, v15, 0);
v16 = sub_1802609A0(&Method_Milody_Audio_JuceMUASourcePlayer_CreateFromBytes__);
sub_180260B90(v14, v16);
}
v8 = ptr;
v9 = sub_180260B80(Milody_Audio_JuceMUASourcePlayer_TypeInfo);
if ( !byte_1832B3433 )
{
sub_180260980(&System_Threading_ReaderWriterLockSlim_TypeInfo);
byte_1832B3433 = 1;
}
v10 = (System_Threading_ReaderWriterLockSlim_o *)sub_180260B80(System_Threading_ReaderWriterLockSlim_TypeInfo);
System_Threading_ReaderWriterLockSlim___ctor_6471138288(v10, 1, 0);
*(_QWORD *)(v9 + 64) = v10;
sub_18025FBF0(v9 + 64, v10);
*(_DWORD *)(v9 + 80) = 1065353216;
*(_DWORD *)(v9 + 84) = 1065353216;
*(_DWORD *)(v9 + 92) = 1;
Paraparty_UnityNative_Base_DisposableNativeObject___ctor_6469717520(
(Paraparty_UnityNative_Base_DisposableNativeObject_o *)v9,
v8,
1,
0);
*(_QWORD *)(v9 + 72) = owner;
sub_18025FBF0(v9 + 72, owner);
v11 = *(Milody_Service_PlayerManager_o **)(v9 + 72);
if ( !v11 )
LABEL_11:
sub_180260BD0();
Milody_Service_PlayerManager__Register(v11, (Milody_Model_IJuceAudioPlayer_o *)v9, 0);
return (Milody_Audio_JuceMUASourcePlayer_o *)v9;
}

其清晰地表明该函数会调用 libMilody 这个 native 库的 MilodyAudioJuceMUASourcePlayerCreate 函数,并持有这个函数返回的指针。

DecodeFullSong 的情况也与其类似,调用 native 库的 MilodyAudioJuceMUASourcePlayerDecodeFullSong 函数。

native 层

好消息:现在不需要看反编译 C# $\to$ il $\to$ cpp $\to$ native 制造出的一坨玩意。

坏消息:libMilody 由 cpp 编写,并且使用了大量 cpp 特性和库,导致反编译的产物可读性也不咋样。

使用 IDA 打开 libMilody.dll

构造函数

先看 MilodyAudioJuceMUASourcePlayerCreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// Hidden C++ exception states: #wind=1
__int64 __fastcall MilodyAudioJuceMUASourcePlayerCreate(__int64 *res, const void *rawBytes, size_t size)
{
__int64 v6; // rbx
void *v7; // rax
__int64 result; // rax
__int64 *inited; // rax
__int64 v10; // rbx
milody::audio::format::MUAException *v11; // rdi
__int64 *v13; // rax
__int64 v14; // rbx
__int64 *v15; // rax
int v16; // eax
_BYTE v17[48]; // [rsp+0h] [rbp-A8h] BYREF
milody::audio::format::MUAException *v18; // [rsp+30h] [rbp-78h] BYREF
std::runtime_error *v19; // [rsp+38h] [rbp-70h] BYREF
const char *v20; // [rsp+40h] [rbp-68h] BYREF
__int64 v21; // [rsp+48h] [rbp-60h]
__int64 v22; // [rsp+50h] [rbp-58h]
int v23; // [rsp+58h] [rbp-50h]
int v24; // [rsp+5Ch] [rbp-4Ch]
__int64 v25; // [rsp+60h] [rbp-48h]
__int128 v26; // [rsp+70h] [rbp-38h] BYREF
__int64 v27; // [rsp+80h] [rbp-28h]
__int64 v28; // [rsp+B0h] [rbp+8h] BYREF

v6 = 0;
*res = 0;
try
{
v7 = wrapped_malloc(328u);
if ( v7 )
v6 = JuceMUASourcePlayerConstructor((__int64)v7, rawBytes, size);
*res = v6;
result = 0;
}
catch ( milody::audio::format::MUAException *v18 )
{
inited = InitLogger();
v10 = sub_180019090((__int64)inited);
v11 = v18;
(*(void (__fastcall **)(milody::audio::format::MUAException *))(*(_QWORD *)v18 + 8LL))(v18);
v20 = "{}, {}";
v21 = 6;
v22 = 0;
v23 = 0;
v24 = 0;
v25 = 0;
v26 = 0u;
v27 = 0;
sub_18001A810(v10, &v26, 4, (__int64)&v20, (__int64)"MilodyAudioJuceMUASourcePlayerCreate", &v28);
return *((_QWORD *)v11 + 3);
}
catch ( std::runtime_error *v19 )
{
v13 = InitLogger();
v14 = sub_180019090((__int64)v13);
(*(void (__fastcall **)(std::runtime_error *))(*(_QWORD *)v19 + 8LL))(v19);
v20 = "{}, {}";
v21 = 6;
v22 = 0;
v23 = 0;
v24 = 0;
v25 = 0;
v26 = 0u;
v27 = 0;
sub_18001A810(v14, &v26, 4, (__int64)&v20, (__int64)"MilodyAudioJuceMUASourcePlayerCreate", &v28);
return -11709394;
}
catch ( ... )
{
v15 = InitLogger();
v16 = sub_180019090((__int64)v15);
v20 = "{}, {}";
v21 = 6;
v22 = 0;
v23 = 0;
v24 = 0;
v25 = 0;
v26 = 0u;
v27 = 0;
sub_18001A620(
v16,
(unsigned int)v17 + 112,
4,
(unsigned int)v17 + 64,
(__int64)"MilodyAudioJuceMUASourcePlayerCreate",
(__int64)"unexpected exception");
return -11709394;
}
return result;
}

首先对比 Unity 层的调用和 native 层的声明,发现函数参数少了一个 MethodInfo *,原因未知,不过还是可以依次恢复前三项参数。

这里分配了 $328$ 字节的空间,下面调用函数理应是这个对象的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// Hidden C++ exception states: #wind=1
__int64 __fastcall JuceMUASourcePlayerConstructor(__int64 this, const void *rawBytes, size_t size)
{
float v3; // xmm3_4
__int64 v7; // rsi
_QWORD *v8; // rax
char *v9; // rbx
__int64 v10; // rbx
void *v11; // rax
void (__fastcall ***v12)(_QWORD, __int64); // rcx
__int64 v14; // rbx
unsigned int v15; // eax
__int64 v16; // rax
__int64 v17; // rax
__int64 v18; // rbx
unsigned int v19; // eax
__int64 v20; // rax
__int64 v21; // rax
_BYTE pExceptionObject[24]; // [rsp+28h] [rbp-190h] BYREF
__int64 v23; // [rsp+40h] [rbp-178h]
_BYTE v24[16]; // [rsp+50h] [rbp-168h] BYREF
_BYTE v25[240]; // [rsp+60h] [rbp-158h] BYREF
_BYTE v26[32]; // [rsp+150h] [rbp-68h] BYREF

v23 = this;
v7 = 0;
*(_QWORD *)this = &milody::audio::JuceMUASourcePlayer::`vftable;
*(_QWORD *)(this + 8) = 0;
*(_QWORD *)(this + 16) = 0;
*(_QWORD *)(this + 24) = 0;
if ( size )
{
if ( size > 0x7FFFFFFFFFFFFFFFLL )
std::vector<void *>::_Xlen();
v8 = aligned_malloc(size);
*(_QWORD *)(this + 8) = v8;
*(_QWORD *)(this + 16) = v8;
*(_QWORD *)(this + 24) = (char *)v8 + size;
v9 = *(char **)(this + 8);
memmove(v9, rawBytes, size);
*(_QWORD *)(this + 16) = &v9[size];
}
v10 = *(_QWORD *)(this + 8);
*(_DWORD *)(this + 40) = 0;
*(_QWORD *)(this + 48) = 0;
*(_QWORD *)(this + 56) = 0;
*(_QWORD *)(this + 64) = 0;
*(_QWORD *)(this + 72) = 0;
*(_QWORD *)(this + 80) = 0;
*(_QWORD *)(this + 88) = 0;
clear_16bytes((_QWORD *)(this + 96));
*(_QWORD *)(this + 112) = 0;
*(_QWORD *)(this + 120) = 0;
*(_QWORD *)(this + 128) = 0;
*(_QWORD *)(this + 136) = v10;
*(_QWORD *)(this + 144) = size;
*(_QWORD *)(this + 152) = 0;
*(_QWORD *)(this + 168) = 0;
*(_QWORD *)(this + 176) = &milody::audio::format::MUAFormat::`vftable;
SubObjectConstructor(this + 32);
*(_QWORD *)(this + 248) = 0;
*(_QWORD *)(this + 256) = 0;
*(_BYTE *)(this + 264) = 0;
*(_QWORD *)(this + 272) = 0;
*(_QWORD *)(this + 280) = 0;
*(_QWORD *)(this + 288) = 0;
*(_QWORD *)(this + 296) = 0;
*(_QWORD *)(this + 304) = 0;
*(_QWORD *)(this + 312) = 0;
*(_QWORD *)(this + 320) = 0;
if ( (unsigned int)sub_180007AD0(this + 32) != 2 )
{
sub_1800049D0(v24, 1);
v18 = sub_180001FC0((__int64)v25, "channel_num 2 expected, but ");
v19 = sub_180007AD0(this + 32);
v20 = std::ostream::operator<<(v18, v19);
sub_180001FC0(v20, " found");
v21 = sub_18000A540(v24, v26);
sub_180004FE0(pExceptionObject, v21);
throw (std::runtime_error *)pExceptionObject;
}
if ( (unsigned int)sub_180007C30(this + 32) != 48000 )
{
sub_1800049D0(v24, 1);
v14 = sub_180001FC0((__int64)v25, "sample_rate 48000 expected, but ");
v15 = sub_180007C30(this + 32);
v16 = std::ostream::operator<<(v14, v15);
sub_180001FC0(v16, " found");
v17 = sub_18000A540(v24, v26);
sub_180004FE0(pExceptionObject, v17);
throw (std::runtime_error *)pExceptionObject;
}
v11 = wrapped_malloc(0xD80u);
if ( v11 )
v7 = AudioSourcePlayerConstructor((__int64)v11);
v12 = *(void (__fastcall ****)(_QWORD, __int64))(this + 280);
*(_QWORD *)(this + 280) = v7;
if ( v12 )
(**v12)(v12, 1);
sub_18004C220(*(_QWORD *)(this + 280), v3);
return this;
}

对比调用处的参数和函数声明,由于第一个参数指向刚才分配出的空间,因此推断其为 this 指针。

由于这个对象前 $8$ 个字节赋值为 &milody::audio::JuceMUASourcePlayer::`vftable,因此这个对象大概率为 milody::audio::JuceMUASourcePlayer

接下来 $24$ 字节,按照如下方式初始化:

1
2
3
4
5
6
7
8
9
10
11
12
if ( size )
{
if ( size > 0x7FFFFFFFFFFFFFFFLL )
std::vector<void *>::_Xlen();
v8 = aligned_malloc(size);
*(_QWORD *)(this + 8) = v8;
*(_QWORD *)(this + 16) = v8;
*(_QWORD *)(this + 24) = (char *)v8 + size;
v9 = *(char **)(this + 8);
memmove(v9, rawBytes, size);
*(_QWORD *)(this + 16) = &v9[size];
}

这 $24$ 字节大概率为一个 std::vector,持有三个指针,分别为内存的起始地址(其实也是存储数据的起始地址)、数据的结束地址、内存的结束地址。这个 std::vector 负责存储原始二进制数据。

函数 clear_16bytes 如下:

1
2
3
4
5
6
7
// Microsoft VisualC v7/14 64bit runtime
_QWORD *__fastcall clear_16bytes(_QWORD *a1)
{
*a1 = 0;
a1[1] = 0;
return a1;
}

看样子像是 MSVC 生成的默认构造函数啥的,不过作用是清空 $16$ 字节的内存,因此命名为 clear_16bytes

接下来在偏移 $136$ 字节处保存了存储原始数据起始内存的指针,$144$ 字节处保存原始数据的长度。

偏移 $176$ 字节处初始化为 &milody::audio::format::MUAFormat::`vftable,因此此处开始应当为一个 milody::audio::format::MUAFormat 的子对象。

然后调用了 SubObjectConstructor,传入了 this + 32 作为参数,猜测 this + 32 也是一个子对象,这是其构造函数。

目前,已经知晓的 milody::audio::JuceMUASourcePlayer 结构信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class JuceMUASourcePlayer //(328 bytes)
{
void* vftable;//(0 ~ 8)
std::vector<std::byte> rawData; //(8 ~ 32)
{
std::byte* begin; //(8 ~ 16)
std::byte* data_end; //(16 ~ 24)
std::byte* buf_end; //(24 ~ 32)
}
class SubClass obj; //(32 ~ 248)
{
Unknown; //(32 ~ 136)
std::byte* data = rawData.begin; //(136 ~ 144)
std::size_t size; //(144 ~ 152)
Unknown; //(152 ~ 176)
class MUAFormat mua; //(176 ~ 248)
{
void* vftable; //(176 ~ 184)
Unknown; //(184 ~ 248)
};
};
Unknown; //(248 ~ 328)
};

接下来看 this + 32 处对象的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
void __fastcall SubObjectConstructor(__int64 this)
{
RTL_SRWLOCK *v2; // r13
__int64 *v3; // rax
__int64 v4; // rdi
int v5; // eax
_QWORD *v6; // rax
void *v7; // rcx
unsigned int frame_size; // r9d
int ignore_frame; // r8d
unsigned int sample_frame; // eax
unsigned int total_frame; // r8d
unsigned int v12; // ecx
unsigned __int64 i; // r10
unsigned int v14; // edx
unsigned int v15; // edx
unsigned int v16; // edx
unsigned int v17; // edx
unsigned int v18; // edx
unsigned int v19; // edx
unsigned int v20; // edx
unsigned int v21; // edx
unsigned int v22; // edx
unsigned int v23; // edx
unsigned int v24; // edx
unsigned int v25; // edx
unsigned int v26; // edx
unsigned int v27; // edx
unsigned int v28; // edx
unsigned int v29; // edx
unsigned int v30; // edx
unsigned int v31; // edx
__int64 v32; // r8
unsigned int v33; // r15d
__int64 v34; // rcx
__int64 v35; // rsi
_QWORD *v36; // rdi
unsigned __int64 v37; // rsi
__int64 v38; // rax
__int64 v39; // r12
unsigned __int64 v40; // r14
__int64 v41; // rax
unsigned __int64 v42; // rcx
unsigned __int64 v43; // rdx
unsigned __int64 v44; // r12
_QWORD *v45; // rdi
__int64 v46; // rax
unsigned int v47; // [rsp+30h] [rbp-D0h] BYREF
unsigned int v48; // [rsp+34h] [rbp-CCh]
unsigned int v49; // [rsp+38h] [rbp-C8h]
unsigned int v50; // [rsp+3Ch] [rbp-C4h]
unsigned int v51[9]; // [rsp+40h] [rbp-C0h]
unsigned int v52; // [rsp+64h] [rbp-9Ch]
unsigned int v53; // [rsp+68h] [rbp-98h]
_QWORD v54[4]; // [rsp+70h] [rbp-90h] BYREF
__int128 v55; // [rsp+90h] [rbp-70h]
__int64 v56; // [rsp+A0h] [rbp-60h]
char v57[8]; // [rsp+A8h] [rbp-58h] BYREF
char v58[16]; // [rsp+B0h] [rbp-50h] BYREF
__int128 pExceptionObject; // [rsp+C0h] [rbp-40h] BYREF
__int64 v60; // [rsp+D0h] [rbp-30h]
__int64 v61; // [rsp+E0h] [rbp-20h]
char v62; // [rsp+E8h] [rbp-18h]
void *Block[2]; // [rsp+F0h] [rbp-10h] BYREF
__m128i si128; // [rsp+100h] [rbp+0h]

v2 = (RTL_SRWLOCK *)(this + 136);
v61 = this + 136;
AcquireSRWLockExclusive((PSRWLOCK)(this + 136));
v62 = 1;
ReadFromRawData(this, &v47, 60u);
*(_DWORD *)(this + 152) = _byteswap_ulong(v47);
*(_DWORD *)(this + 156) = _byteswap_ulong(v48);
*(_DWORD *)(this + 160) = _byteswap_ulong(v49);
*(_DWORD *)(this + 164) = _byteswap_ulong(v50);
*(_DWORD *)(this + 168) = _byteswap_ulong(v51[0]);
*(_DWORD *)(this + 172) = _byteswap_ulong(v51[1]);
*(_DWORD *)(this + 176) = _byteswap_ulong(v51[2]);
*(_DWORD *)(this + 180) = _byteswap_ulong(v51[3]);
*(_DWORD *)(this + 184) = _byteswap_ulong(v51[4]);
*(_DWORD *)(this + 188) = _byteswap_ulong(v51[5]);
*(_DWORD *)(this + 192) = _byteswap_ulong(v51[6]);
*(_DWORD *)(this + 196) = _byteswap_ulong(v51[7]);
*(_DWORD *)(this + 200) = _byteswap_ulong(v51[8]);
*(_DWORD *)(this + 204) = _byteswap_ulong(v52);
*(_DWORD *)(this + 208) = _byteswap_ulong(v53);
v3 = InitLogger();
v4 = sub_180019090((__int64)v3);
v5 = (**(__int64 (__fastcall ***)(__int64, char *))(this + 144))(this + 144, v57);
v6 = (_QWORD *)sub_1800066E0(v5, (__int64)Block, -1, 32, 0, 0);
v54[2] = 0;
v55 = 0u;
v56 = 0;
v54[0] = "Mua File Meta: {}";
v54[1] = 17;
pExceptionObject = 0u;
v60 = 0;
sub_180003C20(v4, &pExceptionObject, 1, (__int64)v54, v6);
if ( si128.m128i_i64[1] > 0xFuLL )
{
v7 = Block[0];
if ( (unsigned __int64)(si128.m128i_i64[1] + 1) >= 0x1000 )
{
v7 = (void *)*((_QWORD *)Block[0] - 1);
if ( (unsigned __int64)((char *)Block[0] - (char *)v7 - 8) > 0x1F )
invoke_watson(0, 0, 0, 0, 0);
}
j_j_free(v7);
}
si128 = _mm_load_si128((const __m128i *)&xmmword_1800F7320);
LOBYTE(Block[0]) = 0;
sub_180006150((void **)v58, v57[0]);
frame_size = *(_DWORD *)(this + 172);
ignore_frame = *(_DWORD *)(this + 180) / frame_size;
*(_DWORD *)(this + 212) = ignore_frame;
sample_frame = (frame_size + *(_DWORD *)(this + 176) - 1) / frame_size + 2;
*(_DWORD *)this = sample_frame;
if ( (ignore_frame & 3) != 0 )
{
v46 = sub_1800047D0((__int64)Block, "ignore_frame % 4 != 0");
sub_180004BC0((__int64)&pExceptionObject, -6, (_QWORD *)v46);
throw (milody::audio::format::MUAException *)&pExceptionObject;
}
total_frame = sample_frame + ignore_frame;
*(_DWORD *)(this + 4) = total_frame;
v52 = 0;
v12 = 0;
for ( i = 0; (__int64)i < 60; i += 20LL )
{
v14 = ((*((unsigned __int8 *)&v47 + i) ^ dword_1800F6260[(unsigned __int64)v12 >> 24] ^ (v12 << 8)) << 8)
^ dword_1800F6260[(unsigned __int64)(*((unsigned __int8 *)&v47 + i)
^ dword_1800F6260[(unsigned __int64)v12 >> 24]
^ (v12 << 8)) >> 24]
^ *((unsigned __int8 *)&v47 + i + 1);
v15 = (v14 << 8) ^ dword_1800F6260[(unsigned __int64)v14 >> 24] ^ *((unsigned __int8 *)&v47 + i + 2);
v16 = (v15 << 8) ^ dword_1800F6260[(unsigned __int64)v15 >> 24] ^ *((unsigned __int8 *)&v47 + i + 3);
v17 = (v16 << 8) ^ dword_1800F6260[(unsigned __int64)v16 >> 24] ^ *((unsigned __int8 *)&v48 + i);
v18 = (v17 << 8) ^ dword_1800F6260[(unsigned __int64)v17 >> 24] ^ *((unsigned __int8 *)&v48 + i + 1);
v19 = (v18 << 8) ^ dword_1800F6260[(unsigned __int64)v18 >> 24] ^ *((unsigned __int8 *)&v48 + i + 2);
v20 = (v19 << 8) ^ dword_1800F6260[(unsigned __int64)v19 >> 24] ^ *((unsigned __int8 *)&v48 + i + 3);
v21 = (v20 << 8) ^ dword_1800F6260[(unsigned __int64)v20 >> 24] ^ *((unsigned __int8 *)&v49 + i);
v22 = (v21 << 8) ^ dword_1800F6260[(unsigned __int64)v21 >> 24] ^ *((unsigned __int8 *)&v49 + i + 1);
v23 = (v22 << 8) ^ dword_1800F6260[(unsigned __int64)v22 >> 24] ^ *((unsigned __int8 *)&v49 + i + 2);
v24 = (v23 << 8) ^ dword_1800F6260[(unsigned __int64)v23 >> 24] ^ *((unsigned __int8 *)&v49 + i + 3);
v25 = (v24 << 8) ^ dword_1800F6260[(unsigned __int64)v24 >> 24] ^ LOBYTE(v51[i / 4 - 1]);
v26 = (v25 << 8) ^ dword_1800F6260[(unsigned __int64)v25 >> 24] ^ *((unsigned __int8 *)&v50 + i + 1);
v27 = (v26 << 8) ^ dword_1800F6260[(unsigned __int64)v26 >> 24] ^ *((unsigned __int8 *)&v50 + i + 2);
v28 = (v27 << 8) ^ dword_1800F6260[(unsigned __int64)v27 >> 24] ^ *((unsigned __int8 *)&v50 + i + 3);
v29 = (v28 << 8) ^ dword_1800F6260[(unsigned __int64)v28 >> 24] ^ LOBYTE(v51[i / 4]);
v30 = (v29 << 8) ^ dword_1800F6260[(unsigned __int64)v29 >> 24] ^ BYTE1(v51[i / 4]);
v31 = (v30 << 8) ^ dword_1800F6260[(unsigned __int64)v30 >> 24] ^ BYTE2(v51[i / 4]);
v12 = (v31 << 8) ^ dword_1800F6260[(unsigned __int64)v31 >> 24] ^ HIBYTE(v51[i / 4]);
}
*(_DWORD *)(this + 8) = v12;
*(_DWORD *)(this + 128) = 0;
v32 = *(_DWORD *)(this + 168) * frame_size * total_frame;
v33 = v32;
v34 = *(_QWORD *)(this + 16);
if ( (unsigned int)v32 > (unsigned __int64)((*(_QWORD *)(this + 32) - v34) >> 1) )
{
v35 = (*(_QWORD *)(this + 24) - v34) >> 1;
v36 = aligned_malloc(2 * v32);
memmove(v36, *(const void **)(this + 16), *(_QWORD *)(this + 24) - *(_QWORD *)(this + 16));
expand_vector(this + 16, (__int64)v36, v35, v33);
}
resize_vector(this + 40, 1276);
v37 = (unsigned int)(*(_DWORD *)(this + 168) * *(_DWORD *)(this + 172));
v38 = *(_QWORD *)(this + 80);
v39 = *(_QWORD *)(this + 88);
v40 = (v39 - v38) >> 1;
if ( v37 < v40 )
{
v41 = v38 + 2 * v37;
LABEL_23:
*(_QWORD *)(this + 88) = v41;
goto LABEL_24;
}
if ( v37 > v40 )
{
v42 = (*(_QWORD *)(this + 96) - v38) >> 1;
if ( v37 <= v42 )
{
memset(*(void **)(this + 88), 0, 2 * (v37 - v40));
v41 = 2 * (v37 - v40) + v39;
goto LABEL_23;
}
v43 = v42 >> 1;
if ( v42 <= 0x7FFFFFFFFFFFFFFFLL - (v42 >> 1) )
{
v44 = (unsigned int)(*(_DWORD *)(this + 168) * *(_DWORD *)(this + 172));
if ( v43 + v42 >= v37 )
v44 = v43 + v42;
if ( v44 > 0x7FFFFFFFFFFFFFFFLL )
Concurrency::cancel_current_task();
}
else
{
v44 = 0x7FFFFFFFFFFFFFFFLL;
}
v45 = aligned_malloc(2 * v44);
memset((char *)v45 + 2 * v40, 0, 2 * (v37 - v40));
memmove(v45, *(const void **)(this + 80), *(_QWORD *)(this + 88) - *(_QWORD *)(this + 80));
expand_vector(this + 80, (__int64)v45, v37, v44);
}
LABEL_24:
OpusDecoderConstructor(this + 64, *(_DWORD *)(this + 164), *(_DWORD *)(this + 168));
ReleaseSRWLockExclusive(v2);
}

AcquireSRWLockExclusive((PSRWLOCK)(this + 136)); 表明偏移 $136+32=168$ 字节处存储的对象为一个锁 RTL_SRWLOCK *

ReadFromRawData 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void __fastcall ReadFromRawData(__int64 this, void *out, size_t count)
{
_QWORD *v5; // rbx
char *v6; // rdi
unsigned __int64 v7; // rsi
__int64 v8; // rbx
__int64 v9; // rax
__int64 v10; // rax
_BYTE v11[32]; // [rsp+38h] [rbp-E0h] BYREF
_BYTE pExceptionObject[32]; // [rsp+58h] [rbp-C0h] BYREF
_BOOL8 v13; // [rsp+78h] [rbp-A0h] BYREF
void *Src; // [rsp+80h] [rbp-98h] BYREF
_BYTE *v15; // [rsp+88h] [rbp-90h]
char v16; // [rsp+A0h] [rbp-78h]
_BYTE v17[32]; // [rsp+A8h] [rbp-70h] BYREF
_BYTE v18[32]; // [rsp+C8h] [rbp-50h] BYREF

CopyFromRaw((_QWORD *)(this + 104), (__int64)&v13, count);
if ( !v13 )
{
v8 = sub_18000B210(&v13, v17);
v9 = sub_1800047D0((__int64)v18, "read error. ");
v10 = sub_180002230(v11, v9, v8);
sub_180004BC0(pExceptionObject, -8, v10);
throw (milody::audio::format::MUAException *)pExceptionObject;
}
v5 = 0;
v6 = 0;
v7 = v15 - (_BYTE *)Src;
if ( v15 != Src )
{
if ( v7 > 0x7FFFFFFFFFFFFFFFLL )
std::vector<void *>::_Xlen();
v5 = aligned_malloc(v15 - (_BYTE *)Src);
v6 = (char *)v5 + v7;
memmove(v5, Src, v15 - (_BYTE *)Src);
}
memcpy(out, v5, count);
if ( v5 )
{
if ( (unsigned __int64)(v6 - (char *)v5) >= 0x1000 )
{
if ( (unsigned __int64)v5 - *(v5 - 1) - 8 > 0x1F )
invoke_watson(0, 0, 0, 0, 0);
v5 = (_QWORD *)*(v5 - 1);
}
j_j_free(v5);
}
if ( v13 )
{
if ( v16 )
sub_180005E60((__int64)&Src);
}
else if ( v16 )
{
sub_180005F80(&Src);
}
}

CopyFromRaw 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Hidden C++ exception states: #wind=7
_BYTE *__fastcall CopyFromRaw(__int64 this, __int64 a2, size_t count)
{
__int128 si128; // xmm2
__int128 v7; // xmm1
size_t v8; // r15
size_t v9; // r12
_QWORD *v10; // rsi
void *v11; // rcx
__int128 v13; // [rsp+60h] [rbp-49h] BYREF
__int128 v14; // [rsp+70h] [rbp-39h]
_QWORD v15[4]; // [rsp+80h] [rbp-29h] BYREF
void *v16[2]; // [rsp+A0h] [rbp-9h]
__int128 v17; // [rsp+B0h] [rbp+7h]

if ( *(_QWORD *)(this + 8) - *(_QWORD *)(this + 16) >= count )
{
v13 = 0;
v8 = 0;
v9 = 0;
if ( count )
{
if ( count > 0x7FFFFFFFFFFFFFFFLL )
std::vector<void *>::_Xlen();
v10 = aligned_malloc(count);
v8 = (size_t)v10 + count;
memset(v10, 0, count);
v9 = (size_t)v10 + count;
}
else
{
v10 = (_QWORD *)v13;
}
memcpy(v10, (const void *)(*(_QWORD *)this + *(_QWORD *)(this + 16)), count);
*(_QWORD *)&v14 = 0;
v13 = 0u;
*(_QWORD *)(this + 16) += count;
*(_BYTE *)a2 = 1;
*(_BYTE *)(a2 + 40) = 0;
*(_QWORD *)&v17 = 0;
v16[1] = 0;
v16[0] = 0;
v15[0] = v10;
v15[1] = v9;
v15[2] = v8;
VectorCopy((_QWORD *)(a2 + 8), (__int64)v15);
*(_BYTE *)(a2 + 40) = 1;
sub_180005E60((__int64)v15);
v11 = v16[0];
if ( v16[0] )
{
if ( (unsigned __int64)v17 - (unsigned __int64)v16[0] >= 0x1000 )
{
v11 = (void *)*((_QWORD *)v16[0] - 1);
if ( (unsigned __int64)((char *)v16[0] - (char *)v11 - 8) > 0x1F )
invoke_watson(0, 0, 0, 0, 0);
}
j_j_free(v11);
}
}
else
{
*(_OWORD *)v16 = 0;
v16[0] = aligned_malloc(0x30u);
si128 = (__int128)_mm_load_si128((const __m128i *)&xmmword_1800F7350);
strcpy((char *)v16[0], "remains length is less than given string length");
v7 = *(_OWORD *)v16;
v17 = (__int128)_mm_load_si128((const __m128i *)&xmmword_1800F7320);
LOBYTE(v16[0]) = 0;
*(_BYTE *)a2 = 0;
*(_BYTE *)(a2 + 40) = 0;
v13 = v7;
v14 = si128;
sub_180004710(a2 + 8, &v13);
*(_BYTE *)(a2 + 40) = 1;
sub_180005F80(&v13);
}
return (_BYTE *)a2;
}

该函数内的 this 指针实际上对应整个结构的 $32+104=136$ 字节处,此处先前已推断出的内容如下:

1
2
std::byte* data = rawData.begin; //(136 ~ 144)
std::size_t size; //(144 ~ 152)

因此 this 为存储原始数据的内存起始指针,this + 8 为原始数据长度,由 *(_QWORD *)(this + 8) - *(_QWORD *)(this + 16) >= count 可推断 this + 16 为目前已经读取的长度。

此处实际结构如下:

1
2
3
4
5
6
struct Record rec; //(136 ~ 160)
{
std::byte* data = rawData.begin; //(136 ~ 144)
std::size_t size; //(144 ~ 152)
std::size_t offset; //(152 ~ 160)
}

CopyFromRaw 负责检查边界,并使用 std::vector 复制所需的部分,ReadFromRawData 则额外包装错误处理。

总体来讲,这两个函数均是负责从原始数据中取出后续指定大小的数据的,因此命名如上。

SubObjectConstructor 读取文件最开头 $60$ 字节。_byteswap_ulong 表明这部分数据以 $4$ 个字节为一个单位,并且以大端序在文件中存储。这些数据转化为小端序之后依次放置在整体偏移的 $184\sim 240$ 字节处。

此处的 this + 144 对应整体偏移的 $144+32=176$ 字节处,为 MUAFormat 的虚函数表。

该虚表内唯一的函数为 FormatMetadata,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
__int64 __fastcall FormatMetadata(__int64 this, __int64 a2)
{
_QWORD *v4; // rax
__int64 v5; // rbx
char *v6; // rax
__int64 v7; // rbx
char *v8; // rax
_DWORD *v9; // rax
_QWORD *v10; // rbx
_QWORD *v11; // rbx
char *v13; // [rsp+20h] [rbp-E0h] BYREF
char *v14; // [rsp+28h] [rbp-D8h]
_BYTE v15[4]; // [rsp+30h] [rbp-D0h] BYREF
int v16; // [rsp+34h] [rbp-CCh]
__int128 v17; // [rsp+38h] [rbp-C8h] BYREF
__int64 v18; // [rsp+48h] [rbp-B8h]
__int128 v19; // [rsp+50h] [rbp-B0h] BYREF
__int64 v20; // [rsp+60h] [rbp-A0h]
__int128 v21; // [rsp+68h] [rbp-98h] BYREF
__int64 v22; // [rsp+78h] [rbp-88h]
__int128 v23; // [rsp+80h] [rbp-80h] BYREF
__int64 v24; // [rsp+90h] [rbp-70h]
__int128 v25; // [rsp+98h] [rbp-68h] BYREF
__int64 v26; // [rsp+A8h] [rbp-58h]
__int128 v27; // [rsp+B0h] [rbp-50h] BYREF
__int64 v28; // [rsp+C0h] [rbp-40h]
_BYTE *v29; // [rsp+C8h] [rbp-38h] BYREF
_QWORD *v30; // [rsp+D0h] [rbp-30h]
__int128 v31; // [rsp+D8h] [rbp-28h] BYREF
__int64 v32; // [rsp+E8h] [rbp-18h]
char v33[24]; // [rsp+F0h] [rbp-10h] BYREF
__int128 v34; // [rsp+108h] [rbp+8h] BYREF
__int64 v35; // [rsp+118h] [rbp+18h]
char v36[24]; // [rsp+120h] [rbp+20h] BYREF
__int128 v37; // [rsp+138h] [rbp+38h] BYREF
__int64 v38; // [rsp+148h] [rbp+48h]
char v39[24]; // [rsp+150h] [rbp+50h] BYREF
__int64 v40; // [rsp+168h] [rbp+68h] BYREF
char v41[24]; // [rsp+170h] [rbp+70h] BYREF
char v42[24]; // [rsp+188h] [rbp+88h] BYREF
char v43[24]; // [rsp+1A0h] [rbp+A0h] BYREF
char v44[24]; // [rsp+1B8h] [rbp+B8h] BYREF
char v45[24]; // [rsp+1D0h] [rbp+D0h] BYREF
char v46[24]; // [rsp+1E8h] [rbp+E8h] BYREF
char v47[24]; // [rsp+200h] [rbp+100h] BYREF
char v48[24]; // [rsp+218h] [rbp+118h] BYREF
char v49[24]; // [rsp+230h] [rbp+130h] BYREF
char v50[24]; // [rsp+248h] [rbp+148h] BYREF
char v51[24]; // [rsp+260h] [rbp+160h] BYREF
char v52[24]; // [rsp+278h] [rbp+178h] BYREF
char v53[24]; // [rsp+290h] [rbp+190h] BYREF
char v54[24]; // [rsp+2A8h] [rbp+1A8h] BYREF
char v55[24]; // [rsp+2C0h] [rbp+1C0h] BYREF
char v56[24]; // [rsp+2D8h] [rbp+1D8h] BYREF
char v57[24]; // [rsp+2F0h] [rbp+1F0h] BYREF
char v58[24]; // [rsp+308h] [rbp+208h] BYREF
_BYTE v59[16]; // [rsp+320h] [rbp+220h] BYREF
__int64 v60; // [rsp+330h] [rbp+230h]
char v61[16]; // [rsp+338h] [rbp+238h] BYREF
__int64 v62; // [rsp+348h] [rbp+248h]
char v63[16]; // [rsp+350h] [rbp+250h] BYREF
__int64 v64; // [rsp+360h] [rbp+260h]
char v65[16]; // [rsp+368h] [rbp+268h] BYREF
__int64 v66; // [rsp+378h] [rbp+278h]
char v67[16]; // [rsp+380h] [rbp+280h] BYREF
__int64 v68; // [rsp+390h] [rbp+290h]
char v69[16]; // [rsp+398h] [rbp+298h] BYREF
__int64 v70; // [rsp+3A8h] [rbp+2A8h]
char v71[16]; // [rsp+3B0h] [rbp+2B0h] BYREF
__int64 v72; // [rsp+3C0h] [rbp+2C0h]
char v73[16]; // [rsp+3C8h] [rbp+2C8h] BYREF
__int64 v74; // [rsp+3D8h] [rbp+2D8h]
char v75[16]; // [rsp+3E0h] [rbp+2E0h] BYREF
__int64 v76; // [rsp+3F0h] [rbp+2F0h]
char v77[16]; // [rsp+3F8h] [rbp+2F8h] BYREF
__int64 v78; // [rsp+408h] [rbp+308h]
char v79[16]; // [rsp+410h] [rbp+310h] BYREF
__int64 v80; // [rsp+420h] [rbp+320h]
char v81[16]; // [rsp+428h] [rbp+328h] BYREF
__int64 v82; // [rsp+438h] [rbp+338h]
char v83[24]; // [rsp+440h] [rbp+340h] BYREF
char v84[24]; // [rsp+458h] [rbp+358h] BYREF
char v85[24]; // [rsp+470h] [rbp+370h] BYREF
char v86; // [rsp+488h] [rbp+388h] BYREF

v40 = a2;
v16 = 0;
v13 = (char *)&v21;
v21 = 0u;
sub_180006150((void **)&v21 + 1, 0);
LOBYTE(v21) = 3;
v4 = wrapped_malloc(0x20u);
v4[1] = 0;
v4[2] = 7;
v4[3] = 15;
*(_DWORD *)v4 = 1936876918;
*((_WORD *)v4 + 2) = 28521;
*((_BYTE *)v4 + 6) = 110;
*((_BYTE *)v4 + 7) = 0;
*((_QWORD *)&v21 + 1) = v4;
v22 = 0;
v23 = 0u;
v5 = *(unsigned int *)(this + 8);
sub_180006150((void **)&v23 + 1, 0);
LOBYTE(v23) = 6;
*((_QWORD *)&v23 + 1) = v5;
v24 = 0;
v13 = (char *)&v21;
v14 = (char *)&v25;
sub_180004310((__int64)v59, (__int64 *)&v13, 1, 2);
v60 = 0;
v13 = (char *)&v17;
v17 = 0u;
sub_180006150((void **)&v17 + 1, 0);
LOBYTE(v17) = 3;
v6 = (char *)wrapped_malloc(0x20u);
*(_OWORD *)v6 = 0;
*((_QWORD *)v6 + 2) = 4;
*((_QWORD *)v6 + 3) = 15;
strcpy(v6, "type");
*((_QWORD *)&v17 + 1) = v6;
v18 = 0;
v19 = 0u;
v7 = *(unsigned int *)(this + 12);
sub_180006150((void **)&v19 + 1, 0);
LOBYTE(v19) = 6;
*((_QWORD *)&v19 + 1) = v7;
v20 = 0;
v13 = (char *)&v17;
v14 = (char *)&v21;
sub_180004310((__int64)v61, (__int64 *)&v13, 1, 2);
v62 = 0;
v13 = (char *)&v25;
v25 = 0u;
sub_180006150((void **)&v25 + 1, 0);
LOBYTE(v25) = 3;
v8 = (char *)wrapped_malloc(0x20u);
*(_OWORD *)v8 = 0;
*((_QWORD *)v8 + 2) = 9;
*((_QWORD *)v8 + 3) = 15;
strcpy(v8, "file_size");
*((_QWORD *)&v25 + 1) = v8;
v26 = 0;
v27 = 0u;
sub_1800032B0(&v27, *(unsigned int *)(this + 16));
v28 = 0;
v13 = (char *)&v25;
v14 = (char *)&v29;
sub_180004310((__int64)v63, (__int64 *)&v13, 1, 2);
v64 = 0;
v13 = (char *)&v37;
v37 = 0u;
sub_180006150((void **)&v37 + 1, 0);
LOBYTE(v37) = 3;
v9 = wrapped_malloc(0x20u);
v9[3] = 0;
*((_QWORD *)v9 + 2) = 11;
*((_QWORD *)v9 + 3) = 15;
strcpy((char *)v9, "sample_rate");
*((_QWORD *)&v37 + 1) = v9;
v38 = 0;
sub_180001DC0(v39, this + 20);
v13 = (char *)&v37;
v14 = (char *)&v40;
sub_180004310((__int64)v65, (__int64 *)&v13, 1, 2);
v66 = 0;
sub_180001E20(v57, "channel_num");
sub_180001DC0(v58, this + 24);
v13 = v57;
v14 = v59;
sub_180004310((__int64)v67, (__int64 *)&v13, 1, 2);
v68 = 0;
sub_180001E20(v55, "frame_size");
sub_180001DC0(v56, this + 28);
v13 = v55;
v14 = v57;
sub_180004310((__int64)v69, (__int64 *)&v13, 1, 2);
v70 = 0;
sub_180001E20(v53, "sample_num");
sub_180001DC0(v54, this + 32);
v13 = v53;
v14 = v55;
sub_180004310((__int64)v71, (__int64 *)&v13, 1, 2);
v72 = 0;
sub_180001E20(v51, "ignore_num");
sub_180001DC0(v52, this + 36);
v13 = v51;
v14 = v53;
sub_180004310((__int64)v73, (__int64 *)&v13, 1, 2);
v74 = 0;
v13 = (char *)&v34;
v34 = 0u;
sub_180006150((void **)&v34 + 1, 0);
LOBYTE(v34) = 3;
v10 = wrapped_malloc(0x20u);
v29 = v15;
v30 = v10;
*(_OWORD *)v10 = 0;
v10[2] = 0;
v10[3] = 0;
sub_180002370(v10, "preview_begin", 0xDu);
*((_QWORD *)&v34 + 1) = v10;
v35 = 0;
sub_180001DC0(v36, this + 40);
v13 = (char *)&v34;
v14 = (char *)&v37;
sub_180004310((__int64)v75, (__int64 *)&v13, 1, 2);
v76 = 0;
sub_180001E20(v49, "preview_end");
sub_180001DC0(v50, this + 44);
v13 = v49;
v14 = v51;
sub_180004310((__int64)v77, (__int64 *)&v13, 1, 2);
v78 = 0;
sub_180001E20(v47, "real_preview_intro");
sub_180001DC0(v48, this + 48);
v13 = v47;
v14 = v49;
sub_180004310((__int64)v79, (__int64 *)&v13, 1, 2);
v80 = 0;
sub_180001E20(v45, "real_preview_begin");
sub_180001DC0(v46, this + 52);
v13 = v45;
v14 = v47;
sub_180004310((__int64)v81, (__int64 *)&v13, 1, 2);
v82 = 0;
v13 = (char *)&v31;
v31 = 0u;
sub_180006150((void **)&v31 + 1, 0);
LOBYTE(v31) = 3;
v11 = wrapped_malloc(0x20u);
v29 = v15;
v30 = v11;
*(_OWORD *)v11 = 0;
v11[2] = 0;
v11[3] = 0;
sub_180002370(v11, "real_preview_end", 0x10u);
*((_QWORD *)&v31 + 1) = v11;
v32 = 0;
sub_180001DC0(v33, this + 56);
v13 = (char *)&v31;
v14 = (char *)&v34;
sub_180004A90(v83, &v13);
sub_180001E20(v43, "CRC_all");
sub_180001DC0(v44, this + 60);
v13 = v43;
v14 = v45;
sub_180004A90(v84, &v13);
sub_180001E20(v41, "ignore_origin_num");
sub_180001DC0(v42, this + 64);
v13 = v41;
v14 = v43;
sub_180004A90(v85, &v13);
v13 = v59;
v14 = &v86;
sub_180004310(a2, (__int64 *)&v13, 1, 2);
v16 = 1;
sub_1800EB6FC(v59, 24, 15, sub_180005200);
sub_1800EB6FC(v41, 24, 2, sub_180005200);
sub_1800EB6FC(v43, 24, 2, sub_180005200);
sub_1800EB6FC(&v31, 24, 2, sub_180005200);
sub_1800EB6FC(v45, 24, 2, sub_180005200);
sub_1800EB6FC(v47, 24, 2, sub_180005200);
sub_1800EB6FC(v49, 24, 2, sub_180005200);
sub_1800EB6FC(&v34, 24, 2, sub_180005200);
sub_1800EB6FC(v51, 24, 2, sub_180005200);
sub_1800EB6FC(v53, 24, 2, sub_180005200);
sub_1800EB6FC(v55, 24, 2, sub_180005200);
sub_1800EB6FC(v57, 24, 2, sub_180005200);
sub_1800EB6FC(&v37, 24, 2, sub_180005200);
sub_1800EB6FC(&v25, 24, 2, sub_180005200);
sub_1800EB6FC(&v17, 24, 2, sub_180005200);
sub_1800EB6FC(&v21, 24, 2, sub_180005200);
return a2;
}

函数 sub_180004310 中抛出的异常类型为 nlohmann::json_abi_v3_12_0::detail::type_error,表明 sub_180004310 大概率为 nlohmann::json 库的一部分。

可以猜测,FormatMetadata 函数中调用的别的函数可能也为 nlohmann::json 库的一部分,其中大量的 STL 容器的构造与析构的痕迹可以证明这一点。

其中出现的字符串常量应该为格式化为 json 字符串时的字段,因此 $14$ 个字段均找到其含义。剩余未命名的字段大概率为文件头的 magic 信息。

MuaFormat 信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MUAFormat mua; //(176 ~ 248)
{
void* vftable; //(176 ~ 184)
int magic; //(184 ~ 188)
int type; //(188 ~ 192)
int file_size; //(192 ~ 196)
int sample_rate; //(196 ~ 200)
int channel_num; //(200 ~ 204)
int frame_size; //(204 ~ 208)
int sample_num; //(208 ~ 212)
int ignore_num; //(212 ~ 216)
int preview_begin; //(216 ~ 220)
int preview_end; //(220 ~ 224)
int real_preview_intro; //(224 ~ 228)
int real_preview_begin; //(228 ~ 232)
int real_preview_end; //(232 ~ 236)
int CRC_all; //(236 ~ 240)
int ignore_origin_num; //(240 ~ 244)
int ignore_frame; //(244 ~ 248)
};

某个 mua 格式的文件第 $12\sim 16$ 字节依次为 00 00 BB 80,按照大端序读取为 $48000$,是一个典型的采样率,验证了我们的猜测。

sub_1800066E0 函数也抛出异常 nlohmann::json_abi_v3_12_0::detail::type_error,因此也为这个库的一部分,对上面构造出的包含 mua 文件元数据的 json 对象进行一些操作。在此处使用 nlohmann::json 库猜测为了便于在出现异常的时候结构化输出其元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
frame_size = *(_DWORD *)(this + 172);
ignore_frame = *(_DWORD *)(this + 180) / frame_size;
*(_DWORD *)(this + 212) = ignore_frame;
sample_frame = (frame_size + *(_DWORD *)(this + 176) - 1) / frame_size + 2;
*(_DWORD *)this = sample_frame;
if ( (ignore_frame & 3) != 0 )
{
v46 = sub_1800047D0((__int64)Block, "ignore_frame % 4 != 0");
sub_180004BC0(&pExceptionObject, -6, v46);
throw (milody::audio::format::MUAException *)&pExceptionObject;
}
total_frame = sample_frame + ignore_frame;
*(_DWORD *)(this + 4) = total_frame;

进行了一些简单的计算,计算出了忽略的帧数和有采样的帧数并计算出了两者的总和,分别存储。

1
2
3
4
5
6
7
8
9
for ( i = 0; (__int64)i < 60; i += 20LL )
{
v14 = ((*((unsigned __int8 *)&v47 + i) ^ dword_1800F6260[(unsigned __int64)v12 >> 24] ^ (v12 << 8)) << 8)
^ dword_1800F6260[(unsigned __int64)(*((unsigned __int8 *)&v47 + i)
^ dword_1800F6260[(unsigned __int64)v12 >> 24]
^ (v12 << 8)) >> 24]
^ *((unsigned __int8 *)&v47 + i + 1);
...
}

此部分依赖数据 dword_1800F6260,其前面两位为 00x4C11DB7,符合 CRC32 算法的特征,因此上述这部分疑似一个循环展开后的 CRC32 计算过程,最终的值存储在偏移 $32+8=40$ 字节处。

1
2
3
4
5
6
7
8
9
10
v32 = *(_DWORD *)(this + 168) * frame_size * total_frame;
v33 = v32;
v34 = *(_QWORD *)(this + 16);
if ( (unsigned int)v32 > (unsigned __int64)((*(_QWORD *)(this + 32) - v34) >> 1) )
{
v35 = (*(_QWORD *)(this + 24) - v34) >> 1;
v36 = aligned_malloc(2 * v32);
memmove(v36, *(const void **)(this + 16), *(_QWORD *)(this + 24) - *(_QWORD *)(this + 16));
expand_vector(this + 16, (__int64)v36, v35, v33);
}
1
resize_vector(this + 40, 1276);  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
v37 = (unsigned int)(*(_DWORD *)(this + 168) * *(_DWORD *)(this + 172));
v38 = *(_QWORD *)(this + 80);
v39 = *(_QWORD *)(this + 88);
v40 = (v39 - v38) >> 1;
if ( v37 < v40 )
{
v41 = v38 + 2 * v37;
LABEL_23:
*(_QWORD *)(this + 88) = v41;
goto LABEL_24;
}
if ( v37 > v40 )
{
v42 = (*(_QWORD *)(this + 96) - v38) >> 1;
if ( v37 <= v42 )
{
memset(*(void **)(this + 88), 0, 2 * (v37 - v40));
v41 = 2 * (v37 - v40) + v39;
goto LABEL_23;
}
v43 = v42 >> 1;
if ( v42 <= 0x7FFFFFFFFFFFFFFFLL - (v42 >> 1) )
{
v44 = (unsigned int)(*(_DWORD *)(this + 168) * *(_DWORD *)(this + 172));
if ( v43 + v42 >= v37 )
v44 = v43 + v42;
if ( v44 > 0x7FFFFFFFFFFFFFFFLL )
Concurrency::cancel_current_task();
}
else
{
v44 = 0x7FFFFFFFFFFFFFFFLL;
}
v45 = aligned_malloc(2 * v44);
memset((char *)v45 + 2 * v40, 0, 2 * (v37 - v40));
memmove(v45, *(const void **)(this + 80), *(_QWORD *)(this + 88) - *(_QWORD *)(this + 80));
expand_vector(this + 80, (__int64)v45, v37, v44);
}

分别是三段 std::vector 的大小调整过程。

第一个最终扩容到可以存储 channel_num * frame_size * total_frame 个 $2$ 字节元素,这个 std::vector 存储于偏移 $16+32=48$ 字节处。

第二个调整到 $1276$ 字节,存储于偏移 $40+32=72$ 字节处。

第三个最终扩容到可以存储 channel_num * frame_size 个 $2$ 字节元素,存储于偏移 $80+32=112$ 字节处。

最后调用 OpusDecoderConstructor,这个函数似乎是偏移 64+32=96 字节处对象的构造函数,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
__int64 __fastcall OpusDecoderConstructor(__int64 this, int sample_rate, unsigned int channel_num)
{
_DWORD *v6; // rax
__int64 *v8; // rax
__int64 v9; // rax
bool v10; // zf
int v11[4]; // [rsp+40h] [rbp-29h] BYREF
_QWORD v12[4]; // [rsp+50h] [rbp-19h] BYREF
__int128 v13; // [rsp+70h] [rbp+7h]
__int64 v14; // [rsp+80h] [rbp+17h]
__int128 v15; // [rsp+90h] [rbp+27h] BYREF
__int64 v16; // [rsp+A0h] [rbp+37h]
bool v17; // [rsp+D0h] [rbp+67h] BYREF
int v18; // [rsp+E8h] [rbp+7Fh] BYREF

v18 = 0;
v6 = opus_decoder_create(sample_rate, channel_num, &v18);
*(_QWORD *)this = v6;
if ( v18 || !v6 )
{
v8 = InitLogger();
v9 = sub_180019090((__int64)v8);
v10 = *(_QWORD *)this == 0;
v13 = 0u;
v17 = v10;
v14 = 0;
v12[2] = 0;
v12[0] = "[{} {}] Error: {}, Decoder is nullptr: {}";
v11[0] = 24;
v12[1] = 41;
v15 = 0u;
v16 = 0;
sub_18000B980(
v9,
&v15,
4,
(__int64)v12,
(__int64)"D:\\a\\Milody\\Milody\\src\\audio\\format\\milody_opus.cpp",
v11,
&v18,
&v17);
return 0xFFFFFFFFLL;
}
else
{
*(_DWORD *)(this + 8) = sample_rate;
*(_DWORD *)(this + 12) = channel_num;
return 0;
}
}

其中泄漏了源码路径 D:\a\Milody\Milody\src\audio\format\milody_opus.cpp

这意味着,mua 文件与 opus 编码密切相关。

opus_decoder_create 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
_DWORD *__fastcall opus_decoder_create(int a1, unsigned int a2, int *a3)
{
unsigned int v7; // r14d
int v8; // eax
_DWORD *v9; // rdi
int v10; // eax
int v11; // eax
unsigned int v12; // eax
char *v13; // r14
unsigned int v14; // [rsp+40h] [rbp+8h] BYREF
int v15; // [rsp+58h] [rbp+20h] BYREF

if ( a1 != 48000 && a1 != 24000 && a1 != 16000 && a1 != 12000 && a1 != 8000 || a2 - 1 > 1 )
{
if ( a3 )
{
*a3 = -1;
return 0;
}
return 0;
}
v7 = a2 - 1;
if ( (unsigned int)sub_1800BE980(&v14) )
{
v8 = 0;
}
else
{
v14 = (v14 + 7) & 0xFFFFFFF8;
v8 = v14 + 96 + sub_1800BABE0(a2);
}
v9 = malloc(v8);
if ( !v9 )
{
if ( a3 )
*a3 = -7;
return 0;
}
if ( (a1 == 48000 || a1 == 24000 || a1 == 16000 || a1 == 12000 || a1 == 8000) && a2 - 1 <= 1 )
{
if ( v7 > 1 || (unsigned int)sub_1800BE980(&v14) )
{
v11 = 0;
}
else
{
v14 = (v14 + 7) & 0xFFFFFFF8;
v11 = v14 + 96 + sub_1800BABE0(a2);
}
memset(v9, 0, v11);
if ( (unsigned int)sub_1800BE980(&v15) )
{
v10 = -3;
}
else
{
v12 = v15 + 7;
v9[1] = 96;
v9[2] = a2;
v9[14] = a2;
v9[12] = 0;
v9[3] = a1;
v15 = 8 * (v12 >> 3);
v13 = (char *)v9 + v15 + 96;
*v9 = v15 + 96;
v9[6] = a1;
v9[4] = a2;
if ( (unsigned int)sub_1800BE990(v9 + 24) )
{
v10 = -3;
}
else if ( (unsigned int)sub_1800BAC20(v13) )
{
v10 = -3;
}
else
{
sub_1800BBE10(v13, 10016, 0);
v9[17] = 0;
v9[18] = a1 / 400;
v9[13] = sub_1800BD160();
v10 = 0;
}
}
}
else
{
v10 = -1;
}
if ( a3 )
*a3 = v10;
if ( v10 )
{
free(v9);
return 0;
}
return v9;
}

观察这段代码,发现这个代码没有用到任何 C++ 特性(如不使用 class 封装,不使用 exception 而是返回错误码),这与先前的代码风格明显不符。

同时,a1 != 48000 && a1 != 24000 && a1 != 16000 && a1 != 12000 && a1 != 8000 判断了几个特定的采样频率。但是事实上在 JuceMUASourcePlayerConstructor 中还会判断采样率是否为 $48000$:

1
2
3
4
5
6
7
8
9
10
11
if ( (unsigned int)sub_180007C30(this + 32) != 48000 )
{
sub_1800049D0(v24, 1);
v14 = sub_180001FC0((__int64)v25, "sample_rate 48000 expected, but ");
v15 = sub_180007C30(this + 32);
v16 = std::ostream::operator<<(v14, v15);
sub_180001FC0(v16, " found");
v17 = sub_18000A540(v24, v26);
sub_180004FE0(pExceptionObject, v17);
throw (std::runtime_error *)pExceptionObject;
}

这些发现表明 opus_decoder_create 函数很可能是外部库函数。

源代码名为 milody_opus.cpp,同时开发组的 Github 中 fork 了仓库 xiph/opus:

推断 opus_decoder_create 为这个库的一部分。

经过对比,为如下片段经过内联后的结果:

故函数 OpusDecoderConstructor 在偏移 $96$ 处创建的对象存储了 opus_decoder_create 返回的 OpusDecoder * 以及采样率和通道数。

再回到 JuceMUASourcePlayerConstructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if ( (unsigned int)sub_180007AD0(this + 32) != 2 )
{
sub_1800049D0(v24, 1);
v18 = sub_180001FC0((__int64)v25, "channel_num 2 expected, but ");
v19 = sub_180007AD0(this + 32);
v20 = std::ostream::operator<<(v18, v19);
sub_180001FC0(v20, " found");
v21 = sub_18000A540(v24, v26);
sub_180004FE0(pExceptionObject, v21);
throw (std::runtime_error *)pExceptionObject;
}
if ( (unsigned int)sub_180007C30(this + 32) != 48000 )
{
sub_1800049D0(v24, 1);
v14 = sub_180001FC0((__int64)v25, "sample_rate 48000 expected, but ");
v15 = sub_180007C30(this + 32);
v16 = std::ostream::operator<<(v14, v15);
sub_180001FC0(v16, " found");
v17 = sub_18000A540(v24, v26);
sub_180004FE0(pExceptionObject, v17);
throw (std::runtime_error *)pExceptionObject;
}

对采样率和通道数进行限制。

1
2
3
4
5
6
7
8
v11 = wrapped_malloc(3456u);
if ( v11 )
v7 = AudioSourcePlayerConstructor((__int64)v11);
v12 = *(void (__fastcall ****)(_QWORD, __int64))(this + 280);
*(_QWORD *)(this + 280) = v7;
if ( v12 )
(**v12)(v12, 1);
sub_18004C220(*(_QWORD *)(this + 280), v3);

然后分配了 $3456$ 字节的空间,调用其构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 __fastcall AudioSourcePlayerConstructor(__int64 this)
{
*(_QWORD *)this = &juce::AudioSourcePlayer::`vftable;
unknown_libname_2(this + 8);
*(_QWORD *)(this + 56) = 0;
*(_QWORD *)(this + 64) = 0;
*(_DWORD *)(this + 72) = 0;
*(_QWORD *)(this + 3168) = this + 3184;
*(_QWORD *)(this + 3152) = 0;
*(_QWORD *)(this + 3160) = 0;
*(_QWORD *)(this + 3176) = 0;
*(_BYTE *)(this + 3440) = 0;
*(_DWORD *)(this + 3448) = 1065353216;
*(_DWORD *)(this + 3452) = 1065353216;
return this;
}

表明这个大小 $3456$ 字节的对象为 juce::AudioSourcePlayer

其指针存储于偏移 $280$ 字节处。

现在,我们目前知道的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class JuceMUASourcePlayer //(328 bytes)
{
void* vftable;//(0 ~ 8)
std::vector<std::byte> rawData; //(8 ~ 32)
{
std::byte* begin; //(8 ~ 16)
std::byte* data_end; //(16 ~ 24)
std::byte* buf_end; //(24 ~ 32)
}
class MUAFileInfo info; //(32 ~ 248)
{
int sample_frame; //(32 ~ 36)
int total_frame; //(36 ~ 40)
int crc_like_value; //(40 ~ 44)
Unknown; //(44 ~ 48)
std::vector<short> pcm_data; //(48 ~ 72)
{
short* begin; //(48 ~ 56)
short* data_end; //(56 ~ 64)
short* buf_end; //(64 ~ 72)
}
std::vector<std::byte> unknown_data; //(72 ~ 96)
{
std::byte* begin;
std::byte* data_end;
std::byte* buf_end;
}
class opus_decoder decoder; //(96 ~ 112)
{
OpusDecoder*decoder; //(96 ~ 104)
int sample_rate; //(104 ~ 108)
int channel_num; //(108 ~ 112)
}
std::vector<short> data2; //(112 ~ 136)
{
short* begin; //(112 ~ 120)
short* data_end; //(120 ~ 128)
short* buf_end; //(128 ~ 136)
}
class Record rec; //(136 ~ 160)
{
std::byte* data = rawData.begin; //(136 ~ 144)
std::size_t size; //(144 ~ 152)
std::size_t offset; //(152 ~ 160)
}
Unknown; //(160 ~ 168)
RTL_SRWLOCK * lock; //(168 ~ 176)
class MUAFormat mua; //(176 ~ 248)
{
void* vftable; //(176 ~ 184)
int magic; //(184 ~ 188)
int type; //(188 ~ 192)
int file_size; //(192 ~ 196)
int sample_rate; //(196 ~ 200)
int channel_num; //(200 ~ 204)
int frame_size; //(204 ~ 208)
int sample_num; //(208 ~ 212)
int ignore_num; //(212 ~ 216)
int preview_begin; //(216 ~ 220)
int preview_end; //(220 ~ 224)
int real_preview_intro; //(224 ~ 228)
int real_preview_begin; //(228 ~ 232)
int real_preview_end; //(232 ~ 236)
int CRC_all; //(236 ~ 240)
int ignore_origin_num; //(240 ~ 244)
int ignore_frame; //(244 ~ 248)
};
};
Unknown; //(248 ~ 280)
AudioSourcePlayer* player; //(280 ~ 288)
Unknown; //(288 ~ 328)
};

解码函数

再看 MilodyAudioJuceMUASourcePlayerDecodeFullSong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
__int64 __fastcall MilodyAudioJuceMUASourcePlayerDecodeFullSong(__int64 this, __int64 Cancellation_Token)
{
__int64 result; // rax
__int64 *inited; // rax
__int64 v4; // rbx
__int64 *v5; // rax
__int64 v6; // rbx
__int64 *v7; // rax
int v8; // eax
_BYTE v9[24]; // [rsp+0h] [rbp-98h] BYREF
__int64 v10; // [rsp+18h] [rbp-80h]
milody::audio::format::MUAException *v11; // [rsp+30h] [rbp-68h] BYREF
std::runtime_error *v12; // [rsp+38h] [rbp-60h] BYREF
const char *v13; // [rsp+40h] [rbp-58h] BYREF
__int64 v14; // [rsp+48h] [rbp-50h]
__int64 v15; // [rsp+50h] [rbp-48h]
int v16; // [rsp+58h] [rbp-40h]
int v17; // [rsp+5Ch] [rbp-3Ch]
__int64 v18; // [rsp+60h] [rbp-38h]
__int128 v19; // [rsp+70h] [rbp-28h] BYREF
__int64 v20; // [rsp+80h] [rbp-18h]
__int64 v21; // [rsp+B0h] [rbp+18h] BYREF

try
{
DecodeFullSong(this, Cancellation_Token);
result = 0;
}
catch ( milody::audio::format::MUAException *v11 )
{
inited = InitLogger();
v4 = sub_180019090((__int64)inited);
(*(void (__fastcall **)(milody::audio::format::MUAException *))(*(_QWORD *)v11 + 8LL))(v11);
v13 = "{}, {}";
v14 = 6;
v15 = 0;
v16 = 0;
v17 = 0;
v18 = 0;
v19 = 0u;
v20 = 0;
sub_18001A810(v4, &v19, 4, (__int64)&v13, (__int64)"MilodyAudioJuceMUASourcePlayerDecodeFullSong", &v21);
return v10;
}
catch ( std::runtime_error *v12 )
{
v5 = InitLogger();
v6 = sub_180019090((__int64)v5);
(*(void (__fastcall **)(std::runtime_error *))(*(_QWORD *)v12 + 8LL))(v12);
v13 = "{}, {}";
v14 = 6;
v15 = 0;
v16 = 0;
v17 = 0;
v18 = 0;
v19 = 0u;
v20 = 0;
sub_18001A810(v6, &v19, 4, (__int64)&v13, (__int64)"MilodyAudioJuceMUASourcePlayerDecodeFullSong", &v21);
return -11709394;
}
catch ( ... )
{
v7 = InitLogger();
v8 = sub_180019090((__int64)v7);
v13 = "{}, {}";
v14 = 6;
v15 = 0;
v16 = 0;
v17 = 0;
v18 = 0;
v19 = 0u;
v20 = 0;
sub_18001A620(
v8,
(unsigned int)v9 + 112,
4,
(unsigned int)v9 + 64,
(__int64)"MilodyAudioJuceMUASourcePlayerDecodeFullSong",
(__int64)"unexpected exception");
return -11709394;
}
return result;
}

导出函数仍然是对类中的成员函数外封装了异常处理。

内层 DecodeFullSong 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
void __fastcall DecodeFullSong(__int64 this, __int64 Cancellation_Token)
{
__int64 v4; // rbx
RTL_SRWLOCK *v5; // r14
__int64 *inited; // rax
__int64 v7; // rsi
__int64 v8; // rdi
__int64 v9; // r15
int v10; // ebx
unsigned __int8 v11; // al
unsigned __int8 v12; // r12
__int64 v13; // rcx
unsigned int ValidNum; // eax
__int64 v15; // r15
size_t v16; // rdi
void *v17; // rdi
unsigned int v18; // ebx
unsigned int v19; // eax
__int64 v20; // r9
void *v21; // rax
__int64 v22; // rbx
void (__fastcall ***v23)(_QWORD, __int64); // rcx
void *v24; // rcx
__int128 v25; // [rsp+30h] [rbp-D0h] BYREF
__int64 v26; // [rsp+40h] [rbp-C0h]
__int64 v27; // [rsp+48h] [rbp-B8h]
void *Buf[2]; // [rsp+50h] [rbp-B0h] BYREF
char *v29; // [rsp+60h] [rbp-A0h]
__int128 v30; // [rsp+68h] [rbp-98h]
__int64 v31; // [rsp+78h] [rbp-88h]
__int128 v32; // [rsp+80h] [rbp-80h] BYREF
__int64 (__fastcall **v33)(); // [rsp+90h] [rbp-70h] BYREF
__int64 *v34; // [rsp+98h] [rbp-68h]
void **v35; // [rsp+A0h] [rbp-60h]
__int64 v36; // [rsp+A8h] [rbp-58h]
__int64 (__fastcall ***v37)(); // [rsp+C8h] [rbp-38h]
__int64 v38; // [rsp+F0h] [rbp-10h]
char v39; // [rsp+F8h] [rbp-8h]
_OWORD v40[4]; // [rsp+100h] [rbp+0h] BYREF
__int64 v41; // [rsp+150h] [rbp+50h] BYREF
__int64 v42; // [rsp+160h] [rbp+60h]

v4 = 0;
v5 = (RTL_SRWLOCK *)(this + 272);
v38 = this + 272;
v39 = 1;
AcquireSRWLockShared((PSRWLOCK)(this + 272));
if ( *(_QWORD *)(this + 320) )
{
inited = InitLogger();
v7 = sub_180019090((__int64)inited);
v8 = 0;
do
++v8;
while ( aFullsongaudios[v8] );
v30 = 0;
v31 = 0;
DWORD2(v30) = 0;
v9 = HIDWORD(*((_QWORD *)&v30 + 1));
v10 = *(_DWORD *)(v7 + 64);
v11 = sub_1800E5190(v7 + 136);
v12 = v11;
if ( v10 <= 2 || v11 )
{
*(_QWORD *)&v32 = "fullSongAudioSource already ready";
*((_QWORD *)&v32 + 1) = v8;
v13 = v7 + 8;
if ( *(_QWORD *)(v7 + 32) > 0xFu )
v13 = *(_QWORD *)(v7 + 8);
*(_QWORD *)&v30 = 0;
DWORD2(v30) = 0;
HIDWORD(v30) = v9;
v31 = 0;
v40[0] = v32;
*(_QWORD *)&v32 = v13;
*((_QWORD *)&v32 + 1) = *(_QWORD *)(v7 + 24);
v25 = v30;
v26 = 0;
sub_1800E3AA0((__int64)&v33, &v25, &v32, 2, v40);
sub_1800EA5B0(v7, &v33, v10 <= 2, v12);
}
}
else
{
ValidNum = CalcValidNum(this + 32);
v15 = ValidNum;
*(_OWORD *)Buf = 0;
v29 = 0;
if ( ValidNum )
{
v16 = 2LL * ValidNum;
Buf[0] = aligned_malloc(v16);
v29 = (char *)Buf[0] + v16;
memset(Buf[0], 0, 2 * v15);
Buf[1] = (char *)Buf[0] + v16;
v4 = 0;
}
v25 = 0;
*(_QWORD *)&v25 = aligned_malloc(0x20u);
v26 = 16;
v27 = 31;
strcpy((char *)v25, "decode full song");
v33 = std::X$$V::Z::_Func_impl_no_alloc<`milody::audio::JuceMUASourcePlayer::DecodeFullSong::`2::_lambda_1_,milody::context::AXPEAVContext * const>::`vftable;
v34 = (__int64 *)this;
v35 = Buf;
v36 = Cancellation_Token;
v37 = &v33;
Function_Call((__int64)&v33, (__int64)&v25);
v17 = wrapped_malloc(0x138u);
v42 = (__int64)v17;
if ( v17 )
{
v18 = (unsigned int)v15 / (unsigned int)sub_180007AD0(this + 32);
v19 = sub_180007AD0(this + 32);
LOBYTE(v20) = 1;
v4 = sub_180015980(v17, v19, v18, v20);
}
v41 = v4;
v21 = wrapped_malloc(0x138u);
v42 = (__int64)v21;
if ( v21 )
v22 = sub_18004EAC0((__int64)v21, v4, 0, 0);
else
v22 = 0;
v42 = v22;
v26 = 14;
v27 = 15;
strcpy((char *)&v25, "copy full song");
HIBYTE(v25) = 0;
v33 = std::X$$V::Z::_Func_impl_no_alloc<`milody::audio::JuceMUASourcePlayer::DecodeFullSong::`2::_lambda_2_,milody::context::AXPEAVContext * const>::`vftable;
v34 = &v41;
v35 = Buf;
v37 = &v33;
Function_Call((__int64)&v33, (__int64)&v25);
(*(void (__fastcall **)(__int64, _QWORD))(*(_QWORD *)v22 + 32LL))(v22, 0);
*(_QWORD *)(this + 256) = v41;
v23 = *(void (__fastcall ****)(_QWORD, __int64))(this + 320);
*(_QWORD *)(this + 320) = v22;
if ( v23 )
(**v23)(v23, 1);
v24 = Buf[0];
if ( Buf[0] )
{
if ( (unsigned __int64)(2 * ((v29 - (char *)Buf[0]) >> 1)) >= 0x1000 )
{
v24 = (void *)*((_QWORD *)Buf[0] - 1);
if ( (unsigned __int64)((char *)Buf[0] - (char *)v24 - 8) > 0x1F )
invoke_watson(0, 0, 0, 0, 0);
}
j_j_free(v24);
*(_OWORD *)Buf = 0;
v29 = 0;
}
}
ReleaseSRWLockShared(v5);
}

首先加锁,锁的位置在偏移 $272$ 字节处,证明这里还有另一个锁。这是整个对象的锁,而上文发现的第一个锁应当仅针对偏移 $32$ 字节处的子对象。

然后检查偏移 $320$ 字节处是否有值,如果有值则打印日志并跳过实际的解码操作。这里应当存储的是解码后的音频数据,避免重复解码。

函数 CalcValidNum 如下:

1
2
3
4
5
6
7
8
9
__int64 __fastcall CalcValidNum(__int64 this)
{
unsigned int v2; // ebx

AcquireSRWLockShared((PSRWLOCK)(this + 136));
v2 = *(_DWORD *)(this + 168) * (*(_DWORD *)(this + 176) - *(_DWORD *)(this + 208));
ReleaseSRWLockShared((PSRWLOCK)(this + 136));
return v2;
}

对照之前已经得到的结构,这个函数计算了 channel_num * (sample_num - ignore_origin_num),应当是解码后音频所需的空间。然后根据计算得到的值分配所需空间。

1
2
3
4
5
6
v33 = std::X$$V::Z::_Func_impl_no_alloc<`milody::audio::JuceMUASourcePlayer::DecodeFullSong::`2::_lambda_1_,milody::context::AXPEAVContext * const>::`vftable;
v34 = (__int64 *)this;
v35 = Buf;
v36 = Cancellation_Token;
v37 = &v33;
Function_Call((__int64)&v33, (__int64)&v25);

这部分看样子是在栈上构造一个 std::function 对象,内部存储一个 lambda 表达式。由于这个 lambda 表达式体积较小,因此不进行堆内存的分配。

不进行堆分配的 std::function 内存布局如下:最开头是虚表,接下来存储可调用对象,最后在末尾八字节存储被调用函数相关信息(此处直接存储了一份虚表的指针)。

FunctionCall 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
__int64 __fastcall Function_Call(__int64 this, __int64 a2)
{
__int64 v4; // rcx
__int64 *inited; // rax
int v6; // eax
__int64 v7; // rdx
__int64 v8; // rcx
double v10[2]; // [rsp+30h] [rbp-49h] BYREF
_QWORD v11[4]; // [rsp+40h] [rbp-39h] BYREF
__int128 v12; // [rsp+60h] [rbp-19h]
__int64 v13; // [rsp+70h] [rbp-9h]
__int128 v14; // [rsp+80h] [rbp+7h] BYREF
__int64 v15; // [rsp+90h] [rbp+17h]
__int64 v16; // [rsp+A0h] [rbp+27h]
__int64 v17; // [rsp+A8h] [rbp+2Fh]

v16 = this;
v17 = a2;
perf_monitor(v10);
v4 = *(_QWORD *)(this + 56);
if ( !v4 )
{
std::_Xbad_function_call();
JUMPOUT(0x180016E5DLL);
}
(*(void (__fastcall **)(__int64))(*(_QWORD *)v4 + 16LL))(v4);
perf_monitor(v11);
inited = InitLogger();
v6 = sub_180019090((__int64)inited);
v10[0] = (double)(int)((double)SLODWORD(v11[0]) - (double)SLODWORD(v10[0])) / 1000000000.0;
v11[2] = 0;
v12 = 0u;
v13 = 0;
v11[0] = "{}: {}";
v11[1] = 6;
v14 = 0u;
v15 = 0;
sub_180015510(v6, (unsigned int)&v14, 1, (unsigned int)v11, a2, (__int64)v10);
v8 = *(_QWORD *)(this + 56);
if ( v8 )
{
LOBYTE(v7) = v8 != this;
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v8 + 32LL))(v8, v7);
*(_QWORD *)(this + 56) = 0;
}
return sub_180005F80(a2);
}

这个函数负责监控性能信息,并在日志中输出所用时间。调用的是虚表偏移 $16$ 字节的函数。对应函数 DecodeStarter

1
2
3
4
5
6
7
void __fastcall DecodeStarter(__int64 this)
{
DecodeMain(
*(_QWORD *)(this + 8) + 32LL,
*(unsigned __int8 (__fastcall ****)(_QWORD))(this + 24),
**(char ***)(this + 16));
}

负责将 lambda 表达式捕获的变量传入 DecodeMain,依次传入了偏移 $32$ 字节的子对象的指针、Cancellation_Token 和输出缓冲区的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void __fastcall DecodeMain(__int64 this, unsigned __int8 (__fastcall ***Cancellation_Token)(_QWORD), char *OutputBuf)
{
int channel_num; // r9d
int preview_begin; // r8d
unsigned __int64 v8; // rbx
int channel_num_1; // r9d
int preview_begin_1; // r10d
size_t v11; // r8
_QWORD *v12; // rax
_BYTE v13[32]; // [rsp+30h] [rbp-48h] BYREF
_BYTE pExceptionObject[32]; // [rsp+50h] [rbp-28h] BYREF

DecodeCore(this, (unsigned __int8 (__fastcall ***)(_QWORD, __int64))Cancellation_Token, *(_DWORD *)(this + 4));
AcquireSRWLockShared((PSRWLOCK)(this + 136));
if ( (**Cancellation_Token)(Cancellation_Token) )
{
v12 = (_QWORD *)sub_1800047D0((__int64)v13, "canceled");
sub_180004BC0((__int64)pExceptionObject, -100, v12);
throw (milody::audio::format::MUAException *)pExceptionObject;
}
channel_num = *(_DWORD *)(this + 168);
preview_begin = *(_DWORD *)(this + 184);
v8 = *(_QWORD *)(this + 16)
+ 2
* ((unsigned int)(*(_DWORD *)(this + 12) * channel_num)
+ (unsigned int)(*(_DWORD *)(this + 180) * channel_num)
+ (unsigned __int64)(unsigned int)(channel_num * (*(_DWORD *)(this + 176) - preview_begin)));
memcpy(
OutputBuf,
(const void *)(v8 + 2LL * (unsigned int)(*(_DWORD *)(this + 208) * channel_num)),
2LL * (unsigned int)(channel_num * preview_begin));
channel_num_1 = *(_DWORD *)(this + 168);
preview_begin_1 = *(_DWORD *)(this + 184);
v11 = 2LL * (unsigned int)(channel_num_1 * (*(_DWORD *)(this + 176) - preview_begin_1));
memcpy(&OutputBuf[2 * channel_num_1 * preview_begin_1], (const void *)(v8 - v11), v11);
ReleaseSRWLockShared((PSRWLOCK)(this + 136));
}

紧接着,DecodeMain 又调用了 DecodeCore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__int64 __fastcall DecodeCore(
__int64 this,
unsigned __int8 (__fastcall ***Cancellation_Token)(_QWORD, __int64),
int total_frame)
{
unsigned int v4; // ebx
__int64 result; // rax
__int64 v7; // rdx
unsigned int i; // esi
_QWORD *v9; // rax
_BYTE v10[32]; // [rsp+20h] [rbp-48h] BYREF
_BYTE pExceptionObject[32]; // [rsp+40h] [rbp-28h] BYREF

v4 = *(_DWORD *)(this + 128);
v7 = (*(_DWORD *)(this + 180) >> 2) % *(_DWORD *)(this + 172);
result = (*(_DWORD *)(this + 180) >> 2) / *(_DWORD *)(this + 172);
for ( i = result + total_frame; v4 < i; ++v4 )
{
if ( (**Cancellation_Token)(Cancellation_Token, v7) )
{
v9 = (_QWORD *)sub_1800047D0((__int64)v10, "canceled");
sub_180004BC0((__int64)pExceptionObject, -100, v9);
throw (milody::audio::format::MUAException *)pExceptionObject;
}
result = DecodeFrame(this, v4);
}
return result;
}

这个函数先进行了一些小计算,将传入的 total_frame 加上了 (ignore_num / 4) / frame_size 作为需要解码的帧的总数,然后调用 DecodeFrame 解码每一帧。

但是坏消息是 DecodeFrame 这个函数根本无法被 IDA 反编译。

不过好在我们有多样的反编译工具,Ghidra 很好地反编译了这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206


/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */

void DecodeFrame(longlong this,uint idx,undefined8 param_3,undefined8 param_4)

{
longlong lVar1;
basic_ostream<> *pbVar2;
undefined8 *puVar3;
longlong *plVar4;
ushort uVar5;
void *_Memory;
uint uVar6;
int iVar7;
int iVar8;
undefined *puVar9;
ulonglong uVar10;
undefined *puVar11;
ulonglong uVar12;
ulonglong uVar13;
uint uVar14;
double dVar15;
undefined1 auStackY_248 [32];
uint local_218;
uint local_214;
undefined *local_208;
undefined8 uStack_200;
undefined8 local_1f8;
undefined8 local_1e8;
undefined4 uStack_1e0;
undefined4 uStack_1dc;
undefined8 local_1d8;
undefined8 local_1c8;
undefined8 uStack_1c0;
undefined8 local_1b8;
PSRWLOCK local_1a8;
undefined1 local_1a0;
int iStack_19c;
undefined *local_198 [2];
undefined *local_188;
undefined **local_180;
basic_iostream<> local_178 [96];
undefined8 local_118;
undefined4 local_110;
basic_ios<> local_100 [104];
void *local_98 [3];
ulonglong local_80;
ulonglong local_78;
int channel_num;
uint frame_size;

local_78 = DAT_1801e1b00 ^ (ulonglong)auStackY_248;
uVar13 = 0;
local_214 = 0;
local_1a8 = (PSRWLOCK)(this + 136);
AcquireSRWLockExclusive(local_1a8);
local_1a0 = 1;
if ((idx == *(uint *)(this + 128)) && (idx < *(uint *)(this + 4))) {
*(uint *)(this + 128) = *(uint *)(this + 128) + 1;
ReadFromRawData(this,&local_218,2,param_4);
uVar5 = (ushort)local_218 >> 8 | (ushort)local_218 << 8;
uVar10 = (ulonglong)uVar5;
uVar6 = *(uint *)(this + 8) << 8 ^
*(uint *)(&DAT_1800f6260 + (ulonglong)(*(uint *)(this + 8) >> 0x18) * 4);
*(uint *)(this + 8) =
(uVar6 ^ local_218 & 0xff) << 8 ^
*(uint *)(&DAT_1800f6260 + (ulonglong)(uVar6 >> 0x18) * 4) ^ local_218 >> 8 & 0xff;
if ((ulonglong)(*(longlong *)(this + 48) - *(longlong *)(this + 40)) < uVar10) {
resize_vector((longlong *)(this + 40),uVar10);
}
ReadFromRawData(this,*(void **)(this + 40),uVar10,param_4);
uVar6 = *(uint *)(this + 8);
uVar12 = uVar13;
if (uVar5 != 0) {
do {
uVar6 = (uint)(*(byte **)(this + 40))[uVar12] ^
*(uint *)(&DAT_1800f6260 + (ulonglong)(uVar6 >> 0x18) * 4) ^ uVar6 << 8;
uVar12 = uVar12 + 1;
} while ((longlong)uVar12 < (longlong)uVar10);
}
*(uint *)(this + 8) = uVar6;
OpusFrameDecode((undefined8 *)(this + 64),*(byte **)(this + 40),(uint)uVar5,*(int *)(this + 1 72)
,*(longlong *)(this + 80));
concat_vector((longlong *)(this + 16),*(void **)(this + 24),*(void **)(this + 80),
*(longlong *)(this + 88) - (longlong)*(void **)(this + 80) >> 1);
channel_num = *(int *)(this + 128);
if (channel_num == *(int *)(this + 212)) {
local_208 = (undefined *)0xffffffffffffffff;
if ((*(uint *)(this + 180) & 3) != 0) {
plVar4 = FUN_1800047d0(local_98,"Pad_Sn_Error2");
FUN_180004bc0(&local_1c8,0xfffffffffffffffa,plVar4);
/* WARNING: Subroutine does not return */
_CxxThrowException(&local_1c8,(ThrowInfo *)&DAT_1801cc718);
}
uVar6 = *(uint *)(this + 180) >> 2;
frame_size = uVar6;
local_214 = uVar6;
if (uVar6 != 0) {
do {
local_218 = frame_size;
puVar9 = (undefined *)0x0;
uVar14 = (uint)uVar13;
frame_size = *(uint *)(this + 172);
lVar1 = *(longlong *)(this + 16);
channel_num = *(int *)(this + 168);
puVar11 = puVar9;
do {
iVar8 = (int)puVar9;
dVar15 = sin((((double)iVar8 * 6.28318530718) / (double)frame_size) * 4.0);
iVar7 = (int)*(short *)(lVar1 + (ulonglong)((iVar8 + uVar14) * channel_num) * 2) -
(int)(short)(int)(dVar15 * 10000.0);
puVar11 = puVar11 + iVar7 * iVar7;
puVar9 = (undefined *)(ulonglong)(iVar8 + 1U);
} while (iVar8 + 1U < uVar6 * 2);
if (puVar11 < local_208) {
local_218 = uVar14;
local_208 = puVar11;
}
uVar13 = (ulonglong)(uVar14 + 1);
frame_size = local_218;
} while (uVar14 + 1 < uVar6 * 2);
channel_num = *(int *)(this + 128);
uVar6 = local_218;
}
uVar6 = uVar6 - local_214;
*(uint *)(this + 12) = uVar6;
frame_size = -uVar6;
if ((int)-uVar6 < 0) {
frame_size = uVar6;
}
if (*(uint *)(this + 172) < frame_size) {
plVar4 = FUN_1800047d0(local_98,"Offset_Too_Big");
FUN_180004bc0(&local_1c8,0xfffffffffffffff9,plVar4);
/* WARNING: Subroutine does not return */
_CxxThrowException(&local_1c8,(ThrowInfo *)&DAT_1801cc718);
}
}
if (channel_num == *(int *)(this + 4)) {
uVar6 = *(uint *)(this + 8) << 8 ^
*(uint *)(&DAT_1800f6660 + (ulonglong)(*(uint *)(this + 8) >> 0x18) * 4);
uVar6 = uVar6 << 8 ^ *(uint *)(&DAT_1800f6660 + (ulonglong)(uVar6 >> 0x18) * 4);
uVar6 = uVar6 << 8 ^ *(uint *)(&DAT_1800f6660 + (ulonglong)(uVar6 >> 0x18) * 4);
uVar6 = uVar6 << 8 ^ *(uint *)(&DAT_1800f6660 + (ulonglong)(uVar6 >> 0x18) * 4);
*(uint *)(this + 8) = uVar6;
if (uVar6 != *(uint *)(this + 204)) {
local_198[0] = &DAT_1800f5cb0;
local_188 = &DAT_1800f5cb8;
std::basic_ios<>::basic_ios<>(local_100);
local_214 = 1;
std::basic_iostream<>::basic_iostream<>
((basic_iostream<> *)local_198,(basic_streambuf<> *)&local_180);
*(undefined ***)((longlong)local_198 + (longlong)*(int *)(local_198[0] + 4)) =
std::basic_stringstream<>::vftable;
*(int *)((longlong)&iStack_19c + (longlong)*(int *)(local_198[0] + 4)) =
*(int *)(local_198[0] + 4) + -0x98;
std::basic_streambuf<>::basic_streambuf<>((basic_streambuf<> *)&local_180);
local_180 = std::basic_stringbuf<>::vftable;
local_118 = 0;
local_110 = 0;
pbVar2 = FUN_180001fc0((basic_ostream<> *)&local_188,"mua crc verification failure: ");
pbVar2 = std::basic_ostream<>::operator<<(pbVar2,*(uint *)(this + 0xcc));
pbVar2 = FUN_180001fc0(pbVar2," saved in mua file, but ");
pbVar2 = std::basic_ostream<>::operator<<(pbVar2,*(uint *)(this + 8));
FUN_180001fc0(pbVar2," calculated");
puVar3 = FUN_180018f20();
plVar4 = (longlong *)FUN_180019090(puVar3);
FUN_18000a330((basic_streambuf<> *)&local_180,(longlong *)local_98);
local_1f8 = 0;
local_1e8 = 0;
uStack_1e0 = 0;
uStack_1dc = 0;
local_1d8 = 0;
local_208 = &DAT_1800f6c00;
uStack_200 = 2;
uStack_1c0 = 0;
local_1c8 = 0;
local_1b8 = 0;
FUN_180003c20(plVar4,&local_1c8,4,(double *)&local_208,local_98);
if (0xf < local_80) {
_Memory = local_98[0];
if ((0xfff < local_80 + 1) &&
(_Memory = *(void **)((longlong)local_98[0] + -8),
0x1f < (ulonglong)((longlong)local_98[0] + (-8 - (longlong)_Memory)))) {
/* WARNING: Subroutine does not return */
_invoke_watson((wchar_t *)0x0,(wchar_t *)0x0,(wchar_t *)0x0,0,0);
}
free(_Memory);
}
*(undefined ***)((longlong)local_198 + (longlong)*(int *)(local_198[0] + 4)) =
std::basic_stringstream<>::vftable;
*(int *)((longlong)&iStack_19c + (longlong)*(int *)(local_198[0] + 4)) =
*(int *)(local_198[0] + 4) + -0x98;
FUN_180005240((basic_streambuf<> *)&local_180);
std::basic_iostream<>::~basic_iostream<>(local_178);
std::basic_ios<>::~basic_ios<>(local_100);
}
local_218 = local_218 & 0xffff0000;
FUN_180002da0((longlong *)(this + 16),
(*(longlong *)(this + 24) - *(longlong *)(this + 16) >> 1) +
(ulonglong)*(uint *)(this + 172),(short *)&local_218);
}
}
ReleaseSRWLockExclusive((PSRWLOCK)(this + 136));
return;
}

多了两个参数,如果假定后两个参数是多余的,后续分析可以进行下去,

判断传入的需解码的编号必须比 total_frame 小,因此上一个函数中加上 (ignore_num / 4) / frame_size 并无用处,可能是一些保险措施。

同时在偏移 $128+32=160$ 字节处维护了一个下一个应该被解码的帧编号,保证解码是按照顺序进行的。

对于每一帧先按照大端序取出两字节,作为这一帧的大小。取出这么多字节作为原始数据存入偏移为 $40+32=72$ 字节处的 std::vector 中。

然后调用 OpusFrameDecode 解码该帧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Hidden C++ exception states: #wind=5
__int64 __fastcall OpusFrameDecode(__int64 *st, _BYTE *data, int len, int frame_size, __int64 pcm)
{
__int64 v5; // rcx
__int64 result; // rax
unsigned int v7; // ebx
__int64 *inited; // rax
int v9; // eax
__int64 *v10; // rax
__int64 v11; // rax
__int64 v12; // rbx
__int64 v13; // rax
__int64 v14; // rax
_QWORD *v15; // rax
int v16; // [rsp+30h] [rbp-D8h] BYREF
int v17; // [rsp+38h] [rbp-D0h] BYREF
_DWORD v18[4]; // [rsp+40h] [rbp-C8h] BYREF
const char *v19; // [rsp+50h] [rbp-B8h] BYREF
__int64 v20; // [rsp+58h] [rbp-B0h]
_BYTE pExceptionObject[32]; // [rsp+70h] [rbp-98h] BYREF
_BYTE v22[32]; // [rsp+90h] [rbp-78h] BYREF
_BYTE v23[32]; // [rsp+B0h] [rbp-58h] BYREF
_BYTE v24[32]; // [rsp+D0h] [rbp-38h] BYREF

v17 = len;
v5 = *st;
if ( !v5 )
return 0xFFFFFFFFLL;
result = opus_decode(v5, data, len, pcm, frame_size, 0);
v7 = result;
v18[0] = result;
if ( (int)result < 0 )
{
inited = InitLogger();
v9 = sub_180019090((__int64)inited);
v16 = 39;
v19 = "[{} {}] Frame size error: {}, Input length: {}\n";
v20 = 47;
sub_18000B630(
v9,
(unsigned int)&v19,
(unsigned int)"D:\\a\\Milody\\Milody\\src\\audio\\format\\milody_opus.cpp",
(unsigned int)&v16,
(__int64)v18,
(__int64)&v17);
v10 = InitLogger();
v11 = sub_180019090((__int64)v10);
v16 = -4;
v19 = "OPUS_INVALID_PACKET: {}\n";
v20 = 24;
sub_18000B6D0(v11, &v19, &v16);
v12 = sub_18000AF50(v22, v7);
v13 = sub_1800047D0((__int64)v23, "decoder error code: ");
v14 = sub_18000B380(v24, "D:\\a\\Milody\\Milody\\src\\audio\\format\\milody_opus.cpp", v13);
v15 = (_QWORD *)sub_180002230(&v19, v14, v12);
sub_180004BC0((__int64)pExceptionObject, -3, v15);
throw (milody::audio::format::MUAException *)pExceptionObject;
}
return result;
}

又是熟悉的 milody_opus.cpp,因此这个函数估计是对 opus 库中的解码函数进行简单封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Hidden C++ exception states: #wind=5
__int64 __fastcall opus_decode(__int64 st, _BYTE *data, int len, __int64 pcm, int frame_size, unsigned int decode_fec)
{
int v6; // ebx
int v12; // eax
int v13; // eax
unsigned __int64 v14; // rcx
__int64 v15; // rax
void *v16; // rsp
int v17; // eax
unsigned int v18; // ebx
_QWORD v19[6]; // [rsp+60h] [rbp+0h] BYREF

v6 = frame_size;
if ( frame_size <= 0 )
return 0xFFFFFFFFLL;
if ( data && len > 0 && !decode_fec )
{
v12 = sub_1800B7840(st, data, len);
if ( v12 <= 0 )
return 4294967292LL;
if ( frame_size < v12 )
v12 = frame_size;
v6 = v12;
}
v13 = *(_DWORD *)(st + 8);
if ( v13 != 1 && v13 != 2 )
sub_1800BC740(
"assertion failed: st->channels == 1 || st->channels == 2",
"D:\\a\\Milody\\Milody\\ext\\opus\\src\\opus_decoder.c",
879);
v14 = 4LL * *(_DWORD *)(st + 8) * v6;
v15 = v14 + 15;
if ( v14 + 15 < v14 )
v15 = 0xFFFFFFFFFFFFFF0LL;
v16 = alloca(v15 & 0xFFFFFFFFFFFFFFF0uLL);
v17 = sub_1800B71A0(st, data, len, (__int64)v19, v6, decode_fec, 0, 0, 1);
v18 = v17;
if ( v17 > 0 )
sub_1800BE9F0(v19, pcm, (unsigned int)(*(_DWORD *)(st + 8) * v17));
return v18;
}

注意看,这里有个 assert 泄露了源代码及其行号。

在仓库中定位这个文件及行号。

确认了就是 opus_decode 函数,逻辑一致,但是行号有偏差。可能是因为实际进行编译的代码在仓库中的基础上进行了少许修改。

解码完的数据存储在偏移 $80+32=112$ 字节处的 std::vector 中,紧接着被拼接入偏移在 $16+32=48$ 字节处的 std::vector 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
channel_num = *(int *)(this + 128);
if (channel_num == *(int *)(this + 212)) {
local_208 = (undefined *)0xffffffffffffffff;
if ((*(uint *)(this + 180) & 3) != 0) {
plVar4 = FUN_1800047d0(local_98,"Pad_Sn_Error2");
FUN_180004bc0(&local_1c8,0xfffffffffffffffa,plVar4);
/* WARNING: Subroutine does not return */
_CxxThrowException(&local_1c8,(ThrowInfo *)&DAT_1801cc718);
}
uVar6 = *(uint *)(this + 180) >> 2;
frame_size = uVar6;
local_214 = uVar6;
if (uVar6 != 0) {
do {
local_218 = frame_size;
puVar9 = (undefined *)0x0;
uVar14 = (uint)uVar13;
frame_size = *(uint *)(this + 172);
lVar1 = *(longlong *)(this + 16);
channel_num = *(int *)(this + 168);
puVar11 = puVar9;
do {
iVar8 = (int)puVar9;
dVar15 = sin((((double)iVar8 * 6.28318530718) / (double)frame_size) * 4.0);
iVar7 = (int)*(short *)(lVar1 + (ulonglong)((iVar8 + uVar14) * channel_num) * 2) -
(int)(short)(int)(dVar15 * 10000.0);
puVar11 = puVar11 + iVar7 * iVar7;
puVar9 = (undefined *)(ulonglong)(iVar8 + 1U);
} while (iVar8 + 1U < uVar6 * 2);
if (puVar11 < local_208) {
local_218 = uVar14;
local_208 = puVar11;
}
uVar13 = (ulonglong)(uVar14 + 1);
frame_size = local_218;
} while (uVar14 + 1 < uVar6 * 2);
channel_num = *(int *)(this + 128);
uVar6 = local_218;
}
uVar6 = uVar6 - local_214;
*(uint *)(this + 12) = uVar6;
frame_size = -uVar6;
if ((int)-uVar6 < 0) {
frame_size = uVar6;
}
if (*(uint *)(this + 172) < frame_size) {
plVar4 = FUN_1800047d0(local_98,"Offset_Too_Big");
FUN_180004bc0(&local_1c8,0xfffffffffffffff9,plVar4);
/* WARNING: Subroutine does not return */
_CxxThrowException(&local_1c8,(ThrowInfo *)&DAT_1801cc718);
}
}

此处先判断当前解码到的编号是否和 ignore_frame 相同。

如果已经相同,则会找到一个最佳的 offset 使波形与一个正弦波的差值的平方和最小。这个正弦波应当是在编码的时候放入辅助定位的。此 offset 被存入偏移 $12+32=44$ 字节处。

后续部分则是继续进行 CRC32 校验以及各种异常和错误处理。

现在看 DecodeMain 中的后半部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
channel_num = *(_DWORD *)(this + 168);
preview_begin = *(_DWORD *)(this + 184);
v8 = *(_QWORD *)(this + 16)
+ 2
* ((unsigned int)(*(_DWORD *)(this + 12) * channel_num)
+ (unsigned int)(*(_DWORD *)(this + 180) * channel_num)
+ (unsigned __int64)(unsigned int)(channel_num * (*(_DWORD *)(this + 176) - preview_begin)));
memcpy(
OutputBuf,
(const void *)(v8 + 2LL * (unsigned int)(*(_DWORD *)(this + 208) * channel_num)),
2LL * (unsigned int)(channel_num * preview_begin));
channel_num_1 = *(_DWORD *)(this + 168);
preview_begin_1 = *(_DWORD *)(this + 184);
v11 = 2LL * (unsigned int)(channel_num_1 * (*(_DWORD *)(this + 176) - preview_begin_1));
memcpy(&OutputBuf[2 * channel_num_1 * preview_begin_1], (const void *)(v8 - v11), v11);

整体是进行了一些复杂的内存复制,大概是需要删除 ignore_num 指定的正弦波所在的部分,再删除 ignore_orogin_num 指定的部分。最后还需要将被提前的 preview 所在的后半部分还原到后面。

后续还进行了标记为 copy full song 的任务,分析的过程类似这个任务,实质是将整数采样转化为浮点采样并将结果指针存入偏移 $244$ 字节处。

到此,mua 文件的解码方式已经明确,同时已经探明的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class JuceMUASourcePlayer //(328 bytes)
{
void* vftable;//(0 ~ 8)
std::vector<std::byte> rawData; //(8 ~ 32)
{
std::byte* begin; //(8 ~ 16)
std::byte* data_end; //(16 ~ 24)
std::byte* buf_end; //(24 ~ 32)
}
class MUAFileInfo info; //(32 ~ 248)
{
int sample_frame; //(32 ~ 36)
int total_frame; //(36 ~ 40)
int crc_like_value; //(40 ~ 44)
int offset; //(44 ~ 48)
std::vector<short> pcm_data; //(48 ~ 72)
{
short* begin; //(48 ~ 56)
short* data_end; //(56 ~ 64)
short* buf_end; //(64 ~ 72)
}
std::vector<std::byte> opus_frame_temp_buf; //(72 ~ 96)
{
std::byte* begin;
std::byte* data_end;
std::byte* buf_end;
}
struct opus_decoder decoder; //(96 ~ 112)
{
OpusDecoder*decoder; //(96 ~ 104)
int sample_rate; //(104 ~ 108)
int channel_num; //(108 ~ 112)
}
std::vector<short> decoded_frame_temp_buf; //(112 ~ 136)
{
short* begin; //(112 ~ 120)
short* data_end; //(120 ~ 128)
short* buf_end; //(128 ~ 136)
}
struct Record rec; //(136 ~ 168)
{
std::byte* data = rawData.begin; //(136 ~ 144)
std::size_t size; //(144 ~ 152)
std::size_t offset; //(152 ~ 160)
std::size_t decoded_idx; //(160 ~ 168)
}
RTL_SRWLOCK * lock1; //(168 ~ 176)
class MUAFormat mua; //(176 ~ 248)
{
void* vftable; //(176 ~ 184)
int magic; //(184 ~ 188)
int type; //(188 ~ 192)
int file_size; //(192 ~ 196)
int sample_rate; //(196 ~ 200)
int channel_num; //(200 ~ 204)
int frame_size; //(204 ~ 208)
int sample_num; //(208 ~ 212)
int ignore_num; //(212 ~ 216)
int preview_begin; //(216 ~ 220)
int preview_end; //(220 ~ 224)
int real_preview_intro; //(224 ~ 228)
int real_preview_begin; //(228 ~ 232)
int real_preview_end; //(232 ~ 236)
int CRC_all; //(236 ~ 240)
int ignore_origin_num; //(240 ~ 244)
int ignore_frame; //(244 ~ 248)
};
};
Unknown; //(248 ~ 272)
RTL_SRWLOCK* lock2; //(272 ~ 280)
AudioSourcePlayer* player; //(280 ~ 288)
Unknown; //(288 ~ 320)
std::byte* result; //(320 ~ 328)
};

总结

mua 文件实际上是自定义的支持 opus 编码的容器。

文件头占 $60$ 字节,大端序存储。正文部分由若干个 opus 帧组成,每帧开头两字节给出该帧的大小,以大端序存储。接下来这么多字节则是一个 opus 帧的内容。

后记

那么图像呢?

其实开发者已经开源了图像格式的编解码。

本质上是 avif 类似物。将图像保存为一个 av1 视频帧。


某游戏的音频和图像格式解析
https://llingy.top/posts/3809643454/
作者
llingy
发布于
2026年2月28日
许可协议