49716 Hz * freq
F = -----------------
2^(19 - octave)
注: 使用FM功能另算.
音量:
$30-$35, 低4位, 非线性值. 单位3.00dB.(衰减值, 15最小, 0最大)
FDS1
波形表: f = CPU * current_picth / 65536 / 64
音量:
master_volume=2/2, 2/3, 2/4, 2/5
volenv_gain 会被钳制到32
MMC5
同2A03
N163
波形表频率:
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)
StepE: 高级音频支持
数数这一步的内容:
音频可视化
终于, 进入本步骤的最后一篇——音频可视化. 已经到E了, 16进制已经快装不下了, 预示着《Re: 从零开始的红白机模拟》也快结束了.
说到音频可视化, 可能就不得不提到傅里叶老人家. 不过在这里, 可能是见不到他老人家了——因为我们能拿到更基础的信息
数据可视化
可视化的目的就是为了直观的了解到相关'信息', 那么音频方面信息有哪些呢?
结合实际情况, 我们需要对音高、音色、音量等信息进行可视化.
音高
音高反映的是频率, 频率可视化自然就是映射到键盘上——
例如440Hz就是A4(A440标准), 中央C大致是262Hz. 键盘上是以'3白夹2黑 + 4白夹3黑'作为一节, 然后向两个方向扩展开来.
白键从左到右依次叫做CDEFGAB, 黑键则是对应的两个白键之间的半音, 记作升(♯, sharp, 就是C#的那个), 或者降(♭, flat). 即C# = D-flat. (♯#或许有区别, 不过'#'比较容易输入, 这里就用#作为半音符号表示).
其中EF, BC其实也是半音的关系. 应该是为了方便钢琴家定位才这么排列的.
标准钢琴则是选取的: 以A0开始的88键这个一区间. 挑战! 回头找一下'中央C'在哪!
键位计算
Equations for the Frequency Table给出了计算公式:
现在自然要做出逆运算:
实际上, 因为一节是以C开始的所以实际实现中, 是以C4作为0. 再具体, 可能需要进行四舍五入转成整型方便查表, 注意:
也就是负数需要特别注意.
具体处理
颜色生成
这里颜色生成是通过HSL色彩空间映射到RGB空间的方式, 生成的颜色. 需要一些随机颜色就可以随机'H'即可, 直接随机RGB生成的颜色不怎么好看, 这也算是一个小技巧.
目前需要的数据:
当然, 音量可以也用不透明度表示——不过交给用户控制, 不然可能看不清. 目前简单乘以2再钳制到1.
自己(正式释出)打算用(类对数)曲线控制:
音色
我们知道, 音色是由其谐波决定的. 不过除开类似ΔPCM、FDS之类的基于波形信息的, 其余声部的音色其实已经预定好了, 我们需要的就是展现其内在信息.
例如方波就展示其'占空比'这一属性.
音量
不少声部都支持音量的调制.
例如看出橙色的方波#1占空比是25%, 音量大约是11. 绿色的方波#2占空比是75%, 音量约8.
各个声道计算
2A03
f = CPU / (16 * (t + 1))
f = CPU/(32*(t + 1))
VRC6
f = CPU / (16 * (t + 1))
f = CPU / (14 * (t + 1))
(rate*6)>>3
, 5bit. 可能是线性值VRC7
频率调制:
注: 使用FM功能另算.
音量:
FDS1
f = CPU * current_picth / 65536 / 64
master_volume=2/2, 2/3, 2/4, 2/5
volenv_gain
会被钳制到32MMC5
N163
波形表频率:
相关参数:
FME7
f = CPU / (2 * 16 * Period)
f1 = CPU / (2 * 256 * Period)
f2 = CPU / (2 * 256 * Period * 2)
波形调制
多试了几个NSF, 在播放FDS时, 某些NSF音调上还是有点问题, 这个感觉找不到原因了, 这个BUG估计得留下来了.
N163, FDS1, 以及VRC7都可以算是通过给定波形进行指定频率输出. 当然还可以算上半个DMC. 对于这种我们可以显示原始波形在一旁作为参考.
N163和FDS1的波形是可以直接获取的. 但是VRC7的不能. DMC的这里暂时不考虑, 如果长度1kb的话就有8k个样本了.
获取VRC7波形
我们再来回顾一下VRC7声道的控制位:
LLLL LLLL
Channel low 8 bits of frequency--ST OOOH
Channel sustain (S), trigger (T), octave (O), high bit of frequency (H)IIII VVVV
Channel instrument (I), volume (V)影响音色的只有 Sustain覆盖位, 以及 乐器索引. 后者是理所当然的, 前者是无关紧要的, 是用来控制衰减.
然后就是样本数量了, 参考FDS1的64个, N163的最大256个. 那就决定采样128个.
49716Hz / 128 = 388.4Hz
, 比G4的392Hz略低, 即octave = 4.ADSR包络
直接生成的话几乎没有声音, 因为正在处于'A'阶段. 最暴力的办法当然就是修改核心代码让这个情况返回0衰减. 这里就采用'ADSR'增加一个阶段, 这这个阶段直接返回0.
这样生成的波还是有点问题, 也就是粗糙模拟而已. 如果精确模拟的话可能太花时间了.
VRC7补漏
补充说明: 这里VRC7有一些不太影响的'小错误', 下面是后面补充的. 果然小细节就容易忘记! VRC7的音量是'衰减值', 15是最小音量, 0是最大音量. 所以单位变成负数了
-3.00dB x N
.然后自己又想到一个稍微精确的波形计算, 更简单但是需要一点额外的运算(也不多, 和上次一样128样本, 只不过是每帧都需要):
这样生成的波形依然不准确, 但是可以体现ASDR的衰减和调制器!
减少DrawCall
减少DrawCall是提示图形显示效率的最直接的办法. 这里, 如果相邻的两条线段斜率一致则可以进行合并.
实时版
这样称为'实时版', 不存在时间的问题, 可以在游戏的时候也能打开, 最后大致结果就是:
测试视频地址
框架优势
现在就要发挥目前框架优势了, 目前这个项目的优势为:
接口意味着允许'动态更换'.
事件驱动本来是自己对于音频的一种实现手段, 并且精度比不上基于周期的'次等手段', 不过本身就包含了音频事件信息, 并且没有真正处理音频.
非单例, 几乎每条函数都自带了一个
sfc_famicom_t*
参数, 这本来是'累赘'.音频事件时间线
这是FT的界面
我们可以看出有一条'时间线', 下方的就是未来的事件, 上方的就是之前的事件. 是时候发挥框架优点了: 变废为宝.
前面提到的优势就是: 我们可以同时运行两台虚拟机, 使用不同的接口. 一个跑在前面, 仅仅处理音频事件并且记录下来, 后面的才是真正的播放.
内容
时间线实际上可以表示很多(几乎所有)东西, 但是出于简单化, 考虑仅仅处理音量和音高.
其中音高值得注意的是, 作曲家可能会手动模拟FM——也就是'抖音'. 频率在一个音符范围内来回摆动, 这样在时间线是看不出区别的——都是一个音. 所以可以打上FM的标记, 键盘上的波浪线'~'就比较合适.
音量用线段表示就行. 而对应的AM暂时不考虑, 背景可以从折线看出来.
目前没有做优化, 每次都是从现有数据生成的. 这种最末端的代码, 自然是放在最后优化, 不然一改就傻了
预载版
这样称为'预载版', 只能在播放NSF时才能启用. 并且, 再配合'即时读档'功能时需要特别注意:
测试视频地址
.
大致是这样的, 不过还是太简陋了, 不过反正仅仅是随便写的, 还没有正式释出.
还有一种比较典型的解决方案就是:
这种也是一种解决方案, 比如可以用颜色表示声道, 宽度表示音量, 位置就是音高了. 不过自己还是希望看一下'谱子', 而不是'键位'.
StepE: 高级音频支持
这就本步的最后一节, 音频可视化. 这一步统称'高级音频支持'. 本步骤项目地址:
Github-StepFC-StepE
REF
附录: 负数MOD运算
((x % 12) + 12) % 12
微软编译器的实现:将原本两次
idiv
优化成一次了. 所以, 膜还是很花时间的, 大家不要随便膜.