前几天在PSP上把《帕露菲》所有线都通了,以前玩过的《青空下的约定》也是GIGA做的。不得不说这个公司做GalGame是相当的厚道:可以无限制回溯上一选择肢,每个选择肢上都标注的有该事件的主要人物,一直选某一个角色的事件,就能走到该角色的结局。这个方法在攻略《青空》时百试不爽,不过《帕露菲》就不太行得通了。不过《帕露菲》也提供了更详细的选择肢提示:每个事件不仅有主要参与任务头像,还有该事件对结局的影响,例如:由飞TRUE NORMAL END;明日香日常事件。GIGA并不打算把玩家引入选择肢的迷宫在里面鬼打墙,而是让玩家把精力集中到品味剧本、音乐上来。从这个角度来看,GIGA算得上是业界良心,比那《恋爱蜡笔》不知高到哪里去了。
GIGA在OST这块圈钱不可谓不厉害,光《帕露菲》的就有两三个版本,《青空》的OST单独出了2CD还不过瘾,再拉上女仆咖啡系列出个SOUNDTRACK COMPLETE CD-BOX Music 共4CD。这还没完,《青空》后来又出了个PSP版(官方出品,我玩的这个《帕露菲》是民间移植的),又单独出一张OST,然后加上之前PC版的2CD,又出一个3CD的OST。
吐槽完了GIGA,回过头来说BGM提取。既然有这么多的OST,干嘛不直接收一个得了。这事得赖GIGA,出了那么多张OST,到头来还是没把游戏里的BGM收录完。下图橙色框中的音乐就都没出现在任何一张OST中。
这些未收录的曲子有一个特点,都不是GIGA拥有版权的。出去最后两个我没有考证,其他的曲子应该都是年代久远,不应该存在什么版权纠纷。《氷菓》都敢把《月光》、《西西里舞曲》收录到BD特典,GIGA又在担心什么呢?而这些未收录的曲子都相当不错,既然没有现成的,只好提取了。
我最先是拿PSP版的BGM.PAC文件开刀,于是还去学习了关于WAVE格式的知识。但遗憾的是虽然拿到了拆包后的WAVE文件,但几乎没有播放器能播放它,QQ影音勉强能打开,但十分卡顿,声音刺耳,勉强能听出是BGM的声音。在网上找了两个拆包的工具——ExtractData和Crass 0.4,但都没能成功提取。ExtractData提取到的跟我自己提取的一样,Crass则是什么都没提取到。
无奈之下,只好去装了个PC版的游戏,因为不少人已经用ExtractData成功提取到PC版的BGM。但我并没有立即拿这个工具去拆包——提取PSP版的失败后我已经把这两个工具都删了。这次就很顺利了,很快就把这个600多MB的PAC文件拆为39个WAVE文件。这种没有经过压缩或者加密的文件拆包其实是非常容易的,就是一个二进制文件读写的问题,关键在于从什么地方开始读,读多少字节。
关于WAVE格式的分析,我主要参考了这样一些文章:
- BlueSoal的专栏-wav文件格式分析详解
- ypynetboy的专栏-WAV文件格式分析
- FrankieWang008的专栏-RIFF文件格式
- 五维空间-采样率、比特率、采样位数、PCM、WAV
- 舒广,丁晓明.RIFF WAVE文件的结构及应用[J].电声技术,2001,(1):49-51.
不过这些文章或多或少都存在一些纰漏,大概是由于成文较久,目前的WAVE格式又有了新的发展吧。我把我注意到的地方提出来,以免别人再走我的弯路。下面我假设读者已经基本了解了WAVE格式的特点,例如知道WAVE格式由RIFF,Format,Fact,Data 4个Chunk组成。
首先是Fact Chunk,文章1中说这个区块是个可选区块,虽然没有说如果存在则必然位于Format Chunk之后,但也没有提到有其他的可能性。但实际上我在提取PSP版的BGM时就发现,这个Fact Chunk跑到Data Chunk之后去了。
然后是Format Chunk,这些文章基本都指明,Format Chunk中的Size要么为16,要么为18。PC版中确实是18,但PSP版中却有32,这也是PSP版BGM令我费解之一。另外文章中提到Format Chunk中的Format Tag通常为0x0001,表示PCM方式,但PSP版的却是0x0270。文章5列举了0x0001~0x0007共7种Format Tag的取值,但都没有0x0270。而采样位数BitsPerSample在PSP版中也为0,这点倒是没有令我太为震惊,文章4中说只有PCM方式时,这里才会有值。
我在PC版里还发现每个文件似乎都还有一个cue chunk,而上述文章都没有提到。这应该也是一个FOURRC,ID为cue ,后面的0x0034表示后面数据区的大小为52B,但这52B全是空的,什么也没有。
另外我还在PSP版中发现了LIST的区块,但内容好像是GoldWave的版权信息。我也不想去研究PSP版的BGM了,费力不讨好。最后是拆包的代码,如果只是提取这些音乐而不做更多的文件格式的详细分析,代码十分简单:分两步读取,首先从RIFF文件头开始读到其Size,根据Size的值确定第二步的读取需要读多少字节。两块合起来,写到一个文件里就可以了。注意所有数据的写入都要使用二进制写入方式,直接用BinaryWritter的Write()方法的各种重载是不行的,只能用Write(byte[] data)这一种,其余的数据需要转换成byte数组才可以。另外,像各个Chunk的Id在写入的时候编码最好选择ASCII,GBK或者UTF-8我没试过。
static void UnPack(string path,string outputPath) { FileStream fs = File.OpenRead(path); BinaryReader br = new BinaryReader(fs); br.BaseStream.Seek(0, SeekOrigin.Begin); Console.WriteLine("Now working...\nTotal Length:" + br.BaseStream.Length); int offset = 12; //扔掉PAC文件前12字节,从RIFF开始读取 br.ReadBytes(offset); int count = 1; while (br.BaseStream.Position < br.BaseStream.Length) { Wave wave = new Wave(); #region RIFF Chunk //这个PAC文件最后还有一些其他数据, try { wave.RIFFChunk.Id = Common.Bytes2String(br.ReadBytes(4)); } catch (ArgumentOutOfRangeException) { break; } wave.RIFFChunk.Size = BitConverter.ToInt32(br.ReadBytes(4), 0); byte[] restData = br.ReadBytes(wave.RIFFChunk.Size); FileStream fs2 = new FileStream(outputPath+"BGM_"+count+".wav", FileMode.Create); BinaryWriter bw = new BinaryWriter(fs2); bw.Write(Encoding.ASCII.GetBytes(wave.RIFFChunk.Id)); bw.Write(BitConverter.GetBytes(wave.RIFFChunk.Size)); bw.Write(restData); bw.Flush(); bw.Close(); fs2.Close(); Console.WriteLine(count+"bgm(s) extracted."); count++; #endregion } Console.WriteLine("Done!"); }
Wave这个类是我为了分析PSP版的BGM为什么会出问题而写的一个封装WAVE文件信息的类,如下:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace BGMUnPack { public class Wave { public RIFF_WAVE_Chunk RIFFChunk; public Format_Chunk FormatChunk; public Fact_Chunk FactChunk; public Data_Chunk DataChunk; public Wave() { RIFFChunk = new RIFF_WAVE_Chunk(); FormatChunk = new Format_Chunk(); DataChunk = new Data_Chunk(); FormatChunk.WaveFormat = new Wave_Format(); } public void ToFile(string path) { FileStream fs = new FileStream(path, FileMode.Create); BinaryWriter bw = new BinaryWriter(fs); bw.Write(RIFFChunk.ToBytes()); bw.Write(FormatChunk.ToBytes()); if (FactChunk != null && FactChunk.Size != 0) { bw.Write(FactChunk.ToBytes()); } bw.Write(DataChunk.ToBytes()); bw.Flush(); bw.Close(); fs.Close(); } public override string ToString() { string result = ""; result += "采样率:" + FormatChunk.WaveFormat.SamplePerSec + "Hz\n" +"声道:"+FormatChunk.WaveFormat.Channels+"\n" +"采样位数:"+FormatChunk.WaveFormat.BitsPerSample +"比特率:"+FormatChunk.WaveFormat.AvgBytesPerSec; return result; } } public class RIFF_WAVE_Chunk { /// <summary> /// 4 Bytes, must be "RIFF" /// </summary> public string Id { get { return "RIFF"; } set { if (value != "RIFF") { throw new ArgumentOutOfRangeException("Id", "Id must be RIFF"); } } } /// <summary> /// 4 Bytes which can convert to a Int32 that indicate the size of the wav file. /// Size=FileLength-8 /// </summary> public int Size { get; set; } /// <summary> /// 4 Bytes, must be WAVE /// </summary> public string Format { get { return "WAVE"; } set { if (value != "WAVE") { throw new ArgumentOutOfRangeException("Format", "Format must be WAVE"); } } } public byte[] ToBytes() { byte[] result = new byte[8 + Size]; result = Encoding.ASCII.GetBytes(Id) .Concat(BitConverter.GetBytes(Size)) .Concat(Encoding.ASCII.GetBytes(Format)) .ToArray(); return result; } } public class Format_Chunk { string id; /// <summary> /// 4 Bytes, must start with "fmt" /// </summary> public string Id { get { return id; } set { if (!value.StartsWith("fmt")) { throw new ArgumentOutOfRangeException("Id", "Id must start with 'fmt'"); } id = value; } } /// <summary> /// 4 Bytes:16 or 18 (extra info in the end) /// </summary> public uint Size { get; set; } /// <summary> /// Wave format /// </summary> public Wave_Format WaveFormat { get; set; } /// <summary> /// optional Extra info, 2 Bytes, exisitence can judged by Size /// </summary> public byte[] Extra { get; set; } public byte[] ToBytes() { byte[] result =Encoding.ASCII.GetBytes(Id) .Concat(BitConverter.GetBytes(Size)) .Concat(WaveFormat.ToBytes()) .Concat(Extra) .ToArray(); return result; } } public class Wave_Format { /// <summary> /// 2 Bytes, encoding manner, usually 0x0001 /// </summary> public ushort FormatTag { get; set; } /// <summary> /// 2 Bytes. The number of the Channels. 1 means Mono, 2 means Dual Channel /// </summary> public ushort Channels { get; set; } /// <summary> /// 4 Bytes, the bitrate /// </summary> public uint SamplePerSec { get; set; } /// <summary> /// 4 Bytes, The bytes it need per second. /// </summary> public uint AvgBytesPerSec { get; set; } /// <summary> /// 2 Bytes, the unit of data block to align. /// </summary> public ushort BlockAlign { get; set; } /// <summary> ///2 Bytes, the bytes every sampleing need. /// </summary> public ushort BitsPerSample { get; set; } public byte[] ToBytes() { byte[] result = new byte[16]; result =BitConverter.GetBytes(FormatTag). Concat(BitConverter.GetBytes(Channels)) .Concat(BitConverter.GetBytes(SamplePerSec)) .Concat(BitConverter.GetBytes(AvgBytesPerSec)) .Concat(BitConverter.GetBytes(BlockAlign)) .Concat(BitConverter.GetBytes(BitsPerSample)) .ToArray(); return result; } } public class Fact_Chunk { /// <summary> /// 4 Bytes, if this chunk exist then must be "fact". /// </summary> public string Id { get { return "fact"; } set { if (value != "fact") { throw new ArgumentOutOfRangeException("Id","Id must be fact"); } } } /// <summary> /// 4 Bytes, Size=4 /// </summary> public uint Size { get; set; } /// <summary> /// 4 Bytes /// </summary> public byte[] Data { get; set; } public byte[] ToBytes() { byte[] result =Encoding.ASCII.GetBytes(Id) .Concat(BitConverter.GetBytes(Size)) .Concat(Data) .ToArray(); return result; } } public class Data_Chunk { /// <summary> /// 4Bytes, must be "data" /// </summary> public string Id { get { return "data"; } set { if (value != "data") { throw new ArgumentOutOfRangeException("Id","Id must be data"); } } } /// <summary> /// size of Data Chunk /// </summary> public uint Size { get; set; } /// <summary> /// the data of wave file which it stored. /// </summary> public byte[] Data { get; set; } public byte[] ToBytes() { byte[] result = Encoding.ASCII.GetBytes(Id) .Concat(BitConverter.GetBytes(Size)) .Concat(Data) .ToArray(); return result; } } }
没有评论:
发表评论