PSF (Portable Sound Format)[可移植声音格式] 规范 v1.5

时间:2022-07-03 11:42:42

-----------------------------------------------------------------------------
PSF (Portable Sound Format){可移植声音格式} 规范 v1.5
by Neill Corlett
-----------------------------------------------------------------------------

绪论
------------

PSF 是把 "NSF", "SID", "SPC" 和 "GBS" 的功能性带给了次时代家用机{next-generation consoles}.
PSF 利用每一个游戏的原始的音乐驱动{设备?}代码完美而可信回放音乐序列, 而且尺寸极佳{size-efficient}{还是太大}.

一般的方法是 一个 PSF 文件包含一个 用zlib压缩过(zlib-compressed) 的程序, 这个程序如果能在真实的家用机上执行, 能简单的回放音乐.

想知道有哪些改变, 请参看底部的本文修正历史.

-----------------------------------------------------------------------------

基本文件结构
--------------------

每个 FSF 文件都有相同的基本结构, 下面描述.

不但保在 PSF 文件中连接到的任何大于一字节(byte)的数据. 练习适当的谨慎.
{翻译的垃圾呀,Tx=xT}

- 首先 3个字节: ASCII字符: "PSF" (大小写敏感)

- 然后 1字节: 版本字节
  版本字节用来决定 PSF 文件的类型. 它完全不会影响到文件的基本结构.{!不会影响哟!}

  当前公认的版本字节是:
    0x01: Playstation (PSF1) {PS}
    0x02: Playstation 2 (PSF2) {PS2}
    0x11: Saturn (SSF) [试验中] {SS 土星哟!!}
    0x12: Dreamcast (DSF) [试验中] {DC!!}
    0x21: Nintendo 64 (USF) N64 [保留]
    0x41: Capcom QSound (QSF) {CPS1-QSound, CPS2-QSound, CP-ZN1-QSound, CP-ZN2-QSound, 还有么??}

- 然后 4字节: 预留空间的大小 ®, little-endian 无符号长整形
  {little-endian: 低位放低字节, 高位放高字节(??对么??). 例如 x86 系列的内存数据格式. 相对应的 68K 就是big-endian.}

- 然后 4字节: 压缩过的程序长 (N), little-endian 无符号长整形
  这个长度是程序{program data}压缩{_after_}后的长度.

- 然后 4字节: 压缩过的程序的 CRC-32 值, little-endian 无符号长整形
  {CRC-32 32位循环冗余校检}
  {!! CRC-32 !!又是它,Tx=xT最近就是它搞不定.}
  这是程序{program data}压缩{_after_}后的 CRC-32 值.
  这个数值必须强制填充, 作为一个 PSF 文件如果这个数值不匹配就意味着是损坏的.

- 然后 R 字节: 预留空间
  如果 R 是 0 字节则为空

- 然后 N 字节: 压缩过的程序, 由 zlib 压缩格式. {compress() 为虾米带(),说不定是个 zlib 的函数.记下来.}
  如果 N 是 0 字节则为空

然后的数据是可选的, 可以省略:

- 然后 5字节: ASCII字符: "[TAG]" (大小写敏感)
  如果这 5字节不匹配, 则剩下的部分当作无效的, 将被丢弃.

- 文件的剩余部分: 没有压缩的 ASCII 标签{TAG} 数据.

如果剩下的数据超过 50,000 字节, 那 标签 数据可以被切断, 也包括切除文件本身的多余数据. 这个设计故意的.

为将来的兼容性考虑, 标签编辑器和压缩器可能会假定 任何 PSF 文件都使用这个基本结构.
可是, 预留的区域必须保持完整, 没有假定可能会造成 解压缩后的程序 和 预留的部分 的 格式 或 内容 附近没有 首先核对的版本字节.
{完了~~这回垃圾大发了.Tx=xT}

有关 zlib 的信息请去 http://www.gzip.org/zlib/ .
-----------------------------------------------------------------------------

标签格式 {TAG}
----------

标签由一系列的单行的格式如 变量=数值 构成, 像下面的例子:

  title=Earth Painting
  artist=Yoko Shimomura
  game=Legend of Mana
  year=1999
{这就是传说中我不知道的LoM,灭哈哈哈哈}

标签将被像下面那样分析:

- 所有为 0x01-0x20 字符将被过滤为 空格{whitespace}
- 必须没有 null (0x00) 字符; 这次发布时, 接收到 null 字节的行为未定义
- 0x0A 是新一行的字符 {0x0A: 就是 ASCII 的换行符, 符合 UNIX 标准, Win 系列回车是 2个字节, 一个是 0x0D, 然后是 0x0A}
- "变量=数值" 这样的附加行可以在后面跟着
- 变量名是大小写不敏感的, 必须是合法的 C语言标识符
- 空格 在一行的 开始/结尾, = 的前/后 将被忽略
- 空行将被忽略
- 多行变量必须为 连续的行且具有相同的变量名. 举例:

    comment=This is a
    comment=multiple-line
    comment=comment.

- 标签的文本用系统默认的编码表来编码/解码.
{哇AAAaaa---!!!为什么不用UTF-8!!!为什么!!!}

变量名出现多于一次, 多行变量零散的在不相干行 等等, 这些行为都没有定义.

以下的变量名已经预先定义:

title, artist, game, year, genre, comment, copyright
  (这些是不需要加以说明的.){我来YY: 标题, 艺术家, 所在游戏, 发行年, 流派, 备注, 版权}

psfby, ssfby, dsfby, usfby, qsfby
  这些名字是创建这些文件的负责人.{???} 这并不意味着这个人写了声音驱动代码.{???}
{WHY!!!算了,大家将就着看吧.偶就素那垃圾~~}

volume
  PSF 相对音量, 作为一个简单缩放系数.  1.0 是默认的. 它可以是任意实数, 包括负数

length
fade
  曲长, 结尾淡出长度
  这些可以是以下3种格式之一:
    秒.小数
    分:秒.小数
    小时:分:秒.小数
    小数部分可能被忽略. 逗号也可以作为 小数 分离器.{译的太直了.寒~~}

以下的变量名是保留的, 不可用的:

- 任何开始带有 下划线 (_) 的
  这些是预留给至关重要的播放信息的, 例如 _lib 标签在 MiniPSF 文件里.

- filedir, filename, fileext 有特别的意义在 Highly Experimental {有高实验性?}
{文件目录, 文件名, 文件扩展名}
  到时这些变量也许会用到, 它们将不能用在标题格式字符串中{title format strings}.

-----------------------------------------------------------------------------

依靠 版本字节 , 预留位 , 程序位 解释很困难.
一些 标签 也很难解释.
参考下面的部分.

-----------------------------------------------------------------------------

版本字节 0x01: Playstation (PSF1)
--------------------------------

程序位: PS-X EXE 产品格式{consumer-format}可执行文件, 包含头{header}.
预留位: 未使用. 可以被忽略, 移除 等.
{拜托, 你是定格式的人,你来决定,不要来 etc, 不要来 May, Must 多好. 寒~~算了,自己YY也没用.}

文件扩展名:
- psf, psf1 (独立的程序)
- minipsf, minipsf1 (程序回放依赖额外的库数据)
- psflib, psf1lib (minipsf 文件播放时需要的库文件)

在 PSF1 的情况, 程序位 是一个原始Playstation家用机的可执行程序.
它负责初始化 SPU, 加载样本, 设置中断, 等. - 一个程序应该做的任何事.
它运行在 shell 级, 有完全有权使用内核函数.

有2个重要的 PSF1 变量: MiniPSF 和 PSFLib 稍后描述.

可执行文件的解压缩后大小必须小于2,033,664字节.

可执行文件必须是标准的 "PS-X EXE 产品格式", 请参考下面的描述.

首先 0x800 字节 - {文件}头{header}
然后 N字节 - 文本区域

头 格式:

0x000 (8字节): ASCII "PS-X EXE"
0x010 (4字节): Initial PC, little-endian 无符号长整形
0x018 (4字节): 文本区域 开始地址, little-endian 无符号长整形
0x01C (4字节): 文本区域 size, little-endian 无符号长整形
0x030 (4字节): Initial SP ($29), little-endian 无符号长整形
0x04C: ASCII标记: "Sony Computer Entertainment Inc. for North America area" (或类似的为其它地区的标记) 剩下的部分都是 0.

文本区域 应该是 2048字节 的整数倍.

在0x4C的 ASCII标记 的区域信息将被用于确定 屏幕刷新率 和 VBlank中断 发生频率(NTSC vs. PAL):

  "North America" = 60Hz
  "Japan" = 60Hz
  "Europe" = 50Hz
  (如果这儿有我应该包括的其它地区请让我知道)

同样, 如果 "_refresh" 标签 出现, 它直接确定刷新率的Hz数.
这个标签覆盖在 EXE头 的区域信息.
当前只有 _refresh=50 和 _refresh=60 是有效地.

-----------------------------------------------------------------------------

PSF1: MiniPSF 和 PSFLib
------------------------

MiniPSF 文件是正规的 PSF1 文件, 它从一个或多个 在同一个目录下的 PSFLib 文件引入数据.
(为了 共享 驱动代码, 声音库, 等.)

PSFLib 文件也是一个正规的 PSF1 文件. 它们能被其它 PSFLib 文件"递归的"引用数据.
{RECURSIVELY "递归的"???}

恰当的 标签变量 被叫做 _lib, _lib2, _lib3, 等.

以下是正确加载 minipsf 的方法:

- 加载 EXE 数据 - this becomes the current EXE

- 检查存在的 "_lib" 标签.
  如果存在:
  - "递归的" 从给定的库文件加载 EXE 数据
    (确定"递归的"限制, 避免崩溃 - 我限制它为10级)
  - 用 _lib EXE 来指定 当前的.
  - 我们将用 _lib EXE 来初始化 PC/SP.{PC/SP???}
  - 在 原先加载的 PSF EXE 上添加当前的 EXE, 用它的文本开始地址和文本大小.

- 检查存在的 "_libN" 标签, N=2 到更高(用 "_lib%d")
  - "递归的" 在原先的 EXE 上 加载和添加 所有这些 EXE 们. 但不修改当前的 PC/SP.
  - 起始于 N=2.停止于第一个不存在的标签名.

- (完成)

EXE 们必须总是相邻的.
当在一个 EXE 上添加另一个, 增加目标 EXE 的 开始/结束 点 是必须的, 用 0 填充不用的空间.

在任何 _lib* 标签 里给定的文件名是 相对路径, 相对于 PSF 文件 本身所在的目录.
"/"和"/"都被认为是路径的分离器.{还是直-_-"}
举例:

- 如果 C:/Something/Demo.minipsf 包含 "_lib=Hello/Library.psflib"
- 那么库将从 "C:/Something/Hello/Library.psflib" 加载

文件名之内可以包含空格, 但开始和结尾不能有空格.

当 无论是检测 "一个PSF1文件 还是 一个MiniPSF文件" 还是 需要附加的数据 时, 你应该用存在的 _lib* 标签 来决定, 胜于用文件扩展名.
如果包含  _lib 或 _lib2 标签, 那它就是一个 MiniPSF.

区域信息在整个 _lib EXE头 将被忽略; 只有原始的 EXE 的区域信息在确定刷新率时被使用.
举例, 一个 "Europe area" 的 MiniPSF, 它从一个 "North America area" 的 PSFLib 引入数据将被认为是 50Hz.

无论哪一个 _refresh 标签在 PSF 加载过程中碰到, 只有第一个 标签 发挥作用, 忽略所有后来的 标签, 也忽略 EXE头 区域信息.
例子:

- MiniPSF 有 _refresh=50, 并且 PSFLib 没有 _refresh 标签:
    50Hz ,忽略 EXE头
- MiniPSF 有 _refresh=50, 并且 PSFLib 有 _refresh=60:
    50Hz ,忽略 EXE头
- MiniPSF 没有 _refresh 标签, 并且 PSFLib 有 _refresh=60:
    60Hz ,忽略 EXE头
- MiniPSF 和 PSFLib 都没有 _refresh 标签:
    MiniPSF 的 EXE头 用来确定刷新率

-----------------------------------------------------------------------------

PSF1 模拟{仿真Emulation}备注
--------------------

虽然它与具体格式无关, 以下的信息还是很有用的. 它只适用于 PSF1.

以下是有用地 PSXCore0008:
- R3000 CPU
- 中断 和 系统调用
- 2MB RAM (镜像贯穿 第一个 8MB ){???}
- 1KB 中间结果暂存器{scratchpad}
- SPU
- DMA通道 4(SPU)
- Root counters 0, 1, 2, VBlank IRQs{???}
- 全部内核函数

以下是无用的应该不被使用和访问的:
- 溢出错误
- 断点
- GTE 指令
- GPU, CDROM, SIO, 等. - 与声音无关硬件
- 其它 DMA通道

以下的 R3000 代码序列被侦测后作为 idle{停机}, 在宿主{指PSF运行环境吧}这边可能不被用于 保存 CPU时钟:

  [jump or branch to the current line]{跳转 或 分支 到 当前行}
  nop

-----------------------------------------------------------------------------

版本字节 0x02: Playstation 2 (PSF2)
----------------------------------

程序位: 未使用. 可以被忽略, 移除 等.
预留位: 虚拟文件系统.

文件扩展名:
- psf2 (自包含文件系统)
- minipsf2 (文件系统 依赖于一个或多个 psf2lib 文件系统)
- psf2lib (文件系统 为 minipsf2 文件系统 提供 额外的数据)

一个 PSF2 文件 由在 保留位 的 一个 虚拟文件系统 定位点 组成.程序位 未使用.
文件系统可能用 _lib, _lib2, ... 标签 组成, 与 MiniPSF 和 PSFLib 格式累死.

播放一个 PSF2 文件 首先从 虚拟文件系统 加载 "psf2.irx" IOP模块.
加载和执行这个模块应当完成所有必须的硬件设置和播放音乐.

PSF2 文件是有限的回放序列音乐, 使用 IOP 和 硬件合成(SPU2).
软件合成, 通常由 EE 完成, 是不被考虑的.
{EE "Emotion Engine" PS2 主CPU}

_refresh 标签能被用来指定刷新率 Hz数(50 or 60), 与 PSF1 相同.

-----------------------------------------------------------------------------

PSF2: 运行时 环境
-------------------------

IOP模块 在 一个 PSF2 可以连接以下的库.
版本字节 可以不迟于列出的版本.

  Name       Ver.   Name       Ver.   Name       Ver.
  --------------    --------------    --------------
  dmacman    102    sifman     101    thmsgbx    101
  excepman   101    ssbusc     101    thrdman    102
  heaplib    101    stdio      103    thsemap    101
  intrman    102    sysclib    103    thvpool    101
  ioman      104    sysmem     101    timrman    103
  loadcore   103    thbase     102    vblank     101
  modload    106    thevent    101
  sifcmd     101    thfpool    101

IOP模块 应当用以下方法在 PSF2 虚拟文件系统 中访问文件:

1. 获取 argv[0] ,在最右面的分离器后直接切断它.
   分离器 定义为 前斜线{除号}, 后斜线{反斜杠}, 或 冒号(/ / :).
   这个字符串变成 "设备前缀".
2. 追加期待的文件的名字到 设备前缀.
3. 使用结果字符串在 标准调用 里, 像 open 或 LoadModule.

举例, Highly Experimental 可能调用 psf2.irx 带有 argv[0] 像 "hefile:/psf2.irx".
这种情况, 设备前缀 是 "hefile:/".
你应当用以下的调用打开叫 "test.file" 的文件:

  fd = open("hefile:/test.file", O_RDONLY);

不同的 播放器{player} 或 环境 用不同的 设备前缀, 所以不总是用 "hefile:/".

IOP模块, 通常, 应当不试图和任何与音乐播放无关的硬件联系.
它们包括, 但不是限制: SIF, CD/DVD, USB, iLink, ATA, 手柄, 记忆卡, 和网络硬件.
这些硬件是模拟的最小限度但是不确保可以工作.

IOP模块 应当使用最小限度的内存数量.
成功的使用 虚拟文件系统 保持大量的数据, 胜于在一个 IRX数据位 设法包括它们所有的数据.

-----------------------------------------------------------------------------

PSF2: 虚拟文件系统格式
-------------------------------

PSF2 虚拟文件系统 是一个有目录层次的 带有 最多36个字符的文件名.
文件名 是对大小写不敏感的, 必须是由唯一的一组 ASCII 32-126 字符组成的序列, 带有除了前斜线{除号}, 后斜线{反斜杠}, 或 冒号(/ / :)的字符.
包括路径在内的总长度不能长于255字节.

所有数量作为 little-endian 无符号格式储存. 所有偏移量 相对于 预留位 的开始.

起始于 偏移量 0 的是根目录.
目录有以下格式:

- 开始 4字节: 目录项 的数目 (N){directory entry 目录项/目录登记项 词义不明}
- 然后 48*N 字节: N 目录项

N 可以为 零, 表示这个目录是空的.

目录项 的格式如下:

- 开始 36字节: 文件名. 必须是非零长度, 必须添补 null (0x00) 字符. 如果它是严格的是 36 字符, 则没有 null-终止.
- 然后 4字节: 文件数据 或 目录 的偏移量 (O)
- 然后 4字节: 解压缩后的大小 (U)
- 然后 4字节: 块大小 (B)

如果 U, B, O 都是零, 那么入口被描述为 零长度 文件.
如果 U, B 都是零, O 是非零, 那么入口被描述为 子目录.

否则, 文件数据 作为连续的 zlib压缩()-格式块 序列储存.

- 开始 4*X 字节: 表大小. 每个入口包含压缩块的大小.
  X = (U + B - 1) / B;
- 文件的剩余部分: 压缩块 的解压缩大小必须等于 B. 最后的数据块的解压缩大小可以小于 B.

所有文件和子目录的偏移量必须大于它入口目录的偏移量.{明白了么???}
这是为了简单的一致性检查.

-----------------------------------------------------------------------------

PSF2: MiniPSF2 和 PSF2Lib
--------------------------

MiniPSF2文件 是正规 PSF2文件, 它从在相同目录下的一个或多个 PSF2文件 里引用数据(为了共享 驱动代码, 音色库, 等.)

PSF2Lib文件 也是一个正规的 PSF2文件. 它们能被其它 PSF2Lib 文件"递归的"引用数据.

恰当的 标签变量 被叫做 _lib, _lib2, _lib3, 等.

以下是正确加载 MiniPSF2 的方法:

1. RECURSIVELY 从 每一个 PSF2文件 库标签 的名字加载 虚拟文件系统.
   最初的标签是"_lib". 其后的标签是"_libN", N>=2 (使用 "_lib%d").
   停止在第一个不存在的标签名.
2. 加载 虚拟文件系统 从当前的 PSF2文件.

当加载一个新 文件系统, 带有相冲突{应该指同名吧}文件名的目录入口将被覆盖(当然实际的 PSF2文件 本身不会被覆盖.)

在任何 _lib* 标签 里给定的文件名是 相对路径, 相对于 PSF2 文件 本身所在的目录.
"/"和"/"都被认为是路径的分离器.{还是直-_-"}
举例:

- 如果 C:/Something/Demo.minipsf2 包含 "_lib=Hello/Library.psf2lib"
- 那么库将从 "C:/Something/Hello/Library.psf2lib" 加载

文件名之内可以包含空格, 但开始和结尾不能有空格.

当 无论是检测 "一个PSF2文件 还是 一个MiniPSF2文件" 还是 需要附加的数据 时, 你应该用存在的 _lib* 标签 来决定, 胜于用文件扩展名.
如果包含  _lib 或 _lib2 标签, 那它就是一个 MiniPSF2.

在 MiniPSF2 和 PSF2Lib 里, _refresh 标签 的行为与 MiniPSF 和 PSFLib 行为相同.

-----------------------------------------------------------------------------

版本字节 0x11: Saturn (SSF) [试验中]
--------------------------

程序位: Raw 68000 声音程序镜像.{RAW 一般是指没有头的数据格式}
预留位: 未使用. 可以被忽略, 移除 等.

文件扩展名:
- ssf (独立的程序)
- minissf (程序回放依赖额外的库数据)
- ssflib (minissf 文件播放时需要的库文件)

一个 SSF 的 程序位 由4字节的 LSB-first 的 加载地址 followed by Raw 68000 程序数据, 它是个 特定地址.
所有程序加载后, 68000 处理器重启, 然后从 重启向量{reset vector} 开始执行.

68000 程序负责初始化和使用 SCSP.
它必须不能试图接触任何其它 Saturn 处理器{SH2,VDP1,VDP2,SCU,SMPC}.

"MiniSSF 和 SSFLib" 与 "MiniPSF 和 PSFLib" 加载方法相同.

程序位 解压缩后必须小于 524,292字节.
{LSB-first 应该是指 big-endless, 因为68k族的系统都是big-endless, 也可能是small-endless,请自己查资料,错了别怪我.}
{SCSP 没查到相关资料,应该是指负责声音的DSP 或者就是指 Yamaha FH1 声音处理器}
{SS 的声音处理器是 68EC000 不是标准 68000, 应该是指自带控制器的那种}
{SS 的文档资料真是匮乏呀,什么都查不到 >_<}
-----------------------------------------------------------------------------

版本字节 0x12: Dreamcast (DSF) [试验中]
-----------------------------

程序位: Raw ARM7 声音程序镜像.
预留位: 未使用. 可以被忽略, 移除 等.

文件扩展名:
- dsf (独立的程序)
- minidsf (程序回放依赖额外的库数据)
- dsflib (minidsf 文件播放时需要的库文件)

一个 DSF 文件的 程序位 由4字节的 LSB-first 的 加载地址 followed by Raw ARM7 程序数据, 它是个 特定地址.
所有程序加载后, ARM7 处理器重启, 然后从 地址 0 开始执行.

ARM7 程序负责初始化和使用 AICA.
它必须不能试图接触 SH4 主 CPU.

"MiniDSF 和 DSFLib" 与 "MiniSSF 和 SSFLib" 加载方法相同.

程序位 解压缩后大小必须小于 2,097,156字节.
{AICA ???}
-----------------------------------------------------------------------------

版本字节 0x21: Nintendo 64 (USF) [保留的]
-------------------------------

当 "Adam Gashlin 的 USF 格式" 完成后这个版本字节是保留给它的.

-----------------------------------------------------------------------------

版本字节 0x41: Capcom QSound (QSF)
---------------------------------

程序位: 紧随的镜像包含 Z80程序/样本ROM 解密密钥
预留为: 未使用. 可以被忽略, 移除 等.

文件扩展名:
- qsf (独立的镜像)
- miniqsf (镜像回放时需要额外的库数据)
- qsflib (miniqsf 文件播放时需要的库文件)

QSF 文件的程序位, 一旦解压包含一系列的数据块, 见下:

3字节 - ASCII 名字标签
4字节 - 起始偏移量 (LSB-first)
4字节 - 长度 (N) (LSB-first)
N字节 - 数据

然后数据加载到由 ASCII 标签段描述的给定的 起始偏移量.

以下的部位这样定义:

"KEY" - Kabuki 解密密钥. 这个部位应该是 11字节, 内容如下:
        4字节 - swap_key1 (MSB-first){MSB是虾米???}
        4字节 - swap_key2 (MSB-first)
        2字节 - addr_key  (MSB-first)
        1字节 - xor_key
"Z80" - Z80 程序 ROM.
"SMP" - QSound 样本 ROM.

KEY位 没有 或 两个 swap_keys 都是 零, 则假定没有加密.

Z80 程序 ROM 必须播放期望的曲目, 而且不接受任何命令经由共享内存.
(通常需要为它做一些次要的中断调用修改).

"MiniQSF 与 QSFLib" 与 "MiniPSF 和 PSFLib" 加载方法相同.

没有预先定义 Z80 或 SMP 的限制, 但 KEY位 不能超过 11字节.

-----------------------------------------------------------------------------

修正历史
----------------

v1.5 (2004-05-15)
- 改名为 "Portable Sound Format" {可移植声音格式}
- 加入 QSF 的信息
- 保留 USF 的 版本字节

v1.4 (2004-01-23)
- 加入 SSF 和 DSF 的信息 [实验性]
- 加入 NTSC 与 PAL 信息 和 为 PSF1 和 PSF2 加入 "_refresh" 标签{刷新率?}

v1.3 (2003-05-31)
- PSF2 格式改变和定稿
- 修正 PSF1 仿真注释为了反映在最新的仿真代码上的改变
{9素那模拟了}

v1.2 (2003-04-17)
- 变量字节 操作的正式化
- 详述将来怎样操作一个 PSF 变量
- 所有开头带下划线(_)的标签被保留
- Various other clarifications
- 详述 PSF2 信息, 仍旧是[实验性]的

v1.1 (2003-03-11)
- 一些 PSF2 的建议信息

修正 (2003-01-24)
- 加入 MiniPSF 和 PSFLib 的信息

原始版本 (?)

-----------------------------------------------------------------------------

怎么找到 Neill Corlett
---------------------------
电邮: neill@neillcorlett.com
网站: http://www.neillcorlett.com/

-----------------------------------------------------------------------------
==================================================