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