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

环境

首先不知道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是会变的- -

直接贴代码(2021.9.12更新):

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卡了一下,然后又重写了一次,有些细节的东西下次再补吧(咕咕咕)
强烈建议站主加个草稿功能!

    Tover 图片刚好就是我今天的状态,听歌做蛋糕,抱走了

    16 天 后

    才发现标题吧 o 打成 p 了,这大概就是当年 own 被打成 pwn 的原因吧 (ノ_ _)ノ

      0x0001 更改标题为「Deemo提取MIDI
      9 个月 后

      luke100
      如果从他代码看的话,只要转换的时候发生了(任何的)错误都会提示is not converted的😅

              try:
                  convert_file(os.path.join(input_folder,json_file), os.path.join(dest_folder,json_file+'.mid'))
              except:
                  print(json_file, ' is not converted.')

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

      然后瞄了一下,应该还是midiutil的上古问题,你可以试试找到midiutil的MidiFile.py这个文件,然后找到图片中对应的位置(他没更新过的话应该还是889行),然后按图片那样改一下就好了。

      考虑到不是计算机/资讯相关背景的话,提供一种可以比较快找到MidiFile.py的方式:

       for json_file in file_names:
              try:
                  convert_file(os.path.join(input_folder,json_file), os.path.join(dest_folder,json_file+'.mid'))
              except:
                  print(json_file, ' is not converted.')

      先改为(注意改完后的tab的对齐):

       for json_file in file_names:
              convert_file(os.path.join(input_folder,json_file), os.path.join(dest_folder,json_file+'.mid'))

      然后运行一遍,估计会有一个报错,报错里就会有MidiFile.py的位置,改完MidiFile.py后再把DeemoToMidi.py恢复回去就好了 🙂

        Tover
        照著你的方式修改midiutil的bug之後果然沒有某些檔轉不成功的情況了,萬分感謝!

        1 年 后

        © 2018-2025 0xFFFF