Phase Modulation for Synthesizers

On nsigma’s request (from the “Extremely Fast sine…” thread)

The code is actually being used for phase modulation at this point, not frequency modulation. So maybe I should change the name to PMOp. While I was developing, I had some actual frequency modulation implementations as well but have “phased” them out.

public interface FMOp
{
	float get();
	float get(float modAmt);
	void setFrequency(float freq);
}

The initials PML derived from Phase Modulation Lookup. I previously had tried calculating the sine values, but couldn’t find an algorithm fast and accurate enough to compete with the lookup I’m using (1024 unit table with linear interpolation, posted on the thread cited below). I’d be most grateful to learn of a faster method (will be reading closely and trying out methods from here: http://www.java-gaming.org/topics/extremely-fast-sine-cosine/36469/msg/346129/view.html#msg346129)

[EDIT 7/30/15: renamed “index” to “cursor” in order to make the three implementations consistent.]

public class OpPML implements FMOp
{
	// "public" okay, for internal use only
	float cursor;
	float pitchIncr;
	final int tblSize = SineTable.getOperationalSize();
	
	public void setFrequency(float freq)
	{
		pitchIncr = (freq * tblSize)/ 44100;
	}
	
	// TODO: should not be used, should set to cause compile error
	@Override
	public float get()
	{
		return get(0);
	}
	
	@Override
	public float get(float modulationAmt)
	{
		cursor += pitchIncr;
		if (cursor >= tblSize ) cursor -= tblSize;
		
		float temp = cursor + modulationAmt;
	
		while (temp < 0) temp += tblSize;
		while (temp >= tblSize) temp -= tblSize;
		
		return SineTable.get(temp); 
	}
}

Variant with no modulator. Yes, lots of duplicate code. But minimizing processing in the .get() has the highest priority.

public class OpN implements FMOp
{
	// "public" okay, for internal use only
	float cursor;
	float pitchIncr;
	final int tblSize = SineTable.getOperationalSize();
	
	public OpN(){}
	
	public OpN(float freq)
	{
		this();
		this.setFrequency(freq);
	}
	
	public OpN(float freq, float startingPhase)
	{
		this();
		this.setFrequency(freq);
		cursor = startingPhase * tblSize;
	}
	
	public void setFrequency(float freq)
	{
		pitchIncr = (freq * tblSize) / 44100f;
	}
	
	@Override
	public float get()
	{
		cursor += pitchIncr;
		if (cursor >= tblSize) cursor -= tblSize;
		
		return SineTable.get(cursor); 
	}

	// TODO: should cause compile error, should not be used
	@Override
	public float get(float modulationAmt)
	{
		return 0;
	}
}

Variant that uses feedback instead of an external modulation source.

public class OpFB implements FMOp
{
	// "public" okay, for internal use only
	float cursor;
	float pitchIncr;
	final int tblSize = SineTable.getOperationalSize();
	private float feedback;
	
	final private float feedbackAmplitude;
	
	public OpFB(float feedbackAmplitude)
	{
		this.feedbackAmplitude = feedbackAmplitude;
	}
	
	public void setFrequency(float freq)
	{
		pitchIncr = (freq * tblSize) / 44100f;
	}
		
	@Override
	public float get()
	{
		// determine feedback amt
		cursor += pitchIncr;
		if (cursor >= tblSize) cursor -= tblSize;
		
		feedback = SineTable.get(cursor);
		
		feedback = cursor + feedback * feedbackAmplitude;
		while (feedback >= tblSize) feedback -= tblSize;
		
		return SineTable.get(feedback); 
	}
	
	// TODO: trigger compiler error, never should be used
	@Override
	public float get(float modulationAmt)
	{
		return 0;
	}
}

With these three, one can replicate all 32 “algorithms” employed by the Yamaha DX-7. Here is an example of the code I use for a simple modulator -> carrier pair:

				currentCEnv.tick(cEnvCursor, frequency);
				currentMEnv.tick(mEnvCursor, frequency);
				
				float noteVal = 
						vmCarrier.get(cEnvCursor.value)
						* cOp.get(
							vmModulator.get(mEnvCursor.value)
							* modDepth1 * mOp.get()
						);

cOp is an OpPML
mOp is an OpN
There are envelop/cursor pairs for the carrier and modulator.
There is also another layer of mapping that converts the linear values to something more exponential/logarithmic. I am calling these “volume maps” (amplitude maps would be better?). For modulators, I usually use a “reverse cosine” mapping (1 - cos, for 1/4 of a cycle), and for carriers an exponential mapping (x^6, x=0…1) works pretty well. I was not able to figure out what Yamaha/Stanford folks were using here. nsigma, maybe you understand this better and have a suggestion for this step? My head is a little far away from the theory at the moment. Maybe there is a decibel/logrithmic function-related that would be more ideal.

[EDIT: following added 7/30/15]
Examples of FM synthesis, previously posted on JGO, can be heard playing the jars listed at the following links:
http://www.java-gaming.org/user-generated-content/members/27722/tanpura141204.jar
http://www.java-gaming.org/user-generated-content/members/27722/hexarasoundtester141130.jar

The first has a simple square and sawtooth included (just 2 ops, 2:1 and 1:1), but with pretty active envelopes, so they sound relatively reasonable. (Mouse over the note “node” on the display dial, and select from the bottommost drop-down.) The two main patches (etanpura, cirrus) have 6 ops, with a lot more internal phasing built in, for a richer sound. Oh, and the tanpura-drone project also has a basic FM-type electric-piano keyboard implemented. The second jar has a nice pad and bell/gongs.

I’ve another half-dozen patches implemented now–will be showing off a little sequencer/motif player soon. I just want to first implement “hairpins” (crescendos/dims) as part of the “event system” before doing so. Might take a couple weeks to get to it.

Excellent resource:
From Dave Benson, from his book posted online on music maths, specifically on FM:
http://homepages.abdn.ac.uk/mth192/pages/html/music.pdf#section.8.8
This chapter has a good reference section at the end to check out! There is also “Appendix O: Online Articles”.