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
CHR $0000-$03FF: 1 KB switchable CHR ROM bank
CHR $0400-$07FF: 1 KB switchable CHR ROM bank
CHR $0800-$0BFF: 1 KB switchable CHR ROM bank
CHR $0C00-$0FFF: 1 KB switchable CHR ROM bank
CHR $1000-$13FF: 1 KB switchable CHR ROM bank
CHR $1400-$17FF: 1 KB switchable CHR ROM bank
CHR $1800-$1BFF: 1 KB switchable CHR ROM bank
CHR $1C00-$1FFF: 1 KB switchable CHR ROM bank
感觉都还科学. 变种区别为
VRC7b 使用 A3 来选寄存器($x008)
VRC7a 使用 A4 来选寄存器($x010)
PRG Select 0 ($8000)
7 bit 0
---------
..PP PPPP
|| ||||
++-++++- Select 8 KB PRG ROM at $8000
64*8=512
PRG Select 1 ($8010, $8008)
7 bit 0
---------
..PP PPPP
|| ||||
++-++++- Select 8 KB PRG ROM at $A000
PRG Select 2 ($9000)
7 bit 0
---------
..PP PPPP
|| ||||
++-++++- Select 8 KB PRG ROM at $C000
拉格朗日点
之所以将游戏名称作为小标题, 自然是说到可乐妹的VRC7, 就不得不说'拉格朗日点'了, 因为这是一款唯一使用了VRC7的游戏. 被不少人冠上'FC最强音乐'的帽子.
当然实际上还有一款'兔宝宝历险记2(日版)'也使用了VRC7, 不过'兔宝宝历险记2'没有使用到VRC7的扩展音源(实体卡带比前者小了不少, 可以看作前者使用了VRC7a, 后者使用了VRC7b).
BANK
感觉都还科学. 变种区别为
PRG Select 0 ($8000)
64*8=512
PRG Select 1 ($8010, $8008)
PRG Select 2 ($9000)
CHR Select 0…7 ($A000…$DFFF)
Mirroring Control ($E000)
IRQ Control ($E008 - $F010)
对比起VRC6起来, 简直不知道友好到哪里去! 根据地址线的规律, 可以使用:
将分散的数据聚集在一起方便
switch
编写.兔宝宝历险记2 模拟出现的问题
一开始这个东西根本就不能运行, 到处出错. 一步一步反汇编后发现, 其实是IRQ的实现有问题.
CLI
后不久, 强行触发中断导致程序乱跑.根本原因还是IRQ实现有问题(废话), APU禁用IRQ后依然触发了已经挂起的IRQ. 目前先将挂起的IRQ清除掉, 以后好好研究一下IRQ.
VRC7 扩展音源
VRC7拥有6个FM合成音源声道, 实现了Yamaha YM2413 OPLL的一个功能子集(阉割版). 似乎叫做'Yamaha DS1001', 下面为了方便描述, VRC7与'Yamaha DS1001'在描述上等价.
前面有一个寄存器
Mirroring Control ($E000)
, 其中Silence
位为1的话, 会让VRC7部分静音并清空相关数据状态.Audio Register Select ($9010)
写入后, 程序不得在6个CPU周期(N制, 实际是12个内部周期)内写入
$9030
以及$9010
, VRC7内部处理需要一点时间.Audio Register Write ($9030)
写入后, 程序不得在42个CPU周期(N制, 实际是84个内部周期)内写入
$9030
以及$9010
, VRC7内部处理需要一点时间.42周期可是比三分之一扫描行还多! 不过, 一般地, 作为模拟器不必担心.
内部寄存器
虽然
$9010
表明几乎有256个寄存器, 似乎内部只有26个:$00-$07
自定义Patch$10-$15, $20-25, $30-35
分别控制拥有的6个FM声道频率调制
翻开(已经不存在的)大学教材, 回想起被傅里叶老人家支配的痛苦, 不由得感慨万分, 于是合上(已经不存在的)这本教材.
频率调制(Frequency modulation)FM, 现在似乎变成了电台的代名词了. 简单来说, 这里就是通过调制振荡器(Modulator)与载波振荡器(Carrier)进行合成, 即双算子-FM.
在音乐合成时, 双算子-FM可以采用:
ADSR包络提供一个比较自然的
A(t)
,I(t)
函数:VRC7内部拥有15个预置好的乐器PATCH和1个自定义PATCH, 也就是每8字节一个乐器:
声道
$5
替换掉PATCH中的'R'阶段原本的数据0->1
我们可以认为是键盘按下,1->0
则可以认为是键盘弹起. 键盘按下就能弹出一个音符, 按键弹起则进入音符ADSR的'R'阶段. 如果键盘敲不响, 肯定是卡住了:0->0, 1->1
.$2X:O
3bit的八度数据. 这两个合在一起定义了一个输出频率:例如, A440, 记作A4, 八度(octave)为4. 这时freq为288.
49716Hz(49715.909Hz)是因为内部需要72周期处理全部声道, 所以可以反推VRC7的内部运行频率大致是
3.579552MHz
, 大约是N制CPU频率的两倍.而且这个东西是硬件, 不会说插在P制红白机上就自动降频了(不过由于是VRC7内部驱动, 所以插在P制上也是发出的是同一个频率音符, 不像内部声部). 并且, 由于49716Hz>44100Hz, 所以我们可以认为, 最后输出是由各个声道通过混频得到的.
下面, 如果没有特殊说明, 说到VRC7的时钟周期, 是指
49716Hz
的周期.VRC7的场合
'VRC7 Audio'里面并没有详细介绍细节, 不过下面论坛链接就有了.
相位计算:
每个算子拥有一个18bit的计数器用于确定当前相位, 每个时钟周期都会递增:
实际使用中会使用'phase_secondary':
phase_secondary = phase + adj
adj
是调制器的输出. 超过18bit的将会舍弃F
为0:adj=0
adj = previous_output_of_modulator >> (8 - F)
phase_secondary
对于调制器就是利用相位差, 定位指定的相位:18bit的'phase_secondary'构造为:
[RI IIII III. .... ....]
R和I都是之后需要使用的:
输出计算:
每个周期每个算子输出一个衰减值:
其中
half_sine_table
并不是真正的正弦函数表, 而是衰减值:其中
base
:base = (0.75dB * L)
, L是$02的6bit基础衰减值base = (3.00dB * V)
, V是$3X的4bit基础衰减值其中
key_scale
是$02$03的2bit值K
:后面的包络
envelope
, AM, 后面说明.包络发生器
每个算子的包络发生器拥有一个23bit的计数器(记为EGC), 为输出添加衰减值(控制音量). 拥有ADSR阶段, 结束后进入空状态(Idle).
除开Attack阶段,
EGC
为零时输出0dB,EGC
为1<<23
时, 输出48dB. 文中建议dB使用(1<<23)/48
作为基础单位, 以做出最少的单位转换.这里列出一些数据之后会用上:
K ? OF : OF >> 2
R*4 + KO
RKS >> 2
, 超过15则被钳制到15RKS & 3
Attack阶段:
R
是对应的Attack速率EGC += (12 * (RL+4)) << RH
EGC
超过23bit范围, 归零, 进入Decay阶段.AO(EGC) = 48 dB - (48 dB * ln(EGC) / ln(1<<23))
Decay阶段:
R
是对应的Decay速率EGC += (RL+4) << (RH-1)
EGC >= (3 * Sustain * (1<<23) / 48)
Sustain阶段:
$0$1: sustain
为0,R
是对应的Release速率R=0
EGC += (RL+4) << (RH-1)
Release阶段:
$2X:S
为1的话,R=5
$0$1: sustain
为0的话,R
是对应的Release速率R=7
EGC += (RL+4) << (RH-1)
Idle阶段:
敲键盘
敲下和弹起后会处理一些事. 前面说了, 卡住的不算, 换一个键盘再来.
敲下:
EGC=0
弹起:
EGC=AO(EGC)
)AM/FM
前面提到的什么抖音颤音就在这了. 与前面不同的是, AM/FM的状态是全算子共用的(VRC7就一个AM/FM). AM/FM拥有一个20bit的计数器, 每个周期加上
rate
, 有一个共有的sinx = sin(2 * PI * counter / (1<<20))
.AM:
rate = 78
AM_output = (1.0 + sinx) * 0.6 dB
(emu2413 使用的是 1.2 dB)FM:
rate = 105
FM_output = 2 ^ (13.75 / 1200 * sinx)
也就是说, FM/AM的计算不受用户(这里是指程序猿)的影响, 用户能控制的是决定用不用AM/FM.
可以看出很多地方的计算相当复杂, 具体可以用查表实现. 涉及到精度的查表, 自己会用常量控制, 然后试试什么精度可以接受.
实际编写
实际编写中自然是遇到了大量问题:
inf
需要特别处理.op->key_scale = (uint32_t)a >> (3 - key_scale_bits)
op->key_scale = ((uint32_t)a << key_scale_bits) >> 3
op->key_scale = (uint32_t)a << (3 - key_scale_bits)
ctrl+z
还一个架构上的问题:
之前提到了, 目前的处理模式是处理上一个状态. 所以外部有一个状态机, 保存了模拟器声部的上一次状态. 但是由于VRC7状态太复杂了, 不方便弄, 所以实现与其他不同:
audio_changed
事件合并输出
(double)(1<<23)
(单精度浮点捉襟见肘了), 理论最大值为0.75(6声道)PATCH表
预置PATCH似乎到目前还没有明确值, 自己使用的自然是wiki提供的:
这有128字节, 之前提到自定义BUS有256字节咩用到, 刚好可以用! 不过由于不在APU区, 储存状态时需要提供接口写入状态(这里是读取, 写入类似):
拉格朗日点 模拟出现的问题
音效: 开头start就出现问题了. 遂搜索论坛, 频率扫描的位移器, 如果为0则不进行扫描.
坑爹呢这是! wiki用0作为例子自己还以为是可以是0的! 这个问题已经在前面的博客修改并注明了(希望不要再出幺蛾子). 之前的代码自然是错的, 不过也不会去改了.
这游戏感觉完全在炫耀机能一般, 第一个BGM就在大量使用FM特性.
REF
附录: 各个表长的研究
FM
第一个说FM是因为有两个考虑的地方, 深度与宽度, 其中如果采用的是浮点的话, '深度'就无需考虑了. 但是代码就这一个地方使用了浮点也太奇怪了.
宽度同其他的, 这里谈谈深度.
F * (1<<O) * M * V / 2
, 转成(F * (1<<O) * M / 2) * V
. 其中(F * (1<<O) * M / 2)
播放一个音符中, 是一个固定值.490560 * 4128 / 4096
之类的8192
a * 1.008
->a + a*0.008
实际实现中, 由于有除以4的操作, 这样会丢失精度. 所以上面函数
uint32_t left
, 实际是还没有除以4的uint32_t left_x4
宽度
精度10位时, 最后几个差距有点大.
精度14位时, 精度大致介于1与2之间.
精度16位时, 精度已经有0.5了.
所以选择: 15位, 这基本是最低要求, 可以使用16位