这个简单的FM合成器设计有什么问题?

39

我正在尝试使用Audiolet(一种合成库,http://oampo.github.io/Audiolet/api.html)在JavaScript中实现Yamaha YM3812声音芯片的一些功能(又称为OPL2,详情请参阅 http://en.wikipedia.org/wiki/YM3812)。

Audiolet允许您将合成器构建为节点图(振荡器、数字信号处理器、包络发生器等)。

OPL2有九个通道,每个通道都有两个运算器(振荡器)。通常情况下,每个通道中的一个振荡器调制另一个振荡器的频率。为了模拟这一点,我为每个通道构建了一系列节点:

合成器节点链(其中之一的九个通道)

已实施的OPL2通道

节点链创建和连接代码:

var FmChannel = function(audiolet) {
    this.car = new ModifiedSine(audiolet);
    this.carMult = 1;
    this.setCarrierWaveform(this.SIN);
    this.mod = new ModifiedSine(audiolet);
    this.modMult = 1;
    this.setModulatorWaveform(this.SIN);
    this.modMulAdd = new MulAdd(audiolet);
    this.carGain = new Gain(audiolet);
    this.carEnv = new ADSREnvelope(audiolet, 0, 0.1, 0.1, 0.1, 0.1,
        function() {
            this.carEnv.reset();
        }.bind(this)
    );
    this.carAtten = new Multiply(audiolet);
    this.modGain = new Gain(audiolet);
    this.modEnv = new ADSREnvelope(audiolet, 0, 0.1, 0.1, 0.1, 0.1,
        function() {
            this.modEnv.reset();
        }.bind(this)
    );
    this.modAtten = new Multiply(audiolet);

    this.modEnv.connect(this.modGain, 0, 1);
    this.mod.connect(this.modGain);
    this.modGain.connect(this.modAtten);
    this.modAtten.connect(this.modMulAdd);
    this.modMulAdd.connect(this.car);
    this.carEnv.connect(this.carGain, 0, 1);
    this.car.connect(this.carGain); 
    this.carGain.connect(this.carAtten);
    // connect carAtten to the mixer from outside
};

然而,当我设置调制器和载波节点的参数(振荡器波形、相对频率、衰减、ADSR参数)并触发音符时,输出与具有相同参数的良好OPL2仿真器几乎没有什么相似之处。一些声音大致相符,另一些则相当不愉快。

我有一些想法可以继续进行(我猜在不同阶段绘制输出可能是一个很好的起点),但我希望有经验的人能指点我方向,或者指出我所做的事情明显错误的地方。我没有信号处理或强大的数学背景。我对FM没有深刻的直观理解。

我怀疑存在以下问题:

1)我的FM实现(如上所示)基本上是错误的。此外,在播放音符的函数中可能存在问题(设置振荡器频率,并在触发ADSR包络之前缩放和偏移调制器):

FmChannel.prototype.noteOn = function (frq) {
    var Fc = frq*this.carMult;
    this.car.reset(Fc);
    this.mod.reset(frq*this.modMult);
    // scale and offset modulator from range (-1, 1) to (0, 2*Fc)
    // (scale and offset is after ADSR gain and fixed attenuation is applied)
    this.modMulAdd.mul.setValue(Fc);
    this.modMulAdd.add.setValue(Fc);
    this.carEnv.reset();
    this.modEnv.reset();
    this.carEnv.gate.setValue(1);
    Thethis.modEnv.gate.setValue(1);
};

2)FM合成器的输出可能对调制器ADSR包络形状的微小差异非常敏感(如果这是正确的,请告诉我!),而我的ADSR包络充其量只是真正OPL2中的ADSR的粗略近似。我的实现还缺少一些看起来相对不重要的功能(例如键盘缩放),但这可能会显着影响FM合成器的声音(同样,我不确定)。


4
看了你的图片,调制器应该与载波频率相关,而不是增益(类似于这个链接中的示例:https://en.wikipedia.org/wiki/Frequency_modulation#/media/File:Amfm3-en-de.gif)。 - eri0o
通常情况下,您会希望小心调制器的增益级别,不要应用过多的增益和调制。如果调制过大,您会听到“不愉快的声音”,因为调制会支配载波。我不确定这是否是您所描述的情况。 - noumenal
艾瑞克,根据这个图表,调制器肯定与增益相关联。我已经很久没有接触这方面的工作或者Audiolet了,所以我现在真的不确定这是不是正在发生的事情!我会仔细研究一下。 - bsa
"noumenal,我认为你可能走在正确的道路上。我会尝试恢复它并看看在调制器上减少增益后会发生什么。" - bsa
1个回答

2

大多数标记为“FM”的合成器实际上是相位调制(PM,请参见https://en.wikipedia.org/wiki/Phase_modulation)。这样做有一些好处(主要是可以在大范围音调下获得更稳定的声音)。

OPL2也可能使用这种方法,尽管我没有找到明确的证据,但维基百科文章中也使用了“相位调制”一词。

简而言之,许多被标记为“FM”的音乐合成器实际上采用了“PM”,因此您可以尝试使用这种方式,并检查它是否更适合预期的OPL2声音。

从快速浏览Audiolet源代码来看,我猜测振荡器正在进行真正的FM,因此您可能需要替换它并添加一个相位输入以允许相位调制。

基本上,这行

output.samples[0] = Math.sin(this.phase);

Sine的载波振荡器所使用的将会读取类似于以下内容:

output.samples[0] = Math.sin(this.phase+phase_offset);

使用模拟振荡器控制phase_offset而非频率。


2
著名的雅马哈DX7键盘与OPL2同时由同一制造商生产,尽管也使用相位调制,但被标记为“FM合成”。 - dronus
谢谢你的回答。你是对的,OPL确实使用相位调制,我以为我的实现是在做“真正”的FM,但是自从我上次看它已经很久了,现在我真的说不清楚了 : )。我的理解是对于正弦波,相位调制和频率调制是等效的,所以我认为我可以通过这种方式实现类似的声音。 - bsa
不,FM和PM除了共享许多属性外,它们是非常不同的。然而,临时频率变化随时间积分以在FM中添加永久相移。 在PM中,临时调制会添加临时相移。 - dronus
在更高的层面上,相对于调制载波频率改变调制频率会改变正交调幅中载波和侧带频率之间的功率分布,在音调范围内FM声音发生了巨大的改变,例如乐器根据音符的音高更明显地改变它们的声音。对于PM声音来说,相同的效果要柔和得多,这使得设计过程更加可控。 - dronus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接