{
   "author" : "walters",
   "name" : "God Rest Ye Merry, Xentlemen",
   "description" : "Xenharmonic rendering of the Xmas favorite",
   "ancestor_list" : [],
   "labels" : [
      "ring modulation",
      "just intonation",
      "christmas",
      "xmas"
   ],
   "code" : "// GOD REST YE MERRY, XENTLEMEN\r\n// The Christmas sensation in just intonation with harmonization by ring modulation\r\n\r\n// Tim Walters, 2023\r\n// see https://timwalters.bandcamp.com for lots of questionable holiday music (some SuperCollider, some not)\r\n\r\n// first boot server, send/store synthdefs and set up melody data\r\n(\r\n~sampleRate = 96000; // much easier than trying to anti-alias ring modulation + distortion\r\nServer.default.options.sampleRate_(~sampleRate);\r\n\r\nServer.default.waitForBoot {\r\n\tvar rawNotes, rawVels, rawTimes, times, freqDrift;\r\n\r\n\t// a little frequency instability still sounds better, even in just intonation\r\n\tfreqDrift = { |freq| (freq.cpsmidi + LFDNoise3.kr(0.2, mul: 0.03)).midicps };\r\n\r\n\t// defs\r\n\tSynthDef(\\ringmod, { |out=0, t_trig=1, amp=0.125, vel=0.5, freq=220, freq2=110, attackTime=0.05, decayTime=1.0|\r\n\t\tvar env, osc1, osc2, osc3, rings, sig, releaser, vol, follower, filtscale, filt;\r\n\r\n\t\t// velocity-dependent decay envelope\r\n\t\tenv = Decay2.kr(t_trig, attackTime * LinLin.kr(vel, 1, 0, 0.5, 2.0), decayTime * LinLin.kr(vel, 0, 1, 0.8, 1.25));\r\n\r\n\t\t// base oscillator\r\n\t\tosc1 = SinOsc.ar(freqDrift.(freq));\r\n\r\n\t\t// scale volume pre- and post-distortion by velocity\r\n\t\tvol = LinLin.kr(vel, 0, 1, -12, 0).dbamp;\r\n\r\n\t\t// ring modulate (multiply) with both freq2 and a just fifth above\r\n\t\t// sent to left and right channels\r\n\t\tosc2 = SinOsc.ar(freqDrift.(freq2));\r\n\t\tosc3 = SinOsc.ar(freqDrift.(freq2 * 1.5));\r\n\t\trings = ([osc1 * osc2, osc1 * osc3] * 8 * vol).distort;\r\n\r\n\t\t// mix some of the base frequencies back in\r\n\t\t// I don't understand why wrapping the env in K2A sounds different, but it does\r\n\t\tsig = (rings + (osc1 + (osc2 * -6.dbamp)).dup) * K2A.ar(env) * amp * vol;\r\n\r\n\t\t// gentle filter controlled by amplitude and velocity\r\n\t\tfollower = Amplitude.ar(sig);\r\n\t\tfiltscale = LinLin.kr(freq2.cpsmidi, 60, 84, 1, 0.5) * vel.sqrt;\r\n\t\tfilt = LPF.ar(sig, LinLin.ar(follower, 0, 1, 7, 10) * freq * filtscale);\r\n\r\n\t\t// give Fletcher and Munson a break\r\n\t\tfilt = BPeakEQ.ar(filt, 3500, 0.5, -5);\r\n\r\n\t\treleaser = DetectSilence.ar(filt, -84.dbamp, doneAction: 2);\r\n\t\tOffsetOut.ar(out, filt);\r\n\t}).store.add;\r\n\r\n\tSynthDef(\\bass, { |out=0, gate=1, t_trig=0, amp=0.125, freq=220, decayTime=8.0, coef=0.25|\r\n\t\tvar pluck = Pluck.ar({ PinkNoise.ar(0.5) } ! 2, t_trig, freq.reciprocal, freq.reciprocal, decayTime, coef);\r\n\t\tvar releaser = EnvGen.ar(Env.asr(0, 1, decayTime), gate, doneAction: 2);\r\n\t\tOffsetOut.ar(out, (pluck * 16).distort * SinOsc.ar(freq) * amp)\r\n\t}).store.add;\r\n\r\n\tSynthDef(\\verb, { |out=0, wet=0.25, predelay=0.05, gate=1|\r\n\t\tvar in = DelayN.ar(In.ar(out, 2), delaytime: predelay);\r\n\t\tXOut.ar(out, wet, GVerb.ar(in.sum, 250, revtime: 2, drylevel: 0, earlyreflevel: -12.dbamp, taillevel: 0.dbamp));\r\n\t}, [\\ir, 0.1, 0.1, 0]).store.add;\r\n\r\n\t// performance data from hand-played MIDI\r\n\trawNotes = [ 64, 64, 71, 71, 69, 67, 66, 64, 62, 64, 66, 67, 69, 71, 64, 64, 71, 71,\r\n\t\t69, 67, 66, 64, 62, 64, 66, 67, 69, 71, 71, 72, 69, 71, 72, 74, 76, 71, 69, 67,\r\n\t\t64, 66, 67, 69, 67, 69, 71, 72, 71, 71, 69, 67, 66, 64, 67, 66, 64, 69, 67, 69,\r\n\t\t71, 72, 74, 76, 71, 69, 67, 66, 64, 64, 64, 71, 71, 69, 67, 66, 64, 62, 64, 66,\r\n\t\t67, 69, 71, 64, 64, 71, 71, 69, 67, 66, 64, 62, 64, 66, 67, 69, 71, 71, 72, 69,\r\n\t\t71, 72, 74, 76, 71, 69, 67, 64, 66, 67, 69, 67, 69, 71, 72, 71, 71, 69, 67, 66,\r\n\t\t64, 67, 66, 64, 69, 67, 69, 71, 72, 74, 76, 71, 69, 67, 66, 64 ];\r\n\trawVels = [ 70, 74, 74, 76, 83, 79, 71, 69, 68, 73, 60, 72, 72, 72, 69, 88, 79, 78,\r\n\t\t83, 77, 62, 55, 83, 79, 74, 86, 82, 73, 85, 81, 82, 82, 85, 83, 104, 86, 78, 88,\r\n\t\t69, 66, 74, 69, 77, 81, 74, 79, 69, 71, 71, 78, 62, 65, 73, 62, 71, 75, 84, 80,\r\n\t\t77, 87, 81, 97, 88, 86, 85, 80, 76, 76, 79, 74, 76, 68, 76, 70, 79, 79, 81, 69,\r\n\t\t80, 72, 74, 74, 82, 79, 83, 84, 74, 74, 72, 79, 69, 66, 79, 79, 78, 86, 88, 83,\r\n\t\t82, 84, 88, 103, 86, 84, 81, 81, 74, 81, 62, 87, 79, 84, 84, 81, 81, 82, 80, 74,\r\n\t\t75, 80, 88, 62, 66, 80, 86, 83, 86, 82, 88, 86, 85, 84, 91, 72\r\n\t];\r\n\trawTimes = [ 30272, 43202, 56016, 68580, 80946, 93382, 106876, 120352, 158818, 172154,\r\n\t\t185970, 199026, 213208, 227362, 288390, 300902, 313402, 325380, 337744, 350324,\r\n\t\t364148, 378000, 415974, 428500, 441878, 455242, 468262, 482612, 548014, 560434,\r\n\t\t573552, 586122, 599528, 613018, 626646, 639698, 680236, 693534, 706470, 720296,\r\n\t\t734578, 749356, 814508, 827088, 839560, 865932, 878806, 892792, 905772, 918564,\r\n\t\t932114, 945794, 972798, 978774, 985758, 998882, 1069610, 1082090, 1095602, 1108404,\r\n\t\t1121810, 1135642, 1148322, 1162650, 1176864, 1190834, 1206372, 1304640, 1319096,\r\n\t\t1332192, 1345078, 1359110, 1372204, 1385426, 1400638, 1440742, 1454038, 1466804,\r\n\t\t1480064, 1493634, 1507450, 1568292, 1580988, 1593828, 1606050, 1619316, 1632210,\r\n\t\t1645800, 1658950, 1703150, 1716222, 1729796, 1742988, 1757022, 1771082, 1830026,\r\n\t\t1843488, 1856088, 1868844, 1882152, 1895876, 1909496, 1923418, 1967164, 1980738,\r\n\t\t1994512, 2007926, 2022132, 2036510, 2098256, 2110542, 2123764, 2150164, 2162802,\r\n\t\t2175516, 2188310, 2201676, 2214308, 2227136, 2252892, 2258638, 2265816, 2278922,\r\n\t\t2344746, 2356914, 2369874, 2381800, 2394730, 2407328, 2420948, 2434120, 2447818,\r\n\t\t2461264, 2477334 ];\r\n\r\n\t// normalize values\r\n\t~degrees = rawNotes - 64; // performed in E minor\r\n\t~vels = rawVels / 127.0;\r\n\ttimes = rawTimes  / 19200.0;\r\n\t// get diffs between times and add a final arbitrary one to get right number\r\n\t~durs = times.differentiate.drop(1) ++ [5];\r\n\t~renderDuration = times.last + 10; // needed for NRT\r\n\r\n\t// data integrity check\r\n\tif (~degrees.size != ~vels.size || ~degrees.size != ~durs.size) { Error(\"Melody data is inconsistent.\").throw; };\r\n}\r\n)\r\n\r\n// turn melody data into piece\r\n(\r\nvar scale, root, degree2s, freq2s, bassPlays, bassDurs, bassDegrees, prevDegree, basePattern;\r\n\r\n// scale is actually minor, but it comes in chromatically from the MIDI data\r\nscale = Scale.chromatic(\\just);\r\n\r\n// could transpose here\r\nroot = 4;\r\n\r\n// will determine ring modulation frequencies\r\ndegree2s = [\r\n\t0, 0, 0, -2, -2, 0, 0, 0, -5, 0, 0, -4, -4, 0,\r\n\t0, 0, 0, -2, -2, 0, 0, 0, -5, 0, 0, -4, -4, -5,\r\n\t-5, 3, 3, 3, 3, 2, 2, 3, 3, 0, 0, 0, 0, -2,\r\n\t0, 0, 3, -4, -4, -2, -2, -5, -5, 3, -4, -4, -4, 5,\r\n\t-2, -2, 3, 3, 2, 2, -2, -2, -5, -5, 0\r\n];\r\n\r\n// Event will handle conversion to \\freq, but we have to do \\freq2 ourselves\r\n// Higher octave on second repetition changes harmonization, timbre, and even apparent\r\n// octave of melody\r\nfreq2s = [5, 6].collect { |octave| degree2s.collect { |n| scale.degreeToFreq(n, root.midicps, octave) } }.flatten;\r\n\r\n// generate bass part doubling ring modulation frequencies, without rearticulation or pickup notes\r\n// lay out first time through\r\nbassPlays = (\\rest ! degree2s.size) ++ [\\rest] ++ (\\on ! 13) ++ [\\rest] ++ (\\on ! 13)\r\n++ [\\rest] ++ (\\on ! 13) ++ [\\rest, \\rest] ++ (\\on ! 12) ++ [\\rest, \\rest] ++ (\\on ! 9);\r\n\r\n// data integrity check\r\nif (~degrees.size != freq2s.size || ~degrees.size != bassPlays.size) { Error(\"Harmony data is inconsistent.\").throw; };\r\n\r\nbassDurs = List[];\r\nbassDegrees = List[];\r\nprevDegree = \\rest;\r\nbassPlays.do { |val, i|\r\n\tvar degree = (val == \\on).if { degree2s.wrapAt(i) } { \\rest };\r\n\tif ((degree != \\rest) && (prevDegree == degree)) {\r\n\t\tbassDurs[bassDurs.lastIndex] = bassDurs.last + ~durs[i]\r\n\t} {\r\n\t\tbassDurs.add(~durs[i]); bassDegrees.add(degree)\r\n\t};\r\n\tprevDegree = degree;\r\n};\r\n\r\n// notes...\r\nbasePattern = Ptpar([\r\n\t0.25,\r\n\tPbind(\r\n\t\t\\instrument, \\ringmod,\r\n\t\t\\scale, scale,\r\n\t\t\\root, root,\r\n\t\t\\degree, Pseq(~degrees, 1),\r\n\t\t\\octave, 6,\r\n\t\t\\dur, Pseq(~durs, 1),\r\n\t\t\\freq2, Pseq([5, 6].collect { |octave| degree2s.collect { |n| scale.degreeToFreq(n, root.midicps, octave) } }.flatten, 1),\r\n\t\t\\decayTime, 4.0,\r\n\t\t\\amp, 0.dbamp,\r\n\t\t\\vel, Pseq(~vels, 1)\r\n\t),\r\n\t0.25,\r\n\tPmono(\r\n\t\t\\bass,\r\n\t\t\\scale, scale,\r\n\t\t\\root, root,\r\n\t\t\\degree, Pseq(bassDegrees, 1),\r\n\t\t\\trig, Pseq(bassDegrees.collect { |d| (d === \\rest).if { 0 } { 1 } }, 1),\r\n\t\t\\octave, 3,\r\n\t\t\\dur, Pseq(bassDurs, 1),\r\n\t\t\\coef, 0.8,\r\n\t\t\\amp, -6.dbamp,\r\n\t\t\\decayTime, 8.0\r\n\t)\r\n]);\r\n\r\n// with reverb\r\n~xentlemen = Pfxb(basePattern, \\verb, \\wet, 0.25, \\predelay, 0.025);\r\n)\r\n\r\n// play...\r\nr = ~xentlemen.play;",
   "id" : "1-5hz",
   "is_private" : null
}
