Generating unique saw wave

The function below composes a saw wave using additive synthesis.

I’m very interested in the synthesis of unique saw waveforms.

  • Is this method standard?
  • How does it compare to Saw.ar?
  • Are there other methods or techniques?

Could one adjust the interval ratio in an interesting way? Perhaps matching it to a certain interval or scale?

I was under the impression that each harmonic runs at half the amplitude, double the rate of each one previous.

The formula is slightly different, and so regarding the possibility of generating unique saw waveforms by adjusting the interval ratio for each harmonic, I’d like to ask if anyone can share their knowledge of it here… this function to demonstrate:

~saw =
{
	|rate 432 lvl 0.2 n 25|

	var sig =
	(
		0 ! 2
	)
	;
	(
		1..n // number of harmonics
	)
	.do
	{
		|n| sig = sig +
		(
			SinOsc.ar
			(
				freq:
				(
					rate * n
				),
				mul:
				(
					lvl / n
				)
			)
		)
	}
	;
	// Pan2 ar: sig
	sig ! 2
}

plot{ ~saw.value }

3 Likes

im normally going for this basic formula with the inharmonic parameter for spreading the individual partials to get an inharmonic spectrum and some random phases. The spectrum is multiplied by an 3dB per oct tilt instead of the static division formula. This could then be adjusted by different additive filters like formant, comb, bpf, lpf etc.
thanks to @nathan

(
{
	
	var numPartials = 64;
	var spectrum = (1..numPartials);
	
	var tension = (1 + (spectrum * spectrum * \inharmonic.kr(0.005))).sqrt;
	var tilt = (log2(spectrum) * \tilt.kr(-3)).dbamp;
	
	var freqs = \freq.kr(110) * spectrum * tension;
	
	var sig = SinOsc.ar(freqs, { Rand(0, 2pi) } ! numPartials);
	
	sig = (sig * tilt).sum;
	
	sig ! 2 * 0.05;
	
}.play;
)
4 Likes

Excellent, thanks for this.

Would love to see as much as possible… even the experimental.

This one is radical:

With additive synthesis, you get the truly awesome power to modulate each partial independently. Here, starting with a basic sawtooth, by modulating frequency and amplitude with different parameters for each partial, you can get a huge variety of interesting sounds:

(
{
    var n, partials, detunes, fenv, freqs, sig, envs;
    n = 64;
    partials = (1..n);
    detunes = (LFNoise1.kr(0.2 ! n) * \det.kr(0.2)).midiratio;
    fenv = XLine.kr(2.0, 1.0, partials.pow(0.7) / 10);
    freqs = \freq.kr(110) * partials * detunes * fenv;
    sig = SinOsc.ar(freqs, {Rand(0, 2pi)} ! n);
    sig = sig / partials.pow(\dampen.kr(0.9)); // dampen >= 0!
    envs = Env.perc(
        \atk.kr(0.1) * partials.pow(0.6) * ({Rand(0.5, 1.5)} ! n),
        \rel.kr(7.0) / (partials + 3).pow(0.9) * ({ExpRand(0.5, 2.0)} ! n)
    ).ar;
    sig = (sig * envs).sum;
    DetectSilence.ar(sig, doneAction: 2);
    sig ! 2 * 0.05;
}.play;
)
4 Likes

Very radical, very interesting sound.

Any others?

additional resonant LPF:

(
{

	var tFreq = LFDNoise3.ar(0.3).linlin(-1, 1, 1, 5);
	var trig = Impulse.kr(tFreq);
	var time = 1 / tFreq;

	var lpfEnv, gainEnv;
	var cutoff, tension, tilt, lpf, peakEQ;
	var sig, freqs, numPartials, spectrum;

	numPartials = 64;
	spectrum = (1..numPartials);

	lpfEnv = EnvGen.kr(Env(
		[0, 1, 0],
		[\fltAtk.kr(0.10), \fltRel.kr(0.90)],
		[\fltAtkCurve.kr(4.0), \fltRelCurve.kr(-8.0)]
	), trig, timeScale: time);

	gainEnv = EnvGen.ar(Env(
		[0, 1, 0],
		[\atk.kr(0.01), \rel.kr(0.99)],
		[\atkCurve.kr(4.0), \relCurve.kr(-4.0)]
	), trig);

	// harmonic tension
	tension = (1 + (spectrum * spectrum * \inharmonic.kr(0.01))).sqrt;

	// frequency spectrum
	freqs = \freq.kr(103.826) * spectrum * tension;
	sig = SinOsc.ar(freqs, { Rand(0, 2pi) } ! numPartials);

	// 3db/octave spectral tilt
	tilt = (log2(spectrum) * \tilt.kr(-3)).dbamp;

	// LPF
	cutoff = \lpfCutoff.kr(100) + lpfEnv.linlin(0, 1, 0, \lpfEnvAmount.kr(8000));
	lpf = ((log2(freqs) - log2(cutoff)) * \lpfSlope.kr(-12)).min(0).dbamp;

	// Peak EQ
	peakEQ = ((log2(freqs) - log2(cutoff)).abs * \peakSlope.kr(-12) + \peakRes.kr(24)).max(0).dbamp;

	sig = (sig * tilt * lpf * peakEQ).sum;

	sig = sig * gainEnv;

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

}.play;
)
1 Like

Great design, really digging the sound!

Phase modulation with feedback can produce exquisite sawtooth, some alternatives here:

Here, the first two harmonics (or operators) feedback in the phase parameter:

(
SynthDef(\PM, {
	arg  freq=110;
	var sig, index=1.5;
	sig = SinOsc.ar(freq: freq, phase: index*LocalIn.ar(numChannels: 1, default: 1)) + (SinOsc.ar(freq: freq * 2, phase: index*LocalIn.ar(numChannels: 1, default: 1)).softclip);
	LocalOut.ar(sig);
	Out.ar(0, sig*0.1);
}).play
)

Going wild in the modulation can lead to some interesting aliasing effects:

(
SynthDef(\PM, {
	arg  freq=110;
	var sig, index=SinOsc.ar(1/10,3pi/2).range(1.0,1.8);
	sig = SinOsc.ar(freq: freq, phase: index*LocalIn.ar(numChannels: 1, default: 1)) + (SinOsc.ar(freq: freq * 0.001, phase: index*LocalIn.ar(numChannels: 1, default: 1)).softclip);
	LocalOut.ar(sig);
	Out.ar(0, sig*0.1);
}).play;
)
1 Like

Its not radical but here’s an alternative band limited saw formula that I like:

    var k = 12000 * (SampleRate.ir/44100) / (freq * log10(freq));
    var sinSig = SinOsc.ar(freq, 0);
    var cosSig = SinOsc.ar(freq, pi/2);
    var sqSig = tanh(sinSig * k);
    var sawSig = sqSig * (cosSig + 1) * 0.5;
2 Likes

Excellent… with slight modification:

{
	|freq 880|	
	var k = 12000 * (SampleRate.ir/44100) / (freq * log10(freq));
    var sinSig = SinOsc.ar(freq, 0);
    var cosSig = SinOsc.ar(freq, pi/2);
    var sqSig = tanh(sinSig * k);
    var sawSig = sqSig * (cosSig + 1) * 0.5;
	sawSig ! 2
}
.plot

1 Like