Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Tuning and Timbre
s.boot;
/*******************************************
TUNING AND TIMBRE
asStream, 11/27/2015
Explores the theory of William Sethares on
the relationship between tuning and timbre.
See http://sethares.engr.wisc.edu/consemi.html
USAGE:
1. Boot server
2. Exectute SynthDef block
3. Execute environment variable block
4. Execute main block
********************************************/
(
/* SYNTHS
timbreTest has five partials and a simple percussive envelope. The tuningFactor
parameter controls the frequency of the partials. A value of 2 corresponds
to "standard" harmonic partials. Values greater than 2 result in a stretched
spectrum, values less than 2 result in a compressed spectrum. SendReply sends
an OSC message from the server to the client on NoteOn. These messages are
used for the visualization.
kraftySnr and kik taken from hjh's patterns tutorial */
SynthDef(\timbreTest, {|freq=220, amp=0.1, attack=0.03, sustain=0.1, tuningFactor=2, pan=0, id=0, out=0|
var partials = #[
0, //log2 1
1, //log2 2
1.5849625007211563, //log2 3
2, //log2 4
2.321928094887362, //log2 5
];
var sig = Array.fill(5, { |i|
SinOsc.ar(tuningFactor ** partials[i] * freq, i*pi/4, 1/(i+1))
}).sum;
var env = EnvGen.kr(Env.perc(attack, sustain, amp), doneAction: 2);
SendReply.kr(Impulse.kr(0), '/tr', [freq, amp, sustain, pan], id);
Out.ar(out, LPF.ar(Pan2.ar(sig*env, pan), 2000));
}).add;
SynthDef(\kraftySnr, { |amp = 1, freq = 2000, rq = 3, decay = 0.1, pan, out|
var sig = BrownNoise.ar(amp);
var env1 = EnvGen.kr(Env.perc(0.002, decay), doneAction: 0);
var env2 = EnvGen.kr(Env.perc(0.02, decay*2,0.25), doneAction: 2);
sig = BPF.ar(sig, freq, rq, env1+env2);
Out.ar(out, Pan2.ar(sig, pan))
}).add;
SynthDef(\kik, { |basefreq = 50, ratio = 7, sweeptime = 0.05, preamp = 1, amp = 1,
decay1 = 0.3, decay1L = 0.8, decay2 = 0.15, out|
var fcurve = EnvGen.kr(Env([basefreq * ratio, basefreq], [sweeptime], \exp)),
env = EnvGen.kr(Env([1, decay1L, 0], [decay1, decay2], -4), doneAction: 2),
sig = SinOsc.ar(fcurve, 0.5pi, preamp).distort * env * amp;
Out.ar(out, sig ! 2)
}).add;
//Simple reverb
SynthDef(\reverber, {|in=16, out=0, wet=0.33|
var sig = In.ar(in, 2);
Out.ar(out, FreeVerb2.ar(sig[0], sig[1], wet, room:0.5));
}).add;
//Simple delay
SynthDef(\delay, {|in=18, out=16, delayTime=0.0625, decayTime=0.25, wet=0.5|
var sig = In.ar(in,2);
Out.ar(out, AllpassC.ar(sig, delayTime, delayTime, decayTime, add:sig));
}).add;
)
(
~root = 205;
~chord = [1, 1.5, 2.5].collect({|item| item.log2});
~scale = [1, 1.25, 1.5, 2, 2.25, 2.5, 3].collect({|item| item.log2 });
~logHarmonics = [1, 2, 3, 4, 5].collect({|item| item.log2 });
~tuningFactor = 2.001;
~width = 1280;
~height = 720;
~colorValues = ( //Colors from http://www.swisscolors.net/
background: "e0e5da",
chord: "00aabb",
splotch: "f43530",
harmonics: "46454b"
);
~colors = ~colorValues.collect({|item| Color.fromHexString(item)});
)
(
var p,q,r,t,u; //patterns
var o; //OSCresponder
var w,v,label; //window, view, text label
var noteEvents, chordEvent; //event logs for visualization
var tuningFactor = ~tuningFactor;
Ndef(\lfo, {SinOsc.kr(1/75, 0, 0.2, 2)}); //controls tuningFactor
Synth(\reverber, [wet: 0.20]);
Synth(\delay);
/* VISUALIZATION
OSCFunc captures data sent by the \timbreTest synth and stores it in
chordEvent and noteEvents. This data is accessed by the visualization code
contained in v.drawFunc. chordEvent responds to the amplitude of each chord
(pattern p). noteEvents contains information from pattern r and stores it in
a stack where index 0 is the most recent event(s) and index 29 is the least
recent. */
chordEvent = 1;
noteEvents = Array.fill(30, { Bag.new });
o=OSCFunc({|m| switch( m[2],
1, { chordEvent = chordEvent + m[4].explin(0.04, 0.16, 1, 32) },
2, { noteEvents[0].add([m[3], m[4], m[5], m[6]])}
)}, '/tr');
w = Window(bounds: Rect(0, 0, ~width, ~height));
v = UserView(w, w.view.bounds);
v.animate = true;
v.background = ~colors['background'];
label = StaticText(v, Rect(40, 15, 400, 50));
label.font = Font("Helvetica", 48, true);
label.stringColor = ~colors['harmonics'];
v.drawFunc = {
var freqs;
//Get current value of tuningFactor
Ndef(\lfo).asBus.get({|value| tuningFactor=value});
label.string = "A = " ++ ~tuningFactor.asStringPrec(4).padRight(5,"0");
//Calculate harmonic positions using current value of tuningFactor
freqs = ~logHarmonics.collect({|item| tuningFactor ** item * ~root});
//Draw grid lines
freqs.do({|item,i|
//Fixed grid lines
Pen.strokeColor = ~colors['harmonics'];
Pen.width = 1;
Pen.moveTo(0@(i+1*~root).explin(180, 1700, ~height, 0));
Pen.lineTo(~width@(i+1*~root).explin(180, 1700, ~height, 0));
Pen.stroke;
//Moving grid lines - line width mapped to chordEvent
Pen.width = 1;
Pen.strokeColor = ~colors['chord'];
Pen.moveTo(0@item.explin(180,1700,~height,0));
Pen.lineTo(~width@item.explin(180, 1700, ~height, 0));
Pen.stroke;
Pen.strokeColor = ~colors['chord'];
Pen.width = chordEvent;
4.do({|j|
Pen.moveTo(2*j+0.5/8 * ~width@item.explin(180,1700,~height,0));
Pen.lineTo(2*j+1.5/8 * ~width@item.explin(180,1700,~height,0));
Pen.stroke;
});
});
//Draw splotches - freq --> y, pan --> x, amp --> size; time --> alpha and size
noteEvents.do{ |item, i|
Pen.fillColor = ~colors['splotch'];
Pen.alpha = (30-i).linexp(1,27,0.01,1);
item.do{ |subItem|
Pen.fillOval(Rect.aboutPoint(
subItem[3].linlin(-1,1,0,~width) @ subItem[0].explin(180, 1800, ~height, 0),
subItem[1].explin(0.002,0.3, 3, 60)*(30-i).linlin(1,27,0.3,1),
subItem[1].explin(0.002,0.3, 3, 60)*(30-i).linlin(1,27,0.3,1)));
};
};
//Increment chordEvent and noteEvents
if ( chordEvent <= 2, { chordEvent = 1 }, { chordEvent = chordEvent*0.985 } );
noteEvents = noteEvents.shift(1, Bag.new);
};
w.front;
/* PATTERNS
Pattern p plays the chords. Use timingOffset to create rhythmic "strum."
Pattern q plays the melody. Notes are played in pairs and through
Pstutter for additional coolness. Patterns t and u are the drum parts.*/
p=Pbind(
\freq, Pfunc({~tuningFactor ** ~chord * ~root}),
\sustain, Pseq([8,4],inf),
\dur, Pseq([2.25,3.75], inf),
\tuningFactor, Pfunc({~tuningFactor}),
\instrument, \timbreTest,
\amp, Pseq([0.10, 0.05], inf)*[2,1,0.5],
\attack, 0.07,
\id, 1,
\out, 16,
\timingOffset, Ptuple([0,Pseq([1,1.125],inf),1.125],inf)
);
q=Pbind(
\octave, Pstutter(Pwhite(2,10), Pfunc({ [1,~tuningFactor].choose })),
\freq, Pfunc({(~tuningFactor ** [~scale.choose,~scale.choose] * ~root)})*Pkey(\octave),
//\sustain, Pwrand([0.25,0.5,1],[0.5,0.25,0.25], inf),
\dur, Pwrand([0.25,0.5,1,2,4],[5,4,3,2,1].normalizeSum,inf)*0.25,
\legato, Prand([1,2],inf),
\tuningFactor, Pfunc({~tuningFactor}),
\attack, 0.025,
\amp, Ptuple([Pexprand(0.002, 0.3), Pwrand([0,Pexprand(0.002, 0.3)],[0.75,0.25],inf)], inf),
\pan, Ptuple([Pwhite(-0.7,0.7, inf), Pwhite(-0.7,0.7, inf)]),
\id, 2,
\timingOffset, Ptuple([0,Pwhite(0,0.02)],inf),
\out, 16,
\instrument, \timbreTest,
);
r = Pstutter(Pwrand([1,3,5],[0.9, 0.05, 0.05], inf), q); //Only stutter 10% of the time
t = Pbind(
\instrument, \kraftySnr,
\decay, Pwhite(0.04,0.08, inf),
\dur, 0.125,
\rq, Pbrown(0.3, 1, 0.05),
\freq, Pbrown(6000, 9000, 200),
\amp, Pseq([0.2, 0.05, 0.03, 0.015, 0.04, 0.02], inf) * 18 * Pexprand(0.5,1.5),
\pan, Pbrown(-0.3, 0.3, 0.1, inf),
\out, Pwrand([16,18],[0.67,0.33],inf),
);
u = Pbind(
\instrument, \kik,
\dur, Pseq([1, 0.5,0.125,0.875, 0.5],inf),
\amp, Pseq([0.5, 0.25, 0.5, 0.25, 0.15], inf),
\baseFreq, [70,75],
);
/* PLAY
We vary tuningFactor sinusoidally and grab a value every six seconds (two
strums). Ptpar starts the patterns. We use a 0.1s delay on all patterns to
make sure tuningFactor is set before the strum. There's probably a more
elegant way to do this. */
Routine({
~tuningFactor=tuningFactor;
Ptpar([6.1,p,6.1,r, 0.6, t, 0.1, u]).play(TempoClock.default);
inf.do({
6.wait;
~tuningFactor=tuningFactor;
});
}).play;
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.