Analogue Modelling Tips and Tricks
Contents:
Analogue Warmth: avoiding aliasing, chorusing
Filter Comparison: standard filters, BEQ Suite, MoogFF
More Server Side Sequencing: Demand rate UGens
Simulating Analogue Warmth
Digital systems have the drawback of setting hard contraints on representable frequencies and amplitude levels
Avoiding aliasing; use band limited waveforms (i.e. Saw not LFSaw for higher frequencies)
(
{
[LFSaw.ar(1000),Saw.ar(1000)]
}.plot(0.01)
)
But then, both are perfectly serviceable for low frequencies and the rougher edge to LFSaw can be useful.
Sidenote on aliasing:
Fundamental frequencies at divisors of the sampling rate have harmonics which only alias at harmonic locations!
//These assume 44100Hz output sampling rate
s.sampleRate
//warning; LOUD, awkward on ear
{LFSaw.ar(4410+(MouseX.kr(0,10).round(1)),0,0.5)}.scope
//aliasing if mouse moved left
{LFSaw.ar(1102.5+(MouseX.kr(0,10).round(1)),0,0.5)}.scope
//no aliasing
{Saw.ar(1102.5+(MouseX.kr(0,10).round(1)),0.5)}.scope
Chorusing (detuned oscillators)
{Saw.ar(440,0.2)}.play //plain
Though it increases sensory dissonance (beats and roughness between partials), a thicker sound is possible by mixing multiple copies of a waveform generator with subtle differences
{Mix(Saw.ar(440*[0.99,1.01],0.2))}.play //plain
//if want perceptual (log freq) same difference each side need 0.99 and 0.99.reciprocal, but we'll overlook that for now
//Because the oscillators are deterministic, there is a potential problem of highly rigid beating patterns
(
var numdetune=4;
{Mix(Saw.ar(Array.rand(numdetune,1,1.01)*440,0.2))}.play
)
//to alter phases need LFSaw; but could also just add some subtle frequency modulation
(
{
Mix.fill(4,{
var freqmult;
//between 1 +- 0.01
freqmult= 1+SinOsc.ar(LFNoise1.kr(rrand(0.25,0.5),4,5),pi.rand,0.01);
LFSaw.ar(440*(freqmult),pi.rand,0.2)
})
}.play
)
//question for you; why don't I need to use Rand rather than rrand in this case?
(
{Mix.fill(4,{Saw.ar(440*(1+SinOsc.ar(LFNoise1.kr(rrand(0.25,0.5),4,5),pi.rand,0.02)),0.2)}) }.play
)
//more like an analogue synth though to combine different waveforms in proportion and more overt detunings (ie octaves, octave+fifth)
//make a random mix
{Mix.fill(3,{|i| [LFTri, LFCub, LFPar].choose.ar(110*(2**i),pi.rand,10.rand.neg.dbamp)})}.play
Now to work on the source+filter model for subtractive synthesis
Comparing Filters
//standard filter
(
z = {
Resonz.ar(
Mix(Saw.ar([0.99,1,1.01]*440,0.3)),
MouseX.kr(100,20000,\exponential), // cutoff freq.
MouseY.kr(0.1, 1.0, \linear), // rq
0.5); // mul
}.play
)
z.free;
//The BEQSuite (sc3-plugins pack) has some nice filters, which take less energy away:
(
z = {
BLowPass4.ar(
Mix(Saw.ar([0.99,1,1.01]*440,0.3)),
MouseX.kr(100,20000,\exponential), // cutoff freq.
MouseY.kr(0.1, 1.0, \linear), // rq
0.5); // mul
}.play
)
z.free;
//can distort at high gain
(
z = {
MoogFF.ar(
Mix(Saw.ar([0.99,1,1.01]*440,0.3)),
MouseX.kr(100,20000,\exponential), // cutoff freq.
MouseY.kr(0.1, 4.0, \linear) //gain
);
}.play
)
z.free;
Demand Rate UGens
A bit like the Patterns library, server side!
Triggers are used in the Demand UGen to cue a 'demand' for a new value from the attached specialist demand rate UGens (which all begin with D and have names analogous to patterns)
(
{var sequence = Dseq([-0.3,0.5,0.0,0.4],inf); //Dseq is demand rate
Demand.ar(Impulse.ar(10),0, sequence);
}.plot(1.0)
)
So far, similar functionality might be constructed with Select, Index, EnvGen, IEnvGen et al
But akin to patterns, nesting is possible:
(
{var sequence = Dseq([-0.3,Drand([-1,1],1),0.0,0.4],inf); //Dseq is demand rate
Demand.ar(Impulse.ar(100),0, sequence);
}.plot(1.0)
)
Musical use:
(
{var freq, sequence = Dseq([60,Drand([48,72],1),63,62.8],inf); //Dseq is demand rate
freq= Demand.kr(Impulse.kr(MouseX.kr(1,100)),0, sequence).midicps; //only need k-rate; used a-rate in last examples because final output in UGen graph needs to be audio rate
Saw.ar(freq, 0.1)
}.play
)
//multichannel use 1 (multichannel expansion gives independent sequences)
(
{var freq, sequence = Dseq([60,Drand([47,73],1),63,61.5],inf); //Dseq is demand rate
freq= Demand.kr(Impulse.kr([5,5.1]),0, sequence).midicps; //output is two channels, since Dseq has two output values
SyncSaw.ar(freq, 300,0.1);
}.play
)
//multichannel use 2 (multichannel sequence itself)
(
{var freq, sequence = Dseq([[60,48],Drand([48,72],1),63,[61,62.8],[55,62.5],[63,62.1]],inf); //Dseq is demand rate
freq= Demand.kr(Impulse.kr(5),0, sequence).midicps; //output is two channels, since Dseq has two output values
[
SyncSaw.ar(freq[0], LFNoise0.kr(7,100,230),0.1),
SyncSaw.ar(freq[1], LFNoise2.kr(17,400,630),0.1)
]
}.play
)
More demanding: Duty allows you to specify a duration sequence for controlling when the next value is demanded
//interaction of durations for holding current value and output value sequence
{Duty.ar(Dseq([0.025,0.05],inf),0,Dseq([-0.5,0.5,0,-1,1],inf))}.plot(0.6)
The next three examples are provided as more involved patches; you might want to try to work out what is going on!
//putting various things together: rhythmic synthesis
(
{var freq, filterfreq, source, filtered;
var tempo;
tempo= 0.5; //seconds per beat
freq= Duty.kr(Dseq([0.25,0.25,0.5,0.75,0.75,0.75,0.25,0.25,0.25]*tempo,inf),0,Dseq([60,62,63,65,67,55,53,Drand([51,49,58,70],1),70,Drand([70, 48,72,36],1)],inf)).midicps;
filterfreq= Duty.kr(Dseq([0.25,0.25,0.25,0.25,1.0]*tempo,inf),0,Dseq(Array.fill(16,{exprand(300,5000)}),inf));
source= Mix(SyncSaw.ar([1,0.5,0.25,1.01, 1.25]*(freq.lag(0.05)),LFNoise2.kr([0.25,0.5,1,2,4]*(tempo*2),200,300), 0.1));
filtered= BLowPass4.ar(source,filterfreq.lag(0.0625),0.5);
Pan2.ar(filtered, LFNoise1.kr(tempo,0.25))
}.play
)
//note that if you make the Duty's .ar you'll see a substantial increase in CPU usage!
(
{
var source, filter, env;
var trig, freq, freq2;
trig= Impulse.kr(8,[0,0.1]); //stereo here forces stereo throughout the graph, including generating different notes
//trig= Impulse.kr(8);
//sequencer via Demand UGens
freq= Demand.kr(trig,0,Drand([60,63,60,63,65,63,70,67, 60,62,60,63,65,63,70,67, 67,72,75,72,67,70,63,55],inf)).midicps;
//portamento via lag
source= Mix.fill(4,{|i| LFSaw.ar((freq*[0.25*1.5,0.125]).lag(MouseY.kr(0.0,0.15))*((2**(i))+SinOsc.ar(LFNoise1.kr(rrand(0.25,0.5),4,5),pi.rand,0.01)),pi.rand,0.2)});
//if using Saw instead
//source= Mix.fill(4,{|i| Saw.ar((freq*[0.25*1.5,0.125]).lag(MouseY.kr(0.0,0.15))*((2**(i))+SinOsc.ar(LFNoise1.kr(rrand(0.25,0.5),4,5),pi.rand,0.01)),0.2)});
//envelope is restarted by trigger MouseX.kr(0.25,0.125)
env= EnvGen.ar(Env([0,1,0],[0.01,0.25]),trig);
filter= BLowPass.ar(0.5*source,300+(MouseX.kr(100,20000,'exponential')*env),0.2, env);
//Pan2.ar(filter,0.0);
}.play
)
//using InterplEnv
(
{
var source, filter;
var freq;
freq= IEnvGen.kr(InterplEnv([60,62,63,67,70,67,70,72,48].scramble,0.125.dup(8)),Phasor.ar(LFNoise0.kr(1)>0,0.5*(1.0/SampleRate.ir),0.0,1.0).round(1/8)).midicps;
source= Mix.fill(5,{|i| Saw.ar(freq*(0.25*(2**(i))+SinOsc.ar(LFNoise1.kr([0.125,0.25,0.5].choose,7,8),pi.rand,0.01)),0.2)});
filter= BLowPass.ar(0.5*source,1000+(2000*EnvGen.ar(Env([0,1,0],[0.01,0.25]),Impulse.kr(2))),0.2);
Limiter.ar(GVerb.ar(filter*0.25) + Pan2.ar(filter))
}.play
)