// Ringz tuner - h. james harkins, http://www.dewdrop-world.net
// Makes analog-style drum sounds with a bank of tunable Ringz filters
// code is released under the LGPL, http://creativecommons.org/licenses/LGPL/2.1/

s.boot;

// double-click inside the parenthesis, then hit enter to evaluate

(
// preparation
m = MixerChannel(\perc, s, 1, 2, postSendReady:true, outbus:s.options.numOutputBusChannels);
n = MixerChannel(\ringz, s, 1, 2);
m.addAsDependantOf(n);

MixingBoard("drum tuner", nil, m, n);

SynthDef(\ringz, { |ffreq = 2500, decay = 0.01, in, out, amp = 0|
	Out.ar(out, Ringz.ar(In.ar(in, 1), ffreq, decay, amp))
}).send(s);

~nodes = Array.new(10);
~ffreqs = 20 ! 10;
~rqs = 1 ! 10;
~decays = ~dt.(~ffreqs, ~rqs);
~log001 = log(0.001);	// constant, will be used repeatedly

// func to calc decay time from freq and rq
~sr = s.sampleRate;
~dt = { |ffreq, rq|
	(~log001 / log(1 - (pi/~sr * ffreq * rq))) / ~sr;
};
~freqspec = \freq.asSpec;
~rqspec = [1, 0.001, \exp].asSpec;
~ampspec = \amp.asSpec;

// gui building
f = FlowView.new(nil, Rect(150, 5, 800, 500));
~leftview = FlowView(f, 470@490);
~rightview = FlowView(f, 280@490);

// left-hand side: filters
SCStaticText(~leftview, 150@20).string_("frequencies");
SCStaticText(~leftview, 150@20).string_("resonances");
SCStaticText(~leftview, 150@20).string_("amplitudes");
~ffreqview = SCMultiSliderView(~leftview, 150@200)
	.size_(10)
	.thumbSize_(14)
	.action_({ |v|
		~ffreqs = ~freqspec.map(v.value);
		~decays = ~dt.(~ffreqs, ~rqs);
		~nodes.do({ |n, i|
			n.set(\ffreq, ~ffreqs[i], \decay, ~decays[i])
		})
	});
~rqview = SCMultiSliderView(~leftview, 150@200)
	.size_(10)
	.thumbSize_(14)
	.action_({ |v|
		~rqs = ~rqspec.map(v.value);
		~decays = ~dt.(~ffreqs, ~rqs);
		~nodes.do({ |n, i|
			n.set(\decay, ~decays[i]);
		})
	});
~ampview = SCMultiSliderView(~leftview, 150@200)
	.size_(10)
	.thumbSize_(14)
	.action_({ |v| ~nodes.do({ |n, i| n.set(\amp, \amp.asSpec.map(v.value[i])) }) });

~rateEdit = NumberEditor(2, [0.5, 10]).action_({ |val| ~trigsynth.set(\trigrate, val) });

~leftview.startRow;
SCStaticText(~leftview, 100@20).string_("trigrate").align_(\right);
~rateEdit.gui(~leftview);

// ActionButtons -- add filter, post parameters, stop, save, load
~post = {
	(~nodes.size > 0).if({
		postf("\nKlank array:\n`[ %,\n   %,\n   % ]\n\n",
			~ffreqs[..~nodes.size-1],
			\amp.asSpec.map(~ampview.value[..~nodes.size-1]),
			~decays[..~nodes.size-1]);
		[~ffreqs, ~decays, \amp.asSpec.map(~ampview.value)]
			.flop[..~nodes.size-1].do({ |item|
				postf("[\\ffreq, %, \\decay, %, \\amp, %]\n", *item);
		});
	});
};

~leftview.startRow;
ActionButton(~leftview, "new resonz", { ~nodes.add(n.play(\ringz, [\in, m.inbus.index])) });
ActionButton(~leftview, "post parameters", ~post);
ActionButton(~leftview, "stop", {
	~post.value;
	~src.free;
	~nodes.do(_.free);
	~trigbus.free;
	"\n\nClose the MixingBoard window to remove the remaining synths.".postln;
});

~leftview.startRow;
ActionButton(~leftview, "save", {
	CocoaDialog.savePanel({ |path|
		[~ffreqview.value, ~rqview.value, ~ampview.value,
			~funcedit.string, ~envedit.string, ~nodes.size]
		.writeTextArchive(path)
	});
});

ActionButton(~leftview, "load", {
	var	values;
	CocoaDialog.getPaths({ |path|
		values = Object.readTextArchive(path[0]);
		(values.size != 6).if({
			Error("File does not contain a ringz spec.").throw;
		}, {
			~funcedit.setString(values[3], 0, ~funcedit.string.size);
			~envedit.setString(values[4], 0, ~envedit.string.size);
			~buildSynthDef.value;
			~ffreqview.value = values[0];
			~rqview.value = values[1];
			~ampview.value = values[2];
			~nodes.do(_.free);
			~nodes = { |i|
				n.play(\ringz, [\in, m.inbus.index]);
			} ! values[5];	// make the nodes
			{	~ffreqview.doAction;
				~rqview.doAction;
				~ampview.doAction;
			}.defer(0.2);
		});
	}, nil, 1)
});

// dynamic synthdef construction based on user input
SCStaticText(~rightview, 100@20).string_("Noise function:");
ActionButton(~rightview, "evaluate", {
	~buildSynthDef.value;
});
~rightview.startRow;
~funcedit = SCTextView.new(~rightview, 275@200)
	.string_("PinkNoise.ar");

~rightview.startRow;
SCStaticText(~rightview, 100@20).string_("Envelope:");
ActionButton(~rightview, "evaluate", {
	~src.setn(\env, ~envedit.string.interpret.asArray);
});
~rightview.startRow;
~envedit = SCTextView.new(~rightview, 275@200)
	.string_("Env.perc(0.01, 0.2)");

~trigbus = GenericGlobalControl(\t);
{ ~trigsynth = ~trigbus.play({ |trigrate| Impulse.kr(trigrate) }, [\trigrate, ~rateEdit.value.debug("rate")], m.synthgroup); }.defer(0.2);

~buildSynthDef = {
	var	func, env;
	~currentDef.notNil.if({
		s.sendMsg(\d_free, ~currentDef.name);
	});
	func = ("{ " ++ ~funcedit.string ++ " }").interpret;
	env = ~envedit.string.interpret;
	~currentDef = SynthDef(\source, { |trigbus, outbus|
		var	trig, sig, envCtl;
		trig = In.kr(trigbus, 1);
		Out.kr(trigbus, trig);
		envCtl = Control.names(\env).kr(0 ! 100);	// allow 25 env nodes maximum
		sig = SynthDef.wrap(func, nil, [trig]) * EnvGen.ar(envCtl, trig);
		Out.ar(outbus, sig)
	}).send(s);
	{	~src.free;
		~src = m.play(\source, [\trigbus, ~trigbus.bus.index]);
		~src.setn(\env, env.asArray);
	}.defer(0.1);
};

{ ~buildSynthDef.value; }.defer(0.5);
)