| 本帖最后由 Zleepwalking 于 2015/3/22 18:26 编辑 
 2015.3
 本贴所包含信息时间过于久远,已废弃。出于保留项目历史原因在此搁置。
 
 这章东西即好玩又水表= =。。。我期待了好几天总算写到这里了。。。感兴趣的话推荐慢慢看。。。
 说不定会对你们调Vocaloid有帮助。。。
 
 注:所有关于反向工程的东西都只是为了学习交流使用,严禁用于任何商业用途!!!
 
 
 不过说实话我也没放很多心思到反工上去,后来发现反工对造Rocaloid没啥用就停掉了。
 最近受委托去详细研究一下ddb文件格式。。。写完文章以后我会的。如果研究出什么了,我就加进这篇文章里。
 本人不是专门搞反汇编的,所以若有大牛路过。。。见笑了。
 
 
 
 
 几个常用工具:WinHex, Ollydbg, Goldwave, Exescope, FileMon, Praat。哈哈都是高能的东西。如果你有个类似暗组的黑客工具箱就更爽了。。。
 
 1. Vocaloid的双声道合成是个幌子
 
 估计这个应该很多人已经发现了吧。。?        Vocaloid合成时勾选Stereo,其实合成出来两个声道是一样的。
 至少我安装的洛天依和初音还有n个初音append都是单声道。
 
 2. 对V3安装目录下一堆文件的作用说明
 
 
 
 
 
  
 
 | VOCALOID3.exe | VOCALOID3主程序 |  | DSE3.dll | VOCALOID3合成引擎 |  | DSE3_DFT.dll | 字面理解是离散傅立叶变换算法,推测应该包括一系列DSE的频域算法 |  | DSCL3.dll | 这个东西作用很杂,控制DSE3、控制编辑器、控制文件读写等等…… |  | dbm3.dll | 用来读写歌手属性的 |  | vedit3.dll | 歌手属性编辑器 |  | xerces-c_3_1.dll | xml解析器 |  | g2pa3_ENG.dll | 英语发音记号支持 |  | g2pa3_KOR.dll | 韩语发音记号支持 |  | g2pa3_ESP.dll | 西班牙语发音记号支持 |  | g2pa3_CHS.dll | 汉语发音记号支持 |  | g2pa3_JPN.dll | 日语发音记号支持 |  | udm3_eng.dll | 英文用户词典编辑器 |  | Vsq3.dll | 对VSQ、VSQX文件和数据结构的支持 |  | VstHost3.dll | Vst插件管理器 | 
 
 
 3. 对g2pa3_JPN.dll的研究
 
 通过这个方法,以后在Vocaloid3中调教初音,或者任何一款日文音源,输入汉语拼音,直接出对应的日文发音记号组合。
 V3的日文发音记号转换在g2pa3_JPN.dll里,这里面有一堆罗马音和对应的发音记号。
 你若用WinHex打开g2pa3_JPN.dll,从十六进制10300附近的偏移地址,会看到:
 
 
  
         每十进制40个字节会有一个新的罗马音的定义,这四十个字节中前32个字节用于存储罗马音(Lyric),后8个用于存储日文音源发音记号。若如下图替换成中文发音的日文记号: 
 
  
         保存。因为是字节对齐的,所以不用担心损坏文件。         打开V3,切到日文音源输入一个ka进去: 
 
  
         ka被自动转换成k h a了,而不是k a。         同理,你可以把其他拼音改掉。         不过汉语拼音的总数远大于日文罗马音,这个dll会塞不下。 
        g2pa3_JPN.dll搜索一个罗马音对应的发音记号的算法在偏移地址00001A20这个函数里: 
  
        上图中标出的那条指令将g2pa3_JPN.dll中存储的(发音词典的)地址载入寄存器。也就是说把这个地址改到我们自己注入的发音词典就可以实现自制发音词典。此地址对应在文件中为: 
 
  
        10 01 3C 30是一个偏移地址,指向dll载入内存后数据段中一个发音词典表的开始地址。        实现自定义g2pa3_JPN.dll的思路:        1. 给dll增加一个数据段。        2. dll文件大小扩展        3. 在扩展的范围内写入自定义词典数据        4. 将偏移地址更改,使其指向新的词典数据。 
 4. ddi文件
 
 
 ddi文件位于V3安装目录的VoiceDB文件夹下的子文件夹里。
 我怀疑ddb = DSE Data Base, ddi = DSE Data Index。。。?这是个数据库索引。
 文件结构略复杂,目前只解析了很小的一部分。
 
 
  
        ddi开头先把所有发音符号罗列了一遍。        事实上如果你把其中一个发音符号改掉,比如把a改成abc,然后打开V3,输入一个abc的发音符号,她照样能把a唱出来。        你还可以看到几个呼吸声:Sil Asp br1 br2 br3 br4 br5 
        接下来是对各种发音记号的语音学上的分类: 
          从分类里整理出的表格: Bresathings似乎是yamaha的拼写错误。。。。| Vowels | a i M e o |  | Nasals | n J m N m' N' N\ |  | VoicedPlosives | g g' d d' b b' |  | VoicedFricatives | z Z h\ |  | VoicedAffricates | dz dZ |  | Liquids | 4 4' |  | Semivowel | j w |  | UnvoicedPlosives | k k' t t' p p' |  | UnvoicedFricatives | s S h C p\ p\' |  | UnvoicedAffricates | ts tS |  | GlottalPlosive | 无 |  | Bresathings | *in *out br5 br4 br3 br2 br1 |  | Silence | Sil Asp | 
 
 
 你可以尝试替换、修改这些分类,但是不要指望这能解决断元音的问题。
 我尝试把n和某个辅音替换了一下,结果无非是:
 原来a n调出来是啊啊啊啊啊嗯
 改了以后a n调出来是啊嗯嗯嗯嗯嗯
 
 后面的东西我基本没有解析,因为找不到可以参照的东西。
 不过我看到了有一段乱码是这样的:
 
 
  
         SND这个标签我在miku.ddb里也看到了,这个标签代表一段时域信号的开始。         我在ddb里搜索SND,找到1664个,而ddi里找到1621个,两个数字非常接近!所以我觉得这个SND附近肯定以某种方法表明了ddb内相应时域信号的地址!但是我在ddi里找不到直接的对应ddb中SND的地址。。。期待有人能把这个坑填上。 
         5月30日自填坑:ddi里可以看到有很多如上图打头的部分重复,后面跟了一段这样的东西: 
          我怀疑这两排字存储的是ddb中的地址,于是我找了第一个:42 A7 86 05,倒序即0586A742,在ddb中这个地址对应的是: 
          于是我看到了一个FRM2的标签,我又把其他的数值当地址代进去,均指向不同位置的FRM2标识。因此可以推测这么一段ddi记录的是ddb中一个完整的Diphone的SND和一堆FRM2的地址。         若要反编DSE3查出这些标签究竟是何意思,这么大的工程恐怕非本人力所能及……求组队。。。 
         6月1日更新:         一般ddb中会连续记录一个发音记号在三个左右不同音调下的发音(FRM2 + ENV + ENV + SND)×3。ddi中由ARTp头开始的数据记录也大多是三个连续在一起,对应ddb中三个相同发音不同音高的数据记录。但是ddi和ddb中发音记号的记录顺序是不同的。下图是ddi中的一个发音记号在某个音高下的记录: 
  这是目前ddi中已经解析的部分 
 5. ddb文件 
 
 
 上面已经提到,ddi是ddb的索引,而且我找到了ddi和ddb的FRM2标识的对应关系。 我到目前一共在ddb中找到了三个标识:FRM2, SND, ENV。 我猜测FRM = Frame(帧),SND = Sound (声音),ENV = Envelope(包络)。仅是猜测。 (6月1日)现在猜测FRM或ENV可能记录了共振峰信息。 
 先抛开这些不管,来看看怎么直接打开ddb。 cxm菊苣很久以前对ddb做过一个拆包程序: http://bbs.ivocaloid.com/thread-58004-1-1.html 这个程序只是对SND波形文件的拆包,但是他的研究提供了一些有价值的信息。 cxm的拆包程序头几行: 
 for i = 0    findloc SNDFILE string "SND "    goto SNDFILE    get SIGN long    get SIZE long    get FREQUENCY long    get CHANNELS short    get DUMMY long
 我目前还不知道SIGN和DUMMY是代表什么,另外貌似这几个get的顺序反掉了。。?我没有用过BMS拆包器所以语法只是靠猜的。。。FREQUENCY虽然字母意思是频率但是我目前还不知道在ddb中是用什么单位表述的。先在ddb中找到一个SND拿它开刀:
 
 
  
 按照cxm的拆法的基础上纠正顺序问题,可以看出: SIZE = 5E12 = 24082 (SND块大小) FREQUENCY = AC44 = 44100 (SND块的采样率) CHANNELS = 0001 = 1 (SND快为单声道) 如果那0593ECC4这个地址加上5E12,也就是size,你会落在05944AD6上,而这个地方正好是一个FRM2。 所以。。。用Goldwave直接打开这个ddb文件,设定为16bits, 44100HZ,mono, PCM:
 
  
 打开以后会变成这个样子: 
 
  
 如果你不停地放大,会看到: 
 
  
 在很多杂音间有时域波形!其实这些波形就是SND段。如果选中播放,你可以听见藤田咲的录音。 这些波形都是一个个diphone,即两个发音符号的过渡,也有少数是只有一个元音的。 SND总共的数量大约有1600个,一个发音一般有三个不同音高的SND。DSE通过频域算法综合多个音高进行变调。 那么分隔开这些时域波形的,就应该是FRM2和ENV,或者什么别的尚未发现的标识了。
 为了了解SND FRM2 ENV的排列顺序,我用vb.net写了个程序专门搜索这些标识,为了提高速度尽量使用字节级的方式比较: 
 '' Created by SharpDevelop.
 ' Sleepwalking
 '
 '
 Imports System.IO
 Module Program
 Public DDBReader As BinaryReader = New BinaryReader(New FileStream("Z:\miku.ddb", FileMode.Open))
 Public Output As StreamWriter = New StreamWriter("Z:\peek.txt")
 Sub Main()
 Dim pos As Integer
 Dim len As Integer = CInt(DDBReader.BaseStream.Length) - 1
 Dim b0, b1, b2, b3 As Byte
 For pos = 0 To len
 b0 = b1:b1 = b2:b2 = b3
 b3 = DDBReader.ReadByte()
 If b0 = 70 AndAlso b1 = 82 AndAlso b2 = 77 AndAlso b3 = 50 Then
 'FRM2
 Output.WriteLine("FRM2    " & pos - 3)
 Else
 If b0 = 83 AndAlso b1 = 78 AndAlso b2 = 68 AndAlso b3 = 32 Then
 'SND
 Output.WriteLine("SND    " & pos - 3)
 Else
 If b0 = 69 AndAlso b1 = 78 AndAlso b2 = 86 AndAlso b3 = 32 Then
 'ENV
 Output.WriteLine("ENV    " & pos - 3)
 End If
 End If
 End If
 If pos Mod 1000000 = 0 Then
 Console.WriteLine(CInt(pos / len * 100) & "%")
 End If
 Next
 Output.Close()
 Console.Write("Press any key to continue . . . ")
 Console.ReadKey(True)
 End Sub
 End Module
 
 
 
 结果在Z盘(我的Ramdisk。。。)生成了一个peak.txt,差不多有3MB大: 
 
  
 由此可见每个FRM2后面跟两个ENV,然后每个SND后面有【好几十】个FRM2和【好几十×2】个ENV 5月31日更新:为了查找ddb中更多的标识符,我写了个程序:
 Imports System.IO
 Module Program
 Public DDBReader As BinaryReader = New BinaryReader(New FileStream("Z:\miku.ddb", FileMode.Open))
 Public Output As StreamWriter = New StreamWriter("Z:\peek.txt")
 Public Idt(100000) As String
 Public IdtNum(100000) As Integer
 Public IdtQ As Integer
 Public Comp As Double = 0
 Function GetIdtNum(ByVal Str As String) As Integer
 Dim i As Integer
 For i = 0 To IdtQ
 If Idt(i) = Str Then
 return i
 End If
 Next
 Return -1
 End Function
 Sub AddIdt(ByVal Str As String)
 Idt(IdtQ) = Str
 IdtQ += 1
 End Sub
 Sub CheckAddIdt(ByVal Str As String)
 Dim iNum As Integer = GetIdtNum(Str)
 If iNum <> -1 Then
 'Existing
 IdtNum(iNum) += 1
 Else
 If Comp < 0.07 Then AddIdt(Str)
 End If
 End Sub
 Sub Main()
 IdtQ = 0
 Dim pos As Integer
 Dim len As Integer = CInt(DDBReader.BaseStream.Length) - 1
 Dim b0, b1, b2, b3 As Byte
 For pos = 0 To len
 b0 = b1:b1 = b2:b2 = b3
 b3 = DDBReader.ReadByte()
 If b0 > 64 AndAlso b0 < 91 AndAlso _
 b1 > 64 AndAlso b1 < 91 AndAlso _
 b2 > 31 AndAlso b2 < 127 AndAlso _
 b3 > 31 AndAlso b3 < 127 AndAlso _
 Not (b0 > 96 AndAlso b0 < 123) AndAlso _
 Not (b1 > 96 AndAlso b1 < 123) AndAlso _
 Not (b2 > 96 AndAlso b2 < 123) AndAlso _
 Not (b3 > 96 AndAlso b3 < 123) Then
 CheckAddIdt(Chr(b0) & Chr(b1) & Chr(b2) & Chr(b3))
 End If
 If pos Mod 1000000 = 0 Then
 Console.WriteLine(CInt(pos / len * 100) & "%")
 Console.WriteLine(IdtQ)
 Comp = pos / len
 End If
 Next
 Dim i As Integer
 For i = 0 To IdtQ
 Output.WriteLine(Idt(i) & "    " & IdtNum(i))
 Next
 Output.Close()
 Console.Write("Press any key to continue . . . ")
 Console.ReadKey(True)
 End Sub
 End Module
 反正执行起来花不了太多时间,我懒得优化算法了。。。该程序寻找一切开头两个是大写,剩下两个字节在ASCII码范围内的标识符。。我还不确认这样能把所有的标识符写出来。有空我会写个更好的分析器。
 
 
 在excel里排序数据,前几排:
 
 | ENV | 130632 |  | FRM2 | 67394 |  | JT2? | 65132 |  | JT2; | 65131 |  | JT2< | 65131 |  | JT2= | 65131 |  | JT2> | 65131 |  | CENV | 13992 |  | NV L | 5478 |  | RM20 | 5474 |  | NV D | 3542 |  | RM2( | 3531 |  | RM2 | 3394 |  | NV | | 3204 |  | RM2` | 3190 |  | NV T | 2182 |  | RM28 | 2181 |  | EH?@ | 2108 |  | QI?@ | 2046 |  | AP&. | 1989 |  | SQ?@ | 1967 |  | AJT2 | 1914 |  | KX?@ | 1865 |  | NA?@ | 1802 |  | HP?@ | 1768 |  | YB?@ | 1746 |  | NV < | 1699 |  | VY?@ | 1692 |  | SND | 1620 |  | VY>@ | 1542 |  | VY=@ | 1453 |  | NV \ | 1262 |  | RM2@ | 1252 |  | NA? | 1092 |  | YB? | 1082 |  | NA?` | 1080 |  | QI?` | 1059 |  | KX? | 1053 |  | VY? | 1048 |  | VY?` | 1036 |  | QI?U | 1033 |  | KX?` | 1032 |  | EH? | 1025 |  | HP?` | 1024 |  | EH?` | 1019 |  | EH?U | 1015 |  | YB?` | 1009 | 
 所以。。。有些长度为3的或为2的标识符被强拆成了n个长度4的标识符,也有些长度为4的被识别错位了。。。不管了,综合一下:
 ENV
 FRM2
 JT2
 SND
 其中有些也有可能是枚举类型或者意外的数据重合什么的。。鬼知道呢。。。
 不过从ddb的HEX看JT2根本不是标识符,更像是枚举类型或者碰巧重复的数据而已。
 结果最后还是卡死在ENV FRM2和SND上。。。
 
 5月31日新研究成果:FRM2后面四个字节保存着FRM2段的长度,如第一个FRM2后面跟着2D10,那么第二个FRM2的地址即在0000 + 2D10 = 2D10处。
 
   
   但是ENV段被跳掉了,说明ENV是FRM2中的一个内部结构。
 
 
 而对于ENV段,ENV后面四个字节保存的也是一个长度,代表这个ENV的长度。108C + 032C = 13B8:
 
   13B8处貌似只是个数据空隙,过了这个空隙后的数据仍然和之前的ENV数据很相似,然后过了一段又有一个数据空隙,然后下一个ENV段就出现了,这个ENV段的长度加上ENV的地址后正好是下个FRM2段的地址。
 
 6月1日更新:每个FRM2后有两个ENV,其中第一个ENV较短,第二个ENV的长度固定,始终为1C17个字节长。第一个ENV的长度约300(HEX)左右浮动。
 
 6. 改ddb
 
 
 
 由于目前没有完全解析ddb,所以如cxm所说,重组包是不可能的。但是,ddb是个好几百个M的文件,如果进行文件校验肯定要花很长时间,所以我猜测yamaha并没有在vocaloid中包含对ddb的文件校验,换而言之,存在可能能够修改ddb!
 其实在我之前Farter(ID: http404)已经试过改ddb了。我在此描述一下,并演示把葛炮的“a”塞进初音的ddb后合成的效果。
 根据上面的分析,找到初音“a”,在C3左右音高的SND。这个SND在ddb最后一段可以找到。记录下SND的长度,然后去葛炮的音源库里把“a.wav”揪出来,FFT时间延长到SND的长度(0.533秒)。
 把原来的SND更改音量到0,然后把延长后的葛炮的a混音上去(为了保证字节仍然对齐)。保存ddb。。。耐心等待。
 我的电脑是固态硬盘,操作这么大的文件会很烧,所以我在Ramdisk里建了个沙箱,然后把Pocaloid装进内存里的沙箱里,下面是合成后的波形:
 
 
  
 (双声道是幌子= =) 听上去是“啊!啊啊啊....” 合成出来的声音: http://pan.baidu.com/share/link?shareid=549292&uk=3423845838 所以DSE2伸缩一个元音的方法是频域的循环过渡。 不过我倒更奇怪了,既然仅仅改SND就能把DSE骗过去,那么FRM2和ENV到底有什么用呢。。。? 
 
 DSCL3、DSE3合成时的函数调用分析 
 
 
 我本来怀着一丝希望能通过改DSE3解决V3的断元音功能(一丝。。。。一丝希望而已),于是去反编DSE3,结果第二天发现音源库的Diphone结构根本不可能实现断元音问题的解决,所以对DSE3的反编就停掉了。 
 VOCALOID.004EE550
 DSCL3.CDaisyMidiBuffer::GetEndTimePosition
 DSCL3.CRtSynthesis::GetDefaultSampleRate
 DSCL3.CRtSynthesis::SetDseVVoiceTable
 DSCL3.CDaisyMidiBuffer::objID
 DSCL3.CRtSynthesis::DoSynthesis (1002F400)
 DSCL3.CRtSynthesis::RTS_DeleteResidentDSE
 Ret
 DSCL3.CRtSynthesis::CreateDSE
 DSE3.DSEGetVersion
 DSCL3.CRtSynthesis::SendVVoiceTableToDse
 DSE3.DSECreate
 DSE3.DSESetStaticSetting
 DSCL3.CRtSynthesis::DoSynthesis (1002A1E0)
 DSCL3.1003B75D
 1002A2A2 DSCL3.CRtSynthesis::PrepareWavFile
 1002A2F5 DSCL3.CRtSynthesis::MsToSample
 1002A322 DSCL3.CDaisyMidiBuffer::SeekToBegin
 1002A32E DSCL3.CDaisyMidiBuffer::ReadNextEntry
 1002A378 DSCL3.CRtSynthesis::RTS_GetFrameSize
 1002A391 DSE3.DSEStart
 1002A3EF DSCL3.CRtSynthesis::MsToSample
 1002A404 DSCL3.CRtSynthesis::MsToSample
 1002A4B0 DSCL3.CRtSynthesis::MsToSample
 1002A4E9 DSCL3.CDaisyMidiBuffer::ReadEvent
 1002A578 DSCL3.CDaisyMidiBuffer::ReadNextEntry
 1002A4B0 DSCL3.CRtSynthesis::MsToSample
 1002A5DB DSE3.DSEDoStepSynthesis
 1002A4B0 DSCL3.CRtSynthesis::MsToSample
 1002A5DB DSE3.DSEDoStepSynthesis
 1002A610 DSCL3.CWaveFile::AppendSample
 1002A4B0 DSCL3.CRtSynthesis::MsToSample
 1002A5DB DSE3.DSEDoStepSynthesis
 1002A610 DSCL3.CWaveFile::AppendSample (LOOP)
 1002A4B0 DSCL3.CRtSynthesis::MsToSample
 1002A5DB DSE3.DSEDoStepSynthesis
 1002A610 DSCL3.CWaveFile::AppendSample
 ...
 DSE3.DSEDelete
 Vsq3.CVSVsqManagerIF::editCurrentCommitPoint
 DSCL3.CSequenceObject::GetPitchBendBreakPointList
 DSCL3.CSequenceObject::GetPitchBendBreakPointList
 DSCL3.CRtSynthesis::MsToSample
 DSCL3.CRtSynthesis::GetDefaultSampleRate
 DSCL3.CRtSynthesis::GetDefaultSampleRate
 DSCL3.CRtSynthesis::PrepareWavFile
 Vsq3.CVSVsqManagerIF::vsqGetVsqObject
 DSCL3.CRtSynthesis::ConcatenatePartWavFiles
 
 
 这是DSE3每次执行DoStepSynthesis的内部调用: DSE3.DSEDoStepSynthesis
 01B617FF DSE3.01B798F0
 01B799AF DSE3.01BBF010
 01B799F5 DSE3.01BA9F30
 01B79A1B DSE3.01B82080
 01B79A36 DSE3.01B81DF0
 01B79A50 DSE3.01B81E20
 01B79A75 DSE3.01B79090
 01B79A7D DSE3.01B70E60
 01B79A94 DSE3.01B78B50
 DSE3.01B787E0
 DSE3.01B77170
 DSE3.01BD4910
 fsetpos
 *DSE3.01BAC000
 DSE3.01BABAC0
 fread
 fread
 DSE3.01BC5710
 DSE3.01BBF010
 DSE3.01BB5BD0
 DSE3.01BB5BD0
 fread
 fread
 fread
 DSE3.01BDA240
 DSE3.01B6E1C0
 ReadFile
 DSE3.01BBF970
 DSE3.01BD0290
 DSE3_DFT.DftiComputeForward
 DSE3.01B76D60
 DSE3.01B6F2D0
 DSE3.01B81E20
 DSE3.01B827E0
 *DSE3.01BABAB0
 DSE3.01BD9F24
 01B79ABC DSE3.01B784A0
 01B79B19 DSE3.01B72800
 01B79B2F DSE3.01B710B0
 01B79B45 DSE3.01B71B70
 01B79B67 DSE3.01B6E920
 01B79B6F DSE3.01B70F80
 01B79BCF DSE3.01B73590
 DSE3.01BCFB90
 DSE3_DFT.DftiComputeForward
 DSE3.01BCFBE0
 DSE3_DFT.DftiComputeBackward
 01B79BF4 DSE3.01B827E0
 01B7A044 DSE3.01BCB7D0
 DSE3.01BCF670
 DSE3_DFT.DftiComputeBackward
 01B618A7 DSE3.01B6AAD0
 01B6AB2A DSE3.01B6A160
 01B618B7 DSE3.01B6AFB0
 01B6AFC3 DSE3.01B6ADE0
 01B6AFD8 DSE3.01B6ACF0
 
 所以。。。累加后的地址。。。可能会让你比较伤脑筋抱歉。。。
 如果有人原意填上这个巨坑就好了…………另外在网上查了一下DftiComputeForward,发现是Intel MKL英特尔核心数学库的一个函数名。同样还有几个别的在DSE3_DFT里出现的导出名也和Intel MKL重合。Intel MKL极有可能被DSE3_DFT包括。
 在某人博客上看到的摘抄作参考:http://blog.sina.com.cn/s/blog_57562d890100xy0j.html 2013-8-27更新:已确认Vocaloid包含Intel MKL核心数学库,主要用途是进行大量FFT、IFFT运算。
 继续反向Vocaloid没有意义,因为其原理部分都以论文形式挂在网上。
 原理上讲修改Vocaloid不可能解决断元音问题,还不如用FECSOLA过渡比较靠谱。
 
 
 |