/* class 8 */
/* GUI interface */
/* Celia says - "Is there a way to control this thing with a button?" */

(
var w, num, node, twodslider, freq, amp, freqspec, testsin;
freq = 440;
amp = 0;
// a test synthdef to control from the GUI through args.
testsin = CtkNoteObject(
SynthDef(\testsin, {arg gate = 1, freq = 440, amp = 0;
Out.ar(0,
SinOsc.ar(freq, 0, amp) *
EnvGen.kr(
Env([0, 1, 0], [0.1, 0.1], \sin, 1),
gate, doneAction: 2)
)
})
);
// Create an instance of SCWindow. This instance will be the parent to all of your
// GUI objects.
// Rect(left, top, width, height)
w = GUI.window.new("My window", Rect(20, 520, 600, 300)).front;
// then, all other objects take as their first arg a parent window. The bounds you
// describe in the new object's creation are relative to the parent
// you can also assign a GUI object a var, which allows other objects to reference it.
num = GUI.numberBox.new(w, Rect(10, 10, 60, 40))
// give it a default value
.value_(0.1)
// give it a Function to execute when you use it
// the GUI object itself is passed in as the first argument
.action_({arg me; me.value.postln; me.boxColor_(Color(me.value, 1-me.value, 1, 0.3))})
// make the box a certain color
.boxColor_(Color(0.1, 0.9, 1, 0.3));
GUI.button.new(w, Rect(10, 60, 90, 40))
// give the button states... it won't show up until states have been set.
// states are an array of state arrays... ["label", Color - text, Color - button]
.states_([
["Random Num", Color.black, Color(0.3, 0.3, 0.7, 0.3)],
["Set to 0.1", Color.black, Color(0.7, 0.3, 0.3, 0.3)]
])
.action_({arg me;
var actions;
("The button's value is: " ++ me.value).postln; // post the buttons value
// this is an array of actions that correspond with a buttons value
actions = [
{num.valueAction_(0.1); num.focus},
{num.valueAction_(0.0.rrand(1.0).round(0.001)); num.focus}
];
// execute a function according to the value of the button
actions[me.value].value
});
// all objects stored to the parent window 'w' are also stored in w's view array
// as children. You can access objects in relation to the order in which they are
// created, and relative to each other.
// create a slider, and link it to a number box
GUI.slider.new(w, Rect(140, 10, 140, 20))
.value_(0.7079) // the values are from 0 - 1
.action_({arg me;
var myindex;
// this will tell you the index of this object inside the view.children array
myindex = w.view.children.indexOf(me);
("My index is: " ++ myindex).postln;
// the number box that I want to tie to this slider is created next...
// its index will be this index, + 1
w.view.children.at(myindex + 1).value_(
// Slider output 0 to 1... but we can use a ControlSpec to map this value
// to a different range. In this case, this will be a dB slider
ControlSpec.new(-90, 6, \db).map(me.value).round(0.1));
});
GUI.numberBox.new(w, Rect(290, 10, 40, 20))
.value_(0) // in dB
.action_({arg me;
var myindex;
myindex = w.view.children.indexOf(me);
("My index is: " ++ myindex).postln;
// link to the slider created just before this NumberBox
w.view.children.at(myindex - 1).value_(
// use ControlSpec to UNmap the dB value back to a 0-1 range
ControlSpec.new(-90, 6, \db).unmap(me.value).round(0.0001).postln);
});
// this button starts and stops a synth
GUI.button.new(w, Rect(290, 50, 80, 40))
.states_([
["Start", Color.black, Color(0.3, 0.7, 0.3, 0.3)],
["Stop", Color.black, Color(0.7, 0.3, 0.3, 0.3)]
])
.action_({arg me;
var actions;
actions = [
// kill the synth, and set the node var to nil
{node.release},
{node = testsin.new.freq_(freq).amp_(amp).play}
];
actions[me.value].value;
});
// this is a shortcut for creating ControlSpecs... [lo, hi, warp, step]
freqspec = [440, 880, \exponential].asSpec;
twodslider = GUI.slider2D.new(w, Rect(10, 110, 580, 120))
.x_(0)
.y_(0)
.action_({arg me;
freq = freqspec.map(me.x).postln;
amp = me.y;
// if node is notNil, set the freq and amp parameters to the values of x
// (mapped) and y
node.notNil.if({
node.freq_(freq).amp_(amp);
})
});
// a button to close the view
GUI.button.new(w, Rect(10, 240, 80, 40))
.states_([
["Close me", Color.black, Color(0.3, 0.7, 0.3, 0.3)]
])
.action_({w.close});
// a function to be executed when the window is closed
w.onClose_({
"The window has been closed!".postln;
node.notNil.if({node.release});
});
)
/* Completely self-contained code for a piece that will be controlled with a GUI
- no global vars (EXCEPT for 's')!
1 - set up a few types of events. Each event will be a function that returns a
Routine or Task that:
- create a unique group for event wide control
- contains a global envelope for that gesture
- contains a function or loop that will process sound and read off that bus
- create a Dictionary that will store data in it:
- the start time of the event
- the group id
- the function itself
2 - a GUI to control when events are turned on and off, amplitude control
3 - The individual event Dictionaries will be stored in a global Dictionary. This
Dictionary will actually be what the GUI controls
*/
(
var masterdict, procs, w, bounds, pwidth, pheight, eventsetup, eventstop, eventplay;
var thisdir, pnotes;
var dbspec, changeamp;
var sfbuf, path, routebus, src, limit, cond, killfunc;
path = "sounds/SoloTuttiMono1.aiff";
// get the Directory where this document exists
thisdir = Document.current.dir;
// execute the SynthDef file that is in the same level as this file
pnotes = (thisdir ++ "/class08SD.rtf").load;
// a condition to control async commands
cond = Condition.new;
// boot the server
s = Server.internal.boot;
Server.default = s; // Ctk object use the default sever by... default. Set that to 's'
// wrap all the following code in a waitForBoot function... this will make sure the
// server is booted before executing code that needs the server.
s.waitForBoot({
// we have some async commands (a soundfile to be loaded) so wrap all this in a
// Routine
Routine.run({
// load our soundfile
sfbuf = CtkBuffer.playbuf(path).load;
s.sync(cond);
"Soundfile loaded".postln;
// allocate a virtual bus to send our source out on
routebus = CtkAudio.new;
// start playing it to the virtual bus at the head of 0...
src = pnotes[\input].new(addAction: \head, target: 0).outbus_(routebus).playbuf_(sfbuf).play;
limit = pnotes[\limiter].new(addAction: \tail, target: 0).inbus_(0).play;
// procs is an array of processes... you can add new procs as you want,
// and the GUI will be set up to create controls based on the number of
// functions inside the procs array. Each proc should be a function that
// can be evaluated and will return its routine, controlbus (for the global
// envelope), a controlnode, a group id and the amount of time it takes your
// global envelope to release, as well as a Clock. Make sure your procs just
// return the Routine or Task - don't play them from within the proc
// function... your GUI will do this
procs = [
// Proc 1 func... our eventsetup function will pass the procid in
// so we can use it inside out function (grab amp values from the GUI)
{arg procid;
var rout, controlbus, controlnode, group, env, releasetime, clock,
psratios, winsize = 6, overlaps = 4, envar;
// this is the processes global envelope
env = Env([0, 1, 0], [2, 2], [4, -4], 1);
// create a new instance of TempoClock for this processes
clock = TempoClock.new;
// this is the amount of time it will take the Env to close off
// we need to know this so we can release nodes and stop the Routine
// from running when we stop hearing sound
releasetime = env.releaseTime;
// allocate a groupID for this processes, so we can kill it without
// touching anything else
group = CtkGroup.new;
// a controlbus for the global env...
controlbus = CtkControl.new;
// .. and a controlnode for it
controlnode = pnotes[\procenv].new(target: group);
// for this process, and array of ps ratios
psratios = [-12, 3, -7, 3, -5, 12, 3, 5].midiratio;
// finally! The Routine.
rout = Routine({
group.play;
controlnode.outbus_(controlbus).amp_(masterdict[procid]["amp"]).env_(env).play;
// create the texture of pitch shifted notes
inf.do({arg i;
pnotes[\pitchshift].new(target: group).inbus_(routebus).outbus_(0).procenv_(controlbus)
.dur_(winsize).pchratio_(psratios.wrapAt(i)).play;
(winsize / overlaps).wait;
})
});
// set the clock of this Routine to the instance of TempoClock above
rout.clock_(clock);
// return information that will be stored in the Dictionary
[rout, controlbus, controlnode, group, releasetime, clock];
},
// Proc 2
{arg procid;
var rout, controlbus, controlnode, group, env, releasetime, clock,
freqs, winsize = 2, overlaps = 8, envar;
env = Env([0, 1, 0], [2, 2], [4, -4], 1);
clock = TempoClock.new;
releasetime = env.releaseTime;
group = CtkGroup.new;
controlbus = CtkControl.new;
controlnode = pnotes[\procenv].new(target: group);
freqs = Array.series(48, 84, 0.25).midicps;
rout = Routine({
group.play;
controlnode.outbus_(controlbus).amp_(masterdict[procid]["amp"]).env_(env).play;
inf.do({arg i;
pnotes[\bandpass].new(target: group).inbus_(routebus).outbus_(0)
.procenv_(controlbus).dur_(winsize).freq_(freqs.choose).play;
(winsize / overlaps).wait;
})
});
rout.clock_(clock);
[rout, controlbus, controlnode, group, releasetime, clock];
},
// proc 3
{arg procid;
var rout, controlbus, controlnode, group, env, releasetime, clock,
deltimes, winsize = 2, overlaps = 8, envar;
env = Env([0, 1, 0], [2, 2], [4, -4], 1);
clock = TempoClock.new;
releasetime = env.releaseTime;
group = CtkGroup.new;
controlbus = CtkControl.new;
controlnode = pnotes[\procenv].new(target: group);
deltimes = Array.series(10, 0.1, 0.1);
rout = Routine({
group.play;
controlnode.outbus_(controlbus).amp_(masterdict[procid]["amp"]).env_(env).play;
inf.do({arg i;
pnotes[\combs].new(target: group).inbus_(routebus).outbus_(0)
.procenv_(controlbus).dur_(winsize).deltime_(deltimes.choose).play;
(winsize / overlaps).wait;
})
});
rout.clock_(clock);
[rout, controlbus, controlnode, group, releasetime, clock];
}
];
masterdict = Dictionary.new(procs.size);
// create a ControlSpec for our dB sliders and number boxes to use:
dbspec = ControlSpec(-90, 6, \db);
// a function that checks to see if a proc is playing... if it is, change
// the amp to reflect the slider or numbox values
changeamp = {arg procid, amp;
// if this process if playing...
masterdict[procid]["isPlaying"].if({
// ... lookup the control node and set the amp value
masterdict[procid]["controlnode"].amp_(amp);
});
// save the new value to the procs Dictionary
masterdict[procid]["amp"] = amp;
};
// this will set-up the event, and save its vals in the masterdict
// if index is nil (if we close the window), nothing happens
eventsetup = {arg procid, index, amp;
var rout, control, controlnode, group, release,
clock;
index.notNil.if({
// evaluate the proc function, and pass in the procid
#rout, control, controlnode, group, release, clock =
procs[index].value(procid);
// create, or overwrite, a new Dictionary with this id
masterdict.add(procid -> Dictionary.new);
// store objects and data to the Dictionary
masterdict.at(procid)
.add("routine" -> rout)
.add("controlbus" -> control)
.add("controlnode" -> controlnode)
.add("group" -> group)
.add("reltime" -> release)
.add("clock" -> clock)
.add("isPlaying" -> false)
.add("amp" -> amp);
})
};
// eventstop will stop events and clean up after them - wait for releasetime,
// stop routines then set-up the event to be run again
eventstop = {arg procid, index, amp;
var controlnode, routine, reltime, group, clock;
// grab some info about this event
controlnode = masterdict[procid]["controlnode"];
routine = masterdict[procid]["routine"];
reltime = masterdict[procid]["reltime"];
group = masterdict[procid]["group"];
clock = masterdict[procid]["clock"];
// check to see if it is even playing
(masterdict[procid]["isPlaying"]).if({
("Stopping event with id: " ++ procid).postln;
// close off the control envelope
controlnode.release;
clock.sched(reltime, {
// stop the routine
routine.stop;
// free all nodes in this group
group.free;
// free the clock
clock.stop;
});
}, {
"No event playing".warn;
});
// after all the book keeping is done... prep everything to run again
eventsetup.value(procid, index, amp);
};
// this function is executed when the button is pressed
eventplay = {arg procid, index;
var routine, clock;
// grab some info
routine = masterdict[procid]["routine"];
clock = masterdict[procid]["clock"];
// play the routine on its instance of clock
routine.play(clock);
// set the "isPlaying" flag to true
masterdict[procid]["isPlaying"] = true;
};
// to be executed when the window closes (and your piece is finished)... basic
// clean up (free memory)
killfunc = {
procs.do({arg me, i;
var procid;
procid = ("proc" ++ i);
masterdict[procid]["isPlaying"].if({
eventstop.value(procid, nil, 0)});
});
src.free;
limit.free;
sfbuf.free;
};
{ // wrap the GUI bits in a function, and defer them to the AppClock
// find out the bounds of this screen
bounds = GUI.window.screenBounds;
// base the width of my GUI window on the width of the screen compared
// to the number of procs I have
pwidth = ((procs.size * 100) > (bounds.width - 40)).if({
((bounds.width - 40) / procs.size).floor;
}, {
100
});
pheight = bounds.height * 0.5;
w = GUI.window.new("My piece", Rect.new(20, bounds.height * 0.5 - 100,
(procs.size * pwidth), pheight)
).front;
// fill masterdict with Dictionaries that will reference each proc and
// give fill some initial data
procs.do({arg me, i;
// me is the proc function itself... i is its slot within the procs
// array
var thisrout, thiscontrol, thiscontrolnode, thisgroup, thisrelease,
procid;
// create a string from the string "proc" and the current 'i' value
procid = ("proc" ++ i);
eventsetup.value(procid, i, 1);
// give each proc its own button to start and stop the event
GUI.button.new(w, Rect((pwidth * 0.1) + (i * pwidth), pheight * 0.05,
pwidth * 0.8, pheight * 0.1))
.states_([
["Start " ++ procid, Color.black, Color(0.3, 0.7, 0.3, 0.3)],
["Stop " ++ procid, Color.black, Color(0.7, 0.3, 0.3, 0.3)]
])
.action_({arg me;
var actions, index;
index = w.view.children.indexOf(me);
actions = [
// stop or start
{eventstop.value(procid, i,
w.view.children[index + 2].value.dbamp)},
{eventplay.value(procid, i)}
];
actions[me.value].value;
});
// give each proc its own slider and number box for controlling
// amplitude
GUI.slider.new(w, Rect((pwidth * 0.25) + (i * pwidth), pheight * 0.2,
pwidth * 0.5, pheight * 0.6))
.value_(dbspec.unmap(0))
.action_({arg me;
var amp, thisindex;
// map the sliders value with ControlSpec
amp = dbspec.map(me.value);
// figure out this GUI objects index
thisindex = w.view.children.indexOf(me);
// round the amp value from above, and send it to the Number
// Box (next index)
w.view.children[thisindex + 1].value_(amp.round(0.01));
// run the changeamp function... make sure you pass in a 0-1
// linear amp
changeamp.value(procid, amp.dbamp);
});
GUI.numberBox.new(w, Rect((pwidth * 0.25) + (i * pwidth), pheight * 0.85,
pwidth * 0.45, pheight * 0.05))
.value_(0.0)
.action_({arg me;
var amp, thisindex;
// map the sliders value with ControlSpec
amp = dbspec.unmap(me.value);
thisindex = w.view.children.indexOf(me);
w.view.children[thisindex - 1].value_(amp.round(0.001));
changeamp.value(procid, me.value.dbamp);
});
GUI.staticText.new(w, Rect((pwidth * 0.75) + (i * pwidth), pheight * 0.85,
pwidth * 0.25, pheight * 0.05))
.string_("dB")
});
// when the window closes, execute this function
w.onClose_({
killfunc.value; "Soundfile freed".postln});
// {}.defer tells the interpreter to run this code on the AppClock
}.defer;
})
});
)