Last active
December 6, 2022 17:33
-
-
Save helmholtz/3df11fd002209fe658a4 to your computer and use it in GitHub Desktop.
Tuning and Timbre
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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