// SuperCollider audio for Dale Parsons' chess music program // H. James Harkins /* for convenience when running the python components adjust the paths for your environment, then copy and paste into terminal not needed for the supercollider side PYTHONPATH=~/Downloads/chess export PYTHONPATH cd Downloads/chess/chess python */ // resources are encapsulated in an environment, shouldn't interfere with other stuff // use server window volume control if it gets too loud // to start: select all between these parens and run ( ~chessAudio = Environment.make({ // change to false if you want to leave scsynth running after cleanup ~quitServerOnCleanup = true; ~log = false; ~run = { ~init.(); }; ~numBanks = 4; ~numOscil = 64; // pairs: name, func, name, func etc. ~synthdefFuncs = [ pawn: { |freq, gate| var sig = BrownNoise.ar, shuftrig = Dust.kr(Rand(2.0, 3.5)), shufenv = EnvGen.kr(Env([0, 1, 0], [TRand.kr(0.08, 0.12, shuftrig), TRand.kr(0.2, 0.5, shuftrig)]), shuftrig); sig = CombL.ar(sig * shufenv, 0.2, freq.reciprocal, ExpRand(0.15, 0.9)); sig = HPF.ar(sig, 180); Limiter.ar(sig, 0.9, 0.01) * EnvGen.kr(Env.adsr(0.1, 0.3, 0.7, 0.15), gate, doneAction: 2) }, knight: { |freq, gate| var car_amp, mod_amp, mod; car_amp = (Latch.kr(gate, gate) - 1) * 0.54 + 1; mod_amp = (Latch.kr(gate, gate) - 1) * 0.6 + 1; mod_amp = 8.433 * mod_amp * EnvGen.kr(Env.adsr(0.12, 0.2, 0.5, 0.25), gate); // sum 1st and 2nd partials; 0.67 is detune mod = Klang.ar(`[[1, 2]], freqscale: freq + 0.67) * mod_amp; SinOsc.ar(freq + (mod * freq), 0, car_amp) * EnvGen.kr(Env.adsr(0.07, 0.16, 0.7, 0.14), gate, doneAction: 2) }, bishop: { |freq, gate, clickamp = 45, clickrq = 0.11| var basefreq = 48.midicps, // base frequency of first buffer numbufs = ~orgbufs.size, numOctaves = numbufs - 1, // note that subtraction of logs corresponds to division of original values freqmap = ((log2(freq) - log2(basefreq)) * (numbufs / numOctaves)) .clip(0, numbufs - 1.001), bufbase = ~orgbufs.first.bufnum, sig = VOsc.ar(bufbase + freqmap, freq) * ((Latch.kr(gate, gate) - 1) * 0.6 + 1), gateTrig = (gate > 0), clickTrig = HPZ1.kr(gateTrig).abs + Impulse.kr(0), click = BPF.ar(PinkNoise.ar * Decay2.kr(clickTrig, 0.01, 0.02), ExpRand(600, 800), clickrq, clickamp); (sig * Linen.kr(gate, 0.08, 1, 0.15, doneAction: 2)) + click }, rook: { |freq, gate| var car_amp, mod_amp, mod; freq = freq * 0.98824429900507; // tuning correction car_amp = (Latch.kr(gate, gate) - 1) * 0.614 + 1; mod_amp = (Latch.kr(gate, gate) - 1) * 0.677 + 1; mod_amp = mod_amp * EnvGen.kr(Env(#[0, 1, 0.7, 0.2, 0], #[0.01, 0.35, 5, 3], releaseNode: 3), gate); mod = SinOsc.ar(freq * 4.2, 0, mod_amp); SinOsc.ar(freq + (mod * freq), 0, car_amp) * EnvGen.kr(Env(#[0, 1, 0.4, 0, 0], [0.01, 0.25, 7, 0.24], curve: #[-3, 2, -3, -3], releaseNode: 3), gate, doneAction: 2) }, queen: { |freq, gate| var basefreq = 48.midicps, // base frequency of first buffer numbufs = ~tribufs.size, numOctaves = numbufs - 1, // note that subtraction of logs corresponds to division of original values freqmap = ((log2(freq) - log2(basefreq)) * (numbufs / numOctaves)) .clip(0, numbufs - 1.001), bufbase = ~tribufs.first.bufnum, sig = VOsc3.ar(bufbase + freqmap, freq, freq * 1.004, freq * (1.004.reciprocal)) * ((Latch.kr(gate, gate) - 1) * 0.6 + 1); LPF.ar(sig, ExpRand(1800, 5000)) * EnvGen.kr(Env.adsr(0.3, 0.5, 0.68, 1.4), gate, doneAction: 2) }, king: { |freq, gate| var sig = VOsc3.ar(~bassbufs.first, Lag.kr(freq, 0.1), freq*1.003, freq*(1.003.reciprocal)), ftrig = Impulse.kr(0) + (HPZ1.kr(freq).abs), ffreq1 = Lag.kr(TExpRand.kr(200, 400, ftrig), 0.1), ffreq2 = Lag.kr(TExpRand.kr(400, 800, ftrig), 0.1), f1 = Formlet.ar(sig, ffreq1, 0.0046003481262002, 0.029182082376441, 0.36560302866414), f2 = Formlet.ar(sig, ffreq2, 0.0080719180518879, 0.051203815681801, 0.23850730124392); Limiter.ar((sig * 0.15630070308275) + f1 + f2) * EnvGen.kr(Env.adsr(0.08, 0.18, 0.75, 0.14), gate, doneAction: 2) } ]; ~makeSynthDefs = { // collect "support" and "conflict" versions of each synth ~synthdefs = Array(~synthdefFuncs.size div: 2); ~synthdefFuncs.pairsDo({ |name, func| ~synthdefs.add([ SynthDef((name ++ "_support").asSymbol, { |out, freq, left, right, gate = 1, rvbus, rvamp| var sig = SynthDef.wrap(func, prependArgs: [freq, gate]); sig = sig * [left, right]; Out.ar(out, sig); Out.ar(rvbus, sig * rvamp); })/*, SynthDef((name ++ "_conflict").asSymbol, { |out, freq, left, right, gate = 1, rvbus, rvamp| var sig = SynthDef.wrap(func, prependArgs: [freq, gate]), ffreq = ExpRand(100, 900), topmul = 15000 / ffreq, // start with a whooping-type envelope, then fade in a random curve ffreqcurve = XFade2.kr( EnvGen.kr(Env([1, topmul, 1], #[0.2, 0.8], \sine), timeScale: ExpRand(0.15, 2)), LFDNoise1.kr(LFNoise1.kr(freq).range(2.0, 12.0)).exprange(1.0, topmul), pan: Line.kr(-1, 1, dur) ); sig = RLPF.ar(sig, ffreq * ffreqcurve, ExpRand(0.008, 0.03), Rand(3, 9)).distort; sig = sig * [left, right]; Out.ar(out, sig); Out.ar(rvbus, sig * rvamp); })*/ ]) }); ~synthdefs }; ~init = { ~msgLog = List.new; ~responder = OSCresponderNode(nil, 'list', { |time, resp, msg| if(~log == true) { ~msgLog.add([time, msg]) }; ~hitOscillator.(*msg); }).add; ~oscils = { Array.newClear(~numOscil) } ! ~numBanks; ~s = Server.default; ~watcher = NodeWatcher.newFrom(~s); ~releasingNodes = IdentityDictionary.new; ~s.waitForBoot(inEnvir { ~grp = Group(~s); ~bus = Bus.audio(~s, 2); // all synths play here ~limiter = { var sig = In.ar(~bus, 2); Limiter.ar(sig, 0.95) }.play(~grp); ~rvbus = Bus.audio(~s, 2); ~rvb = { var sig, new, dlys; sig = In.ar(~rvbus, 2); new = sig; dlys = Array.fill(4, { new = AllpassN.ar(new, 0.1, Array.fill(2, { rrand(0.01, 0.03) }) + 0.05, 0.472); }); Mix.ar(dlys * Array.series(4, 1, -1/5)) }.play(target: ~limiter, outbus: ~bus, addAction: \addBefore); // for the "queen" synthdef, to save cpu ~tribufs = Buffer.allocConsecutive(8, s, 2048, 1, completionMessage: { |buf, i| var base = (i + 4) * 12, topPartial = (20000 / base.midicps).asInteger; // triangle wave spectrum buf.sine1Msg([(1, 3 .. topPartial).reciprocal.squared * #[1, -1], 0].lace(topPartial)); }); // for "bishop" to band-limit an organ sound ~orgAmps = #[ 1, 0.20782873570713, 0.11440313078953, 0.08223398246361, 0, 0.03379108867637, 0.012164789293877, 0.078070949930634, 0, 0.015625, 0.031141864799711, 0.026167812128794 ]; ~orgbufs = Buffer.allocConsecutive(8, s, 2048, 1, completionMessage: { |buf, i| var base = (i + 4) * 12, topPartial = (20000 / base.midicps).asInteger; buf.sine1Msg(~orgAmps[0 .. (topPartial-1)]); }); ~bassbufs = Buffer.allocConsecutive(2, s, 2048, 1, { |buf| buf.sine1Msg((1..70).reciprocal) }); ~makeSynthDefs.(); ~synthdefs.flat.do { |def| def.memStore }; ~initLevels = #[1, 0.367, 0.301, 0.393, 0.281, 1]; ~initRvb = #[0.536, 0.106, 0.148, 0.236, 0.202, 0]; // mixing framework ~buses = { Bus.audio(~s, 2) } ! ~synthdefs.size; fork { var condition = Condition(); SynthDef(\mixer2, { |inbus, outbus, level = 1| Out.ar(outbus, In.ar(inbus, 2) * level) }).send(~s); ~s.sync(condition); ~mixers = { |i| Synth(\mixer2, [inbus: ~buses[i], outbus: ~bus, level: ~initLevels[i]], target: ~limiter, addAction: \addBefore); } ! ~buses.size; ~rvbcontrols = Bus.control(~s, ~buses.size); ~rvbcontrols.setn(~initRvb); // to facilitate mapping synth inputs to these control buses // this yields an array ["c0", "c1", "c2"...] etc. // starting with ~rvbcontrols first bus index ~rvbIDs = { |i| "c" ++ (~rvbcontrols.index + i) } ! ~buses.size; ~makeGui.(); }; }); }; // for mixer ~makeGui = { defer { var height = 60 * ~mixers.size, sbounds = GUI.window.screenBounds, wbounds = Rect(sbounds.width - 310, sbounds.height - 50 - height, 300, height); ~window = GUI.window.new("mixers", wbounds); ~window.view.decorator = FlowLayout(wbounds.moveTo(0, 0)); ~mixers.do({ |mixer, i| EZSlider(~window, Point(290, 20), ~synthdefFuncs[i*2], controlSpec: [0.1, 10, \exp], action: { |view| mixer.set(\level, view.value) }, initVal: ~initLevels[i], labelWidth: 80, numberWidth: 50); EZSlider(~window, Point(290, 20), "reverb", \amp, action: { |view| ~s.sendMsg(\c_set, ~rvbcontrols.index + i, view.value); // ~rvblevels[i] = view.value; }, initVal: ~initRvb[i], labelWidth: 80, numberWidth: 50); // spacer - no text, will be invisible GUI.staticText.new(~window, 290@8); // EZSlider(window, dimensions, label, controlSpec, action, initVal, initAction, labelWidth, numberWidth) }); ~window.front; }; }; ~hitOscillator = { |cmd, bank, osc, freq, phase, left, right, player, piecetype, relationship, otherplayer, otherpiecetype, movenumber| var updater; if(freq <= 0 or: { left == 0 and: { right == 0 } }) { // stop oscil if(~oscils[bank][osc].notNil) { // latency + 0.1 to guard against zombie nodes ~s.sendBundle(0.3, ~oscils[bank][osc].setMsg(\gate, 0)); ~updateGui.(bank, osc, freq); } } { // start oscil if(~oscils[bank][osc].notNil) { // -1.04 = quick release in 40 ms ~s.sendBundle(0.3, [\error, -1], ~oscils[bank][osc].setMsg(\gate, -1.04)); }; ~oscils[bank][osc] = ~playSynth.(bank, osc, freq, phase, left, right, piecetype, player, relationship); if(~oscils[bank][osc].notNil) { // register to clear oscillator slot when release is done ~watcher.register(~oscils[bank][osc]); updater = Updater(~oscils[bank][osc], { |node, msg| if(msg == \n_end) { node.removeDependant(updater); try { if(node === ~oscils[bank][osc]) { ~oscils[bank][osc] = nil; }; } { |error| if(error.isException) { [bank, osc, ~oscils, ~oscils.tryPerform(\at, bank)].debug("\n\ngot n_end"); error.throw; }; }; }; }); }; ~updateGui.(bank, osc, freq); }; }; ~playSynth = { |bank, osc, freq, phase, left, right, piecetype, player, relationship| var return; // piecetype = 5.rand; // 0-4 // do something special for king if(piecetype == 5) { nil // but not yet } { ~s.makeBundle(0.2, { return = Synth(~synthdefs.wrapAt(piecetype)[0].name, [out: ~buses[piecetype], freq: freq, left: left, right: right, rvbus: ~rvbus, rvamp: ~rvbIDs[piecetype]], target: ~grp, addAction: \addToHead); }); }; return }; ~cleanup = { ~responder.remove; if(~quitServerOnCleanup) { // server quit obviously kills synthesis objects ~s.quit; } { // remove synthesis objects without stopping the synth ~oscils.do { |bank| bank.do { |osc| osc.release; } }; ~grp.free; ~bus.free; // ~limiter is freed by clearing the group [~tribufs, ~orgbufs, ~bassbufs].flat.do(_.free); }; ~closeGui.(); }; }); ~chessAudio.push; ~run.(); ) // to finish: ( ~cleanup.(); Environment.pop; ~chessAudio = nil; )