The FM7 appreciation thread

At yesterday’s SuperCollider meetup, Scott mentioned the FM7 UGen which I myself am guilty of overlooking and / or underestimating. It is truly an awesome UGen that offers a landscape of Yamaha DX7-like opportunities.

One thing that Scott pointed out that I had overlooked is that you can use all 32 “algorithms” (aka oscillator patches) from the DX7.

Here is an example of blasting through all 32 “algos” with a pattern

~fm7SynthNames = 32.collect{|algoNum| "fm7algo%".format(algoNum).asSymbol};

~fm7SynthNames.do{|synthName, algo|
	SynthDef(synthName, {|out=0, dur=0.01, sustain=0, freqScale=0.5, spread=0.25, pan=0, feedback=1.0, amp=0.5|
		var env = Env.perc.kr(gate:1, timeScale: dur * (1+sustain), doneAction: Done.freeSelf);
		var ctls =
		[
			// freq, phase, amp
			[freqScale * 300, pi.rand, 1],
			[freqScale * 2500, pi.rand, 1],
			[freqScale * SinOsc.ar(Rand(0.1,10.0)).exprange(3,100), 0, 1],
			[freqScale * LFNoise2.ar(10).exprange(1300,0.5), 0, 1],
			[freqScale * ExpRand(30,1500), pi.rand, 1],
			[freqScale * ExpRand(30,500), pi.rand, 1]
		];

		var sig = FM7.arAlgo(algo, ctls , feedback * 2.0);
		sig = Splay.ar(sig, spread: spread, center: pan);
		sig = sig * env * amp;

		Out.ar(out, sig);
	}).add;
};

Pbind(
	\instrument, Pshuf(~fm7SynthNames, inf).trace, 
	\dur, Pwhite(0.01,0.125), 
	\sustain, Pwhite(0.0,1.0),
	\feedback, Pshuf(Array.rand(8, 0.25,1.0),inf),
	\freqScale, Pxrand(Array.rand(8, 0.25,4.0),inf),
	\amp, Pwhite(0.125,0.5),
	\spread, Pwhite(0.0,1.0),
	\pan, Pwhite(-1.0,1.0)
).play;


10 Likes

This sounds great. The FM7 is amazing, as was the DX7. They DX7 doesn’t even come close to tapping all the possibilities of the circuit. I am particularly fond of Fredrik’s experiments from a while back:

https://fredrikolofsson.com/f0blog/n-fm7-patches/

I used this as the basis of my neural net controlled synths, which uses an N dimensional controller to control 12 of the control parameters and 24 of the mod parameters:

All of the oscillators can feed back into each other, and I’m not certain of this, but I think all have just one sample of latency on feedback. The circuit is incredible for noise music.

Sam

3 Likes

I totally agree! I fell in love with this UGen and it helped me dig into FM synthesis which I didn’t know anything about. I ended up developing a SynthDef around the version without algorithms for the SuperDirt library to be used with TidalCycles, so some of the commuity over there are using it now.

// 6-op FM synth (DX7-like)
//
// Works a bit different from the original DX7.  Instead of algorithms, you set the amount
// of modulation every operator receives from other operators and itself (feedback), virtually
// providing an endless number of possible combinations (algorithms).
//
// Responds to
//   voice (preset number: [0] is user-defined; [1-5] are randomly generated presets).
//   lfofreq (overall pitch modulation frequency)
//   lfodepth (overall pitch modulation amplitude)
//
// Each operator responds to
//   amp (operator volume - becomes carrier)
//   ratio (frequency ratio)
//   detune (in Hz)
//   eglevel[1-4] (4 envelope generator levels)
//   egrate[1-4] (4 envelope generator rates)
//
// The syntax for operator arguments is <argumentName + opIndex>[modulatorIndex | egIndex]
//
// For example:
// amp1 1      (op1 as carrier with full volume)
// ratio2 2.3  (op2 frequency ratio)
// mod11 0.5   (op1 feedback)
// mod12 0.78  (op1 modulation amount by op2)
// detune1 0.2 (op1 detune)
// eglevel12 0.1  (op1 EG level2)
// egrate11 0.01  (op1 EG rate1) -- WARNING: higher values go FASTER!
(
SynthDef(\superfm, {
	var sustain = \sustain.kr(1);
	var lfofreq = \lfofreq.kr(1);
	var lfodepth = \lfodepth.kr(0);
	var freq = \freq.kr(440);
	var tremolo = 1 + (LFTri.kr(lfofreq) * lfodepth);
	var out = \out.kr(0);
	var pan = \pan.kr(0);
	var voice = \voice.kr(0);
	// overall envelope
	var env = EnvGen.ar(Env.linen(0.01, 0.98, 0.01, 1, -3), timeScale:sustain, doneAction:2);
	// operator output levels
	var amps = Array.fill(6, { |i| (\amp++(i+1)).asSymbol.kr(1)});
	// operator frequency ratios
	var ratios = Array.fill(6, {|i| (\ratio++(i+1)).asSymbol.kr(1)});
	// operator frequency detuners
	var detunes = Array.fill(6, {|i| (\detune++(i+1)).asSymbol.kr(rand2(0.1))});
	// feedback -- for presets only
	var feedback = \feedback.kr(0.0);
	// operator envelopes
	var eglevels = Array.fill(6, {|i|
		Array.fill(4, { |n| (\eglevel++(i+1)++(n+1)).asSymbol.kr(1) })
	});
	var egrates = Array.fill(6, {|i| [
		// Supercollider envelopes use seconds for the durations of segments.
		// So higher values mean transitions are slower.
		// DX7s envelopes use rates, which is the inverse of time, 1/time.
		// Higher values in DX7 mean transitions are faster.
		max(0.1 / ((\egrate++(i+1)++1).asSymbol).ir(10), 0.001),
		max(0.1 / ((\egrate++(i+1)++2).asSymbol).ir(0.3), 0.001),
		max(0.1 / ((\egrate++(i+1)++3).asSymbol).ir(0.1), 0.001),
		max(0.1 / ((\egrate++(i+1)++4).asSymbol).ir(0.1), 0.001),
	]});
	// modulation matrix
	var mods = Array.fill2D(6, 6, { |r, c|
		(\mod++(r+1)++(c+1)).asSymbol.kr(0) * if(r == c, feedback, 1)
	});
	var presets = SelectX.kr(voice, [
		[ // user-defined
			ratios,	detunes, amps, eglevels, egrates, mods,
		],
	] ++
	// randomly generated presets
	Array.fill(5, { [
		// ratios
		Array.fill(6, {
			[0.25, 0.5, 1, 2, 3, 4, 5, 6, 7, 11.rand + 1, 13.rand + 1, 15.rand + 1].wchoose(
				[1, 2, 8, 4, 3, 0.5, 0.5, 0.5, 0.5, 0.25, 0.25, 0.25].normalizeSum)
		}),
		// detunes
		Array.fill(6, { rand2(7) }),
		// amps
		Array.fill(6, { 1.0.rand * 0.5.coin.asInteger }),
		// EG levels
		Array.fill2D(6, 4, {1.0.rand}),
		// EG rates
		Array.fill2D(6, 4, {1.0.rand}),
		// mods
		Array.fill2D(6, 6, {|r,c| 1.0.rand * 0.25.coin.asInteger * if(r == c, feedback, 1)}),
	]})
	);

	var envs = Array.fill(6, { |i|
		EnvGen.kr(
			Env.new(
				// EG levels
				[0]++Array.fill(4, { |n| presets[3][i][n] }),
				// EG rates
				Array.fill(4, { |n| presets[4][i][n] })
			),
			timeScale:sustain,
		);
	});

	var ctls = Array.fill(6, { |i|
		[freq * tremolo * presets[0][i] + presets[1][i], 0, envs[i]]
	});

	var sound = FM7.ar(ctls, presets[5]) * amps;
	sound = Mix.ar(sound) * (-15.dbamp);
	Out.ar(out, DirtPan.ar(sound, ~dirt.numChannels, pan, env));
}).add;
);

I love the freedom it gives for live coding FM. It’s absolutely brilliant. I’ve been using it almost exclusively for quite a while now. I really really like it. You can achieve very cool and interesting sounds with very little.

I also developed a SynthDef without envelope and trigger to be used with other UGens for modulation. It’s a superb UGen.

That is a very interesting project. That was my next planned step, so I will definitely look into it. Thanks for sharing

2 Likes

Here is a paternized version of Fredrik’s patch referenced above…

(
SynthDef(\fm7noise, {
	
	var sig;
	var chans = #[0, 1];
	
	var ctrls = 6.collect({|ctrl|
		["f", "p", "a"].collect({|val|
			["freq", "phase", "mul", "add"].collect({|param|
				"op_%_%_%".format(ctrl, val, param).asSymbol().kr(0)
			});
		});
	});
	
	var mods = 6.collect({|row|
		6.collect({|col|
			["freq", "phase", "mul", "add"].collect({|param|
				"mod_%_%_%".format(row, col, param).asSymbol().kr(0)
			});
		});
	});
	
	ctrls = ctrls.collect({|ctrl| ctrl.collect({|vals| LFSaw.ar(*vals) })});
	mods = mods.collect({|mod| mod.collect({|vals| LFSaw.ar(*vals) })});
	
	sig = FM7.ar(ctrls, mods).slice(chans) * \amp.kr(0.1);
	sig = sig * Env.asr.ar(doneAction:Done.freeSelf, gate:\gate.kr(1));
	sig = Splay.ar(sig);
	Out.ar(\out.kr(0), sig);
	
}).add;
)

(
var func = {|seed=1000000|
	
	var randseed = thisThread.randSeed_(seed);
	var ctrls = 6.collect({|ctrl|
		["f", "p", "a"].collect({|val|
			["freq", "phase", "mul", "add"].collect({|param|
				["op_%_%_%".format(ctrl, val, param).asSymbol(), 1.5.linrand.round(0.5)]
			}).flatten;
		}).flatten;
	});
	
	var mods = 6.collect({|row|
		6.collect({|col|
			["freq", "phase", "mul", "add"].collect({|param|
				["mod_%_%_%".format(row, col, param).asSymbol, 1.5.linrand.round(0.5)]
			}).flatten;
		}).flatten;
	});
	
	var pairs = ctrls.flatten ++ mods.flatten;
	pairs;
};

Pdef(\fm7noise, {
	
	var seed = nil; //83506
	var dur = 4;
	
	Pspawner({|sp|
		inf.do({
			var myseed = seed ?? { 1000000.rand; };	
			var pairs = func.(myseed.debug(\seed));
			var ptrn = sp.par(Pbind(\instrument, \fm7noise) <> Pbind(*pairs));
			sp.wait(dur);
			sp.suspend(ptrn);
		});
	})
})
)

Pdef(\fm7noise).play;
Pdef(\fm7noise).stop;

Programming the DX7 is famously difficult. In order to simplify this, I made a patch recently to simply choose random values for the routing matrix, using a single overall seed value. To make it more fun to use, you can vary the seed value in time and it will smoothly cross-fade between different randomized matrices. Now you can play 7-op FM with the control scheme you always wanted: a single knob. :slight_smile:

The exact way it’s randomized and crossfaded is done to produce… relatively coherent results. For most values, you can give max/min ranges to constrain the randomization of values. The result can be wild af, but sometimes sounds great.

SynthDef and an example pattern - feel free to ask questions and have fun!

2 Likes

Thanks for sharing, atm I get this error – is ‘fadeSteps’ contained in a quark extension ?

^^ The preceding error dump is for ERROR: Message 'fadeSteps' not understood.
RECEIVER: a BinaryOpUGen

Just updated the gist with the extension for this, thanks for the heads up :slight_smile:

not actually playing the FM7 Ugen, but a simple hand-knit topology, this is yet another approach to meta-controlling the parameter explosion inherent in FM.

// FM Exploration as shown in SRH DMI Class, Berlin

// Ramdom Seed parametrized FM perc. synths. HH ca. 2014 - 21
// 2 operators, 1 carrier.
(
SynthDef( \fmSanSimple, { arg seed = 123, amp=0.2, sustain=1;
	var carrier, op1, op2;

	RandID.ir(10);         // choose a specific RandID (read help:)
	RandSeed.ir(1, seed);  // the Rand Seed may be set from outside at creation time.

	// 2 operators, with efficient FSinOScand random freqs, phases, amps
	op1 = FSinOsc.kr(ExpRand(10, 800), Rand(-pi, pi),     Rand(-100, 400));
	op2 = FSinOsc.ar(ExpRand(1, 8000), {Rand(-pi, pi)}!2, ExpRand(0.2, 100));

	carrier = SinOsc.ar(
		ExpRand(0.2, 1000) // carrier freq
		+ op1 //  adding op1
		* op2 // scaled by op2
		,
		{ Rand(-pi, pi) }!2,
		amp
	);

	carrier = EnvGen.ar(Env.perc(ExpRand(0.01, 0.2), sustain), doneAction: 2)
	* carrier;
	Out.ar(0, carrier);

}).add;
);

// different, reproducable sound with different seeds. 
(instrument: \fmSanSimple, seed: 100.rand.postcln, amp: 0.125).play;
// pick favourite seeds
(instrument: \fmSanSimple, seed: 31.postcln, amp: 0.25).play;
(instrument: \fmSanSimple, seed: 55.postcln, amp: 0.25).play;
(instrument: \fmSanSimple, seed: 52.postcln, amp: 0.25).play;

(
// quick run through the first 100 of them:
Pdef(\fmExplorer,
	Pbind(
		\instrument, \fmSanSimple,
		\seed, Pseq((0 .. 100), 1).trace,
		\amp, 0.25,
		\dur, 0.5,
		\legato, Pbrown(0.1, 1.5, 0.1)
	)
).play;
)

Bests,
Hannes

… and yet another approach at taming the multi-dimension beast:

This time, as an Ndef with random freqs and an Influx for managing the 36 mod levels with one joystick.



///=== FM Ndef with Ndef/Influx
Ndef(\fm7).clear;
(
// FM7 full matrix is 2 x 6 x 6 = 72 variables
~modNames = {|i|{|j|("mod"++i++j).asSymbol } ! 6 } ! 6;
~modNames.flat.collect{|n| Ndef(\fm7).addSpec(n, [0, 220, 12]); };
Ndef(\fm7).addSpec(\ffreq, [75, 8000, \exp]);
Ndef(\fm7).addSpec(\rq, [2, 0.1, \exp]);

Ndef(\fm7, { arg ffreq=500, rq=1.5;
	var snd;
	var ctls =  { [ExpRand(1, 440), 0, 1] } ! 6; // freqs randomized
	var mods = {|i|{|j| // mod table as control inputs
		var name = ("mod"++i++j).asSymbol;
		name.kr( 1.0.linrand )
	} ! 6 } ! 6;

	var chans = [0, 1];
	snd = FM7.ar(ctls, mods).slice(chans);
	// snd = Splay.ar(FM7.ar(ctls, mods));
	RLPF.ar(snd, ffreq, rq, 0.1) + snd.madd(0.01); // a bit of filtering doen't hurt
}).play;
);

Ndef(\fm7).fadeTime = 4;
Ndef(\fm7).gui;

// new freqs:
Ndef(\fm7).send;

// new batch of mods:
Ndef(\fm7).set( * ~modNames.flat.collect{|k| [k, [0, 10.0.linrand.linrand].choose ] }.flat  );

// crossfade
Ndef(\fm7).xset( * ~modNames.flat.collect{|k| [k, [0, 10.0.linrand.linrand].choose ] }.flat  );


// ==

// now, play it thru an Influx
Quarks.install("Influx"); // needs Quark installed

(
// make an influx - fan-out 4 params to 38
a = Influx([\x, \y, \z, \w], [\aa, \bb] ++ ~modNames.flat, (x: 0.5, y: 0.1));
///// interactive gui  // recommended for direct use:
InfluxKtlGui(a);
// attach Influx to the Ndef
a.attachMapped(Ndef(\fm7));
);

Thanks for hinting at this great UGen. I never really played around with it - though I like to use it’s equivalent in TidalCycles.

What I can’t wrapped my head around right now is, why can’t I step through the algorithms with another UGen? - like so:

(
SynthDef(\fm7, {
		var env = Env.perc.kr(gate:1, timeScale: 1, doneAction: Done.freeSelf);
		var ctls =
		[
			// freq, phase, amp
			[100,0,1],
			[200,0,1],
			[300,0,1],
			[400,0,1],
			[500,0,1],
			[600,0,1],
		];

	var sig = FM7.arAlgo(LFNoise0.kr(10,30,1), ctls , 0.0);
		sig = Splay.ar(sig);
		sig = sig * env;
		Out.ar(0, sig);
	}).add;
)

That gives me the following error:

^^ The preceding error dump is for ERROR: Primitive '_BasicAt' failed.
Index not an Integer
RECEIVER: [ a Function, etc etc.