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