GlitchFree Vowel synthesis - with Soprano/tenor etc Formant tables

Hi,

This guy posted this a while ago, which is great:

The oscillator in his basic form :

(
Ndef(\Glitch_Free,  { 
	
	var freq = 900;

	var modshift = 2pi;

	var modr = 1;

	var vibrato_freq_env = 1;

	var vibrato_width_env = 0.1;

    var detune = LFNoise2.ar (7) / 77;

	var vibrato = SinOsc.ar (2 ** (vibrato_freq_env - 6)) * vibrato_width_env / 12;

	var fund_phasor = LFSaw.ar (freq * (2 ** (detune + vibrato))).range (0, modr);

	var modulator = sin(fund_phasor *  modshift);

	var car_0 = 1 ** 0.5 * Crossfade_Formant.ar (fund_phasor, modulator,   K2A.ar (Rand(0.1, 1)), Rand(0.1, 0.7), 1); 
	var car_1  = 1 ** 1.5 * Crossfade_Formant.ar (fund_phasor, modulator,  K2A.ar (Rand(0.1, 1)), Rand(0.1, 1.3), 1);
	var car_2  = 1 ** 2 * Crossfade_Formant.ar (fund_phasor, modulator,    K2A.ar (Rand(0.1, 1)), Rand(0.1, 1.3), 1);

	var sig = (car_0 + car_1 + car_2).tanh;

	var env = EnvGen.ar(Env([0, 1, 1, 0], [0.1, 2, 1],  [3, 0, -7]),
		doneAction: 2);

	OffsetOut.ar(0, sig * env * 0.2 ! 2);
			
}).play;
		
)

With those custom classes in the extension folder before playing it

Nearest_Even {
	*ar {
		arg val;
		var val_floor, val_ceil, res, distance;
		val_floor = val.floor;
		val_ceil = val.ceil;
		res = Select.ar (val % 2,
			[ val_floor, val_ceil ],
		);
		distance = (val - res).abs;
		^ [ res, distance ];
	}
}

Nearest_Odd {
	*ar {
		arg val;
		var val_floor, val_ceil, res, distance;
		val_floor = val.floor;
		val_ceil = val.ceil;
		res = Select.ar (val + 1 % 2,
			[ val_floor, val_ceil ],
		);
		distance = (val - res).abs;
		^ [ res, distance ];
	}
}

Crossfade_Formant {
	*ar {
		arg phasor = 0, phase_mod = 0, harm = 1, pm_index = 1, amp = 1; // lag = 0;
		var harm_even, harm_odd, sig_even, sig_odd, sig, phase;

		phase = phase_mod * pm_index;

		harm_even = Nearest_Even.ar (harm);
		sig_even = cos (phasor * 2pi * harm_even[0] + phase);

		harm_odd = Nearest_Odd.ar (harm);
		sig_odd = cos (phasor * 2pi * harm_odd[0] + phase);

		sig = XFade2.ar (sig_even, sig_odd, harm_even[1] * 2 - 1) * amp;
		^ sig;
	}
}

1/ I was wondering how to implement - in this code - the formant tables that came with the classic Vowel UGen (tables that come originally from CSound)
https://csound.com/docs/manual/MiscFormants.html

2/ How to implement an FM modulation ?

Thanks a lot

maybe have a look here: FM formant synthesis (e.g. Chowning, "Phone")

ive implemented single sideband modulation here which cancels out all the negative sidebands for a really well behaving spectrum by multiplying the carrier with a raised cosine window, which is driven by the same phase as the modulator and has the same index:

(
var raisedCos = { |phase, index|
	var cosine = cos(phase * 2pi);
	exp(index.abs * (cosine - 1));
};

{
	var rate = 110;
	var modRatio = 2.5;
	var index = SinOsc.ar(0.3).linlin(-1, 1, 0, 30);

	var modPhase = Phasor.ar(DC.ar(0), rate * modRatio * SampleDur.ir);
	var mod = sin(modPhase * 2pi);
	var raisedCosWindow = raisedCos.(modPhase, index);

	var carrPhase = Phasor.ar(DC.ar(0), rate * SampleDur.ir);
	var carr = sin(carrPhase * 2pi + (mod * index));

	var sig = carr * raisedCosWindow;

	sig = LeakDC.ar(sig);

	sig!2 * 0.1;

}.play;
)

s.freqscope;

you could multichannel expand this for additional formants.

EDIT: for further investigation i recommend these videos by miller puckette: https://msp.ucsd.edu/syllabi/267.21f/movies/ (as far as i remember PWM starts in Nov), the book generating sound & organising time - thinking with ~gen which has a chapter about that and this paper: https://mural.maynoothuniversity.ie/4693/1/VL_synthesis%20of%20resonance.pdf

What a great sounding piece of code. And minimal, I really like when it combines this quality of sound in 10 lines of codes ! Thanks you. But I would also like to have an answer for the formant table question using the Glitch Free vowel synth of capogreco ! :slight_smile:

this could be written even more compact, but as you can see this example is already using arrays for harmonics and amps where you could fill in the values from the Appendix D. Formant Values (for the table implementation look at the thread i have shared above).
PM and FM are relatives so there is not necessarily the need for additional FM when you are already doing PM.
If you additionally multiply the formants by a gaussian window you can shape the bandwidth of the formants by adjusting the waveshaping index of the window, you are doing PWM then. The gaussian window is not driven directly by the phase but by a rectified sine wave, so that the output is differentiable. Otherwise you would get a kink in the waveform when the phasor wraps around.

The gaussian window has to line up with the formants, like this:

(
var gaussianWindow = { |phase, index|
	var halfSine = sin(phase * pi) * index;
	exp(halfSine.neg * halfSine);
};

{
	var rate = 100;
	var phase = Phasor.ar(DC.ar(0), rate * SampleDur.ir);
	var pulse = gaussianWindow.(phase, 4);
	var formant = cos(phase * 2pi);
	var sig = pulse * formant;
	[pulse, formant, sig];
}.plot(0.02);
)

grafik

The gaussian window has a really steep spectral cutoff, compare it to a sinc function here:
gaussian:
gaussian_pulse
sinc:
sinc_2

which makes it perfect to prevent aliasing when you shift the formants up and down.

Here is the code:

(
var crossfade_formants = { |phase, phaseMod, pmIndex, harm, amp|
	var sig_even, sig_odd, pmod;

	pmod = phaseMod * pmIndex;

	sig_even = cos(phase * 2pi * harm.round(2) + pmod);
	sig_odd = cos(phase * 2pi * ((harm + 1).round(2) - 1) + pmod);

	XFade2.ar(sig_even, sig_odd, harm.fold(0, 1) * 2 - 1) * amp;
};

var gaussianWindow = { |phase, index|
	var halfSine = sin(phase * pi) * index;
	exp(halfSine.neg * halfSine);
};

{
	var freq, detune, vibrato, phase, phaseMod;
	var sig, pulses, formants, gainEnv, harmonics, amps, pmIndices, bwIndices;

	gainEnv = EnvGen.ar(Env([0, 1, 1, 0], [0.1, 2, 1],  [3, 0, -7]), doneAction: 2);

	detune = LFNoise2.ar(7) / 77;
	vibrato = SinOsc.ar(2 ** (\vibrato_freq.kr(1) - 6)) * \vibrato_width.kr(0.1) / 12;
	freq = \freq.kr(440) * (2 ** (detune + vibrato));

	phase = Phasor.ar(DC.ar(0), freq * SampleDur.ir, 0, 1);
	phaseMod = (0.5 - sin(phase + 0.25 * 2pi));

	pmIndices = [
		Rand(0.1, 0.7),
		Rand(0.1, 1.3),
		Rand(0.1, 1.3)
	];

	harmonics = [
		1,
		Rand(2, 3),
		Rand(4, 5)
	];

	amps = [
		1,
		Rand(0.5, 1),
		Rand(0.5, 1)
	];

	bwIndices = [
		Rand(1, 8),
		Rand(1, 8),
		Rand(1, 8)
	];

	pulses = gaussianWindow.(phase, bwIndices);
	formants = crossfade_formants.(phase, phaseMod, pmIndices, harmonics, amps);

	sig = (pulses * formants).sum;

	sig = sig.tanh * \amp.kr(-25.dbamp);
	
	sig = LeakDC.ar(sig);

	sig = sig * gainEnv;

	sig = Pan2.ar(sig, \pan.kr(0));

}.play;
)

the single sideband approach might be even better for modulation (sounds like woodwind multiphonics):

(
var raisedCos = { |phase, index|
	var cosine = cos(phase * 2pi);
	exp(index.abs * (cosine - 1));
};

{
	var freq, detune, vibrato, carrPhase, modPhase, mod;
	var sig, gaussWindow, carr, gainEnv, amps, bwIndices;

	gainEnv = EnvGen.ar(Env([0, 1, 1, 0], [0.1, 2, 1],  [3, 0, -7]), doneAction: 2);

	detune = LFNoise2.ar(7) / 77;
	vibrato = SinOsc.ar(2 ** (\vibrato_freq.kr(1) - 6)) * \vibrato_width.kr(0.1) / 12;
	freq = \freq.kr(440) * (2 ** (detune + vibrato));

	carrPhase = Phasor.ar(DC.ar(0), freq * SampleDur.ir);
	modPhase = Phasor.ar(DC.ar(0), freq * \mRatio.kr(1.05) * SampleDur.ir);

	amps = [
		1,
		Rand(0.5, 1),
		Rand(0.5, 1)
	];

	bwIndices = [
		4,
		Rand(1, 8),
		Rand(1, 8)
	];

	gaussWindow = raisedCos.(modPhase, bwIndices);
	mod = sin(modPhase * 2pi);
	carr = sin(carrPhase * 2pi + (mod * bwIndices)) * amps;

	sig = (carr * gaussWindow).sum;

	sig = sig.tanh * \amp.kr(-25.dbamp);

	sig = LeakDC.ar(sig);

	sig = sig * gainEnv;

	sig = Pan2.ar(sig, \pan.kr(0));

}.play;
)

to demistify the two approaches:

The two windows i have been using for either of the approaches are nearly identical in behaviour and have one common feature, they collapse to 1 if the index is 0.

(
var raisedCos = { |phase, index|
	var cosine = cos(phase * 2pi);
	exp(index.abs * (cosine - 1));
};

var gaussianWindow = { |phase, index|
	var halfCosCycle = sin(phase * pi) * index;
	exp(halfCosCycle.neg * halfCosCycle);
};

{
	var index = 2;
	var rate = 100;
	var phase = Phasor.ar(DC.ar(0), rate * SampleDur.ir);
	var sigA = raisedCos.(phase, index);
	var sigB = gaussianWindow.(phase, index);

	[sigA * cos(phase * 2pi), sigB * cos(phase * 2pi)];
}.plot(0.02);
)

grafik

with the mod FM you have control over the bandwidth via the index and the harmonics independently, but the spectrum is always harmonic:

(
var crossfade_formants = { |phase, harm|
	var harm_even = harm.round(2);
	var harm_odd = ((harm + 1).round(2) - 1);
	var sig_even = cos(phase * 2pi * harm_even);
	var sig_odd = cos(phase * 2pi * harm_odd);
	XFade2.ar(sig_even, sig_odd, harm.fold(0, 1) * 2 - 1);
};

var raisedCos = { |phase, index|
	var cosine = cos(phase * 2pi);
	exp(index.abs * (cosine - 1));
};

{
	var freq, phase, harmonic, index, gaussWindow, formants, sig;

	freq = \freq.kr(440);
	phase = Phasor.ar(DC.ar(0), freq * SampleDur.ir);

	harmonic = MouseX.kr(1, 10);
	index = MouseY.kr(0, 10);
	
	gaussWindow = raisedCos.(phase, index);
	formants = crossfade_formants.(phase, harmonic);

	sig = formants * gaussWindow;
	
	sig = LeakDC.ar(sig);

	sig!2 * 0.1;
	
}.play;
)

with the single sideband PM approach the bandwidth and the harmonics are controlled both via the index, but you can additionally create inharmonic sounds by adjusting the modRatio:

(
var raisedCos = { |phase, index|
	var cosine = cos(phase * 2pi);
	exp(index.abs * (cosine - 1));
};

{
	var carrFreq, modFreq, carrPhase, modPhase, index;
	var gaussWindow, mod, carr, sig;

	carrFreq = \freq.kr(440);
	modFreq = carrFreq * MouseY.kr(1, 2);

	carrPhase = Phasor.ar(DC.ar(0), carrFreq * SampleDur.ir);
	modPhase = Phasor.ar(DC.ar(0), modFreq * SampleDur.ir);

	index = MouseX.kr(0, 30);
	
	gaussWindow = raisedCos.(modPhase, index);
	mod = sin(modPhase * 2pi);
	carr = sin(carrPhase * 2pi + (mod * index));

	sig = carr * gaussWindow;

	sig = LeakDC.ar(sig);

	sig!2 * 0.1;
	
}.play;
)

you decide what suits your needs!

1 Like