-
Notifications
You must be signed in to change notification settings - Fork 23
Description
南梦宫163
不过说到男猛攻南梦宫, 南梦宫在05年和万代合并了, 发行的游戏感觉上比较偏向于宅系. 为N163分配的Mapper编号是019, 同一个编号的还有N129. 但是存在很多的submapper的情况——这个情况还是到wiki查看吧. 使用了了N163扩展音源的游戏有10个. 例如:
- 数字恶魔物语 女神转生II(女神転生Ⅱ デジタルデビル物語) (使用了4个声道)
- 万王之王(King of Kings) (使用了8个声道)
总览来看, 值得注意的是:
- CHR window 1Kx8 (PT) + 1Kx4 (NT)
- 这意味着支持使用ROM代替名称表, 好在上一节统一支持了这一特性
- These chips contain 128 bytes of internal RAM that can be used either for expansion audio or, together with a battery, for 128 bytes of save RAM.
- 可能有些游戏没有8kb的SRAM, 但是用电池支持这内部128字节的RAM
- 也就是如果没有扩展音源可以用这128字节保存游戏进度(?)
目前的存档信息:
enum {
// 需要储存进度SRAM-8KiB
SFC_ROMINFO_SRAM_HasSRAM = 0x01,
// 该位为真的话, 储存的不是SRAM-8KiB, 而是扩展区的32KiB
SFC_ROMINFO_SRAM_More8KiB = 0x02,
// 该位为真的话, 储存SRAM-8KiB外, 还要储存扩展区的偏移8kiB后128字节
SFC_ROMINFO_SRAM_M128_Of8 = 0x04,
};所以又更新了保存接口, 暴力读写:
/// <summary>
/// 数据
/// </summary>
typedef struct {
// 地址
void* address;
// 长度
uintptr_t length;
} sfc_data_set_t;
// 保存SRAM
void(*save_sram)(void*, const sfc_rom_info_t*, const sfc_data_set_t*, uint32_t);
// 读取SRAM
void(*load_sram)(void*, const sfc_rom_info_t*, const sfc_data_set_t*, uint32_t);Banks
- CPU $6000-$7FFF: 8 KB PRG RAM bank, if WRAM is present
- CPU $8000-$9FFF: 8 KB switchable PRG ROM bank
- CPU $A000-$BFFF: 8 KB switchable PRG ROM bank
- CPU $C000-$DFFF: 8 KB switchable PRG ROM bank
- CPU $E000-$FFFF: 8 KB PRG ROM bank, fixed to the last bank
- PPU $0000-$03FF: 1 KB switchable CHR bank
- PPU $0400-$07FF: 1 KB switchable CHR bank
- PPU $0800-$0BFF: 1 KB switchable CHR bank
- PPU $0C00-$0FFF: 1 KB switchable CHR bank
- PPU $1000-$13FF: 1 KB switchable CHR bank
- PPU $1400-$17FF: 1 KB switchable CHR bank
- PPU $1800-$1BFF: 1 KB switchable CHR bank
- PPU $1C00-$1FFF: 1 KB switchable CHR bank
- PPU $2000-$23FF: 1 KB switchable CHR bank
- PPU $2400-$27FF: 1 KB switchable CHR bank
- PPU $2800-$2BFF: 1 KB switchable CHR bank
- PPU $2C00-$2FFF: 1 KB switchable CHR bank
CHR and NT Select ($8000-$DFFF) w
写入数据:
- ①-$00-$DF: 选择1kb的CHR-ROM
- ②-$E0-$FF: 根据$E800的情况, 决定是否选择内部VRAM(偶数A面, 奇数B面)
- ②-如果没有选择则按照①的情况进行
写入地址:
- $8000-$87FF: BANK-0, ②-[$E800.6 = 0]
- $8800-$8FFF: BANK-1, ②-[$E800.6 = 0]
- $9000-$97FF: BANK-2 ②-[$E800.6 = 0]
- $9800-$9FFF: BANK-3 ②-[$E800.6 = 0]
- $A000-$A7FF: BANK-4 ②-[$E800.7 = 0]
- $A800-$AFFF: BANK-5 ②-[$E800.7 = 0]
- $B000-$B7FF: BANK-6 ②-[$E800.7 = 0]
- $B800-$BFFF: BANK-7 ②-[$E800.7 = 0]
- $C000-$C7FF: BANK-8 ②-一直允许
- $C800-$CFFF: BANK-9 ②-一直允许
- $D000-$D7FF: BANK-a ②-一直允许
- $D800-$DFFF: BANK-b ②-一直允许
令人吃惊的是允许将内置2kb的VRAM(CIRAM)接入图样表作为CHR-RAM.
PRG Select 1 ($E000-$E7FF) w
7 bit 0
---- ----
.MPP PPPP
||| ||||
|++-++++- Select 8KB page of PRG-ROM at $8000
+-------- Disable sound if set
64*8=512, D6是禁止位
PRG Select 2 / CHR-RAM Enable ($E800-$EFFF) w
7 bit 0
---- ----
HLPP PPPP
|||| ||||
||++-++++- Select 8KB page of PRG-ROM at $A000
|+-------- Disable CHR-RAM at $0000-$0FFF
| 0: Pages $E0-$FF use NT RAM as CHR-RAM
| 1: Pages $E0-$FF are the last $20 banks of CHR-ROM
+--------- Disable CHR-RAM at $1000-$1FFF
0: Pages $E0-$FF use NT RAM as CHR-RAM
1: Pages $E0-$FF are the last $20 banks of CHR-ROM
D6D7位很强
PRG Select 3 ($F000-$F7FF) w
7 bit 0
---- ----
..PP PPPP
|| ||||
++-++++- Select 8KB page of PRG-ROM at $C000
没什么好说的.
Write Protect for External RAM AND Chip RAM Address Port ($F800-$FFFF) w
7 bit 0
---- ----
KKKK DCBA
|||| ||||
|||| |||+- 1: Write-protect 2kB window of external RAM from $6000-$67FF (0: write enable)
|||| ||+-- 1: Write-protect 2kB window of external RAM from $6800-$6FFF (0: write enable)
|||| |+--- 1: Write-protect 2kB window of external RAM from $7000-$77FF (0: write enable)
|||| +---- 1: Write-protect 2kB window of external RAM from $7800-$7FFF (0: write enable)
++++------ Additionally the upper nybble must be equal to b0100 to enable writes
这个写入保护有点烦人, 或许到现在自己还没有实现或许还是一个好主意(为懒人正言).
IRQ Counter (low) ($5000-$57FF) r/w
7 bit 0
---- ----
IIII IIII
|||| ||||
++++-++++- Low 8 bits of IRQ counter
IRQ Counter (high) / IRQ Enable ($5800-$5FFF) r/w
7 bit 0
---- ----
EIII IIII
|||| ||||
|+++-++++- High 7 bits of IRQ counter
+--------- IRQ Enable: (0: disabled; 1: enabled)
IRQ是一个15bit的计数器, 读取$5000和$5800能够读取计数器的当前值, 可以实时读取.
IRQ这个计数器在每个CPU周期都会递增, 达到$7fff时就触发IRQ, 并停止计数. 写入这两个寄存器会确认IRQ.
一帧CPU周期大约是3万所以15bit还是足够了的.
N163音源
N163音源(如果有的话)使用的就是内部的128字节数据.
Address Port ($F800-$FFFF)
这个地址还有另一个用处: 为之后写入内部RAM的数据指定地址.
7 bit 0 (write only)
---- ----
IAAA AAAA
|||| ||||
|+++-++++- Address
+--------- Auto-increment
D7:I 为真的话, 写入或者读取'Data Port'地址会递增. 由于是在'Audio'里面的, 所以将储存在N163音源端, 而不是Mapper端.
Data Port ($4800-$4FFF)
为内部RAM写入或者读取数据, 并且根据$F800:D7是否自动+1.
Sound RAM $78 - Low Frequency
7 bit 0
---------
FFFF FFFF
|||| ||||
++++-++++- Low 8 bits of Frequency
Sound RAM $79 - Low Phase
7 bit 0
---------
PPPP PPPP
|||| ||||
++++-++++- Low 8 bits of Phase
Sound RAM $7A - Mid Frequency
7 bit 0
---------
FFFF FFFF
|||| ||||
++++-++++- Middle 8 bits of Frequency
吃惊, 别的芯片频率设置用11bit, 12bit, 这用16bit还不够.
Sound RAM $7B - Mid Phase
7 bit 0
---------
PPPP PPPP
|||| ||||
++++-++++- Middle 8 bits of Phase
Sound RAM $7C - High Frequency and Wave Length
7 bit 0
---------
LLLL LLFF
|||| ||||
|||| ||++- High 2 bits of Frequency
++++-++--- Length of waveform ((64-L)*4 4-bit samples)
18bit的频率与6bit的长度. 长度的单位是: 样本, 可以计算为:
length = 256 - (value & 0xFC);Sound RAM $7D - High Phase
7 bit 0
---------
PPPP PPPP
|||| ||||
++++-++++- High 8 bits of Phase
24bit的相位数据决定了当前波的相位. 每次声道更新时, 18bit的频率数据就会添加到24bit的相位数据上.
写入这三个相位寄存器会立即重置相位. 对于开发者来说最好先将频率置0, 不然写入3个寄存器之间存在时间差(虽然很短就是了), 可能中途会更新(不禁想起了线程安全).
Sound RAM $7E - Wave Address
7 bit 0
---------
AAAA AAAA
|||| ||||
++++-++++- Address of waveform (in 4-bit samples)
Sound RAM $7F - Volume
7 bit 0
---------
.CCC VVVV
||| ||||
||| ++++- Linear Volume
+++------ Enabled Channels (1+C)
'C'位只有$7F是有效的, 其他的对应地址是无效的.
- 0: Ch7有效
- 1: Ch76有效
- ...
- 7: Ch76543210都有效
其他声道
- Channel 7: $78-$7F
- Channel 6: $70-$77
- Channel 5: $68-$6F
- Channel 4: $60-$67
- Channel 3: $58-$5F
- Channel 2: $50-$57
- Channel 1: $48-$4F
- Channel 0: $40-$47
- 可以考虑把编号掉一下头, 这样从零开始
波形信息
波形信息以一个样本深度为4bit的规则储存的, 一个字节可以保存2个样本, 小端形式保存:
/// <summary>
/// StepFC: N163 采样
/// </summary>
/// <param name="famicom">The famicom.</param>
/// <param name="addr">The addr.</param>
/// <returns></returns>
static inline int8_t sfc_n163_sample(sfc_famicom_t* famicom, uint8_t addr) {
const uint8_t data = sfc_n163_internal_chip(famicom)[addr >> 1];
return (data >> ((addr & 1) << 2)) & 0xf;
}'Sound RAM $7E - Wave Address'储存的样本的地址, 本身内部RAM是128字节. 换句话说: 在不考虑重叠的情况下, 如果使用2个N163声道, 每个声道能用56个字节, 差不多100个样本. 而使用8个N163声道, 就只能独立使用8个字节了(当然这种情况, 只能互相重叠或者重新利用).
'Sound RAM $7C - High Frequency and Wave Length'储存了波表的长度, 单位是样本. 不过由于4个样本对齐, 所以直接除以2就是字节长度.
$00: 00 00 00 A8 DC EE FF FF EF DE AC 58 23 11 00 00
$10: 10 21 53 00 00 00 00 00 00 00 00 00 00 00 00 00
Wave Address = $06, 表示从RAM:$03低地址开始
Wave Length = $38(变成高6bit的话就是$E0)
表示长度是32样本
F - *****
E - ** **
D - * *
C - * *
B -
A - * *
9 -
8 - * *
7 -
6 -
5 - * *
4 -
3 - * *
2 - * *
1 - ** **
0 - *****
更新
与其他扩展音源不同的是, 8个声道不在一起更新, 而是一个一个更新的. 每更新一个声道需要15个CPU周期:
| 声道数量 | 更新频率 |
|---|---|
| 1 | 119318 Hz |
| 2 | 59659 Hz |
| 3 | 39773 Hz |
| 4 | 29830 Hz |
| 5 | 23864 Hz |
| 6 | 19886 Hz |
| 7 | 17045 Hz |
| 8 | 14915 Hz |
可以看出如果只是用两个的话效果还行(>44100Hz), 使用8个的话反而会差一点, 噪音会有点重.
具体过程, wiki也是直接贴出代码:
* w[$80] = the 163's internal memory
* sample(x) = (w[x/2] >> ((x&1)*4)) & $0F
* phase = (w[$7D] << 16) + (w[$7B] << 8) + w[$79]
* freq = ((w[$7C] & $03) << 16) + (w[$7A] << 8) + w[$78]
* length = 256 - (w[$7C] & $FC)
* offset = w[$7E]
* volume = w[$7F] & $0F
phase = (phase + freq) % (length << 16)
output = (sample(((phase >> 16) + offset) & $FF) - 8) * volume
phase信息会写回去.output会减去8也就是说, 是有符号的.- 所以
sfc_n163_sample函数返回的int8_t.
频率计算
f = wave frequency
l = wave length
c = number of channels
p = 18-bit frequency value
n = CPU clock rate (≈1789773 Hz)
f = (n * p) / (15 * 65536 * l * c)
合并输出
与VRC7不同的是, N163如果只有一个声道, 这个声道会以近120kHz更新. 有8个, 这个声道就以15kHz更新. 所以理论上一个声道的最大声音, 其实和8个声道一起放的(最大声音)是一样大的.
精确的高频模拟比较困难, wiki提到可以简单将数据加在一起再除以当前声道数量. 不过不同游戏可能音量有差异, 详细的可以查看wiki. 这里列出submapper:
- 3: N163比2A03方波大了大约11.0-13.0 dB
- 4: N163比2A03方波大了大约16.0-17.5 dB
- 5: N163比2A03方波大了大约18.0-19.5 dB
- 一般来说, 可以简单看作12dB-4倍, 18dB-8倍
但是声道数量在6以上时, 这个时候几乎已经达到了人类可听的频率了. 其中就算放出01方波, 频率会再除以2以至于完全可闻.
这里自己就用'3声道模式'. 这里说3声道模式, 并不是指仅仅模拟3声道, 而是每次将最后处理的3个声道(接近40kHz)进行输出, 当然, 这时候采样输出不能低于40kHz(所以作为参数放入N163, 假设支持22kHz就用'6声道模式').
- 声道模式为n
- 总倍数为N
- 单声道输出8bit有符号, 大约是方波的16倍
- 输出
Sum(hist, n) / n / 16 * N - 适当增加n可以视为一个滤波器, n越高截止频率越低
- 当然, 继续使用之前的context+per_sample模式
// N163
if (g_famicom->rom_info.extra_sound & SFC_NSF_EX_N163) {
// 3声道模式
const float out = sfc_n163_per_sample(g_famicom, &n163_ctx, cpu_cycle_per_sample, 3);
output += out * (0.00752f / 16.f) * n163_ctx.subweight;
}N163
N163看上去有8个声道简直逆天, 但是实际上拥有很大的限制:
- 公用一个波形表地址空间
- 如果声道少的话, 就可以用很长的波表了
- 相反, 声道多就只能用短一点了
- 每15CPU周期仅仅更新一个声道, 如果声道多了杂音会很重.
- 上面这一点, 我们可以考虑进行声部强化
- 总之, N163的特点就是用的声道越多, 限制就越大
- 不过目前的游戏: 8个使用了4声道, 2个使用了8声道
编写中遇到的问题
N163的编写感觉一帆风顺, 除了一两个丢人的小问题, 比如Mapper编号19的16进制不小心弄成'15'.
模拟女神转生2出现的问题
都说了'一帆风顺'. 不过一开始....
一开始就花屏了, 由于只打开了N163声道, 还以为是IRQ触发有问题, 后来发现本来就是这样.
精度问题
进入游戏后, 人物上下移动瞬间, 背景错位(仅仅一帧). wiki提到修改是下一帧起效, 所以就把
void sfc_ppu_do_end_of_vblank(sfc_ppu_t* ppu) {
// v: .....F.. ...EDCBA = t: .....F.. ...EDCBA
ppu->data.v = (ppu->data.v & (uint16_t)0x841F) | (ppu->data.t & (uint16_t)0x7BE0);
}这段调用放在最后. (具体是精度还是本来就是这样, 等待验证)
REF
附录: FamiTracker对N163的支持
FamiTracker可以粗暴地认为是NSF的创作软件.
FamiTracker allows waveforms between 4 and 32 samples long (in increments of four), though recent discoveries have shown the actual chip is capable of reading the entire channel/waveform memory space, for a total of 256 samples per wave.
应该是实现难度的问题, N163本身可以支持到256个样本(当然就包含声道7的寄存器数据了), 但是FT只支持4~32个样本. 手动的话, 自由度很大, 不仅能使用256个, 通道之间还能交叉使用部分.
* * ****
* * *****
* *
[ CH0 ]
[ CH1 ]
[ CH2 ]
