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!

3 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

1 Like

… 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));
);
1 Like

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. 

I think the SynthDef graph has to be fixed at run-time - similar to the age-old question about having a SynthDef with a flexible number of partials or channels.

// doesn't work in a SynthDef
FM7.arAlgo(Rand(0, 31), ctls, 0.0);

// does
FM7.arAlgo(rrand(0, 31), ctls, 0.0);

Mads’ code in the OP isn’t changing the algo in one synth def - he’s generated 32 SynthDefs and is working through them.

With that said, if anyone has better understanding of this I’m happy to be corrected!

According to the documentation linked above the algo can’t be modulated.

// Rand is a Ugen that runs when a compiled synthdef is playing as a synth. Therefore it is a sort of modulator and won't work with the arAlgo method.
FM7.arAlgo(Rand(0, 31), ctls, 0.0);
// rrand is a function that executes at compile time. Therefore whenever the synth plays, the value is fixed to whatever value was chosen randomly when the synthdef was compiled. 
FM7.arAlgo(rrand(0, 31), ctls, 0.0);
1 Like