From 26f744c575f7bdc37971f3499c480e0734d5f28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 29 Apr 2024 12:10:30 +0200 Subject: [PATCH] Saving state after latest Lyon Algorave This is the current state of the system after the 27/04 algorave. NOTE: this is my personal live coding system, it is not fine-tuned for general usage. You might have to update paths and various parts of the code to get it to run on your system. --- .DS_Store | Bin 0 -> 6148 bytes BuboQuark.quark | 4 - Classes/.DS_Store | Bin 0 -> 6148 bytes Classes/Bank.sc | 332 ------------------ Classes/BuboBoot.sc | 42 ++- Classes/BuboClock.sc | 10 + Classes/BuboUtils.sc | 6 +- Classes/Configuration/Startup.scd | 1 - Classes/Configuration/Synthdefs.scd | 42 +++ Classes/Controllers/MIDIMix.sc | 268 +++++++-------- Classes/Patterns/Pmod.sc | 502 ++++++++++++++++++++++++++++ 11 files changed, 724 insertions(+), 483 deletions(-) create mode 100644 .DS_Store create mode 100644 Classes/.DS_Store create mode 100644 Classes/Patterns/Pmod.sc diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..304f7a675e8638c1da50ce62f63f90116acea7c9 GIT binary patch literal 6148 zcmeHKy-EW?5T1<(MpQ^?rH5cEdj6z7?vW<@K+h&XBW{n_2`VwgXdS-xGnkyS(>){SsS+M`orht^Vic(=h246>h2W=946xO?s=qAq3Bne61| zrRXd6aIpD)c>Sn6Xl}9kG=Et4itCyf1IB5GScXNp*U}aFPyttZ|8AE-2~b^zh-dveOfa z^VP9_?8C_=f;Jih#z4rxf?U>k|KFQ^{tuJv$`~*P{uKi*N}Fi|x1@V(>E?K^jiD_l p3&-Vx;}lH%R*YEQir1iCV2^nMOcZlLSRnRCz|&xZF)&vKz5%J2Xxab( literal 0 HcmV?d00001 diff --git a/BuboQuark.quark b/BuboQuark.quark index ceafb83..1c1db41 100644 --- a/BuboQuark.quark +++ b/BuboQuark.quark @@ -20,10 +20,6 @@ ], url: "https://raphaelforment.fr", isCompatible: {Main.versionAtLeast(3, 1)}, - preInstall: {|data| - File.mkdir("~/.config/livecoding/samples/"); - "/!\\ BuboQuark: Creating folder at ~./config/livecoding/samples/".postln; - }, postUninstall: { "/!\\ BuboQuark: Samples at '~/.config/livecoding/samples/' must be deleted manually".warn; }, diff --git a/Classes/.DS_Store b/Classes/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a30a04d5e5709ad008c37708d08b1d7bf988caec GIT binary patch literal 6148 zcmeHKJx>Bb5S;}IB5G(%EU&hb#`?q)LuW#4Of(88k^n~(`iq^3m5IN?(%wq>3nuy( z`~!Brncc{p$46|`m>IJB_GV{p-@V-Juta3a!)A%7NJM!w#@qtB3C8o>QkHQZPBwCn zHnnI-=X6YiRJ1x&0af6yDZt&vD7%A#}1(HFtP{_%-B?*O*QU_VQf15o{RGxMiy;48Cel<`Qewx%AAYpsWNgvQ2k mkwr;C=gP4RxD>CViDAy;0nm3CS%e3se*{DZ?NosuRp0~q7M-~O literal 0 HcmV?d00001 diff --git a/Classes/Bank.sc b/Classes/Bank.sc index 42919ef..3301116 100644 --- a/Classes/Bank.sc +++ b/Classes/Bank.sc @@ -1,335 +1,3 @@ -/* -* This file is taken from: https://gist.github.com/scztt/73a2ae402d9765294ae8f72979d1720e -* I have added a method to list the samples in the bank. -*/ - -// Bank : Singleton { -// classvar <>root, <>extensions, <>lazyLoading=true; -// var buffers.size) { buffers = buffers.extend(paths.size) }; -// -// paths.do { -// |path, i| -// var buffer; -// -// buffer = buffers[i]; -// -// if (path.notNil) { -// if (lazyLoading.not) { -// this.bufferAt(i) -// } -// } { -// if (buffer.notNil) { -// buffer.free; -// buffers[i] = buffer = nil; -// } -// } -// }; -// -// buffers.extend(paths.size); -// } -// } -// -// doOnServerBoot { -// if (paths.size > 0) { -// buffers = []; -// this.prUpdateBuffers(); -// "***Loaded samples for %***".format(this.asString).postln; -// } -// } -// -// doOnServerQuit { -// buffers = []; -// } -// -// pat { -// |keyPat| -// ^Pindex(Pseq([this], inf), keyPat) -// } -// -// asBuffer { -// ^this.singleSampleWrap(nil) -// } -// -// asControlInput { -// |...args| -// ^this.prSingleSampleWrap(\asControlInput, *args) -// } -// -// play { -// |...args| -// ^this.prSingleSampleWrap(\play, *args) -// } -// -// prSingleSampleWrap { -// |method ...args| -// var buffer; -// if (buffers.size == 1) { -// buffer = this.bufferAt(0); -// -// if (method.isNil) { -// ^buffer -// } { -// if (buffer.numFrames.isNil) { -// fork { -// Server.default.sync; -// buffer.performList(method, args) -// }; -// ^nil; -// } { -// ^buffer.performList(method, args) -// } -// } -// } { -// Error("Trying to % a bank with multiple buffers".format(method)).throw; -// } -// } -// } - Bank : Singleton { classvar <>root, <>extensions, <>lazyLoading=true; var Booting using default server configuration".postln; s = Server.default; - s.options.numBuffers = (2048 * 2048) * 2; // Some arbitrary number + s.options.numBuffers = (2048 * 2048) * 2; + s.options.maxLogins = 1; s.options.memSize = 8192 * 64; s.options.numWireBufs = 2048; + s.options.outDevice = "BlackHole 64ch"; s.options.maxNodes = 1024 * 32; - s.options.numOutputBusChannels = 16; + s.options.numOutputBusChannels = 24; s.options.numInputBusChannels = 16; - s.options.outDevice = "BlackHole 16ch"; }, { - "-> Booting using user server configuration".postln; + "-> Booting using custom server configuration".postln; s = Server.default; // Imposing a very high number of buffers! serverOptions.numBuffers = (2048 * 512) * 2; @@ -44,20 +46,42 @@ Boot { this.samplePath = samplePath ? "/Users/bubo/.config/livecoding/samples"; // Setting up the audio samples/buffers manager - Bank.lazyLoading = false; + Bank.lazyLoading = true; Bank.root = this.samplePath; // Post actions: installing behavior after server boot Server.default.waitForBoot({ + d = (); + // Exceptional Dual Sardine Boot + d.dirt = SuperDirt(2, s); + d.dirt.fileExtensions = ["wav","aif","aiff","aifc","mp3"]; + d.dirt.loadSoundFiles("/Users/bubo/Library/Application\ Support/Sardine/SON/*"); + d.dirt.doNotReadYet = true; + d.dirt.start(57120, [ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]); + ( + d.d1 = d.dirt.orbits[0]; d.d2 = d.dirt.orbits[1]; d.d3 = d.dirt.orbits[2]; + d.d4 = d.dirt.orbits[3]; d.d5 = d.dirt.orbits[4]; d.d6 = d.dirt.orbits[5]; + d.d7 = d.dirt.orbits[6]; d.d8 = d.dirt.orbits[7]; d.d9 = d.dirt.orbits[8]; + d.d10 = d.dirt.orbits[9]; d.d11 = d.dirt.orbits[10]; d.d12 = d.dirt.orbits[11]; + ); + d.dirt.soundLibrary.addMIDI(\midi, MIDIOut.newByName("MIDI", "Bus 1")); + d.dirt.soundLibrary.addMIDI(\midi2, MIDIOut.newByName("MIDI", "Bus 2")); + s.latency = 0.3; + + // Resume normal boot sequence "-> Loading config from: %".format(configPath ? (this.localPath +/+ "Startup.scd")).postln; (configPath ? (this.localPath +/+ "Startup.scd")).load; - Safety.all; - Safety(s).defName = \safeLimit; - Safety.setLimit(1); BuboUtils.fancyPrint(BuboUtils.ready, 40); this.installServerTreeBehavior(); this.clock.enableMeterSync(); + Safety.all; + Safety(s).defName = \safeLimit; + Safety.setLimit(1); + e = currentEnvironment; + + }); + } *installServerTreeBehavior { diff --git a/Classes/BuboClock.sc b/Classes/BuboClock.sc index 507045d..f335b3a 100644 --- a/Classes/BuboClock.sc +++ b/Classes/BuboClock.sc @@ -4,4 +4,14 @@ ^this.beatDur } + mod { + arg modulo; + ^this.beats % modulo + } + + modbar { + arg modulo; + ^this.bar % modulo + } + } diff --git a/Classes/BuboUtils.sc b/Classes/BuboUtils.sc index b36be13..318a027 100644 --- a/Classes/BuboUtils.sc +++ b/Classes/BuboUtils.sc @@ -1,9 +1,9 @@ BuboUtils { *banner { - var banner = "┳┓ ┓ ┳┓\n" - "┣┫┓┏┣┓┏┓ ┣┫┏┓┏┓╋\n" - "┻┛┗┻┗┛┗┛ ┻┛┗┛┗┛┗"; + var banner = "┳┓ ┓ ┳┓ ┓ ┳┓\n" + "┣┫┓┏┣┓┏┓┣┫┓┏┣┓┏┓ ┣┫┏┓┏┓╋\n" + "┻┛┗┻┗┛┗┛┻┛┗┻┗┛┗┛ ┻┛┗┛┗┛┗"; ^banner } diff --git a/Classes/Configuration/Startup.scd b/Classes/Configuration/Startup.scd index 3e4fafb..651a818 100644 --- a/Classes/Configuration/Startup.scd +++ b/Classes/Configuration/Startup.scd @@ -3,4 +3,3 @@ p = currentEnvironment; c = currentEnvironment.clock; "Loading SynthDefs".postln; "Synthdefs.scd".loadRelative; -m = MIDIControl(); diff --git a/Classes/Configuration/Synthdefs.scd b/Classes/Configuration/Synthdefs.scd index a9ea798..bc57a46 100644 --- a/Classes/Configuration/Synthdefs.scd +++ b/Classes/Configuration/Synthdefs.scd @@ -226,4 +226,46 @@ f.vardel = { d.tides = z; ); +( + z = SynthDef('pink', { + arg out; + var pink = PinkTrombone.ar( + noiseSource: BPF.ar(WhiteNoise.ar(), \noiseFilter.kr(2000)), + freq: \freq.kr(800), + tenseness: \tenseness.kr(0.4), + tongueIndex: \tongueIndex.kr(30), + tongueDiameter: \tongueDiameter.kr(3.5), + constrictionX: \constrictionX.kr(1.5), + constrictionY: \constrictionY.kr(2.5), + fricativeIntens: \fricativeIntens.kr(1.5) + ); + var env = Env.perc(\attack.kr(0.01), releaseTime: \release.kr(2.0)).kr(doneAction: 2); + var sound = pink * env; + Out.ar(out, Pan2.ar(sound, pos: \pan.kr(0.0))) + }).add; + d.pink = z; +); + + +( + z = SynthDef('kick', { + |out=0, freq, mul=512, vsweep=0.5, hold=0.25, release=0.25, amp=0.5, pan=0| + var p0, p1, p, freq0, freq1, freqEnv, sig; + p0 = 0.006699687; + p1 = 0.00001884606; + p = (1-vsweep)*p0 + (vsweep*p1); + freq1 = freq; + freq0 = freq1 * mul; + freqEnv = EnvGen.ar(Env([0,1], [1.0], [0])); + freqEnv = freq1 + ((freq0-freq1)/(1.0 + (freqEnv/p))); + sig = SinOsc.ar(freqEnv); + sig = sig * EnvGen.ar(Env([1,1,0], [hold,release], [0,0]), doneAction: Done.freeSelf) * amp; + sig = Pan2.ar(sig, pan); + Out.ar(out, sig); + }).add; + d.kick = z; +); + z = nil; // We don't need that variable anymore + + diff --git a/Classes/Controllers/MIDIMix.sc b/Classes/Controllers/MIDIMix.sc index bbc5719..2e4ddd3 100644 --- a/Classes/Controllers/MIDIMix.sc +++ b/Classes/Controllers/MIDIMix.sc @@ -1,135 +1,135 @@ -ControllerValue { - - /* - * A ControllerValue represents a MIDI Controller value. - * It has a minimum and maximum value, and a curve. This - * is used to convert from the MIDI value to a value that - * is considered usable by the user. - * - * The curve is similar to the one used by the Env object. - */ - - var <>min = 0; - var <>max = 1; - var <>curve = 0; - var <>currentValue; - var <>bipolar = false; - - *new { - arg min, max, curve; - ^super.new.init() - } - - init { - this.min = min; - this.max = max; - this.curve = curve; - this.currentValue = Bus.control; - this.bipolar = false; - } - - set { - arg value; - // If bipolar is true, then the value must go from -1 to 1 - var conversion = value.lincurve( - inMin: 0, - inMax: 127, - outMin: this.min.neg, - outMax: this.max, - curve: this.curve - ); - this.currentValue.set(conversion); - ^this.currentValue; - } - -} - - -MIDIControl { - - /* - * This is my personal MIDI controller interface. I am using a - * MIDIMix. It has 8 faders, 24 knobs, and 16 buttons. I am only - * using the knobs and faders. Two buttons are used to change "bank" - * (increments the CC number value). - */ - - var <>currentBank = 0; - var <>values; - - *new { - ^super.new.init() - } - - init { - this.values = IdentityDictionary.new(); - this.connect(); this.installCallbacks(); - } - - getInit { - arg number; - if (this.values[number] == nil) { - this.values[number] = ControllerValue.new( - min: 0, max: 127, curve: 0 - ); - ^this.values[number] - } { - ^this.values[number] - } - } - - setCurve { - arg number, curve; - this.getInit(number).curve = curve; - } - - setBounds { - arg number, min, max; - var controller = this.getInit(number); - controller.min = min; - controller.max = max; - } - - at { - arg number; - var control = this.getInit(number); - var choices = ( - value: this.getInit(number).currentValue.getSynchronous, - bus: this.getInit(number).currentValue, - map: this.getInit(number).currentValue.asMap, - kr: In.kr(this.getInit(number).currentValue), - ); - ^choices - } - - connect { - MIDIClient.init; - MIDIIn.connectAll(verbose: true); - } - - installCallbacks { - MIDIIn.addFuncTo(\control, { - arg src, chan, num, val; - ("CONTROL:" + (num + (this.currentBank * 24)) + "=>" + val).postln; - this.getInit(num + (this.currentBank * 24)).set(val); - }); - MIDIIn.addFuncTo(\noteOn, { - arg src, chan, num, val; - "Changing bank".postln; - if (chan == 8 && num == 22) { - if (this.currentBank > 0) { - this.currentBank = this.currentBank - 1; - }; - this.currentBank.postln; - }; - if (chan == 8 && num == 24) { - if (this.currentBank < 3) { - this.currentBank = this.currentBank + 1; - }; - this.currentBank.postln; - }; - }); - } -} - +// ControllerValue { +// +// /* +// * A ControllerValue represents a MIDI Controller value. +// * It has a minimum and maximum value, and a curve. This +// * is used to convert from the MIDI value to a value that +// * is considered usable by the user. +// * +// * The curve is similar to the one used by the Env object. +// */ +// +// var <>min = 0; +// var <>max = 1; +// var <>curve = 0; +// var <>currentValue; +// var <>bipolar = false; +// +// *new { +// arg min, max, curve; +// ^super.new.init() +// } +// +// init { +// this.min = min; +// this.max = max; +// this.curve = curve; +// this.currentValue = Bus.control; +// this.bipolar = false; +// } +// +// set { +// arg value; +// // If bipolar is true, then the value must go from -1 to 1 +// var conversion = value.lincurve( +// inMin: 0, +// inMax: 127, +// outMin: this.min.neg, +// outMax: this.max, +// curve: this.curve +// ); +// this.currentValue.set(conversion); +// ^this.currentValue; +// } +// +// } +// +// +// MIDIControl { +// +// /* +// * This is my personal MIDI controller interface. I am using a +// * MIDIMix. It has 8 faders, 24 knobs, and 16 buttons. I am only +// * using the knobs and faders. Two buttons are used to change "bank" +// * (increments the CC number value). +// */ +// +// var <>currentBank = 0; +// var <>values; +// +// *new { +// ^super.new.init() +// } +// +// init { +// this.values = IdentityDictionary.new(); +// this.connect(); this.installCallbacks(); +// } +// +// getInit { +// arg number; +// if (this.values[number] == nil) { +// this.values[number] = ControllerValue.new( +// min: 0, max: 127, curve: 0 +// ); +// ^this.values[number] +// } { +// ^this.values[number] +// } +// } +// +// setCurve { +// arg number, curve; +// this.getInit(number).curve = curve; +// } +// +// setBounds { +// arg number, min, max; +// var controller = this.getInit(number); +// controller.min = min; +// controller.max = max; +// } +// +// at { +// arg number; +// var control = this.getInit(number); +// var choices = ( +// value: this.getInit(number).currentValue.getSynchronous, +// bus: this.getInit(number).currentValue, +// map: this.getInit(number).currentValue.asMap, +// kr: In.kr(this.getInit(number).currentValue), +// ); +// ^choices +// } +// +// connect { +// MIDIClient.init; +// MIDIIn.connectAll(verbose: true); +// } +// +// installCallbacks { +// MIDIIn.addFuncTo(\control, { +// arg src, chan, num, val; +// ("CONTROL:" + (num + (this.currentBank * 24)) + "=>" + val).postln; +// this.getInit(num + (this.currentBank * 24)).set(val); +// }); +// MIDIIn.addFuncTo(\noteOn, { +// arg src, chan, num, val; +// "Changing bank".postln; +// if (chan == 8 && num == 22) { +// if (this.currentBank > 0) { +// this.currentBank = this.currentBank - 1; +// }; +// this.currentBank.postln; +// }; +// if (chan == 8 && num == 24) { +// if (this.currentBank < 3) { +// this.currentBank = this.currentBank + 1; +// }; +// this.currentBank.postln; +// }; +// }); +// } +// } +// diff --git a/Classes/Patterns/Pmod.sc b/Classes/Patterns/Pmod.sc new file mode 100644 index 0000000..185529d --- /dev/null +++ b/Classes/Patterns/Pmod.sc @@ -0,0 +1,502 @@ +Pmod : Pattern { + + classvar defHashLRU, synthName, <>patternPairs, channels, asValues=false; + + *new { + |synthName ... pairs| + ^super.newCopyArgs(synthName, pairs) + } + + *kr { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\control) + } + + *kr1 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\control).channels_(1) + } + + *kr2 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\control).channels_(2) + } + + *kr3 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\control).channels_(3) + } + + *kr4 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\control).channels_(4) + } + + *ar { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\audio) + } + + *ar1 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\audio).channels_(1) + } + + *ar2 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\audio).channels_(2) + } + + *ar3 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\audio).channels_(3) + } + + *ar4 { + |synthName ... pairs| + ^this.new(synthName, *pairs).rate_(\audio).channels_(4) + } + + *initClass { + defCache = (); + defNames = (); + defHashLRU = LinkedList(); + defNamesFree = IdentitySet(); + (1..16).do { + |n| + [\kr, \ar].do { + |rate| + this.wrapSynth( + rate: rate, + func: { \value.perform(rate, (0 ! n)) }, + channels: n, + defName: "Pmod_constant_%_%".format(n, rate).asSymbol, + ); + } + } + } + + // Wrap a func in fade envelope / provide XOut + *wrapSynth { + |rate, func, channels, defName| + var hash, def, args; + + defName = defName ?? { + hash = [func, rate].hash; + defHashLRU.remove(hash); + defHashLRU.addFirst(hash); + defNames[hash] ?? { + defNames[hash] = this.getDefName(); + defNames[hash] + }; + }; + + if (defCache[defName].isNil) { + + def = SynthDef(defName, { + var fadeTime, paramLag, fade, sig; + + fadeTime = \fadeTime.kr(0); + paramLag = \paramLag.ir(0); + + fade = Env([1, 1, 0], [0, fadeTime], releaseNode:1).kr(gate:\gate.kr(1), doneAction:2); + sig = SynthDef.wrap(func, paramLag ! func.def.argNames.size); + sig = sig.asArray.flatten; + + if (channels.isNil) { + channels = sig.size; + }; + + if (rate.isNil) { + rate = sig.rate.switch(\audio, \ar, \control, \kr); + }; + + \channels.ir(channels); // Unused, but helpful to see channelization for debugging + + sig = sig.collect { + |channel| + if ((channel.rate == \scalar) && (rate == \ar)) { + channel = DC.ar(channel); + }; + + if ((channel.rate == \audio) && (rate == \kr)) { + channel = A2K.kr(channel); + "Pmod output is \audio, \control rate expected".warn; + } { + if ((channel.rate == \control) && (rate == \ar)) { + channel = K2A.ar(channel); + "Pmod output is \control, \audio rate expected".warn; + } + }; + channel; + }; + + if (sig.shape != [channels]) { + sig.reshape(channels); + }; + + XOut.perform(rate, \out.kr(0), fade, sig); + }); + args = def.asSynthDesc.controlNames.flatten.asArray; + defCache[defName] = [rate, channels, def, args]; + } { + #rate, channels, def, args = defCache[defName]; + }; + + def.add; + + ^( + instrument: defName, + args: [\value, \fadeTime, \paramLag, \out] ++ args, + pr_rate: rate, + pr_channels: channels, + pr_instrumentHash: hash ?? { [func, rate].hash }, + hasGate: true + ) + } + + rate_{ + |r| + rate = ( + control: \kr, + audio: \ar, + kr: \kr, + ar: \kr + )[r] + } + + embedInStream { + |inEvent| + var server, synthStream, streamPairs, endVal, cleanup, + synthGroup, newSynthGroup, modGroup, newModGroup, + buses, currentArgs, currentBuses, currentSize, currentEvent, fadeTime, + nextEvent, nextSynth, streamAsValues, currentChannels, currentRate, cleanupFunc; + + // CAVEAT: Server comes from initial inEvent and cannot be changed later on. + server = inEvent[\server] ?? { Server.default }; + server = server.value; + + streamAsValues = asValues; + + // Setup pattern pairs + streamPairs = patternPairs.copy; + endVal = streamPairs.size - 1; + forBy (1, endVal, 2) { |i| streamPairs[i] = streamPairs[i].asStream }; + synthStream = synthName.asStream; + + // Prepare busses + buses = List(); + + // Cleanup + cleanupFunc = Thunk({ + currentEvent !? { + if (currentEvent[\isPlaying].asBoolean) { + currentEvent.release(currentEvent[\fadeTime]) + }; + + this.recycleDefName(currentEvent); + { + newModGroup !? _.free; + buses.do(_.free); + }.defer(currentEvent[\fadeTime] ? 10) + { + newSynthGroup !? _.free; + }.defer(5); + } + }); + cleanup = EventStreamCleanup(); + cleanup.addFunction(inEvent, cleanupFunc); + + loop { + // Prepare groups, reusing input group if possible. + // This is the group that the outer event - the one whose parameters + // we're modulating - is playing to. + // + // If newSynthGroup.notNil, then we allocated and we must clean up. + if (inEvent.keys.includes(\group)) { + synthGroup = inEvent.use({ ~group.value }); + } { + inEvent[\group] = synthGroup = newSynthGroup ?? { + newSynthGroup = Group(server.asTarget); + }; + }; + + // Prepare modGroup, which is our modulation group and lives before + // synthGroup. + // If newModGroup.notNil, then we allocated and we must clean up + if (inEvent.keys.includes(\modGroup)) { + modGroup = inEvent[\modGroup]; + } { + inEvent[\modGroup] = modGroup = newModGroup ?? { + newModGroup = Group(synthGroup.asTarget, \addBefore); + }; + }; + + // We must set group/addAction early, so they are passed to the .next() + // of child streams. + nextEvent = (); + nextEvent[\synthDesc] = nil; + nextEvent[\msgFunc] = nil; + nextEvent[\group] = modGroup; + nextEvent[\addAction] = \addToHead; + nextEvent[\resend] = false; + + // Get nexts + nextSynth = synthStream.next(nextEvent.copy); + nextSynth = this.prepareSynth(nextSynth); + nextEvent = this.prNext(streamPairs, nextEvent); + + if (inEvent.isNil || nextEvent.isNil || nextSynth.isNil) { + ^cleanup.exit(inEvent); + } { + cleanup.update(inEvent); + + nextEvent.putAll(nextSynth); + + // 1. We need argument names in order to use (\type, \set). + // 2. We need size to determine if we need to allocate more busses for e.g. + // an event like (freq: [100, 200]). + currentArgs = nextEvent[\instrument].asArray.collect(_.asSynthDesc).collect(_.controlNames).flatten.asSet.asArray; + currentSize = nextEvent.atAll(currentArgs).maxValue({ |v| v.isArray.if(v.size, 1) }).max(1); + + currentChannels = nextSynth[\pr_channels]; + currentRate = nextSynth[\pr_rate]; + + buses.first !? { + |bus| + var busRate = switch(bus.rate, \audio, \ar, \control, \kr, bus.rate); + if (busRate != currentRate) { + Error("Cannot use Synths of different rates in a single Pmod (% vs %)".format( + bus.rate, currentRate + )).throw; + } + }; + + (currentSize - buses.size).do { + if (currentRate == \ar) { + buses = buses.add(Bus.audio(server, currentChannels)) + } { + buses = buses.add(Bus.control(server, currentChannels)) + }; + }; + currentBuses = buses.collect(_.index).extend(currentSize); + if (currentBuses.size == 1) { currentBuses = currentBuses[0] }; + + // If we've got a different instrument than last time, send a new one, + // else just set the parameters of the existing. + if (nextEvent[\resend] + or: {nextEvent[\pr_instrumentHash] != currentEvent.tryPerform(\at, \pr_instrumentHash)}) + { + nextEvent[\parentType] = \note; + nextEvent[\type] = \note; + nextEvent[\sustain] = nil; + nextEvent[\sendGate] = false; + nextEvent[\fadeTime] = fadeTime = nextEvent[\fadeTime] ?? 0; + nextEvent[\out] = currentBuses; + nextEvent[\group] = modGroup; + nextEvent[\addAction] = \addToHead; // SUBTLE: new synths before old, so OLD synth is responsible for fade-out + + // Free existing synth + currentEvent !? { + |e| + // Assumption: If \hasGate -> false, then synth will free itself. + if (e[\isPlaying].asBoolean && e[\hasGate]) { + e[\sendGate] = true; + e.release(nextEvent[\fadeTime]); + e[\isPlaying] = false; + } + }; + } { + nextEvent[\parentType] = \set; + nextEvent[\type] = \set; + nextEvent[\id] = currentEvent[\id]; + nextEvent[\args] = currentEvent[\args]; + nextEvent[\out] = currentEvent[\out]; + }; + + nextEvent.parent ?? { nextEvent.parent = Event.parentEvents.default }; + + // SUBTLE: If our inEvent didn't have a group, we set its group here. + // We do this late so previous uses of inEvent aren't disrupted. + if (newSynthGroup.notNil) { + inEvent[\group] = newSynthGroup; + }; + + // Yield our buses via .asMap + inEvent = currentSize.collect({ + |i| + var group; + { + if (i == 0) { + cleanup.addFunction(currentEnvironment, cleanupFunc) + }; + // In this context, ~group refers to the event being modulated, + // not the Pmod event. + + ~group = ~group.value; + if (~group.notNil and: { ~group != synthGroup }) { + modGroup.moveBefore(~group.asGroup) + }; + + if (nextEvent[\isPlaying].asBoolean.not) { + currentEvent = nextEvent; + nextEvent[\isPlaying] = true; + nextEvent.playAndDelta(cleanup, false); + }; + + if (streamAsValues) { + buses[i].getSynchronous; + } { + buses[i].asMap; + } + } + }); + if (currentSize == 1) { + inEvent = inEvent[0].yield; + } { + inEvent = inEvent.yield; + } + }; + } + + ^cleanup.exit(inEvent); + } + + // This roughly follows the logic of Pbind + prNext { + |streamPairs, inEvent| + var event, endVal; + + event = this.prScrubEvent(inEvent); + endVal = streamPairs.size - 1; + + forBy (0, endVal, 2) { arg i; + var name = streamPairs[i]; + var stream = streamPairs[i+1]; + var streamout = stream.next(event); + if (streamout.isNil) { ^inEvent }; + + if (name.isSequenceableCollection) { + if (name.size > streamout.size) { + ("the pattern is not providing enough values to assign to the key set:" + name).warn; + ^inEvent + }; + name.do { arg key, i; + event.put(key, streamout[i]); + }; + }{ + event.put(name, streamout); + }; + }; + + ^event; + } + + recycleDefName { + |event| + var hash, name; + if (defHashLRU.size > maxDefNames) { + hash = defHashLRU.pop(); + name = defNames[hash]; + defNames[hash] = nil; + defCache[name] = nil; + defNamesFree.add(name); + } + } + + *getDefName { + if (defNamesFree.notEmpty) { + ^defNamesFree.pop() + } { + defCount = defCount + 1; + ^"Pmod_unique_%".format(defCount).asSymbol; + } + } + + // Scrub parent event of Pmod-specific values like group - these will disrupt + // the way we set up our groups and heirarchy. + prScrubEvent { + |event| + event[\modGroup] = nil; + ^event; + } + + // Convert an item from our instrument stream into a SynthDef name. + // This can possible add a new SynthDef if supplied with e.g. a function. + prepareSynth { + |synthVal| + var synthDesc, synthOutput; + ^case + { synthVal.isKindOf(Array) } { + synthVal.collect(this.prepareSynth(_)).reduce({ + |a, b| + a.merge(b, { + |a, b| + a.asArray.add(b) + }) + }) + } + { synthVal.isKindOf(SimpleNumber) } { + var constRate = rate ?? { \ar }; // default to \ar, because this works for both ar and kr mappings; + var constChannels = channels ?? { 1 }; + + this.class.wrapSynth( + channels: constChannels, rate: constRate, + defName: "Pmod_constant_%_%".format(constChannels, constRate).asSymbol + ).putAll(( + value: synthVal + )) + } + { synthVal.isKindOf(Symbol) } { + synthDesc = synthVal.asSynthDesc; + synthOutput = synthDesc.outputs.detect({ |o| o.startingChannel == \out }); + + if (synthOutput.isNil) { + Error("Synth '%' needs at least one output, connected to an \out synth parameter".format(synthVal)).throw; + }; + + ( + instrument: synthVal, + args: synthDesc.controlNames.flatten.asSet.asArray, + pr_instrumentHash: synthVal.identityHash, + pr_rate: synthOutput.rate.switch(\audio, \ar, \control, \kr), + pr_channels: synthOutput.numberOfChannels + ) + } + { synthVal.isKindOf(AbstractFunction) } { + this.class.wrapSynth(rate, synthVal, channels) + } + { synthVal.isNil } { + nil + } + { + synthVal.putAll(this.prepareSynth(synthVal[\instrument])); + } + } + + asValues { + asValues = true; + } + + expand { + ^( + Pfunc({ + |in| + var thunk; + + if (in.isArray) { in = in[0] }; + thunk = Thunk({ + in.value + }); + + this.channels.collect { + |i| + { + thunk.value.asArray[i] + } + } + }) <> this + ) + } +}