«God Rest Ye Merry, Xentlemen» by walters

on 01 Dec'23 17:30 in ring modulationjust intonationchristmasxmas

Xenharmonic rendering of the Xmas favorite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// 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;
raw 8130 chars (focus & ctrl+a+c to copy)
comments