[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[sc-dev] SF.net SVN: quarks:[2672] JITLibExtensions



Revision: 2672
          http://sourceforge.net/p/quarks/code/2672
Author:   decampo
Date:     2013-12-26 23:54:10 +0000 (Thu, 26 Dec 2013)
Log Message:
-----------
NPVoicer and ProxyChain: cleanup help files, remove cruft

Modified Paths:
--------------
    JITLibExtensions/HelpSource/Classes/NPVoicer.schelp
    JITLibExtensions/classes/ProxyChain.sc
    JITLibExtensions/classes/ProxyChainGui.sc

Added Paths:
-----------
    JITLibExtensions/HelpSource/Classes/ProxyChain.schelp
    JITLibExtensions/HelpSource/Classes/ProxyChainGui.schelp

Modified: JITLibExtensions/HelpSource/Classes/NPVoicer.schelp
===================================================================
--- JITLibExtensions/HelpSource/Classes/NPVoicer.schelp	2013-12-26 22:23:23 UTC (rev 2671)
+++ JITLibExtensions/HelpSource/Classes/NPVoicer.schelp	2013-12-26 23:54:10 UTC (rev 2672)
@@ -217,7 +217,6 @@
 g.unmap(\pan); // autopan should stop ... not working yet! (why?)
 
 
-
 // for comparison, test with a plain Ndef:
 
 Ndef(\a, { |amp=0.1, pan| Pan2.ar(PinkNoise.ar(amp), pan) });
@@ -230,4 +229,11 @@
 Ndef(\a).unset(\pan); // unset to default
 
 Ndef(\a).set(\pan, 1); // this does unset properly
+
+
+
+// do post-processing on the proxy: 
+g.proxy.filter(150, { |in| (in * 10).distort * 0.2 });
+g.proxy.put(150);
+
 ::

Added: JITLibExtensions/HelpSource/Classes/ProxyChain.schelp
===================================================================
--- JITLibExtensions/HelpSource/Classes/ProxyChain.schelp	                        (rev 0)
+++ JITLibExtensions/HelpSource/Classes/ProxyChain.schelp	2013-12-26 23:54:10 UTC (rev 2672)
@@ -0,0 +1,231 @@
+TITLE:: ProxyChain
+summary:: play multiple synth and filter functions flexibly in one proxy
+categories:: Libraries>JITLib>JITLibExtensions
+related:: Classes/NodeProxy, Classes/Ndef, Classes/ProxyChainGui, Classes/MasterFX, Guides/JITLib
+
+DESCRIPTION::
+ProxyChain keeps a global repertoire of sound functions by name. 
+A ProxyChain has an ordered collection of sound functions and uses a nodeproxy 
+to add or remove the sound functions to/from the signal chain individually, by name.
+
+Like Ndef, Pdef, Tdef, ProxyChain keeps all named instances in a class variable "all".
+ProxyChain(<name>) accesses a ProxyChain by name, 
+ProxyChain(<name>, slotNames, ... ) puts a new instance there. 
+
+
+CLASSMETHODS::
+
+METHOD:: allSources
+a dict of all available synth and filter functions
+
+
+METHOD:: all
+a dict of all ProxyChain instances.
+
+METHOD:: add
+put functions into global dict, by name, func, name, func ...
+
+code::
+(
+q = q ? ();
+
+q.numChans = 5; 
+
+		// add a sound source
+ProxyChain.add(
+	\dust, \mix -> { |dens=20, dustdec=0.02, dustfreq= 600| 
+		Ringz.ar(Dust.ar(dens).lag(0.0001), dustfreq, dustdec) 
+	}
+);
+
+		// an association with \filter becomes a filter,
+		// and creates a wet/dry balance on the output.
+		// several funcs can be added as key1, func1, key2, type -> func2, etc.
+ProxyChain.add(	
+	\ringmod, \filter -> { |in, randrate=5| 
+		in.asArray[0] 	// force mono inputs
+		* SinOsc.ar(LFNoise0.kr([randrate, randrate]).exprange(300, 3000)).sum 
+	}, 
+	\dist, \filter -> { |in, drive=10, amp=0.2| (in.asArray[0] * drive).clip2(0.5) * amp }
+);
+	
+		// an association with \filterIn also becomes a filter,
+		// but creates the wet/dry balance control on the filter input, 
+		// on on the output like \filter. this can be useful for delays, reverbs etc.
+ProxyChain.add(
+	\riseComb5, \filterIn -> { arg in, delay = 0.023, dlDrift = 0.02, spread=0.5, 
+		decayRise=0.5, decayFall=100;
+		
+		var delayscales = 2 ** ((0 .. q.numChans - 1) * 2 / (q.numChans - 1) - 1 * spread); 
+		
+		var dels = delayscales.scramble.collect { |dscale| 
+		
+			var timedrift = LFDNoise3.kr(0.3, dlDrift, 1) * dscale;
+			var ampcomp = (20 * decayRise).dbamp * (decayFall ** -0.25);
+			
+			var combs; 
+			in = in.asArray[0] * ampcomp.lag(0.2);
+			combs = (decayFall * [ 1, decayRise]).collect { |decay| 
+				CombL.ar(in, 1, delay * dscale, decay * delay) 
+			};
+			combs[0] - combs[1];	// combs come in slowly, like formlet. 
+			
+		};
+		Splay.ar(dels)
+	}, 
+	\ampFin, \filter -> { |in, drive=1, ampLimit=1, lAmp=1| 
+		Limiter.ar(in * drive, ampLimit) * lAmp;
+	}
+);
+
+	// add specs for the controls used (for NodeProxyEditor).
+Spec.add(\dens, [0.1, 1000, \exp]);
+Spec.add(\dustamp, [0, 1, \amp]);
+Spec.add(\dustdec, [0.0001, 0.1, \exp]); 
+Spec.add(\dustfreq, \freq); 
+
+Spec.add(\dt, [0.001, 0.2, \exp]); 
+Spec.add(\dc, [0.01, 100, \exp]); 
+
+Spec.add(\drive, [1, 100, \exp]); 
+
+Spec.add(\spread, [0, 1, \amp]); 
+Spec.add(\decayRise, [0, 0.9, \amp]); 
+Spec.add(\decayFall, [1, 1000, \exp]); 
+Spec.add(\dlDrift, [0, 0.1, \amp]); 
+
+s.boot;
+)
+::
+
+METHOD:: new
+look up an existing ProxyChain, or (if slotNames are given), 
+make a new ProxyChain which can use named functions in the order in slotNames. 
+
+code::
+	// the functions can be sources (func, \mix -> func) 
+	// or filters (\filter -> func, \filterIn -> func)
+(
+c = ProxyChain(\alpha, [\dust, \ringmod, \dist, \riseComb5, \test]);
+c.play;	// play the proxy
+g = c.gui(12);	// make a gui for it with 12 slots - see ProxyChainGui
+)
+c.add(\dust);
+::
+
+ARGUMENT:: key
+name (creates a NodeProxy of the same name)
+
+ARGUMENT:: slotNames
+the names of the functions to have available.
+
+ARGUMENT:: numChannels
+number of audio channels. default = 2.
+
+ARGUMENT:: server
+server to use. default = s.
+
+
+METHOD:: from
+same as new, but using an existing nodeproxy.
+
+code::
+Ndef(\bla).ar(2);
+ProxyChain.from(Ndef(\bla), [\dust, \ringmod, \dist, \riseComb5, \test]);
+ProxyChain(\bla).play;
+ProxyChain(\bla).add(\dust);
+
+ProxyChain.all;
+::
+
+INSTANCEMETHODS::
+
+METHOD:: proxy
+the proxy inside the chain
+
+strong::methods that get forwarded to proxy: ::
+
+METHOD:: play
+METHOD:: playN
+METHOD:: stop
+METHOD:: end
+
+METHOD:: add
+kick in a source by name.
+
+code::
+c.add(\dust, 0.123);
+c.add(\dust, 0.2);
+c.add(\ringmod, 0.5);
+c.add(\dist, 1);
+::
+
+ARGUMENT:: key
+which function to kick in
+
+ARGUMENT:: wet
+wet/dry mix ratio
+
+ARGUMENT:: func
+an optional func that can locally replace the global func with that name.
+
+METHOD:: remove
+remove a currently playing source by name.
+code::
+c.remove(\dist);	
+c.remove(\ringmod);
+c.remove(\riseComb5);	
+::
+
+METHOD:: sources
+a dict of local source funcs. 
+
+METHOD:: slotsInUse
+currently playing slots.
+
+METHOD:: setSlots
+set multiple slots at once.
+
+METHOD:: slotNames
+get slotNames, change to new slotNames.
+
+METHOD:: gui
+make a ProxyChainGui for the ProxyChain - see examples.
+
+code::
+	// by default, buttonList nil is replaced with control buttons for all slots.
+c.gui(20);
+
+	// if specified, can be friendlier
+(
+g = c.gui(20,
+[ 	
+	[ \generators, \label ],  	// a label only
+	[ \dust, \slotCtl, 0.25 ], 		// a control for a slot, starting level
+
+	[ '1 > 1', \label ],  
+	[ \ringmod, \slotCtl ], 		// 0 - dry  by default
+	[ \dist, \slotCtl, 1 ], 		// 1 - all wet
+
+	[ '1 > 5', \label ],  
+	[ \riseComb5, \slotCtl ], 
+	[ ],
+		// extras:
+		// e.g. an editor with more space for controls
+	[\phatEdit, \extra, { c.makeEdit('Test', 40) } ],
+	
+		// or maybe bigger buttons play, end buttons
+	[\play, \extra, { c.playN } ],	 
+	[\end, \extra, { c.end(2, true) } ],
+	
+]
+)
+)
+::
+
+METHOD:: informEditor
+internal method to prepare replaceKeys for the gui.
+
+METHOD:: addSlot
+internal method for adding a func. 
+

Added: JITLibExtensions/HelpSource/Classes/ProxyChainGui.schelp
===================================================================
--- JITLibExtensions/HelpSource/Classes/ProxyChainGui.schelp	                        (rev 0)
+++ JITLibExtensions/HelpSource/Classes/ProxyChainGui.schelp	2013-12-26 23:54:10 UTC (rev 2672)
@@ -0,0 +1,113 @@
+TITLE:: ProxyChainGui
+summary:: a gui for ProxyChain
+categories:: Libraries>JITLib
+related:: Classes/ProxyChain, Classes/Ndef, Classes/MasterFX
+
+DESCRIPTION::
+A Gui class for ProxyChain. For more example uses, see link::ProxyChain::.
+
+code::
+		// prepare for making a proxy chain
+(
+q = ();
+q.numChans = 2;
+
+ProxyChain.add(
+	\dust, \mix -> { |dens=20, dustdec=0.02, dustfreq= 600| 
+		Ringz.ar(Dust.ar(dens).lag(0.0001), dustfreq, dustdec) 
+	}
+);
+
+ProxyChain.add(	
+	\ringmod, \filter -> { |in, randrate=5| 
+		in.asArray[0] 	// force mono inputs
+		* SinOsc.ar(LFNoise0.kr([randrate, randrate]).exprange(300, 3000)).sum 
+	}, 
+	\dist, \filter -> { |in, drive=10, amp=0.2| (in.asArray[0] * drive).clip2(0.5) * amp }
+);
+	// add specs for the controls used (for NodeProxyEditor).
+Spec.add(\dens, [0.1, 1000, \exp]);
+Spec.add(\dustamp, [0, 1, \amp]);
+Spec.add(\dustdec, [0.0001, 0.1, \exp]); 
+Spec.add(\dustfreq, \freq); 
+
+c = ProxyChain(\test, [\dust, \ringmod, \dist, \riseComb5 ]);
+
+s.boot;
+)
+::
+
+CLASSMETHODS::
+
+METHOD:: new
+make a new ProxyCHainGui.
+
+code::
+g = ProxyChainGui.new(c, 12);
+c.key.postcs;
+c.proxy.key;
+
+	// define buttons more specifically
+(
+g = c.gui(20, 
+[ 	
+	[ \generators, \label ],  	// a label only
+	[ \dust, \slotCtl, 0.25 ], 		// a control for a slot, and initial volume
+
+	[ 'mono FX', \label ],  
+	[ \ringmod, \slotCtl, 0.5 ], 	// initial mix level
+	[ \dist, \slotCtl ], 
+
+	[ 'multichan', \label ],  		
+	[ \riseComb5, \slotCtl ], 		// off (0) by default.
+	[],
+		// extras:
+		// e.g. an editor with more space for controls
+	[\phatEdit, \extra, { c.gui(40) } ],
+	
+		// or maybe bigger buttons play, end buttons
+	[\play, \extra, { c.playN } ],	 
+	[\end, \extra, { c.end } ],
+	
+]
+)
+)
+::
+
+ARGUMENT:: chain
+the proxychain to show 
+
+ARGUMENT:: numItems
+the number of param sliders to prepare.
+
+ARGUMENT:: parent
+the parent to put it into
+
+ARGUMENT:: bounds
+bounds within which to display it. 
+
+INSTANCEMETHODS::
+
+METHOD:: chain
+the proxychain 
+
+METHOD:: buttonSpecs
+the specs for the buttons to make
+
+METHOD:: butZone
+the CompositeView where the buttons live
+METHOD:: editGui
+the NdefGui built into the view
+
+METHOD:: buttons, guiFuncs, namedButtons
+
+strong:: usual JITGui methods ::
+
+METHOD:: setDefaults
+METHOD:: accepts
+METHOD:: getState
+METHOD:: checkUpdate
+METHOD:: makeViews
+METHOD:: makeEditGui
+
+

Modified: JITLibExtensions/classes/ProxyChain.sc
===================================================================
--- JITLibExtensions/classes/ProxyChain.sc	2013-12-26 22:23:23 UTC (rev 2671)
+++ JITLibExtensions/classes/ProxyChain.sc	2013-12-26 23:54:10 UTC (rev 2672)
@@ -1,8 +1,42 @@
+/********
+///////////////// Possible next extensions ////////////////
+
+*	insert new slotNames by name, or remove existing slotnames, keeping the structure consistent;
+	for reconfiguration of the list of proxychain slots that can be used.
+	That would require a better gui where the buttons can be updated.
+
+	////////////// not done yet //////////
+
+	// replace a slot given by name
+c.replace(\dust, \noyz, mix ->  { |nfreq1=1200| LFDNoise0.ar(nfreq1) });
+
+
+	insertAt(index, name, funcOrAssoc)
+		inserts in the chain at this index,
+		replaces if a slot exists there.
+
+c.insertAt(5, \noyz, mix ->  { |nfreq2=1200| GrayNoise.ar(nfreq2) });
+
+	insertAfter(index, name, funcOrAssoc)
+	insertBefore(index, name, funcOrAssoc)
+		inserts after (or before) a given slot - halfway toward the neighbour.
+		e.g.
+c.insertAfter(\dust, \klong, \filter -> { |in, freq=400, att=0.01, decay=0.3, slope=0.8|
+	Formlet.ar(in, freq * [0.71, 1, 1.4], att, decay * [1/slope, 1, slope]).sum;
+});
+
+	// after which slot, name, funcOrAssoc;
+c.insertBefore(\dust, \klong, \filter -> { |in, freq=400, att=0.01, decay=0.3, slope=0.8|
+	Formlet.ar(in, freq * [0.71, 1, 1.4], att, decay * [1/slope, 1, slope]).sum;
+});
+******/
+
+
 ProxyChain {
 
-	classvar <allSources; 
+	classvar <allSources;
 	classvar <all;
-	
+
 	var <slotNames, <slotsInUse, <proxy, <sources;
 
 	*initClass {
@@ -17,10 +51,10 @@
 	*from { arg proxy, slotNames = #[];
 		^super.new.init(proxy, slotNames)
 	}
-	
+
 	key { ^all.findKeyForValue(this) }
-	
-	*new { arg key, slotNames, numChannels, server; 
+
+	*new { arg key, slotNames, numChannels, server;
 		var proxy;
 		var res = all.at(key);
 		if(res.isNil) {
@@ -28,12 +62,12 @@
 			res = this.from(proxy, slotNames);
 			if (key.notNil) { all.put(key, res) };
 		};
-		
+
 		if(slotNames.notNil) { res.slotNames_(slotNames) }
 
 		^res
 	}
-		
+
 	init { |argProxy, argSlotNames|
 
 		slotNames = Order.new;
@@ -43,11 +77,11 @@
 
 		proxy = argProxy;
 		if (proxy.key.notNil) { all.put(proxy.key, this) };
-		
+
 		this.slotNames_(argSlotNames);
 	}
-	
-	slotNames_ { |argSlotNames| 
+
+	slotNames_ { |argSlotNames|
 		slotNames.clear;
 		argSlotNames.do { |name, i| slotNames.put(i + 1 * 10, name) };
 	}
@@ -121,16 +155,17 @@
 	gui { |numItems = 16, buttonList, parent, bounds, isMaster = false|
 		^ProxyChainGui(this, numItems, parent, bounds, true, buttonList, isMaster);
 	}
-		// this is probably not needed anymore
-		// old NodeProxyEditor 
-	informEditor { |ed|
-		slotNames.do { |name, i| ed.replaceKeys.put(("wet" ++ i).asSymbol, name) };
-		slotNames.do { |name, i| ed.replaceKeys.put(("mix" ++ i).asSymbol, name) };
-	}
-		
-	makeEdit { |name, nSliders=24, parent, bounds|
-		var ed = NdefGui(proxy, nSliders, parent, bounds);
-	//	this.informEditor(ed);
-		^ed
-	}
+
+	// // this is probably not needed anymore
+	// // old NodeProxyEditor
+	// informEditor { |ed|
+	// 	slotNames.do { |name, i| ed.replaceKeys.put(("wet" ++ i).asSymbol, name) };
+	// 	slotNames.do { |name, i| ed.replaceKeys.put(("mix" ++ i).asSymbol, name) };
+	// }
+	//
+	// makeEdit { |name, nSliders=24, parent, bounds|
+	// 	var ed = NdefGui(proxy, nSliders, parent, bounds);
+	// 	//	this.informEditor(ed);
+	// 	^ed
+	// }
 }

Modified: JITLibExtensions/classes/ProxyChainGui.sc
===================================================================
--- JITLibExtensions/classes/ProxyChainGui.sc	2013-12-26 22:23:23 UTC (rev 2671)
+++ JITLibExtensions/classes/ProxyChainGui.sc	2013-12-26 23:54:10 UTC (rev 2672)
@@ -1,37 +1,37 @@
 ProxyChainGui : JITGui {
-	var <guiFuncs;
+	var <guiFuncs; // move to classvar!
 	var <butZone, <buttonSpecs, <buttons, <namedButtons, <editGui;
 
 	*new { |chain, numItems = 16, parent, bounds, makeSkip = true, options|
-		
+
 		options = options ?? { if (chain.notNil) { chain.slotNames.asArray } };
 		^super.new(nil, numItems, parent, bounds, makeSkip, options)
 			.chain_(chain)
 	}
-	
+
 	accepts { |obj| ^(obj.isNil or: { obj.isKindOf(ProxyChain) }) }
-		
+
 	chain_ { |chain| ^this.object_(chain) }
 	chain { ^object }
-		
-	setDefaults { |options| 
-		if (parent.isNil) { 
+
+	setDefaults { |options|
+		if (parent.isNil) {
 			defPos = 610@260
-		} { 
+		} {
 			defPos = skin.margin;
 		};
 		minSize = 510 @ (numItems * skin.buttonHeight + (skin.headHeight * 2));
 	//	"minSize: %\n".postf(minSize);
 	}
-	
+
 	makeViews { |options|
-		
+
 		namedButtons = ();
-		
+
 		// "PCGui:makeViews: options are %\n\n".postf(options);
-		
+
 		options = options ?? { if(object.notNil) { object.slotNames.asArray } };
-			
+
 		guiFuncs =  (
 			btlabel: { |but, name| but.states_([[name, Color.black, Color(1, 0.5, 0)]]) },
 			label: { |but, name| but.states_([[name, Color.white, Color(1, 0.5, 0)]]) },
@@ -51,29 +51,29 @@
 		butZone = CompositeView(zone, Rect(0,0, 110, bounds.height - (skin.margin.y * 2)));
 		butZone.addFlowLayout;
 		buttons = numItems.collect { Button.new(butZone, Rect(0,0, 100, skin.buttonHeight)).states_([["-"]]); };
-		
+
 		this.buttons_(options.asArray);
 
 		this.makeEditGui;
 	}
-	
+
 	makeEditGui { editGui = NdefGui(nil, numItems, zone); }
 
-	buttons_ { |specs| 
-		
+	buttons_ { |specs|
+
 		var objSlotNames = if (object.notNil) { object.slotNames.asArray } { [] };
-		
+
 		specs = (specs ? []);
-		if (specs.size > buttons.size) { 
+		if (specs.size > buttons.size) {
 			"ProxyChainGui: out of buttons... fix later".postln;
 		};
-						
+
 		buttons.do { |but, i|
 			var name, kind, func, setup;
 			var list = specs[i];
 			but.visible_(list.notNil);
-		
-			if (list.notNil) { 
+
+			if (list.notNil) {
 				#name, kind, func, setup = list.asArray;
 				kind = kind ? \slotCtl;
 				if (name.notNil) {
@@ -83,64 +83,64 @@
 				but.enabled_(name.notNil);
 			}
 		};
-		
+
 		buttonSpecs = specs;
 	}
-	
-	getState { 
+
+	getState {
 		var state = (object: object, slotsInUse: [], slotNames: []);
-		if (object.notNil) { 
+		if (object.notNil) {
 			state
 				.put(\slotsInUse, object.slotsInUse.asArray)
 				.put(\slotNames, object.slotNames.asArray)
 		};
 		^state
 	}
-	
-	checkUpdate { 
+
+	checkUpdate {
 		var newState = this.getState;
-		
-		if (newState[\object].isNil) { 
+
+		if (newState[\object].isNil) {
 			this.name_('none');
 			editGui.object_(object);
-			butZone.enabled_(false); 
-			
+			butZone.enabled_(false);
+
 			prevState = newState;
 			^this
 		};
-		
+
 		if (newState == prevState) { ^this };
-		
-		if (newState[\object] != prevState[\object]) { 
+
+		if (newState[\object] != prevState[\object]) {
 			this.name_(object.key);
 			butZone.enabled_(true);
 			editGui.object_(object.proxy);
 			editGui.name_(object.key);
-		} { 
+		} {
 			editGui.checkUpdate;
 		};
-		
-		if (newState[\slotNames] != prevState[\slotNames]) { 
-		//	"new slotnames: ".post; newState[\slotNames].postcs; 
-			
-			namedButtons = (); 
-			buttons.do { |but| 
+
+		if (newState[\slotNames] != prevState[\slotNames]) {
+		//	"new slotnames: ".post; newState[\slotNames].postcs;
+
+			namedButtons = ();
+			buttons.do { |but|
 				var butname = but.states[0][0].asString.drop(2).drop(-2).asSymbol;
 			//	[\butname, butname].postcs;
-				if (newState[\slotNames].includes(butname)) { 
+				if (newState[\slotNames].includes(butname)) {
 					namedButtons.put(butname, but);
 				};
 			};
-			
+
 			object.slotNames.do { |name, i|
-				editGui.addReplaceKey(("wet" ++ i).asSymbol, name, \amp.asSpec); 
-				editGui.addReplaceKey(("mix" ++ i).asSymbol, name, \amp.asSpec); 
+				editGui.addReplaceKey(("wet" ++ i).asSymbol, name, \amp.asSpec);
+				editGui.addReplaceKey(("mix" ++ i).asSymbol, name, \amp.asSpec);
 			};
 		};
-				
-		if (newState[\slotsInUse] != prevState[\slotsInUse]) { 
+
+		if (newState[\slotsInUse] != prevState[\slotsInUse]) {
 			namedButtons.keysValuesDo { |name, but|
-				but.value_(newState[\slotsInUse].includes(name).binaryValue); 
+				but.value_(newState[\slotsInUse].includes(name).binaryValue);
 			}
 		};
 
@@ -148,15 +148,15 @@
 	}
 }
 
-MasterFXGui : ProxyChainGui { 
-	
-	name_ { |name| 
+MasterFXGui : ProxyChainGui {
+
+	name_ { |name|
 		if (hasWindow) { parent.name_(name.asString) };
 	}
-		
-	makeEditGui { 
+
+	makeEditGui {
 		var editGuiOptions = [ 'CLR', 'reset', 'doc', 'fade', 'wake', 'end', 'pausR', 'sendR' ];
-		editGui = NdefGui(nil, numItems, zone, bounds: 400@0, options: editGuiOptions); 
+		editGui = NdefGui(nil, numItems, zone, bounds: 400@0, options: editGuiOptions);
 	}
 }
 

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.


_______________________________________________
sc-dev mailing list

info (subscription, etc.): http://www.beast.bham.ac.uk/research/sc_mailing_lists.shtml
archive: https://listarc.bham.ac.uk/marchives/sc-dev/
search: https://listarc.bham.ac.uk/lists/sc-dev/search/