( 原文地址:https://0xffff.one/d/487 )

图:自己和自己过情人节。。。

环境·

首先不知道deemo是什么的估计对这篇帖子也不会感兴趣,这部分就省下不写了(咕咕咕)。由于deemo也是Unity做的,所以在Android破解进阶之:元气骑士中的大部分工具这里也适用。

  • Deemo安装包:要带有obb文件的(1G+的那个),因为数据主要在obb文件里,找不到的可以在这里下载。
  • AssetStudio:提取asset,下载
  • Python环境:人生苦短啊。。。

解包·

下载完后把apk/apkx文件解包,然后再找到obb文件把obb文件解压,AssetStudio打开并提取asset文件夹的内容。提取后的AudioClip文件夹里是游戏原声,Texture2D文件夹里是游戏原画(含剧透,慎重打开)。提取MIDI的话主要是TextAsset文件夹里的json文件,里面存有音符的信息,对于每首曲子有easy、normal、hard(、extra)难度的对应json文件,但其实MIDI信息是一样的,随便用一个就好。

json文件里主要关注notes数组的信息,尤其是sounds里面的d、p、v和notes的_time,由于MIDI是二进制的文件,所以就用了Python的MIDIUtil来生成。文档里面有例程,可以直接用,其中sounds的d、p、v分别对应MIDIUtil的duration、pitch、volume,notes的_time对应MIDIUtil的time,track和channel是轨道之类的信息,默认0就可。BPM的话可以用librosa提取(其实librosa也不是很准的,还慢,所以还是自己搞了个- -),嫌麻烦的话可以直接填60。要注意一下json文件里的_time单位是秒,MIDIUtil的time单位是one beat。

另:

  • duration是一个音的延续时间。
  • pitch是音高,即do re mi那些。
  • volume是音量,都懂的。
  • time是出现时间,注意单位。
  • 还有bpm只能其参考作用,毕竟有些bpm是会变的- -

直接贴代码:

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
import json
from midiutil.MidiFile import MIDIFile
import numpy as np
import os
import pandas as pd


RESULT_DIR = 'path\\to\\your\\result_dir'
JSON_DIR = 'path\\to\\your\\asset_exported_dir\\TextAsset\\'

# get file name
tmp = []
for _,_,f in os.walk(JSON_DIR):
tmp = f
files = []
for t in tmp:
if 'hard' in t:
files.append(t)

#print(files)

# run
werrors = []
jerrors = []
for name in files:
print('starting -> '+name)
# get notes in file
try:
with open(JSON_DIR+name,'r') as f:
datas = json.loads(f.read())
except:
jerrors.append(name)
print('get json error -> '+name)
print('------------------')
continue
speed = datas['speed']
#print(speed)
#print(list(datas))

notes = datas['notes']
#print(list(notes[0]))

sounds = []
for n in notes:
try:
try:
t = n['_time']
except:
t = 0
for s in n['sounds']:
sounds.append({'d':s['d'],'p':s['p'],'v':s['v'],'t':t})
except KeyError:
#print('!')
None
#print(len(sounds))
#print(list(notes[0]['sounds'][0]))
try:
start = sounds[0]['t']
for s in sounds:
s['t'] -= start
except:
None

# get bpm
times = []
for s in sounds:
#if s['p']<50:
times.append(s['t'])
#times = [s['t'] for s in sounds]
if(len(times)<2):
bpm = 60
else:
dt = []
for i in range(len(times))[1:]:
r = times[i]-times[i-1]
if r != 0:
dt.append(r)
gm = pd.Series(data=dt)
bpm = 60/gm.mode()[0]
while(bpm>200):
bpm /= 2
while(bpm<60):
bpm *= 2
bpm = round(bpm)
#bpm = int(bpm)
print('bpm -> '+str(bpm))

# write midi
MyMIDI = MIDIFile(1)
track = 0
time = 0
bias = float(bpm/60)
# Add track name and tempo.
MyMIDI.addTrackName(track,time,name+'_deemo')
MyMIDI.addTempo(track,time,bpm)

for s in sounds:
MyMIDI.addNote(0,0,s['p'],s['t']*bias,s['d']*bias,s['v'])
# And write it to disk.
try:
binfile = open(RESULT_DIR + name.split('.')[0]+'.mid', 'wb')
MyMIDI.writeFile(binfile)
binfile.close()
except:
werrors.append(name)
print('write file error -> '+name)
#continue

#print('finished -> '+name)
print('------------------')

print('json_errors:')
for e in jerrors:
print(e)

print('write_error:')
for e in werrors:
print(e)

代码是提取全部的,如果要提取单个的话可以把for循环的东西拿出来用,name填单个的名字。

MIDIUtil的bug·

用MIDIUtil时有的文件会报下面的错误(github上有同样的issue的,不过2018年到现在都没人修,估计是不维护了- -):

可以找到有问题的MidiFile.py文件,找到下面地方:

然后改成这样:

测试过运行起来没问题。

结果·

MIDI文件可以直接用播放器播放(如果播放器支持的话),也可以用Synthesia之类的软件打开(上次破解没破文件打开hhhh),打开后是这样:

(level10的曲子enmmm)

Ps: 写的时候Chrome卡了一下,然后又重写了一次,有些细节的东西下次再补吧(咕咕咕)强烈建议站主加个草稿功能!