前几天在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;
}
}
}

没有评论:
发表评论