├── .clang-format
├── .github
└── workflows
│ └── build-msys2.yml
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── editor.xml
├── misc.xml
├── modules.xml
├── pmlxzj_unlocker.iml
└── vcs.xml
├── CMakeLists.txt
├── LICENSE
├── README.MD
├── assets
├── locked.webp
└── unlocked.webp
├── cmake
└── mingw-w64-x86_64.cmake
├── cmd_disable_audio.c
├── cmd_extract_audio.c
├── cmd_info.c
├── cmd_remove_watermark.c
├── cmd_unlock_exe.c
├── docs
└── exe_player_spec.adoc
├── main.c
├── pmlxzj.c
├── pmlxzj.h
├── pmlxzj_audio.c
├── pmlxzj_audio_aac.h
├── pmlxzj_commands.h
├── pmlxzj_enum_names.h
├── pmlxzj_frame.c
├── pmlxzj_utils.h
└── pmlxzj_win32.h
/.clang-format:
--------------------------------------------------------------------------------
1 | BasedOnStyle: Chromium
2 | ColumnLimit: 120
--------------------------------------------------------------------------------
/.github/workflows/build-msys2.yml:
--------------------------------------------------------------------------------
1 | name: Build (msys2)
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 | runs-on: windows-latest
13 | name: "🚧 ${{ matrix.icon }} ${{ matrix.sys }}"
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | include:
18 | - icon: 🟦
19 | sys: mingw32
20 | - icon: 🟨
21 | sys: ucrt64
22 | defaults:
23 | run:
24 | shell: msys2 {0}
25 | steps:
26 | - uses: actions/checkout@v4
27 |
28 | - name: '${{ matrix.icon }} Set up msys2-${{ matrix.sys }}'
29 | uses: msys2/setup-msys2@v2
30 | with:
31 | msystem: "${{matrix.sys}}"
32 | update: true
33 | install: curl git
34 | pacboy: >-
35 | toolchain:p
36 | cmake:p
37 | ninja:p
38 |
39 | - name: Configure & Build
40 | run: |
41 | cmake \
42 | -DCMAKE_BUILD_TYPE=Release \
43 | -DSTATIC_ZLIB=ON \
44 | -B build -S .
45 | cmake --build build \
46 | --config Release \
47 | --target pmlxzj_unlocker
48 |
49 | - name: Prepare Archive
50 | run: |
51 | rm -rf dist
52 | mkdir -p dist
53 | cp -R LICENSE README.MD assets build/*.exe dist/
54 |
55 | - uses: actions/upload-artifact@v4
56 | with:
57 | name: "pmlxzj_unlocker_${{matrix.sys}}"
58 | path: dist/
59 | bundle:
60 | needs: [ build ]
61 | runs-on: ubuntu-latest
62 | steps:
63 | - uses: actions/download-artifact@v4
64 | with:
65 | name: pmlxzj_unlocker_ucrt64
66 | path: dist_ucrt64
67 |
68 | - uses: actions/download-artifact@v4
69 | with:
70 | name: pmlxzj_unlocker_mingw32
71 | path: dist_mingw32
72 |
73 | - name: bundle all
74 | run: |
75 | mkdir -p dist
76 | for exe_path in dist_mingw32/*.exe; do
77 | mv "$exe_path" "${exe_path%.exe}_i686.exe"
78 | done
79 | # for exe_path in dist_ucrt64/*.exe; do
80 | # mv "$exe_path" "${exe_path%.exe}_ucrt64.exe"
81 | # done
82 |
83 | cp -r dist_mingw32/*.exe dist/.
84 | cp -r dist_ucrt64/. dist/.
85 |
86 | - uses: actions/upload-artifact@v4
87 | with:
88 | name: pmlxzj_unlocker
89 | path: dist/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | sample/
2 | build/
3 | *.7z
4 |
5 | ### C template
6 | # Prerequisites
7 | *.d
8 |
9 | # Object files
10 | *.o
11 | *.ko
12 | *.obj
13 | *.elf
14 |
15 | # Linker output
16 | *.ilk
17 | *.map
18 | *.exp
19 |
20 | # Precompiled Headers
21 | *.gch
22 | *.pch
23 |
24 | # Libraries
25 | *.lib
26 | *.a
27 | *.la
28 | *.lo
29 |
30 | # Shared objects (inc. Windows DLLs)
31 | *.dll
32 | *.so
33 | *.so.*
34 | *.dylib
35 |
36 | # Executables
37 | *.exe
38 | *.out
39 | *.app
40 | *.i*86
41 | *.x86_64
42 | *.hex
43 |
44 | # Debug files
45 | *.dSYM/
46 | *.su
47 | *.idb
48 | *.pdb
49 |
50 | # Kernel Module Compile Results
51 | *.mod*
52 | *.cmd
53 | .tmp_versions/
54 | modules.order
55 | Module.symvers
56 | Mkfile.old
57 | dkms.conf
58 |
59 | ### CLion template
60 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
61 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
62 |
63 | # User-specific stuff
64 | .idea/**/workspace.xml
65 | .idea/**/tasks.xml
66 | .idea/**/usage.statistics.xml
67 | .idea/**/dictionaries
68 | .idea/**/shelf
69 |
70 | # AWS User-specific
71 | .idea/**/aws.xml
72 |
73 | # Generated files
74 | .idea/**/contentModel.xml
75 |
76 | # Sensitive or high-churn files
77 | .idea/**/dataSources/
78 | .idea/**/dataSources.ids
79 | .idea/**/dataSources.local.xml
80 | .idea/**/sqlDataSources.xml
81 | .idea/**/dynamic.xml
82 | .idea/**/uiDesigner.xml
83 | .idea/**/dbnavigator.xml
84 |
85 | # Gradle
86 | .idea/**/gradle.xml
87 | .idea/**/libraries
88 |
89 | # Gradle and Maven with auto-import
90 | # When using Gradle or Maven with auto-import, you should exclude module files,
91 | # since they will be recreated, and may cause churn. Uncomment if using
92 | # auto-import.
93 | # .idea/artifacts
94 | # .idea/compiler.xml
95 | # .idea/jarRepositories.xml
96 | # .idea/modules.xml
97 | # .idea/*.iml
98 | # .idea/modules
99 | # *.iml
100 | # *.ipr
101 |
102 | # CMake
103 | cmake-build-*/
104 |
105 | # Mongo Explorer plugin
106 | .idea/**/mongoSettings.xml
107 |
108 | # File-based project format
109 | *.iws
110 |
111 | # IntelliJ
112 | out/
113 |
114 | # mpeltonen/sbt-idea plugin
115 | .idea_modules/
116 |
117 | # JIRA plugin
118 | atlassian-ide-plugin.xml
119 |
120 | # Cursive Clojure plugin
121 | .idea/replstate.xml
122 |
123 | # SonarLint plugin
124 | .idea/sonarlint/
125 |
126 | # Crashlytics plugin (for Android Studio and IntelliJ)
127 | com_crashlytics_export_strings.xml
128 | crashlytics.properties
129 | crashlytics-build.properties
130 | fabric.properties
131 |
132 | # Editor-based Rest Client
133 | .idea/httpRequests
134 |
135 | # Android studio 3.1+ serialized cache file
136 | .idea/caches/build_file_checksums.ser
137 |
138 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/editor.xml:
--------------------------------------------------------------------------------
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 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/pmlxzj_unlocker.iml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.22.1)
2 | project(pmlxzj_unlocker VERSION 0.1.8)
3 | cmake_policy(SET CMP0135 NEW)
4 |
5 | option(STATIC_ZLIB "static link zlib" OFF)
6 |
7 | set(CMAKE_C_STANDARD 17)
8 |
9 | include(FetchContent)
10 | FetchContent_Declare(zlib
11 | URL https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz
12 | )
13 | FetchContent_MakeAvailable(zlib)
14 |
15 | string(CONCAT pmlxzj_unlocker_ver
16 | ${pmlxzj_unlocker_VERSION_MAJOR} "."
17 | ${pmlxzj_unlocker_VERSION_MINOR} "."
18 | ${pmlxzj_unlocker_VERSION_PATCH})
19 |
20 | add_executable(pmlxzj_unlocker
21 | main.c
22 | pmlxzj.c
23 | pmlxzj_audio.c
24 | pmlxzj_frame.c
25 | cmd_disable_audio.c
26 | cmd_extract_audio.c
27 | cmd_info.c
28 | cmd_unlock_exe.c
29 | cmd_remove_watermark.c
30 | )
31 | target_compile_definitions(pmlxzj_unlocker PRIVATE PROJECT_VERSION="${pmlxzj_unlocker_ver}")
32 |
33 | if (STATIC_ZLIB)
34 | add_library(ZLIB::ZLIB ALIAS zlibstatic)
35 | message("using static zlib")
36 | else ()
37 | add_library(ZLIB::ZLIB ALIAS zlib)
38 | message("using dynamic zlib")
39 |
40 | add_custom_command(TARGET pmlxzj_unlocker POST_BUILD
41 | COMMAND ${CMAKE_COMMAND} -E copy $
42 | $)
43 | endif ()
44 |
45 | target_link_libraries(pmlxzj_unlocker ZLIB::ZLIB)
46 |
47 | if (MSVC)
48 | target_compile_options(pmlxzj_unlocker PRIVATE /W4 /WX)
49 | else ()
50 | target_compile_options(pmlxzj_unlocker PRIVATE -Wall -Wextra -Wpedantic -Werror)
51 | if (CMAKE_BUILD_TYPE STREQUAL "Release")
52 | target_link_options(pmlxzj_unlocker PRIVATE -static-libgcc -Wl,--gc-sections)
53 | endif ()
54 | endif ()
55 |
56 | if (WIN32)
57 | target_link_libraries(pmlxzj_unlocker shell32.lib)
58 | endif ()
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 爱飞的猫
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # 解除《屏幕录像专家》编辑锁定
2 |
3 | [](LICENSE)
4 | [](https://github.com/FlyingRainyCats/pmlxzj_unlocker/releases/latest)
5 | [](https://github.com/FlyingRainyCats/pmlxzj_unlocker/actions/workflows/build-msys2.yml)
6 |
7 | 用于解除使用屏幕录像专家对录像进行 “已进行编辑加密” 文件的解密。
8 |
9 | | 解锁前 | 解锁后 |
10 | |:---------------------------:|:------------------------------:|
11 | |  |  |
12 |
13 | by 爱飞的猫@52pojie.cn - 仅供学习交流用途
14 |
15 | ## 特性说明
16 |
17 | 支持:
18 |
19 | - 音频提取 (支持使用 WAV、MP3、AAC 音频格式的 EXE 播放器)
20 | - 禁用音频[^disable_audio]
21 | - 解除“编辑加密”锁定
22 | - 解除“播放加密”锁定 (需要提供正确密码 [^password_note] [^non_ascii_password_note])
23 |
24 | [^password_note]: 密码的第一位字符不参与解密。
25 | [^non_ascii_password_note]: 包含非 ASCII 字符的密码需要储存到文件,并透过 `-P` 指定。
26 | [^disable_audio]: 禁用音频后,《屏幕录像专家》可以将视频转码到无音频的 MP4 文件。
27 |
28 | 测试于下述《屏幕录像专家》版本生成的 EXE 播放器文件:
29 |
30 | - V7.5 Build 200701224
31 | - V2014 Build 0318
32 | - V2023 Build 0828
33 |
34 | 用到了 `zlib` 进行数据解压缩操作。
35 |
36 | ## 两个版本
37 |
38 | 默认分发的二进制文件(通过 GitHub Actions 构建)有两个版本:
39 |
40 | - `pmlxzj_unlocker-i686.exe`
41 | - msys2-mingw32 构建,支持 32 位系统。
42 | - 文件名包含非 ASCII 字符时可能有兼容性问题。
43 | - `pmlxzj_unlocker.exe`
44 | - msys2-ucrt64 构建,对新系统支援较好,不支持 32 位系统。
45 | - 支持包含非 ASCII 字符的文件名。
46 |
47 | ## 编译
48 |
49 | 使用 cmake + mingw 工具链编译。VS 或许可以,未测试。
50 |
51 | ```sh
52 | cmake -B build -DCMAKE_BUILD_TYPE=Release .
53 | cmake --build build
54 | ```
55 |
56 | ## 使用方法
57 |
58 | ### 解锁视频
59 |
60 | 使用 `unlock` 指令解锁:
61 |
62 | ```shell
63 | ./pmlxzj_unlocker.exe unlock "录像1.exe" "录像1_解锁.exe"
64 | ./pmlxzj_unlocker.exe unlock -p password "录像1.exe" "录像1_解锁.exe"
65 | ./pmlxzj_unlocker.exe unlock -P /path/to/password.txt "录像1.exe" "录像1_解锁.exe"
66 | ```
67 |
68 | ※ 若密码包含中文或其它非 ASCII 字符,先存储至文件 (GBK 编码),然后透过 `-P` 参数指定。
69 |
70 | 此外可以指定 `-r` 在密码错误时继续、`-v` 输出更多信息。
71 |
72 | ### 提取音频
73 |
74 | 目前只支持下述“声音压缩”选项的 EXE 播放器:
75 |
76 | - 不压缩 (WAV)
77 | - 无损压缩 (WAV + zlib 压缩)
78 | - 有损压缩 (MP3 格式)
79 | - 有损压缩 (AAC 格式)
80 |
81 | 尚未支援:
82 |
83 | - 有损压缩 (TrueSpeech)
84 |
85 | ```shell
86 | ./pmlxzj_unlocker.exe audio-dump "录像1.exe" "录像1.wav"
87 | ```
88 |
89 | ### 禁用音频
90 |
91 | 如果 EXE 播放器启用了声音压缩,那么《屏幕录像专家》将拒绝转换视频到 MP4 等格式。
92 |
93 | 为了绕过这一限制,可以使用工具将 EXE 播放器中的音频禁用,然后进行转换。
94 |
95 | ```shell
96 | ./pmlxzj_unlocker.exe audio-disable "录像1.exe" "录像1_无音频.exe"
97 | ```
98 |
99 | ※ 如果处理《屏幕录像专家》7.0 或之前的版本生成的 EXE 播放器,需要使用《屏幕录像专家》升级播放器然后再处理。
100 |
101 | ### 查看信息
102 |
103 | 查看部分读取的信息。
104 |
105 | ```shell
106 | ./pmlxzj_unlocker.exe info "录像1.exe"
107 | ./pmlxzj_unlocker.exe info -v "录像1.exe"
108 | ```
109 |
110 | 此外可以指定 `-v` 输出更多信息。
111 |
112 | ### 意义不明
113 |
114 | 有很多地方还没搞懂,也有很多参数能控制读取文件的偏移。
115 |
116 | 目前只是对着部分已知的文件进行测试。
117 |
118 | ## 碎碎念
119 |
120 | OBS 永远的神,屏幕录像专家可以说是时代的眼泪了…
121 |
122 | XP 时代应该是它的巅峰(默认 5fps,录教程够了,低资源设备友好)。
123 | 不过感觉从 Win7 开始就有点力不从心 + 摆烂了。
124 |
125 | 本质上只是为了翻录到 MP4 格式方便传输。利用 OBS + 自动翻录脚本也能做到类似的效果。
126 |
127 | 一开始也想过所谓的“高度无损压缩”会是什么黑科技,结果却发现就是简单的 gzip 压缩有点失望。
128 |
129 | 此外逆向过程中发现了其内嵌了两个 txt 文件(换行分割的数组),怀疑是用来重放鼠标点击事件的信息。
130 | 在代码中的 `idx1` 和 `idx2` 为解析后的对应数据。
131 |
132 | ## 致谢
133 |
134 | - Hmily 老师帮忙定位到了关键解密代码和大致解密流程。
135 |
--------------------------------------------------------------------------------
/assets/locked.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlyingRainyCats/pmlxzj_unlocker/11930717266edcdc1b488a4845163113fd9ae18b/assets/locked.webp
--------------------------------------------------------------------------------
/assets/unlocked.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlyingRainyCats/pmlxzj_unlocker/11930717266edcdc1b488a4845163113fd9ae18b/assets/unlocked.webp
--------------------------------------------------------------------------------
/cmake/mingw-w64-x86_64.cmake:
--------------------------------------------------------------------------------
1 | # Sample toolchain file for building for Windows from an Ubuntu Linux system.
2 | #
3 | # Typical usage:
4 | # *) install cross compiler: `sudo apt-get install mingw-w64`
5 | # *) cd build
6 | # *) cmake -DCMAKE_TOOLCHAIN_FILE=~/mingw-w64-x86_64.cmake ..
7 | # This is free and unencumbered software released into the public domain.
8 |
9 | set(CMAKE_SYSTEM_NAME Windows)
10 | set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)
11 |
12 | # cross compilers to use for C, C++ and Fortran
13 | set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
14 | set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
15 | set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran)
16 | set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres)
17 |
18 | # target environment on the build host system
19 | set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX})
20 | set(CMAKE_SYSTEM_PROCESSOR x86_64)
21 |
22 | # modify default behavior of FIND_XXX() commands
23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
24 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
25 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
26 |
--------------------------------------------------------------------------------
/cmd_disable_audio.c:
--------------------------------------------------------------------------------
1 | #include "pmlxzj.h"
2 | #include "pmlxzj_commands.h"
3 | #include "pmlxzj_utils.h"
4 |
5 | #include
6 | #include
7 | #include
8 |
9 | int pmlxzj_cmd_disable_audio(int argc, char** argv) {
10 | if (argc <= 2) {
11 | pmlxzj_usage(argv[0]);
12 | return 1;
13 | }
14 |
15 | const char* exe_input_path = argv[1];
16 | const char* exe_output_path = argv[2];
17 | if (strcmp(exe_input_path, exe_output_path) == 0) {
18 | printf("ERROR: input and output file cannot be the same.\n");
19 | return 1;
20 | }
21 |
22 | FILE* f_src = fopen(exe_input_path, "rb");
23 | if (f_src == NULL) {
24 | perror("ERROR: open source input");
25 | return 1;
26 | }
27 |
28 | pmlxzj_state_t app = {0};
29 | pmlxzj_user_params_t params = {0};
30 | params.input_file = f_src;
31 | pmlxzj_state_e status = pmlxzj_init(&app, ¶ms);
32 | if (status != PMLXZJ_OK) {
33 | printf("ERROR: Init pmlxzj exe failed: %d\n", status);
34 | fclose(f_src);
35 | return 1;
36 | }
37 |
38 | FILE* f_dst = fopen(exe_output_path, "wb");
39 | if (f_dst == NULL) {
40 | perror("ERROR: open output");
41 | fclose(f_src);
42 | return 1;
43 | }
44 |
45 | // Get the size of the input file
46 | fseek(f_src, 0, SEEK_END);
47 | size_t src_file_size = (size_t)ftell(f_src);
48 | fseek(f_src, 0, SEEK_SET);
49 |
50 | // Copy until the start of the data section
51 | pmlxzj_util_copy(f_dst, f_src, app.footer.offset_data_start);
52 |
53 | uint32_t zero = {0};
54 | fwrite(&zero, sizeof(zero), 1, f_dst);
55 |
56 | // Copy metadata
57 | if (app.audio_metadata_version == PMLXZJ_AUDIO_VERSION_LEGACY) {
58 | // Legacy: audio data followed by metadata and frame data.
59 | fseek(f_src, app.frame_metadata_offset, SEEK_SET);
60 | int64_t metadata_and_frame_size =
61 | (signed)src_file_size - app.frame_metadata_offset - (signed)sizeof(pmlxzj_footer_t);
62 | pmlxzj_util_copy(f_dst, f_src, metadata_and_frame_size);
63 | } else {
64 | // Current: metadata + frame data, followed by audio data, timecodes, and then header.
65 | // no easy way to strip audio data, let's just ignore them for now.
66 | fseek(f_src, sizeof(uint32_t), SEEK_CUR);
67 | int64_t data_size =
68 | (signed)src_file_size - app.footer.offset_data_start - (signed)sizeof(pmlxzj_footer_t) - 4;
69 | pmlxzj_util_copy(f_dst, f_src, data_size);
70 | fseek(f_dst, app.file_size - (long)(sizeof(pmlxzj_footer_t)), SEEK_SET);
71 | }
72 |
73 | pmlxzj_footer_t footer = {0};
74 | memcpy(&footer, &app.footer, sizeof(footer));
75 | footer.config.audio_codec = PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED;
76 | fwrite(&footer, sizeof(footer), 1, f_dst);
77 |
78 | fclose(f_dst);
79 | fclose(f_src);
80 |
81 | return 0;
82 | }
83 |
--------------------------------------------------------------------------------
/cmd_extract_audio.c:
--------------------------------------------------------------------------------
1 | #include "pmlxzj.h"
2 | #include "pmlxzj_commands.h"
3 | #include "pmlxzj_enum_names.h"
4 |
5 | #include
6 | #include
7 |
8 | int pmlxzj_cmd_extract_audio(int argc, char** argv) {
9 | if (argc <= 2) {
10 | pmlxzj_usage(argv[0]);
11 | return 1;
12 | }
13 |
14 | const char* exe_input_path = argv[1];
15 | const char* audio_output_path = argv[2];
16 | if (strcmp(exe_input_path, audio_output_path) == 0) {
17 | printf("ERROR: input and output file cannot be the same.\n");
18 | return 1;
19 | }
20 |
21 | FILE* f_src = fopen(exe_input_path, "rb");
22 | if (f_src == NULL) {
23 | perror("ERROR: open source input");
24 | return 1;
25 | }
26 |
27 | pmlxzj_state_t app = {0};
28 | pmlxzj_user_params_t params = {0};
29 | params.input_file = f_src;
30 | pmlxzj_state_e status = pmlxzj_init(&app, ¶ms);
31 | if (status != PMLXZJ_OK) {
32 | printf("ERROR: Init pmlxzj exe failed: %d (%s)\n", status, pmlxzj_get_state_name(status));
33 | fclose(f_src);
34 | return 1;
35 | }
36 | status = pmlxzj_init_audio(&app);
37 | if (status != PMLXZJ_OK) {
38 | printf("ERROR: Init pmlxzj audio failed: %d (%s)\n", status, pmlxzj_get_state_name(status));
39 | fclose(f_src);
40 | return 1;
41 | }
42 |
43 | FILE* f_audio = fopen(audio_output_path, "wb");
44 | if (f_audio == NULL) {
45 | perror("ERROR: open audio out");
46 | fclose(f_src);
47 | return 1;
48 | }
49 |
50 | status = pmlxzj_audio_dump_to_file(&app, f_audio);
51 | if (status == PMLXZJ_OK) {
52 | fseek(f_audio, 0, SEEK_END);
53 | long audio_len = ftello(f_audio);
54 | printf("audio dump ok, len = %ld\n", audio_len);
55 | } else {
56 | printf("ERROR: failed to dump: %d (%s)\n", status, pmlxzj_get_state_name(status));
57 | }
58 | fclose(f_audio);
59 | fclose(f_src);
60 |
61 | return 0;
62 | }
63 |
--------------------------------------------------------------------------------
/cmd_info.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "pmlxzj.h"
4 | #include "pmlxzj_audio_aac.h"
5 | #include "pmlxzj_commands.h"
6 | #include "pmlxzj_enum_names.h"
7 |
8 | typedef struct {
9 | bool verbose;
10 | bool print_frames;
11 | bool print_indexes;
12 | } pmlxzj_cmd_print_info_param_t;
13 |
14 | pmlxzj_enumerate_state_e enum_frame_print_info(pmlxzj_state_t* app,
15 | pmlxzj_frame_info_t* frame,
16 | void* extra_callback_data) {
17 | pmlxzj_cmd_print_info_param_t* param = extra_callback_data;
18 |
19 | long frame_start_offset = ftell(app->file);
20 | if (frame->compressed_size > 10240) {
21 | long patch_start_offset = frame_start_offset + (long)frame->compressed_size / 2;
22 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x, patch=0x%x)\n", frame->frame_id, frame->image_id,
23 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset, (int)patch_start_offset);
24 | } else if (param->verbose) {
25 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x)\n", frame->frame_id, frame->image_id,
26 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset);
27 | }
28 |
29 | return PMLXZJ_ENUM_CONTINUE;
30 | }
31 |
32 | int pmlxzj_cmd_print_info(int argc, char** argv) {
33 | pmlxzj_cmd_print_info_param_t param = {0};
34 | int option = -1;
35 | while ((option = getopt(argc, argv, "vfi")) != -1) {
36 | switch (option) {
37 | case 'v':
38 | param.verbose = true;
39 | break;
40 | case 'f':
41 | param.print_frames = true;
42 | break;
43 | case 'i':
44 | param.print_indexes = true;
45 | break;
46 | default:
47 | fprintf(stderr, "ERROR: unknown option '-%c'\n", optopt);
48 | pmlxzj_usage(argv[0]);
49 | return 1;
50 | }
51 | }
52 |
53 | if (argc < optind + 1) {
54 | fprintf(stderr, "ERROR: 'input' required.\n");
55 | pmlxzj_usage(argv[0]);
56 | return 1;
57 | }
58 |
59 | FILE* f_src = fopen(argv[optind], "rb");
60 | if (f_src == NULL) {
61 | perror("ERROR: open source input");
62 | return 1;
63 | }
64 |
65 | pmlxzj_state_t app = {0};
66 | pmlxzj_user_params_t params = {0};
67 | params.input_file = f_src;
68 | pmlxzj_state_e status = pmlxzj_init_all(&app, ¶ms);
69 | if (status != PMLXZJ_OK) {
70 | printf("ERROR: Init pmlxzj failed: %d (%s)\n", status, pmlxzj_get_state_name(status));
71 | return 1;
72 | }
73 | printf("\n");
74 |
75 | if (param.print_indexes) {
76 | printf("idx1(len=%d):\n", (int)app.idx1_count);
77 | for (size_t i = 0; i < app.idx1_count; i++) {
78 | printf(" idx1[%3d]: %11d / 0x%08x\n", (int)i, app.idx1[i], app.idx1[i]);
79 | }
80 | printf("idx2(len=%d):\n", (int)app.idx2_count);
81 | for (size_t i = 0; i < 20; i++) {
82 | printf(" idx2[%3d]: %11d / 0x%08x\n", (int)i, app.idx2[i], app.idx2[i]);
83 | }
84 | }
85 |
86 | printf("offset: 0x%x\n", app.footer.offset_data_start);
87 | printf("frame:\n");
88 | printf(" offset: 0x%lx\n", app.first_frame_offset);
89 | printf(" type: %d\n", app.footer.config.image_compress_type);
90 |
91 | printf("audio:\n");
92 | printf(" offset: ");
93 | if (app.audio_metadata_offset) {
94 | printf("0x%08lx\n", app.audio_metadata_offset);
95 | } else {
96 | printf("(none)\n");
97 | }
98 | printf(" codec: %u # %s\n", app.footer.config.audio_codec,
99 | pmlxzj_get_audio_codec_name(app.footer.config.audio_codec));
100 |
101 | switch (app.footer.config.audio_codec) {
102 | case PMLXZJ_AUDIO_TYPE_WAVE_RAW: {
103 | pmlxzj_audio_wav_t* audio = &app.audio.wav;
104 | printf(" offset: 0x%08lx\n", audio->offset);
105 | printf(" size: 0x%08x\n", audio->size);
106 | break;
107 | }
108 |
109 | case PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED: {
110 | pmlxzj_audio_wav_zlib_t* audio = &app.audio.wav_zlib;
111 | printf(" offset: 0x%08lx\n", audio->offset);
112 | printf(" segments: 0x%08x\n", audio->segment_count);
113 | break;
114 | }
115 |
116 | case PMLXZJ_AUDIO_TYPE_LOSSY_MP3: {
117 | pmlxzj_audio_mp3_t* audio = &app.audio.mp3;
118 | printf(" offset: 0x%08lx\n", audio->offset);
119 | printf(" size: 0x%08x\n", audio->size);
120 | if (param.verbose) {
121 | printf(" chunks:\n");
122 | for (uint32_t i = 0; i < audio->segment_count; i++) {
123 | printf(" mp3_chunk[%04u].offset: 0x%08x\n", i, audio->offsets[i]);
124 | }
125 | }
126 |
127 | break;
128 | }
129 |
130 | case PMLXZJ_AUDIO_TYPE_LOSSY_AAC: {
131 | pmlxzj_audio_aac_t* audio = &app.audio.aac;
132 |
133 | printf(" offset: 0x%08lx\n", audio->offset);
134 | printf(" size: 0x%08x\n", audio->size);
135 | printf(" segments: 0x%08x\n", audio->segment_count);
136 | printf(" format: ");
137 | for (size_t i = 0; i < sizeof(audio->format); i++) {
138 | printf("%02x ", audio->format[i]);
139 | }
140 | printf("\n");
141 | printf(" profile: %2d (%s)\n", audio->config.profile_id,
142 | pmlxzj_adts_get_profile_name(audio->config.profile_id));
143 | printf(" sample rate: %2d (%d Hz)\n", audio->config.sample_rate_id,
144 | pmlxzj_adts_get_sample_rate(audio->config.sample_rate_id));
145 | printf(" channel: %2d\n", audio->config.channels_id);
146 |
147 | break;
148 | }
149 | }
150 |
151 | printf("encrypt edit_lock nonce: ");
152 | if (app.footer.edit_lock_nonce) {
153 | printf("0x%08x\n", app.footer.edit_lock_nonce);
154 | } else {
155 | printf("(unset)\n");
156 | }
157 | printf("encrypt play_lock password checksum: ");
158 | if (app.footer.play_lock_password_checksum) {
159 | printf("0x%08x\n", app.footer.play_lock_password_checksum);
160 | } else {
161 | printf("(unset)\n");
162 | }
163 |
164 | if (param.print_frames) {
165 | pmlxzj_enumerate_images(&app, enum_frame_print_info, ¶m);
166 | }
167 | fclose(f_src);
168 |
169 | return 0;
170 | }
171 |
--------------------------------------------------------------------------------
/cmd_remove_watermark.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "pmlxzj.h"
4 | #include "pmlxzj_commands.h"
5 | #include "pmlxzj_utils.h"
6 |
7 | typedef struct {
8 | bool verbose;
9 | bool remove_unregistered_watermark;
10 | bool remove_playback_text_watermark;
11 | } pmlxzj_cmd_remove_watermark_param_t;
12 |
13 | int pmlxzj_cmd_remove_watermark(int argc, char** argv) {
14 | pmlxzj_cmd_remove_watermark_param_t param = {0};
15 | int option = -1;
16 | while ((option = getopt(argc, argv, "vrt")) != -1) {
17 | switch (option) {
18 | case 'v':
19 | param.verbose = true;
20 | break;
21 | case 'r':
22 | param.remove_unregistered_watermark = true;
23 | break;
24 | case 't':
25 | param.remove_playback_text_watermark = true;
26 | break;
27 | default:
28 | fprintf(stderr, "ERROR: unknown option '-%c'\n", optopt);
29 | pmlxzj_usage(argv[0]);
30 | return 1;
31 | }
32 | }
33 |
34 | // Default to remove both watermarks, if none specified
35 | if (!param.remove_unregistered_watermark && !param.remove_playback_text_watermark) {
36 | param.remove_unregistered_watermark = true;
37 | param.remove_playback_text_watermark = true;
38 | }
39 |
40 | if (argc < optind + 2) {
41 | fprintf(stderr, "ERROR: missing parameters for input and output");
42 | pmlxzj_usage(argv[0]);
43 | return 1;
44 | }
45 |
46 | const char* exe_input_path = argv[optind];
47 | const char* exe_output_path = argv[optind + 1];
48 | if (strcmp(exe_input_path, exe_output_path) == 0) {
49 | printf("ERROR: input and output file cannot be the same.\n");
50 | return 1;
51 | }
52 |
53 | FILE* f_src = fopen(exe_input_path, "rb");
54 | if (f_src == NULL) {
55 | perror("ERROR: open source input");
56 | return 1;
57 | }
58 |
59 | FILE* f_dst = fopen(exe_output_path, "wb");
60 | if (f_dst == NULL) {
61 | perror("ERROR: open dest input");
62 | fclose(f_src);
63 | return 1;
64 | }
65 |
66 | pmlxzj_state_t app = {0};
67 | pmlxzj_user_params_t params = {0};
68 | params.input_file = f_src;
69 | pmlxzj_state_e status = pmlxzj_init_all(&app, ¶ms);
70 | if (status != PMLXZJ_OK) {
71 | printf("ERROR: Init pmlxzj exe failed: %d\n", status);
72 | fclose(f_src);
73 | return 1;
74 | }
75 |
76 | pmlxzj_watermark_t watermark = app.watermark;
77 | if (param.remove_unregistered_watermark) {
78 | static uint8_t lic_data_1[20] = {0x41, 0x69, 0x46, 0x65, 0x69, 0x44, 0x65, 0x4D, 0x61, 0x6F,
79 | 0x40, 0x35, 0x32, 0x70, 0x6F, 0x6A, 0x69, 0x65, 0x00, 0x00};
80 | static uint8_t lic_data_2[20] = {0x4C, 0x53, 0x48, 0x4E, 0x48, 0x53, 0x47, 0x4D, 0x48, 0x00,
81 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
82 | memcpy(watermark.lic_data_1, lic_data_1, sizeof(lic_data_1));
83 | memcpy(watermark.lic_data_2, lic_data_2, sizeof(lic_data_2));
84 | }
85 |
86 | if (param.remove_playback_text_watermark) {
87 | static uint8_t blank_playback_watermark[20] = {0x64, 0x21, 0x41, 0x69, 0x46, 0x65, 0x69, 0x44, 0x65, 0x4D,
88 | 0x61, 0x6F, 0x40, 0x35, 0x32, 0x70, 0x6F, 0x6A, 0x69, 0x65};
89 | memcpy(watermark.user_watermark, blank_playback_watermark, sizeof(blank_playback_watermark));
90 | }
91 |
92 | pmlxzj_util_copy_file(f_dst, f_src);
93 | fseek(f_dst, app.watermark_offset, SEEK_SET);
94 | fwrite(&watermark, sizeof(watermark), 1, f_dst);
95 |
96 | return 0;
97 | }
98 |
--------------------------------------------------------------------------------
/cmd_unlock_exe.c:
--------------------------------------------------------------------------------
1 | #include "pmlxzj.h"
2 | #include "pmlxzj_commands.h"
3 | #include "pmlxzj_enum_names.h"
4 | #include "pmlxzj_utils.h"
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | typedef struct {
11 | bool verbose;
12 | bool resume_on_bad_password;
13 | char password[21];
14 | FILE* file_destination;
15 | } pmlxzj_cmd_unlock_exe_param_t;
16 |
17 | pmlxzj_enumerate_state_e enum_frame_patch(pmlxzj_state_t* app, pmlxzj_frame_info_t* frame, void* extra) {
18 | pmlxzj_cmd_unlock_exe_param_t* cli_params = extra;
19 | long frame_start_offset = ftell(app->file);
20 |
21 | if (frame->compressed_size > 10240) {
22 | long key_pos = frame_start_offset + 4;
23 | long cipher_pos = frame_start_offset + (long)frame->compressed_size / 2;
24 |
25 | char frame_key[20];
26 | char frame_ciphered[20];
27 | fseek(app->file, key_pos, SEEK_SET);
28 | fread(frame_key, sizeof(frame_key), 1, app->file);
29 | fseek(app->file, cipher_pos, SEEK_SET);
30 | fread(frame_ciphered, sizeof(frame_ciphered), 1, app->file);
31 |
32 | for (int i = 0; i < 20; i++) {
33 | frame_ciphered[i] = (char)(frame_ciphered[i] ^ frame_key[i] ^ app->nonce_buffer[i]);
34 | }
35 | fseek(cli_params->file_destination, cipher_pos, SEEK_SET);
36 | fwrite(frame_ciphered, sizeof(frame_ciphered), 1, cli_params->file_destination);
37 |
38 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x, patch=0x%x)\n", frame->frame_id, frame->image_id,
39 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset, (int)cipher_pos);
40 | } else if (cli_params->verbose) {
41 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x)\n", frame->frame_id, frame->image_id,
42 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset);
43 | }
44 |
45 | return PMLXZJ_ENUM_CONTINUE;
46 | }
47 |
48 | int pmlxzj_cmd_unlock_exe(int argc, char** argv) {
49 | pmlxzj_cmd_unlock_exe_param_t cli_params = {0};
50 |
51 | int opt;
52 | while ((opt = getopt(argc, argv, "vrp:P:")) != -1) {
53 | switch (opt) {
54 | case 'v':
55 | cli_params.verbose = true;
56 | break;
57 |
58 | case 'r':
59 | cli_params.resume_on_bad_password = true;
60 | break;
61 |
62 | case 'p':
63 | strncpy(cli_params.password, optarg, sizeof(cli_params.password) - 1);
64 | break;
65 |
66 | case 'P': {
67 | FILE* f_password = fopen(optarg, "rb");
68 | if (f_password == NULL) {
69 | perror("read password file");
70 | return 1;
71 | }
72 | fread(cli_params.password, sizeof(cli_params.password) - 1, 1, f_password);
73 | fclose(f_password);
74 | break;
75 | }
76 |
77 | default:
78 | fprintf(stderr, "ERROR: unknown option '-%c'\n", optopt);
79 | pmlxzj_usage(argv[0]);
80 | break;
81 | }
82 | }
83 |
84 | if (argc < optind + 2) {
85 | fprintf(stderr, "ERROR: missing parameters for input and output");
86 | pmlxzj_usage(argv[0]);
87 | return 1;
88 | }
89 |
90 | const char* exe_input_path = argv[optind];
91 | const char* exe_output_path = argv[optind + 1];
92 | if (strcmp(exe_input_path, exe_output_path) == 0) {
93 | printf("ERROR: input and output file cannot be the same.\n");
94 | return 1;
95 | }
96 |
97 | FILE* f_src = fopen(exe_input_path, "rb");
98 | if (f_src == NULL) {
99 | perror("ERROR: open source input");
100 | return 1;
101 | }
102 |
103 | FILE* f_dst = fopen(exe_output_path, "wb");
104 | if (f_dst == NULL) {
105 | perror("ERROR: open dest input");
106 | fclose(f_src);
107 | return 1;
108 | }
109 |
110 | cli_params.file_destination = f_dst;
111 |
112 | pmlxzj_state_t app = {0};
113 | pmlxzj_user_params_t pmlxzj_param = {0};
114 | pmlxzj_param.input_file = f_src;
115 | pmlxzj_param.resume_on_bad_password = cli_params.resume_on_bad_password;
116 | memcpy(pmlxzj_param.password, cli_params.password, sizeof(pmlxzj_param.password));
117 | pmlxzj_state_e status = pmlxzj_init(&app, &pmlxzj_param);
118 | if (status != PMLXZJ_OK) {
119 | printf("ERROR: Init failed (exe): %d (%s)\n", status, pmlxzj_get_state_name(status));
120 | fclose(f_dst);
121 | fclose(f_src);
122 | return 1;
123 | }
124 | status = pmlxzj_init_frame(&app);
125 | if (status != PMLXZJ_OK) {
126 | printf("ERROR: Init failed (frame): %d (%s)\n", status, pmlxzj_get_state_name(status));
127 | fclose(f_dst);
128 | fclose(f_src);
129 | return 1;
130 | }
131 |
132 | pmlxzj_util_copy_file(f_dst, f_src);
133 |
134 | if (app.encrypt_mode == 1 || app.encrypt_mode == 2) {
135 | pmlxzj_enumerate_images(&app, enum_frame_patch, &cli_params);
136 |
137 | pmlxzj_footer_t new_footer = {0};
138 | memcpy(&new_footer, &app.footer, sizeof(new_footer));
139 | new_footer.edit_lock_nonce = 0;
140 | new_footer.play_lock_password_checksum = 0;
141 | fseek(f_dst, app.file_size - (long)(sizeof(pmlxzj_footer_t)), SEEK_SET);
142 | fwrite(&new_footer, sizeof(new_footer), 1, f_dst);
143 | } else {
144 | printf("error: edit_lock_nonce is zero. Unsupported cipher or not encrypted.\n");
145 | }
146 |
147 | fclose(f_dst);
148 | fclose(f_src);
149 |
150 | return 0;
151 | }
152 |
--------------------------------------------------------------------------------
/docs/exe_player_spec.adoc:
--------------------------------------------------------------------------------
1 | = 《屏幕录像专家》EXE 播放器文件格式
2 |
3 | 该文档简单介绍了利用“屏幕录像专家”生成的 EXE 播放器文件的部分格式信息。
4 |
5 | == EXE 与 LXE
6 |
7 | EXE 播放器其实就是一个 `LXE` 数据播放器。将 EXE 转换为 LXE 格式只是将 EXE 可执行程序部分进行加密,余下数据部分一致。
8 |
9 | == 数据“尾”
10 |
11 | 录像数据通常在 EXE 的 Overlay 区域。该区域的起始标识符为 `DATASTART\0`:
12 |
13 | [source,text]
14 | ----
15 | +---------+--------------------------------------------------+------------------+
16 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | 预览 |
17 | +---------+--------------------------------------------------+------------------+
18 | |00A:AE00 | 44 41 54 41 53 54 41 52 54 00 BB CD 28 FE 01 00 | DATASTART.»Í(þ.. |
19 | +---------+--------------------------------------------------+------------------+
20 | ----
21 |
22 | 不过 EXE 并不是从该数据头开始读,而是从文件结尾的 `0x2C` 字节开始:
23 |
24 | [source,text]
25 | ----
26 | +------+--------------------------------------------------+------------------+
27 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | 预览 |
28 | +------+--------------------------------------------------+------------------+
29 | | 0000 | 25 3B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | %;.............. |
30 | | 0010 | 00 00 00 00 08 00 00 00 08 00 00 00 0A AE 0A 00 | ................ |
31 | | 0020 | 70 6D 6C 78 7A 6A 74 6C 78 00 00 00 | pmlxzjtlx... |
32 | -------+--------------------------------------------------+-------------------
33 | ----
34 |
35 | - 偏移 `0x00` (`uint32_t`) 是 “播放加密” 锁定时的随机值,记作 `nonce`。
36 | - 偏移 `0x1C` (`uint32_t`) 是数据起始位置偏移。此处为 `0x0aae0a`。
37 | - 偏移 `0x20` (`char[0x0c]`) 是播放器特征码。
38 |
39 | == 数据“头”
40 |
41 | 回到数据头指向的 `0x0aae0a`,此处的数据部分没有深入研究,只给出研究过的那些:
42 |
43 | [source,c]
44 | -----
45 | struct header {
46 | uint32_t audio_offset;
47 | struct {
48 | uint32_t field_0;
49 | uint32_t field_4;
50 | uint32_t field_8;
51 | int32_t total_frame_count;
52 | uint32_t field_10;
53 | uint32_t field_14;
54 | uint32_t field_18;
55 | uint32_t field_1C;
56 | uint32_t field_20;
57 | uint32_t field_24;
58 | } field_14D8;
59 | char[20] field_2C;
60 | char[20] field_40;
61 | char[40] field_54;
62 | uint32_t field_7C;
63 | uint32_t field_80;
64 | uint32_t field_84;
65 | char[20] field_88;
66 | uint32_t field_9C;
67 | uint8_t field_A0;
68 | uint8_t field_A1;
69 | uint8_t field_A2;
70 | uint8_t field_A3;
71 |
72 | if (field_14D8.field_24) {
73 | float unk1;
74 | float unk2;
75 | uint32_t stream2_len;
76 | uint8_t stream2[stream2_len];
77 | }
78 | };
79 | -----
80 |
81 | 过了 `header` 结构体后,就是视频数据了。
82 |
83 | == 视频数据
84 |
85 | 就是第一帧 `first_frame` 的内容,然后后面就是重复的 `other_frame` 数据:
86 |
87 | [source,c]
88 | ----
89 | struct frame_data {
90 | uint32_t len;
91 | uint32_t decompressed_len;
92 | uint8_t data[len - 4];
93 | };
94 |
95 | // 入口的第一帧
96 | struct first_frame {
97 | frame_data frame; // 帧信息
98 | int32_t frame_id; // 帧序号
99 |
100 | // field_24 == 1 的情况,会有两个额外的 f32 数据。
101 | if (field_24 == 1) {
102 | float unknown_1;
103 | float unknown_2;
104 | }
105 | }
106 |
107 | // 后续则是一直读取 `other_frame` 解构,直到结束
108 | struct other_frame {
109 | // 未知数据流
110 | uint32_t stream2_len;
111 | uint8_t stream2[stream2_len];
112 |
113 | uint32_t frame_state; // 帧状态
114 |
115 | if (frame_state > 0) {
116 | RECT patch_cord; // 坐标相关
117 |
118 | while (frame_state > 0) {
119 | frame_data frame; // 帧信息
120 | int32_t frame_id; // 帧序号
121 | }
122 | }
123 |
124 | // field_24 == 1 的情况,会有两个额外的 f32 数据。
125 | if (field_24 == 1) {
126 | float unknown_1;
127 | float unknown_2;
128 | }
129 | };
130 | ----
131 |
132 | 读取完最后一帧时,`other_frame.frame_id` 等于 `-(field_14d8.total_frame_count - 1)`。
133 |
134 | == 音频数据
135 |
136 | 音频数据偏移存储在初始偏移处(如下方地址 `00AAE0A` 处):
137 |
138 | [source,text]
139 | ----
140 | +---------+--------------------------------------------------+------------------+
141 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | 预览 |
142 | +---------+--------------------------------------------------+------------------+
143 | |00A:AE00 | 44 41 54 41 53 54 41 52 54 00 BB CD 28 FE 01 00 | DATASTART.»Í(þ.. |
144 | +---------+--------------------------------------------------+------------------+
145 | ----
146 |
147 | 其中 `BB CD 28 FE` 为 `0xfe28cdbb`,取对应负数得到偏移 `0x1d73245`。
148 |
149 | [source,text]
150 | ----
151 | +----------+--------------------------------------------------+
152 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |
153 | +----------+--------------------------------------------------+
154 | | 1D7:3240 | F8 02 00 00 18 59 00 00 78 DA 85 |
155 | | 1D7:3250 | 7D CB AE 64 4B 73 56 44 AE EA 73 6C 10 60 83 84 |
156 | +----------+--------------------------------------------------+
157 | ----
158 |
159 | 第一个值 `F8 02 00 00` 表示音频共有 `0x2f8` 段,随后每一段都是地址前缀编码的数据。
160 |
161 | [source,c]
162 | -----
163 | struct audio_data {
164 | uint32_t segment_count;
165 | struct audio_segment {
166 | uint32_t len;
167 | uint8_t data[len];
168 | } segments[segment_count];
169 | };
170 | -----
171 |
172 | 每一段通过 GZip 解压,然后拼接就能得到完整的 `.wav` 格式音频了。
173 |
174 | == “编辑加密” 锁定
175 |
176 | * 每个“大帧”会加密中间的 20 字节
177 | ** “大帧”的判定条件是 `frame.len > 10240`。
178 | ** `&frame.data[frame.len / 2 - 4 .. frame.len / 2 + 16]`。
179 | * 加密算法是 `xor`,密钥为 `(&data[0..20] XOR nonce_key)`。
180 |
181 | `nonce_key` 的获取方式:
182 |
183 | [source,c]
184 | ----
185 | // 文件结尾 -0x2c 偏移处的值
186 | uint32_t nonce = 0x3b25;
187 |
188 | char nonce_key[20];
189 | char buffer[21] = { 0 };
190 | snprintf(buffer, 20, "%d", nonce);
191 | nonce_key = reverse(&buffer[1..21]);
192 | ----
193 |
--------------------------------------------------------------------------------
/main.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "pmlxzj_commands.h"
5 | #include "pmlxzj_win32.h"
6 |
7 | #ifndef PROJECT_VERSION
8 | #define PROJECT_VERSION "0.0.0-unknown"
9 | #endif
10 |
11 | #ifdef _MSC_VER
12 | #pragma execution_character_set("utf-8")
13 | #endif
14 |
15 | struct pmlxzj_commands_t {
16 | const char* name;
17 | int (*handler)(int argc, char** argv);
18 | };
19 |
20 | static struct pmlxzj_commands_t g_pmlxzj_commands[] = {
21 | {.name = "info", .handler = pmlxzj_cmd_print_info},
22 | {.name = "unlock", .handler = pmlxzj_cmd_unlock_exe},
23 | {.name = "audio-dump", .handler = pmlxzj_cmd_extract_audio},
24 | {.name = "audio-disable", .handler = pmlxzj_cmd_disable_audio},
25 | {.name = "remove-watermark", .handler = pmlxzj_cmd_remove_watermark},
26 | };
27 |
28 | int main(int argc, char** argv) {
29 | FixWindowsUnicodeSupport(&argv);
30 |
31 | printf("pmlxzj_unlocker v" PROJECT_VERSION " by 爱飞的猫@52pojie.cn (FlyingRainyCats)\n");
32 | if (argc <= 2) {
33 | pmlxzj_usage(argv[0]);
34 | return 1;
35 | }
36 |
37 | char* command = argv[1];
38 | argv[1] = argv[0];
39 | argv++;
40 | argc--;
41 |
42 | const size_t command_count = sizeof(g_pmlxzj_commands) / sizeof(g_pmlxzj_commands[0]);
43 | for (size_t i = 0; i < command_count; i++) {
44 | struct pmlxzj_commands_t* cmd = &g_pmlxzj_commands[i];
45 | if (strcmp(command, cmd->name) == 0) {
46 | return cmd->handler(argc, argv);
47 | }
48 | }
49 |
50 | pmlxzj_usage(argv[0]);
51 | return 1;
52 | }
53 |
--------------------------------------------------------------------------------
/pmlxzj.c:
--------------------------------------------------------------------------------
1 | #include "pmlxzj.h"
2 | #include "pmlxzj_enum_names.h"
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | int scan_rows(uint32_t* dest_array, const char* str, size_t len) {
9 | const char* end = str + len - 1;
10 |
11 | uint32_t value = 0;
12 | int rows = 0;
13 | const char* p = str;
14 | while (p < end) {
15 | if (p[0] == '\r' && p[1] == '\n') {
16 | if (dest_array) {
17 | *dest_array++ = value;
18 | }
19 | p += 2;
20 | rows++;
21 | value = 0;
22 | continue;
23 | }
24 | value = value * 10 + (*p - '0');
25 | p++;
26 | }
27 |
28 | return rows;
29 | }
30 |
31 | pmlxzj_state_e scan_rows_to_u32_array(uint32_t** scanned_array, size_t* count, const char* str, size_t len) {
32 | *count = 0;
33 | int c = scan_rows(NULL, str, len);
34 | *scanned_array = calloc(c, sizeof(uint32_t));
35 | if (*scanned_array == NULL) {
36 | return PMLXZJ_ALLOCATE_INDEX_LIST_ERROR;
37 | }
38 | *count = c;
39 | scan_rows(*scanned_array, str, len);
40 | return PMLXZJ_OK;
41 | }
42 |
43 | pmlxzj_state_e scan_file_u32_array(uint32_t** scanned_array, size_t* count, FILE* f, long* offset) {
44 | long off = *offset - 4;
45 | fseek(f, off, SEEK_SET);
46 | uint32_t text_count = 0;
47 | fread(&text_count, sizeof(text_count), 1, f);
48 | off -= (long)text_count;
49 | char* text = malloc(text_count + 1);
50 | if (text == NULL) {
51 | return PMLXZJ_SCAN_U32_ARRAY_ALLOC_ERROR;
52 | }
53 | text[text_count] = 0;
54 | fseek(f, off, SEEK_SET);
55 | fread(text, 1, text_count, f);
56 | pmlxzj_state_e result = scan_rows_to_u32_array(scanned_array, count, text, text_count);
57 | free(text);
58 | if (result != PMLXZJ_OK) {
59 | *offset = 0;
60 | free(*scanned_array);
61 | *scanned_array = NULL;
62 | } else {
63 | *offset = off;
64 | }
65 | return result;
66 | }
67 |
68 | pmlxzj_state_e pmlxzj_init(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params) {
69 | FILE* f_src = params->input_file;
70 |
71 | memset(ctx, 0, sizeof(*ctx));
72 | ctx->file = f_src;
73 |
74 | fseek(f_src, 0, SEEK_END);
75 | ctx->file_size = ftell(f_src);
76 |
77 | fseek(f_src, -(int)sizeof(ctx->footer), SEEK_END);
78 | fread(&ctx->footer, sizeof(ctx->footer), 1, f_src);
79 |
80 | if (strcmp(ctx->footer.magic, "pmlxzjtlx") != 0) {
81 | return PMLXZJ_MAGIC_NOT_MATCH;
82 | }
83 |
84 | if (ctx->footer.edit_lock_nonce) {
85 | ctx->encrypt_mode = 1;
86 | char buffer[21] = {0};
87 | snprintf(buffer, sizeof(buffer) - 1, "%d", ctx->footer.edit_lock_nonce);
88 | for (int i = 1; i < 20; i++) {
89 | ctx->nonce_buffer[i] = buffer[20 - i];
90 | }
91 | } else if (ctx->footer.play_lock_password_checksum) {
92 | ctx->encrypt_mode = 2;
93 |
94 | uint32_t expected_checksum = ctx->footer.play_lock_password_checksum;
95 | uint32_t actual_checksum = pmlxzj_password_checksum(params->password);
96 | if (actual_checksum != expected_checksum) {
97 | fprintf(stderr, "ERROR: Password checksum mismatch.\n");
98 | fprintf(stderr, " Expected: 0x%08x, got 0x%08x.\n", expected_checksum, actual_checksum);
99 | if (params->resume_on_bad_password) {
100 | fprintf(stderr, "WARN: Continue without valid password...\n");
101 | } else {
102 | return PMLXZJ_PASSWORD_ERROR;
103 | }
104 | }
105 |
106 | char buffer[21] = {0};
107 | strncpy(buffer, params->password, sizeof(buffer));
108 | for (int i = 1; i < 20; i++) {
109 | ctx->nonce_buffer[i] = buffer[20 - i];
110 | }
111 | } else {
112 | ctx->encrypt_mode = 0;
113 | }
114 |
115 | fseek(f_src, (long)ctx->footer.offset_data_start, SEEK_SET);
116 | int32_t variant_i32 = 0;
117 | fread(&variant_i32, sizeof(variant_i32), 1, f_src);
118 | if (variant_i32 <= 0) {
119 | // New format
120 | ctx->audio_metadata_version = PMLXZJ_AUDIO_VERSION_CURRENT;
121 | ctx->audio_metadata_offset = (long)-variant_i32;
122 | ctx->frame_metadata_offset = (long)ctx->footer.offset_data_start + (long)sizeof(variant_i32);
123 | } else {
124 | // Legacy format
125 | ctx->audio_metadata_version = PMLXZJ_AUDIO_VERSION_LEGACY;
126 | ctx->frame_metadata_offset = (long)variant_i32;
127 | ctx->audio_metadata_offset = (long)ctx->footer.offset_data_start + (long)sizeof(variant_i32);
128 | }
129 | ctx->watermark_offset = ctx->frame_metadata_offset + (long)sizeof(ctx->field_14d8);
130 |
131 | long offset = (long)(ctx->file_size - sizeof(ctx->footer));
132 | pmlxzj_state_e status = scan_file_u32_array(&ctx->idx1, &ctx->idx1_count, f_src, &offset);
133 | if (status != PMLXZJ_OK) {
134 | return status;
135 | }
136 | status = scan_file_u32_array(&ctx->idx2, &ctx->idx2_count, f_src, &offset);
137 | if (status != PMLXZJ_OK) {
138 | return status;
139 | }
140 |
141 | return PMLXZJ_OK;
142 | }
143 |
144 | pmlxzj_state_e pmlxzj_init_all(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params) {
145 | pmlxzj_state_e status = pmlxzj_init(ctx, params);
146 | if (status != PMLXZJ_OK) {
147 | return status;
148 | }
149 | status = pmlxzj_init_audio(ctx);
150 | if (status != PMLXZJ_OK && status != PMLXZJ_NO_AUDIO) {
151 | return status;
152 | }
153 | status = pmlxzj_init_frame(ctx);
154 | if (status != PMLXZJ_OK) {
155 | return status;
156 | }
157 |
158 | return PMLXZJ_OK;
159 | }
160 |
161 | uint32_t pmlxzj_password_checksum(const char* password) {
162 | uint32_t checksum = 0x7D5;
163 | for (int i = 0; i < 20 && *password; i++) {
164 | uint8_t chr = *password++;
165 | checksum += chr * (i + i / 5 + 1);
166 | }
167 | return checksum;
168 | }
169 |
--------------------------------------------------------------------------------
/pmlxzj.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | typedef enum {
8 | PMLXZJ_AUDIO_TYPE_WAVE_RAW = 1,
9 | PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED = 2,
10 | PMLXZJ_AUDIO_TYPE_LOSSY_MP3 = 5,
11 | PMLXZJ_AUDIO_TYPE_LOSSY_TRUE_SPEECH = 6,
12 | PMLXZJ_AUDIO_TYPE_LOSSY_AAC = 7,
13 | } pmlxzj_audio_codec_e;
14 |
15 | typedef enum {
16 | PMLXZJ_AUDIO_VERSION_LEGACY = 0,
17 | PMLXZJ_AUDIO_VERSION_CURRENT = 1,
18 | } pmlxzj_audio_version;
19 |
20 | #pragma pack(push, 1)
21 | // size = 0xB4
22 | typedef struct {
23 | uint32_t color;
24 | uint32_t wnd_state;
25 | uint32_t field_8;
26 | uint32_t field_C;
27 | uint32_t field_10;
28 | uint32_t border_style_2;
29 | uint32_t field_18;
30 | uint32_t field_1C;
31 | uint32_t field_20;
32 | uint32_t field_24;
33 | uint32_t border_style_1;
34 | uint32_t field_2C;
35 | uint32_t field_30;
36 | uint32_t field_34;
37 | char wnd_title[24];
38 | uint32_t image_compress_type;
39 | uint32_t audio_codec;
40 | uint32_t field_58;
41 | uint32_t field_5C;
42 | uint32_t field_60;
43 | uint32_t field_64;
44 | uint32_t field_68;
45 | uint32_t field_6C;
46 | uint32_t field_70;
47 | uint32_t field_74;
48 | uint32_t field_78;
49 | uint32_t field_7C;
50 | uint32_t field_80;
51 | uint32_t field_84;
52 | uint32_t field_88;
53 | uint32_t field_8C;
54 | uint32_t field_90;
55 | uint32_t field_94;
56 | uint32_t field_98;
57 | uint32_t field_9C;
58 | uint32_t field_A0;
59 | uint32_t field_A4;
60 | uint32_t field_A8;
61 | uint32_t field_AC;
62 | uint32_t field_B0;
63 | } pmlxzj_initial_ui_state_t;
64 |
65 | typedef struct {
66 | pmlxzj_initial_ui_state_t config;
67 | uint32_t edit_lock_nonce;
68 | uint32_t play_lock_password_checksum;
69 | uint32_t key_3;
70 | uint32_t key_4;
71 | uint32_t key_5;
72 | uint32_t unknown_3;
73 | uint32_t unknown_4;
74 | uint32_t offset_data_start;
75 | char magic[0x0C];
76 | } pmlxzj_footer_t;
77 |
78 | typedef struct {
79 | uint32_t field_0;
80 | uint32_t field_4;
81 | uint32_t field_8;
82 | int32_t total_frame_count;
83 | uint32_t field_10;
84 | uint32_t field_14;
85 | uint32_t field_18;
86 | uint32_t field_1C;
87 | uint32_t field_20;
88 | uint32_t field_24; // some kind of special flag...
89 | } pmlxzj_config_14d8_t;
90 |
91 | typedef struct {
92 | uint8_t lic_data_1[20];
93 | uint8_t lic_data_2[20];
94 | uint8_t user_watermark[40];
95 | } pmlxzj_watermark_t;
96 |
97 | typedef struct {
98 | uint16_t wFormatTag;
99 | uint16_t nChannels;
100 | uint32_t nSamplesPerSec;
101 | uint32_t nAvgBytesPerSec;
102 | uint16_t nBlockAlign;
103 | uint16_t wBitsPerSample;
104 | uint16_t cbSize;
105 | } pmlxzj_wave_format_ex;
106 | #pragma pack(pop)
107 |
108 | typedef struct {
109 | uint8_t profile_id;
110 | uint8_t sample_rate_id;
111 | uint8_t channels_id;
112 | } pmlxzj_aac_audio_config_t;
113 |
114 | typedef struct {
115 | long offset;
116 | uint32_t size;
117 | uint32_t segment_count;
118 |
119 | // decoder specific data
120 | uint8_t format[4];
121 | uint32_t* segment_sizes;
122 | pmlxzj_aac_audio_config_t config;
123 | } pmlxzj_audio_aac_t;
124 |
125 | typedef struct {
126 | long offset;
127 | uint32_t size;
128 | } pmlxzj_audio_wav_t;
129 |
130 | typedef struct {
131 | long offset;
132 | uint32_t segment_count;
133 | } pmlxzj_audio_wav_zlib_t;
134 |
135 | typedef struct {
136 | long offset;
137 | uint32_t size;
138 | uint32_t segment_count;
139 | uint32_t* offsets;
140 | } pmlxzj_audio_mp3_t;
141 |
142 | typedef struct {
143 | FILE* file;
144 |
145 | long file_size;
146 | // encrypt_mode: 0(no_encryption), 1(EditLock), 2(PlayLock)
147 | int encrypt_mode;
148 | char nonce_buffer[20];
149 | pmlxzj_footer_t footer;
150 | uint32_t* idx1;
151 | size_t idx1_count;
152 | uint32_t* idx2;
153 | size_t idx2_count;
154 |
155 | pmlxzj_config_14d8_t field_14d8;
156 |
157 | long watermark_offset;
158 | pmlxzj_watermark_t watermark;
159 |
160 | long frame_metadata_offset;
161 | long first_frame_offset;
162 | long frame;
163 |
164 | // Audio metadata
165 | int audio_metadata_version;
166 | long audio_metadata_offset;
167 |
168 | union {
169 | pmlxzj_audio_mp3_t mp3;
170 | pmlxzj_audio_wav_t wav;
171 | pmlxzj_audio_wav_zlib_t wav_zlib;
172 | pmlxzj_audio_aac_t aac;
173 | } audio;
174 | } pmlxzj_state_t;
175 |
176 | typedef struct {
177 | char password[20];
178 | FILE* input_file;
179 | bool verbose;
180 | bool resume_on_bad_password;
181 | } pmlxzj_user_params_t;
182 |
183 | typedef enum {
184 | PMLXZJ_OK = 0,
185 | PMLXZJ_MAGIC_NOT_MATCH = 1,
186 | /** @deprecated */
187 | PMLXZJ_UNSUPPORTED_MODE_2 = 2,
188 | PMLXZJ_ALLOCATE_INDEX_LIST_ERROR = 3,
189 | PMLXZJ_SCAN_U32_ARRAY_ALLOC_ERROR = 4,
190 | PMLXZJ_AUDIO_OFFSET_UNSUPPORTED = 5,
191 | PMLXZJ_AUDIO_TYPE_UNSUPPORTED = 6,
192 | PMLXZJ_PASSWORD_ERROR = 7,
193 | PMLXZJ_AUDIO_INCORRECT_TYPE = 8,
194 | PMLXZJ_AUDIO_NOT_PRESENT = 9,
195 | PMLXZJ_GZIP_BUFFER_ALLOC_FAILURE = 10,
196 | PMLXZJ_GZIP_BUFFER_TOO_LARGE = 11,
197 | PMLXZJ_GZIP_INFLATE_FAILURE = 12,
198 | PMLXZJ_NO_AUDIO = 13,
199 | PMLXZJ_AUDIO_AAC_DECODER_INFO_TOO_SMALL = 14,
200 | PMLXZJ_AUDIO_AAC_DECODER_UNSUPPORTED_SAMPLE_RATE = 15,
201 | PMLXZJ_AUDIO_AAC_INVALID_DECODER_SPECIFIC_INFO = 16,
202 | PMLXZJ_AUDIO_AAC_INVALID_FRAME_SIZE = 17,
203 | } pmlxzj_state_e;
204 |
205 | typedef enum {
206 | PMLXZJ_ENUM_CONTINUE = 0,
207 | PMLXZJ_ENUM_ABORT = 1,
208 | } pmlxzj_enumerate_state_e;
209 |
210 | typedef struct {
211 | // might be wrong
212 | uint32_t left;
213 | uint32_t top;
214 | uint32_t right;
215 | uint32_t bottom;
216 | } pmlxzj_rect;
217 |
218 | typedef struct {
219 | int32_t frame_id;
220 | int32_t image_id;
221 | pmlxzj_rect cord; // image position. all zero = full canvas size or missing
222 | uint32_t compressed_size;
223 | uint32_t decompressed_size;
224 | } pmlxzj_frame_info_t;
225 | typedef pmlxzj_enumerate_state_e(pmlxzj_enumerate_callback_t)(pmlxzj_state_t* ctx,
226 | pmlxzj_frame_info_t* frame,
227 | void* extra_callback_data);
228 |
229 | uint32_t pmlxzj_password_checksum(const char* password);
230 | pmlxzj_state_e pmlxzj_init(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params);
231 | pmlxzj_state_e pmlxzj_init_audio(pmlxzj_state_t* ctx);
232 | pmlxzj_state_e pmlxzj_init_frame(pmlxzj_state_t* ctx);
233 | pmlxzj_state_e pmlxzj_init_all(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params);
234 |
235 | // Frames / images
236 | pmlxzj_enumerate_state_e pmlxzj_enumerate_images(pmlxzj_state_t* ctx,
237 | pmlxzj_enumerate_callback_t* callback,
238 | void* extra_callback_data);
239 | // Audio
240 | pmlxzj_state_e pmlxzj_audio_dump_to_file(pmlxzj_state_t* ctx, FILE* f_audio);
241 |
242 | pmlxzj_state_e pmlxzj_audio_dump_raw_wave(pmlxzj_state_t* ctx, FILE* f_audio);
243 | pmlxzj_state_e pmlxzj_audio_dump_compressed_wave(pmlxzj_state_t* ctx, FILE* f_audio);
244 | pmlxzj_state_e pmlxzj_audio_dump_mp3(pmlxzj_state_t* ctx, FILE* f_audio);
245 | pmlxzj_state_e pmlxzj_audio_dump_aac(pmlxzj_state_t* ctx, FILE* f_audio);
246 |
--------------------------------------------------------------------------------
/pmlxzj_audio.c:
--------------------------------------------------------------------------------
1 | #include "pmlxzj.h"
2 | #include "pmlxzj_audio_aac.h"
3 | #include "pmlxzj_enum_names.h"
4 | #include "pmlxzj_utils.h"
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #define MAX_COMPRESSED_CHUNK_SIZE (0x1E848)
13 | #define ZLIB_INFLATE_BUFFER_SIZE (4096)
14 |
15 | pmlxzj_state_e pmlxzj_init_audio(pmlxzj_state_t* ctx) {
16 | if (ctx->audio_metadata_offset == 0) {
17 | // no audio
18 | return PMLXZJ_NO_AUDIO;
19 | }
20 |
21 | FILE* f_src = ctx->file;
22 | switch (ctx->footer.config.audio_codec) {
23 | case PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED: {
24 | pmlxzj_audio_wav_zlib_t* audio = &ctx->audio.wav_zlib;
25 |
26 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET);
27 | fread(&audio->segment_count, sizeof(audio->segment_count), 1, f_src);
28 | audio->offset = ftell(f_src);
29 | break;
30 | }
31 |
32 | case PMLXZJ_AUDIO_TYPE_WAVE_RAW: {
33 | pmlxzj_audio_wav_t* audio = &ctx->audio.wav;
34 |
35 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET);
36 | fread(&audio->size, sizeof(audio->size), 1, f_src);
37 | audio->offset = ftell(f_src);
38 | break;
39 | }
40 |
41 | case PMLXZJ_AUDIO_TYPE_LOSSY_MP3: {
42 | pmlxzj_audio_mp3_t* audio = &ctx->audio.mp3;
43 |
44 | assert(sizeof(pmlxzj_wave_format_ex) == 0x12);
45 | pmlxzj_wave_format_ex wav_spec;
46 | pmlxzj_wave_format_ex mp3_spec;
47 | uint32_t unk_audio_prop;
48 | uint32_t mp3_chunk_count;
49 |
50 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET);
51 | fread(&wav_spec, sizeof(wav_spec), 1, f_src);
52 | fread(&mp3_spec, sizeof(mp3_spec), 1, f_src);
53 | fread(&unk_audio_prop, sizeof(unk_audio_prop), 1, f_src);
54 | fread(&mp3_chunk_count, sizeof(mp3_chunk_count), 1, f_src);
55 | if (mp3_chunk_count == 0) {
56 | break;
57 | }
58 |
59 | audio->segment_count = mp3_chunk_count + 1;
60 | audio->offsets = calloc(audio->segment_count, sizeof(uint32_t));
61 | fread(audio->offsets, sizeof(uint32_t), audio->segment_count, f_src);
62 | fread(&audio->size, sizeof(audio->size), 1, f_src);
63 | assert(audio->offsets[0] == 0);
64 | audio->offsets[mp3_chunk_count] = audio->size;
65 | audio->offset = ftell(f_src);
66 | break;
67 | }
68 |
69 | case PMLXZJ_AUDIO_TYPE_LOSSY_AAC: {
70 | pmlxzj_audio_aac_t* audio = &ctx->audio.aac;
71 |
72 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET);
73 |
74 | pmlxzj_wave_format_ex wav_spec;
75 | uint32_t unused_field_80;
76 |
77 | fread(&wav_spec, sizeof(wav_spec), 1, f_src);
78 | fread(&unused_field_80, sizeof(unused_field_80), 1, f_src);
79 |
80 | uint32_t format_len;
81 | fread(&format_len, sizeof(format_len), 1, f_src);
82 | if (format_len > sizeof(audio->format)) {
83 | return PMLXZJ_AUDIO_AAC_INVALID_DECODER_SPECIFIC_INFO;
84 | }
85 | fread(&audio->format, format_len, 1, f_src);
86 | pmlxzj_state_e state = pmlxzj_aac_parse_decoder_specific_info(&audio->config, audio->format, format_len);
87 | if (state != PMLXZJ_OK) {
88 | return state;
89 | }
90 |
91 | uint32_t segment_count_in_bytes;
92 | fread(&segment_count_in_bytes, sizeof(segment_count_in_bytes), 1, f_src);
93 | audio->segment_sizes = malloc(segment_count_in_bytes);
94 | audio->segment_count = segment_count_in_bytes / sizeof(uint32_t);
95 | fread(audio->segment_sizes, 1, segment_count_in_bytes, f_src);
96 |
97 | for (uint32_t i = 0; i < audio->segment_count; i++) {
98 | if (audio->segment_sizes[i] > PMLXZJ_AAC_MAX_FRAME_DATA_LEN) {
99 | free(audio->segment_sizes);
100 | audio->segment_sizes = NULL;
101 | return PMLXZJ_AUDIO_AAC_INVALID_FRAME_SIZE;
102 | }
103 | }
104 |
105 | fread(&audio->size, sizeof(audio->size), 1, f_src);
106 | audio->offset = ftell(f_src);
107 | }
108 | }
109 |
110 | return PMLXZJ_OK;
111 | }
112 |
113 | bool inflate_chunk(FILE* output, uint8_t* input, size_t length) {
114 | z_stream strm = {0};
115 | strm.next_in = (Bytef*)input;
116 | strm.avail_in = (uInt)length;
117 |
118 | if (inflateInit(&strm) != Z_OK) {
119 | fprintf(stderr, "Failed to initialize inflate\n");
120 | return false;
121 | }
122 |
123 | bool ok = true;
124 | char buffer[ZLIB_INFLATE_BUFFER_SIZE];
125 |
126 | do {
127 | strm.next_out = (Bytef*)buffer;
128 | strm.avail_out = ZLIB_INFLATE_BUFFER_SIZE;
129 |
130 | int ret = inflate(&strm, Z_NO_FLUSH);
131 |
132 | if (ret == Z_STREAM_ERROR) {
133 | fprintf(stderr, "Zlib stream error\n");
134 | ok = false;
135 | break;
136 | }
137 |
138 | if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) {
139 | fprintf(stderr, "Zlib inflate error %s\n", strm.msg);
140 | ok = false;
141 | break;
142 | }
143 |
144 | size_t output_length = ZLIB_INFLATE_BUFFER_SIZE - strm.avail_out;
145 | fwrite(buffer, 1, output_length, output);
146 | } while (strm.avail_out == 0);
147 |
148 | inflateEnd(&strm);
149 | return ok;
150 | }
151 |
152 | pmlxzj_state_e pmlxzj_audio_dump_to_file(pmlxzj_state_t* ctx, FILE* f_audio) {
153 | if (ctx->audio_metadata_offset == 0) {
154 | fprintf(stderr, "File does not contain audio or not initialized.\n");
155 | return PMLXZJ_AUDIO_NOT_PRESENT;
156 | }
157 |
158 | pmlxzj_state_e result = PMLXZJ_AUDIO_TYPE_UNSUPPORTED;
159 | switch (ctx->footer.config.audio_codec) {
160 | case PMLXZJ_AUDIO_TYPE_LOSSY_MP3:
161 | result = pmlxzj_audio_dump_mp3(ctx, f_audio);
162 | break;
163 |
164 | case PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED:
165 | result = pmlxzj_audio_dump_compressed_wave(ctx, f_audio);
166 | break;
167 |
168 | case PMLXZJ_AUDIO_TYPE_WAVE_RAW:
169 | result = pmlxzj_audio_dump_raw_wave(ctx, f_audio);
170 | break;
171 |
172 | case PMLXZJ_AUDIO_TYPE_LOSSY_AAC:
173 | result = pmlxzj_audio_dump_aac(ctx, f_audio);
174 | break;
175 |
176 | default:
177 | break;
178 | }
179 |
180 | if (result != PMLXZJ_OK) {
181 | printf("ERROR: %s: failed to inflate audio: %d (%s)", __func__, result, pmlxzj_get_state_name(result));
182 | }
183 | return result;
184 | }
185 |
186 | pmlxzj_state_e pmlxzj_audio_dump_raw_wave(pmlxzj_state_t* ctx, FILE* f_audio) {
187 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_WAVE_RAW) {
188 | return PMLXZJ_AUDIO_INCORRECT_TYPE;
189 | }
190 |
191 | pmlxzj_audio_wav_t* audio = &ctx->audio.wav;
192 |
193 | FILE* f_src = ctx->file;
194 | fseek(f_src, audio->offset, SEEK_SET);
195 | pmlxzj_util_copy(f_audio, ctx->file, audio->size);
196 | return PMLXZJ_OK;
197 | }
198 |
199 | pmlxzj_state_e pmlxzj_audio_dump_compressed_wave(pmlxzj_state_t* ctx, FILE* f_audio) {
200 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED) {
201 | return PMLXZJ_AUDIO_INCORRECT_TYPE;
202 | }
203 |
204 | pmlxzj_audio_wav_zlib_t* audio = &ctx->audio.wav_zlib;
205 |
206 | FILE* f_src = ctx->file;
207 | uint32_t len = audio->segment_count;
208 | fseek(f_src, audio->offset, SEEK_SET);
209 |
210 | pmlxzj_state_e result = PMLXZJ_OK;
211 | uint8_t buffer[MAX_COMPRESSED_CHUNK_SIZE] = {0};
212 | for (uint32_t i = 0; i < len; i++) {
213 | uint32_t chunk_size = 0;
214 | fread(&chunk_size, sizeof(chunk_size), 1, f_src);
215 | if (chunk_size > MAX_COMPRESSED_CHUNK_SIZE) {
216 | fprintf(stderr, "compressed audio chunk size too large (chunk=%d, offset=%ld)\n", i, ftell(f_src));
217 | result = PMLXZJ_GZIP_BUFFER_TOO_LARGE;
218 | break;
219 | }
220 | fread(buffer, chunk_size, 1, f_src);
221 | if (!inflate_chunk(f_audio, buffer, chunk_size)) {
222 | result = PMLXZJ_GZIP_INFLATE_FAILURE;
223 | break;
224 | }
225 | }
226 | return result;
227 | }
228 |
229 | pmlxzj_state_e pmlxzj_audio_dump_mp3(pmlxzj_state_t* ctx, FILE* f_audio) {
230 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_LOSSY_MP3) {
231 | return PMLXZJ_AUDIO_INCORRECT_TYPE;
232 | }
233 |
234 | pmlxzj_audio_mp3_t* audio = &ctx->audio.mp3;
235 |
236 | fseek(ctx->file, audio->offset, SEEK_SET);
237 | pmlxzj_util_copy(f_audio, ctx->file, audio->size);
238 | return PMLXZJ_OK;
239 | }
240 |
241 | pmlxzj_state_e pmlxzj_audio_dump_aac(pmlxzj_state_t* ctx, FILE* f_audio) {
242 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_LOSSY_AAC) {
243 | return PMLXZJ_AUDIO_INCORRECT_TYPE;
244 | }
245 |
246 | pmlxzj_audio_aac_t* audio = &ctx->audio.aac;
247 | fseek(ctx->file, audio->offset, SEEK_SET);
248 | uint8_t buffer[PMLXZJ_AAC_ADTS_HEADER_LEN + PMLXZJ_AAC_MAX_FRAME_DATA_LEN] = {0};
249 |
250 | uint32_t n = audio->segment_count;
251 | for (uint32_t i = 0; i < n; i++) {
252 | uint32_t chunk_size = audio->segment_sizes[i];
253 | assert(chunk_size < PMLXZJ_AAC_MAX_FRAME_DATA_LEN);
254 |
255 | size_t header_len = pmlxzj_aac_adts_header(buffer, &audio->config, chunk_size);
256 | assert(header_len == PMLXZJ_AAC_ADTS_HEADER_LEN);
257 |
258 | size_t bytes_read = fread(&buffer[header_len], 1, chunk_size, ctx->file);
259 | assert(bytes_read == chunk_size);
260 |
261 | fwrite(buffer, 1, header_len + bytes_read, f_audio);
262 | }
263 | assert(ftell(ctx->file) == (long)(audio->offset + audio->size));
264 |
265 | return PMLXZJ_OK;
266 | }
267 |
--------------------------------------------------------------------------------
/pmlxzj_audio_aac.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "pmlxzj.h"
4 |
5 | #include
6 | #include
7 |
8 | #define PMLXZJ_COUNT_OF(arr) (sizeof(arr) / sizeof(arr[0]))
9 | #define PMLXZJ_AAC_ADTS_HEADER_LEN (7)
10 | #define PMLXZJ_AAC_MASK_BY_BITS(val, n) ((val) & ((1 << n) - 1))
11 | #define PMLXZJ_AAC_MAX_FRAME_DATA_LEN ((1 << 13) - 1 - PMLXZJ_AAC_ADTS_HEADER_LEN)
12 |
13 | inline static const char* pmlxzj_adts_get_profile_name(uint8_t profile) {
14 | static const char* names[4] = {"aac-main", "aac-lc", "aac-ssr", "aac-he?"};
15 | if (profile < PMLXZJ_COUNT_OF(names)) {
16 | return names[profile];
17 | }
18 | return "unknown";
19 | }
20 |
21 | inline static uint32_t pmlxzj_adts_get_sample_rate(uint8_t sample_rate_idx) {
22 | static const uint32_t sample_rates[] = {
23 | 96000, 88200, 64000, 48000, 44100, 32000,
24 | 24000, 22050, 16000, 12000, 11025, 8000
25 | };
26 |
27 | if (sample_rate_idx < PMLXZJ_COUNT_OF(sample_rates)) {
28 | return sample_rates[sample_rate_idx];
29 | }
30 |
31 | return 0;
32 | }
33 |
34 | inline static uint8_t pmlxzj_aac_map_profile_to_adts(uint8_t profile) {
35 | switch (profile) {
36 | case 1: // AAC Main
37 | return 0;
38 | case 2: // AAC LC
39 | return 1;
40 | case 3: // AAC SSR
41 | return 2;
42 | default: // (5) SBR or other: HE?
43 | return 3;
44 | }
45 | }
46 | inline static uint8_t pmlxzj_aac_map_sample_rate_idx(uint8_t sample_rate_idx) {
47 | // Sample rate index is (0..11) inclusive.
48 | if (sample_rate_idx < 12) {
49 | return sample_rate_idx;
50 | }
51 |
52 | return 0xFF;
53 | }
54 |
55 | inline static pmlxzj_state_e pmlxzj_aac_parse_decoder_specific_info(pmlxzj_aac_audio_config_t* result,
56 | const uint8_t* buf,
57 | size_t len) {
58 | if (len < 2) {
59 | return PMLXZJ_AUDIO_AAC_DECODER_INFO_TOO_SMALL;
60 | }
61 |
62 | const uint8_t profile = PMLXZJ_AAC_MASK_BY_BITS(buf[0] >> 3, 5);
63 | const uint8_t sample_rate = PMLXZJ_AAC_MASK_BY_BITS((buf[0] << 1) | (buf[1] >> 7), 4);
64 | const uint8_t channels = PMLXZJ_AAC_MASK_BY_BITS(buf[1] >> 3, 4);
65 |
66 | if (sample_rate == 0xFF) {
67 | return PMLXZJ_AUDIO_AAC_DECODER_UNSUPPORTED_SAMPLE_RATE;
68 | }
69 |
70 | result->profile_id = pmlxzj_aac_map_profile_to_adts(profile);
71 | result->sample_rate_id = sample_rate;
72 | result->channels_id = channels;
73 | return PMLXZJ_OK;
74 | }
75 |
76 | // header should have size of 9 or more to be safe.
77 | // returns the number of bytes required by the header.
78 | // `frame_data_len` should be `PMLXZJ_AAC_MAX_FRAME_DATA_LEN` or lower.
79 | inline static size_t pmlxzj_aac_adts_header(uint8_t* header,
80 | const pmlxzj_aac_audio_config_t* config,
81 | uint16_t frame_data_len) {
82 | header[0] = 0xff; // sync word (12-bits)
83 |
84 | // sync word: 4-bits
85 | const uint8_t mpeg_version = PMLXZJ_AAC_MASK_BY_BITS(1, 1); // 1-bit
86 | const uint8_t layer = PMLXZJ_AAC_MASK_BY_BITS(0, 2); // 2-bits
87 | const uint8_t protection_absence = PMLXZJ_AAC_MASK_BY_BITS(1, 1); // 1-bit
88 | header[1] = 0xf0 | (mpeg_version << 3) | (layer << 1) | protection_absence;
89 |
90 | const uint8_t profile = config->profile_id; // 2-bits
91 | const uint8_t sample_rate_idx_mapped = PMLXZJ_AAC_MASK_BY_BITS(config->sample_rate_id, 4); // 4-bits
92 | const uint8_t private_bit = 0; // 1-bit
93 | const uint8_t channels_idx = PMLXZJ_AAC_MASK_BY_BITS(config->channels_id, 3); // 3-bits (1 + 2)
94 | header[2] = (profile << 6) | (sample_rate_idx_mapped << 2) | (private_bit << 1) | (channels_idx >> 2);
95 |
96 | // channel (2-bits)
97 | const uint8_t originality = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit
98 | const uint8_t home_usage = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit
99 | const uint8_t copyrighted = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit
100 | const uint8_t copyright_start = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit
101 | const uint16_t frame_size =
102 | PMLXZJ_AAC_MASK_BY_BITS(frame_data_len + PMLXZJ_AAC_ADTS_HEADER_LEN, 13); // 13-bits (2 + 8 + 3)
103 | header[3] = (channels_idx << 6) | (originality << 5) | (home_usage << 4) //
104 | | (copyrighted << 3) | (copyright_start << 2) | (frame_size >> 11);
105 | header[4] = frame_size >> 3;
106 |
107 | // frame_size (3-bits)
108 | const uint16_t buffer_fullness = PMLXZJ_AAC_MASK_BY_BITS(0x7FF, 11); // 11-bits (5 + 6)
109 | header[5] = (uint8_t)((frame_size << 5) | (buffer_fullness >> 6));
110 |
111 | // buffer_fullness (6-bits)
112 | const uint8_t number_of_aac_frames = PMLXZJ_AAC_MASK_BY_BITS(0, 2); // 2-bits
113 | header[6] = (uint8_t)(buffer_fullness << 2) | (number_of_aac_frames);
114 |
115 | return 7;
116 | }
117 |
--------------------------------------------------------------------------------
/pmlxzj_commands.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | int pmlxzj_cmd_disable_audio(int argc, char** argv);
6 | int pmlxzj_cmd_extract_audio(int argc, char** argv);
7 | int pmlxzj_cmd_print_info(int argc, char** argv);
8 | int pmlxzj_cmd_unlock_exe(int argc, char** argv);
9 | int pmlxzj_cmd_remove_watermark(int argc, char** argv);
10 |
11 | static inline void pmlxzj_usage(char* argv0) {
12 | char* name = basename(argv0);
13 | printf("Usage:\n\n");
14 | printf(" %s audio-dump