dustpg / BlogFM

Blog for Me
MIT License
155 stars 23 forks source link

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

Open dustpg opened 5 years ago

dustpg commented 5 years ago

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).

寄存器

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

命令: 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个问题, 模拟出现问题(瑕疵), 但是使用其他模拟器也是同样的:

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等价.

声调

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

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

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

噪音

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

// 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)的频率如下:

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

YM2149-fig1

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

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

包络形状

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

   值  |  续 |  起  | 换  |持|    形状
-------|-----|-----|-----|--|------------
$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]).

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

输出

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

// 输出
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;

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

音量

模拟器吉米克出现的问题

这个很恼火.

bug2

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

output

然后发现CPU$173储存了数据, 但是老为0. 一步一步发现读取了WRAM区域, 吉米克! 将ROM切换到这里了. 最后的最后发现是把 PRG Bank 0 ($8) 的RAM/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 commented 5 years ago

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