38
votes

I'm trying to implement some features of a Yamaha YM3812 sound chip (aka OPL2 http://en.wikipedia.org/wiki/YM3812) in JavaScript using Audiolet (a synthesis library, http://oampo.github.io/Audiolet/api.html)

Audiolet allows you to build a synthesizer as a graph of nodes (oscillators, DSPs, envelope generators etc).

The OPL2 has nine channels with two operators (oscillators) each. Usually, one oscillator in each channel modulates the frequency of the other. To simulate this, I've built up a chain of nodes for each channel:

Synth node chain (one of nine channels)

OPL2 channel as implemented

Node chain creation and connection code:

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
};

However, when I set the parameters of the modulator and carrier nodes (oscillator waveforms, relative frequencies, attenuation, ADSR parameters) and trigger notes, the output bears very little resemblance to a decent OPL2 emulator with approximately the same parameters. Some sounds are in the ballpark. Others are fairly unpleasant.

I have some ideas on how to proceed (I guess plotting the output at different stages would be a good starting point), but I'm hoping someone experienced can point me in the right direction, or point out something obviously wrong with what I'm doing. I don't have a signal-processing or strong mathematical background. I don't have a deep intuitive understanding of FM.

Some issues I suspect are:

1) My FM implementation (as shown above) is fundamentally wrong. Also, there may be an issue in the function where play a note (set the oscillator frequencies, and scale and offset the modulator before triggering the ADSR envelopes):

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) Output of FM synths may be highly sensitive to small differences in the shape of the modulator ADSR envelope (please tell me if this is true!), and my ADSR envelopes are crude approximations at best of the ADSRs in a real OPL2. My implementation is also missing some features which seem relatively unimportant (eg key scaling), but which may affect the sound of an FM synth significantly (again, I'm not sure).

1
Looking your image, the modulator should be linked to carrier frequency and not gain (like this: en.wikipedia.org/wiki/Frequency_modulation#/media/… ).eri0o
Usually, you would want to be careful with the gain stage of the modulator and not apply too much gain and hence modulation. If the modulation is too large you get "unpleasant sounds" with the modulation dominating over the carrier. I am not sure this is what you describe.noumenal
Elric, based on the diagram it certainly does look like the modulator is linked to gain. It's been so long since I worked on this or looked at Audiolet that I'm really not sure now if that's what's happening! I'll have a dig into it.bsa
noumenal, I think you might be on the right path there. I'll see if I can resurrect it and see what happens with less gain on the modulator.bsa

1 Answers

1
votes

Most synthesizers labled 'FM' in fact do phase modulation (PM, see https://en.wikipedia.org/wiki/Phase_modulation ). There are some benefits (mostly leading to more stable sound over a large tonal range). The OPL2 may use this too, I found no clear evidence, but the Wikipedia article also uses the term 'phase modulation'.

For short, many musical synthesizers labeled 'FM' in fact featured 'PM', so you might try go with that, and check if it better fits the expected OPL2 sounds.

From a quick glance to the Audiolet source, I would guess the Sine oscilator is doing true FM, so you may need to replace it and add a phase input to allow phase modulation.

Basically, the line

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

used by Sine of the carrier oscilator would have to read something like

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

with phase_offset controlled by the mod oscilator instead of the frequency.