BlogFM icon indicating copy to clipboard operation
BlogFM copied to clipboard

Re: 从零开始的红白机模拟 - [35] FME7 怒吼

Open dustpg opened this issue 5 years ago • 1 comments

Sunsoft FME-7

其实FME-7并不是NSF扩展音源所支持的芯片名称, 而是其变种——Sunsoft 5B. Sunsoft FMT-7, 5A以及5B共用一个Mapper编号——69. 由于格式原因, 程序中FME7代指Mapper069, 包括(甚至特指)Sunsoft 5B, 但是其实FME7是5B的子集.

Sun电子

比起其他厂商, 例如NSF就有提到的科乐美, 任天堂以及南梦宫. Sun电子似乎进入21世纪后就逐渐淡出游戏和行业了. 翻了一下发行游戏列表, 近10年就发行了几款, 不过似乎在吃老本——还有当初FC很好玩的《超惑星战记》的续作.

Sun电子在FC时代出了很多不错的作品, 但其实在这里都不重要, 重要的其编曲水平之高. 例如仅仅使用2A03的《RAF世界》, 这样一个高编曲水平的开发商的Sunsoft 5B又如何呢?

吉米克!

Gimmick!》, 感觉和 拉格朗日点 很相似——唯一一个使用了独立扩展音源的游戏, 一个是VRC7, 一个是5B.

但是其实 吉米克! 也没有完全了利用5B的机能——没有使用到噪音发生器和包络发生器.

FME7总览

wiki介绍FME7支持到512kb的PRG-RAM, 这是目前来看最多的PRG-RAM, 但是自己一个一个看了目前的游戏列表, 最多的也就是8kb的WRAM.

对于PRG-RAM来说, 目前还是直接声明在结构里面, 直接增加512k的PRG-RAM感觉没有什么必要. 如果真的需要实现, 这部分最好使用动态申请, 然后ROM-RAM区分需要从1bit提高到2bit用来区分这部分的RAM(并且, 目前没有一些游戏内运行时错误的处理, 可能需要使用long_jmp).

  • CPU $6000-$7FFF: 8 KB Bankable PRG ROM or PRG RAM
  • CPU $8000-$9FFF: 8 KB Bankable PRG ROM
  • CPU $A000-$BFFF: 8 KB Bankable PRG ROM
  • CPU $C000-$DFFF: 8 KB Bankable PRG ROM
  • CPU $E000-$FFFF: 8 KB PRG ROM, fixed to the last bank of ROM
  • PPU $0000-$03FF: 1 KB Bankable CHR ROM
  • PPU $0400-$07FF: 1 KB Bankable CHR ROM
  • PPU $0800-$0BFF: 1 KB Bankable CHR ROM
  • PPU $0C00-$0FFF: 1 KB Bankable CHR ROM
  • PPU $1000-$13FF: 1 KB Bankable CHR ROM
  • PPU $1400-$17FF: 1 KB Bankable CHR ROM
  • PPU $1800-$1BFF: 1 KB Bankable CHR ROM
  • PPU $1C00-$1FFF: 1 KB Bankable CHR ROM

寄存器

FME7与其他的MMC有所不同的是, 先将命令写入命令寄存器($8000-9FFF), 再把参数写入参数寄存器($A000-BFFF).

  • $0-7 控制CHR切换
  • $8-C 控制PRG切换
  • $C 控制名称表的镜像规则
  • $D-F IRQ控制

命令: CHR Bank 0-7 ($0-7)

7  bit  0
---- ----
BBBB BBBB
|||| ||||
++++-++++- The bank number to select for the specified bank.

8个就是1kb的BANK. 注意防止溢出的操作.

命令: PRG Bank 0 ($8)

7  bit  0
---- ----
ERbB BBBB
|||| ||||
||++-++++- The bank number to select at CPU $6000 - $7FFF
|+------- RAM / ROM Select Bit
|         0 = PRG ROM
|         1 = PRG RAM
+-------- RAM Enable Bit (6264 +CE line)
          0 = PRG RAM Disabled
          1 = PRG RAM Enabled

支持切换512kb的ROM/RAM. 之前提到了, 最多的就是使用了8kb的WRAM. ROM方面, 其实还是没有游戏真正利用可以切换ROM-BANK. 不过现在没有实现写入保护, 切换到ROM再写入的话有点危险.

命令: PRG Bank 1-3 ($9-B)

7  bit  0
---- ----
..bB BBBB
  || ||||
  ++-++++- The bank number to select for the specified bank.

用于切换$8000-$9FFF, $A000-$BFFF, $C000-$DFFF的BANK. 注意防止溢出的操作.

命令: Name Table Mirroring ($C)

7  bit  0
---- ----
.... ..MM
       ||
       ++- Mirroring Mode
            0 = Vertical
            1 = Horizontal
            2 = One Screen Mirroring from $2000 ("1ScA")
            3 = One Screen Mirroring from $2400 ("1ScB")

中规中矩没什么可以说, 自己目前的是[2,3,0,1]的顺序, 可以用[2,3,0,1][mode]查表外, 自然就是mode XOR 2就行了.

命令: IRQ Control ($D)

7  bit  0
---- ----
C... ...T
|       |
|       +- IRQ Enable
|           0 = Do not generate IRQs
|           1 = Do generate IRQs
+-------- IRQ Counter Enable
            0 = Disable Counter Decrement
            1 = Enable Counter Decrement

FME7的IRQ是通过一个16bit的计数器, D7启动时会在每个CPU周期递减. 当D0启动并且计数器从$0000变成$FFFF触发IRQ.

写入该命令以确认IRQ.

命令: IRQ Counter Low Byte ($E)

IRQ计数器的低8位

命令: IRQ Counter High Byte ($F)

IRQ计数器的高8位

模拟蝙蝠侠出现的问题

Sun电子出品的《蝙蝠侠》难度比较高, 反正小时候没通关就是了, 还是老问题, 让蝙蝠侠变成字母侠了:

bug1

游戏问题

有2个问题, 模拟出现问题(瑕疵), 但是使用其他模拟器也是同样的:

  • 标题BGM: Sunsoft出品感觉肯定没问题, 但是发现标题的BGM在播放时, 切换声道音高有点不自然. 其他模拟器也是这样的, 所以应该是本身问题.
  • 标题过场画面闪烁, 其他模拟器也是同样的.

5B扩展音源

FME7写入高地址有两个空缺的位置, 就是为音频准备的. 硬件方面是Yamaha YM2149F(出现了, 又是雅马哈)的一种.

Audio Register Select ($C000-$DFFF)

7......0
----RRRR
    ++++- The 4-bit internal register to select for use with $E000

Audio Register Write ($E000-$FFFF)

7......0
VVVVVVVV
++++++++- The 8-bit value to write to the internal register selected with $C000

YM2149F 内部寄存器

寄存器 位域 说明
$00 LLLL LLLL 声道A周期 低字节
$01 ---- HHHH 声道A周期 高4位
$02 LLLL LLLL 声道B周期 低字节
$03 ---- HHHH 声道B周期 高4位
$04 LLLL LLLL 声道C周期 低字节
$05 ---- HHHH 声道C周期 高4位
$06 ---P PPPP 噪音周期
$07 --CB A--- 声道A/B/C上禁用噪声(Noise)
--- ---- -cba 声道a/b/c上禁用声调(Tone )
$08 ---E VVVV 声道A包络使能(E) 音量(V)
$09 ---E VVVV 声道B包络使能(E) 音量(V)
$0A ---E VVVV 声道C包络使能(E) 音量(V)
$0B LLLL LLLL 包络周期 低字节
$0C HHHH HHHH 包络周期 高字节
$0D ---- CAaH 包络重置与形状
--- --- continue (C)
--- --- attack (A)
--- --- alternate (a)
--- --- hold (H)
$0E ---- ---- I/O端口A(未使用)
$0F ---- ---- I/O端口B(未使用)

$07音调/噪声禁用位:

  • 禁用噪音: 输出音调
  • 禁用音调: 输出噪音
  • 都禁用: 输出恒定音量
  • 都启动: 输出音调'逻辑与'噪音

声音

一共拥有3个声道输出方波, 当然还有一个噪音发生器与包络发生器, 允许这三个声道使用.

5B通过CPU驱动, 不过YM2149F和APU类似, 内部有一个分频器可以让频率降低一半. 而 吉米克! 正是使用了这个模式.

与其他芯片声道对比, 5B的周期就是真正的周期, 不用+1s, 周期0似乎同周期1等价.

声调

声调发生器用来产生方波:

  • 频率 Frequency = Clock / (2 * 16 * Period)
  • 周期 Period = Clock / (2 * 16 * Frequency)
  • 多除了2是因为使用了倍分频器

方波(square), 之前的2A03也提到, 真正的应该叫做脉冲波(pulse), 其中50%占空比的称为方波. 由于wiki整篇没有提到占空比, 所以5B发出的应该就是真正的方波. 其中, 方波的01的'交替频率'自然是上面的两倍.

如果包络使能位设为1, 音量由包络控制, 否则输出自身的音量.

噪音

噪音发生器利用$06通过的5bit周期生成一个1bit的随机波.

  • 频率 Frequency = Clock / (2 * 16 * Period)
  • 周期 Period = Clock / (2 * 16 * Frequency)
  • 多除了2是因为使用了倍分频器
  • 随机数发生器可能是一个17bit的LFSR, 抽头(taps)为D16, D13.
  • 由于抽头在高位, 应该是伽罗瓦的LFSR实现(one-to-many LFSR):
// LFSR FME7模式 - Galois LFSR
static inline uint32_t sfc_lfsr_fme7(uint32_t v) {
    // D16 D13
    const uint32_t bit = v & 1; v >>= 1;
    // 实现1
    //if (bit) v ^= (uint32_t)0x12000;
    // 实现2
    const uint32_t mask = (uint32_t)(-(int32_t)bit);
    v ^= mask & (uint32_t)0x12000;
    return v;
}

包络频率

包络的每一个斜面(ramp)的频率如下:

  • 频率Frequency = Clock / (2 * 256 * Period)
  • 周期Period = Clock / (2 * 256 * Frequency)
  • 多除了2是因为使用了倍分频器

每个Ramp又被细分成32步, 对应的就是'音量'的改变.

YM2149-fig1

左边部分就是对应的固定音量. 右边部分则是包络使用的, 可以看出:

  • [L15] = [R31]
  • [L14] = [R29]
  • ...
  • [L1] = [R3] (虽然看不清但是应该是)
  • [L0] = [R0] (虽然看不清但是应该是) = 0

也就是可以用两个LUT, 反正要用, 不差这一个.

包络形状

通过写入$0D能够重置包络, 然后从4个参数选择其形状:

  • 续 Continue, Continue指定包络在Attack后是否继续震荡. 如果为0(续不了), 后两个参数不起作用
  • 起 Attack, Attack指定是高到底(0), 还是低到高(1)
  • 换 Alternate, Alternate指定信号在Attack后是否上下交换
  • 持 Hold, Hold指定信号在Attack后的保持值.
  • Alternate+Hold时, 会在Attack后交换Hold值然后保持下去
  • 其实不用理解, 只需要查表就行
   值  |  续 |  起  | 换  |持|    形状
-------|-----|-----|-----|--|------------
$00-$03|  0  |  0  |  x  |x |   \_______
$04-$07|  0  |  1  |  x  |x |   /_______
$08    |  1  |  0  |  0  |0 |   \\\\\\\\
$09    |  1  |  0  |  0  |1 |   \_______
$0A    |  1  |  0  |  1  |0 |   \/\/\/\/
$0B    |  1  |  0  |  1  |1 |   \¯¯¯¯¯¯¯
$0C    |  1  |  1  |  0  |0 |   ////////
$0D    |  1  |  1  |  0  |1 |   /¯¯¯¯¯¯¯
$0E    |  1  |  1  |  1  |0 |   /\/\/\/\
$0F    |  1  |  1  |  1  |1 |   /_______

(注: 图表中斜面的'斜率'是1)

也就是可以利用$08或者$0C模拟锯齿波, $0A或者$0E模拟三角波(不过前面了解到, 其实是指数版的锯齿波与三角波).

具体实现中, 希望体现出'形状', 于是这样实现: 化为4步, 然后执行12343434...


/// <summary>
/// StepFC: YM2149F 触发包络
/// </summary>
/// <param name="famicom">The famicom.</param>
static inline void sfc_ym2149f_tick_evn_times(sfc_famicom_t* famicom, uint8_t times) {
    // 不能太大
    //assert(times < 64);
    sfc_fme7_data_t* const fme7 = &famicom->apu.fme7;
    fme7->evn_index += times;
    fme7->evn_repeat |= fme7->evn_index & 64;
    fme7->evn_index &= 63;
    fme7->evn_index |= fme7->evn_repeat;
}

形状则是:

// 5B用 包络形状
#define FME7_SHAPE(a, b, c, d, e, f, g, h) \
 (a<<7) | (b<<6) | (c<<5) | (d<<4) | (e<<3) | (f<<2) | (g<<1) | (h<<0)

/// <summary>
/// FME-7 包络形状查找表
/// </summary>
static const uint8_t sfc_fme7_evn_shape_lut[] = {
    // $00 \___
    FME7_SHAPE(1, 0, 0, 0, 0, 0, 0, 0),
    // $01 \___
    FME7_SHAPE(1, 0, 0, 0, 0, 0, 0, 0),
    // $02 \___
    FME7_SHAPE(1, 0, 0, 0, 0, 0, 0, 0),
    // $03 \___
    FME7_SHAPE(1, 0, 0, 0, 0, 0, 0, 0),
    // $04 /___
    FME7_SHAPE(0, 1, 0, 0, 0, 0, 0, 0),
    // $05 /___
    FME7_SHAPE(0, 1, 0, 0, 0, 0, 0, 0),
    // $06 /___
    FME7_SHAPE(0, 1, 0, 0, 0, 0, 0, 0),
    // $07 /___
    FME7_SHAPE(0, 1, 0, 0, 0, 0, 0, 0),
    // $08 \\\\ 
    FME7_SHAPE(1, 0, 1, 0, 1, 0, 1, 0),
    // $09 \___
    FME7_SHAPE(1, 0, 0, 0, 0, 0, 0, 0),
    // $0A \/\/
    FME7_SHAPE(1, 0, 0, 1, 1, 0, 0, 1),
    // $0B \¯¯¯
    FME7_SHAPE(1, 0, 1, 1, 1, 1, 1, 1),
    // $0C ////
    FME7_SHAPE(0, 1, 0, 1, 0, 1, 0, 1),
    // $0D /¯¯¯
    FME7_SHAPE(0, 1, 1, 1, 1, 1, 1, 1),
    // $0E /\/\ 
    FME7_SHAPE(0, 1, 1, 0, 0, 1, 1, 0),
    // $0F /__
    FME7_SHAPE(0, 1, 0, 0, 0, 0, 0, 0),
};

/// <summary>
/// StepFC: YM2149F 获取包络音量
/// </summary>
/// <param name="shape">The shape.</param>
/// <param name="index">The index.</param>
static inline uint16_t sfc_ym2149f_get_evn_value(uint8_t shape, uint8_t index) {
    const uint8_t shift = (index & 0x60) >> 4;
    shape <<= shift;
    shape &= 0xc0;
    index &= 0x1f;
    index <<= 1;
    const uint8_t* const lut = (const uint8_t*)sfc_fme7_env_lut;
    const uint8_t* const data = &lut[shape | index];
    return *(uint16_t*)data;
}

还可以使用大号的两级LUT(uint8_t[16*64] + uint16_t[32]), 或更大号的1级LUT(uint16_t[16*64]).

不过桌面平台应该是自己这种实现要快一点(缓存友好).

输出

可以看出难点仅仅在于包络发生器的模拟, 其他的非常简单.

  • 一个声道如果是Tone模式, 即模拟方波. 方波01两位中出现1时输出音量
  • 一个声道如果是Noise模式, 即混了噪音, 噪音LFSR最低位是1时输出音量
  • 都没有(disable)就认为是1, 都有就认为将两个(方波01和LFSR最低位)做'与', 输出音量
  • 如果采用了包络, 输出音量是指包络的输出音量, 否则就是声道本身的音量
// 输出
bool flag;
if (tone & noise) flag = square & lfsr & 1;
else flag = disable | (tone & square) | (noise & lfsr);
// 检测
if (flag) output += fme7->ch[i].env ? fme7->env_volume : fme7->ch[i].volume;

还能够简化, 但是不想动脑筋了.

音量

  • 包络音量每格表示是大约1.5dB, 就是4次根号2倍
  • 总共32格也就是至少用8bit(还是9bit?)表示
  • 避免误差, 那就我们用13bit+1(0x2000‬)表示一个声道的音量大小
  • wiki虽然提到音量增益, 不过并没有提到与原来的比较, 只好作: 3声道最大输出1.0(不过和2A03一样, 只有正数, 其实只有一半)

模拟器吉米克出现的问题

这个很恼火.

bug2

开始一切正常, 但是进入游戏就一上来就死. 一步一步追踪, 老是以为IRQ实现有问题(为什么自己老是以为是IRQ有问题???).

output

然后发现CPU$173储存了数据, 但是老为0. 一步一步发现读取了WRAM区域, 吉米克! 将ROM切换到这里了. 最后的最后发现是把 PRG Bank 0 ($8) 的RAM/ROM位看反了:

  • D6:0 = PRG ROM
  • D6:1 = PRG RAM

看成了

  • D6:0 = PRG RAM
  • D6:1 = PRG ROM

...

最后, 自然还有老BUG(游戏状态栏一部分用精灵拼的). 看来FC游戏都喜欢在IRQ中切换BANK.

REF

附录: 查询表生成

自己用的LUT生成如下:


static uint16_t sfc_fme7_env_lut[32*4];
static uint16_t sfc_fme7_vol_lut[16];


enum { SFC_FME7_CH_MAX = 0x2000, SFC_FME7_CH3_MAX = SFC_FME7_CH_MAX * 3 };

/// <summary>
/// 初始化FME-7用LUT
/// </summary>
extern void sfc_fme7_init_lut(void) {
    double table[32];
    // 1 / 2^(0.25)
    const double qqrt2 = 0.84089641525;
    table[0] = 0;
    table[31] = SFC_FME7_CH_MAX;
    for (int i = 0; i != 30; ++i)
        table[30 - i] = table[31 - i] * qqrt2;
    // 建立LUT-A
    sfc_fme7_vol_lut[0] = 0;
    for (int i = 1; i != 16; ++i) {
        sfc_fme7_vol_lut[i] = (uint16_t)(table[i * 2 + 1] + 0.5);
    }
    // 建立LUT-B 0->0
    for (int i = 0; i != 32; ++i) 
        sfc_fme7_env_lut[i] = 0;
    // 建立LUT-B 0->1
    for (int i = 0; i != 32; ++i)
        sfc_fme7_env_lut[i+32] = (uint16_t)(table[i] + 0.5);
    // 建立LUT-B 1->0
    for (int i = 0; i != 32; ++i)
        sfc_fme7_env_lut[i+64] = (uint16_t)(table[31 - i] + 0.5);
    // 建立LUT-B 1->1
    for (int i = 0; i != 32; ++i)
        sfc_fme7_env_lut[i+96] = SFC_FME7_CH_MAX;
}

附录: 相关音色简单探索

这里简单测试了一下 吉米克! 没有使用过的包络、噪音的音色.

噪音, 将噪音和方波的周期弄到最大.

    CMD  PARAM
    0x0B, 0x04,
    0x0C, 0x00,
    0x06, 0x1f,
    0x00, 0xff,
    0x01, 0x0f,
    0x07, 0x30,
    0x08, 0x0e,
    0x09, 0x00,
    0x0A, 0x00,
    0x0D, 0x0E,
  • 听起来很像直升机的声音(直升机的音效原来可以这么简单模拟)!
  • 频率适当加高后听起来像是枪械开火的声音.
  • 也就是这个噪声很适合音效.

包络, 由于使用包络后不能调整音量了, 只能调整频率, 和2A03的三角波差不多.

tri

saw

听起来像......噪音..? 不好意思, 声音开得太大了

dustpg avatar Dec 02 '18 02:12 dustpg

由于 吉米克! 在92年初(FC末年)发布, 本拟定标题为'绝唱', 不过感觉时代交替并不是一件悲伤的事情, 于是作罢.

dustpg avatar Dec 02 '18 02:12 dustpg