/* class 8 */


/*  GUI interface */


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

attachments/class08/DSCF0071.png


(

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;

})

});

)