Graphical User Interface Classes for SuperCollider
Server.default=s=Server.local;
Graphical user interface capabilities in SC let us build frontends for our computer music projects. They are a convenient way to create custom virtual synths, and package up novel programs for ourselves and other users.
GUI classes include various forms of slider, buttons, dials, drop down lists, drag and drop facilities and many more custom views.
This file will use code which should work on all platforms without any worry about the underlying implementation (SC 3.3.1 on). There may be slight differences between versions of SuperCollider on the available GUI capabilities.
[side note: On the Mac, for SC3.5 and earlier, you can press shift+cmd+N immediately to see a selection of the available GUI widgets.]
Just in case you want to know more about the implementation:
Because GUIs tend to be quite operating system specific, under the surface, there are different main GUI implementations available.
From SC 3.6, the standard GUI library is qt.
Historically, there are also OS X ('cocoa') specific classes (usually with prefix SC before the class names used here) and SwingOSC ('swing') Java cross platform classes (usually with prefix JSC).
You can call the standard cross-platform GUI class names, like Slider, Window, View, without worrying about which of qt, Cocoa or SwingOSC is operative.
Both qt and SwingOSC act like servers, sending and receiving messages from the language app. On OS X, a native Cocoa implementation is built into the standard language environment for SC3.5 and earlier.
Test which GUI library you are using by default:
GUI.current
For more on this, see:
[GUI] //main GUI help file
[GUI-Classes] //list of all GUI classes, with cross-
Quick swap of implementation:
GUI.cocoa //SC 3.5 and earlier on a Mac
GUI.swing //will only work if SwingOSC is installed, see instructions with SwingOSC
//make sure SwingOSC server is running if you are using that, before you run any GUI code:
SwingOSC.default.boot
To make a window
//The Rect(angle) takes the initial screen position and the window size
//as screenx, screeny, windowwidth, windowheight, where screeny is 0 at the bottom
(
var w;
w=Window("My Window", Rect(100,500,200,200));
//A 200 by 200 window appears at screen co-ordinates (100, 500)
w.front; //this line is needed to make the window actually appear
)
Note that we count on the y axis from screen origin at bottom left, to the bottom left corner of the window.
We add controls to our window, defining any parameters of their use. We pass in the window we wish the control to appear in and use a Rect again to specify where in the window the control will appear and how large it is. However, this time the co-ordinates are no longer relative to the screen, but relative to the top left corner of the window, and x and y positions indicate distance from left and from top respectively.
(
var w, slid;
w=Window("My Window", Rect(100,500,200,100));
//A 200 by 100 window appears at screen co-ordinates (100, 500)
slid=Slider(w,Rect(10,10,180,40)); //a basic slider object of size 180 by 40 appears 10 pixels in from the left, and 10 pixels down from the top
slid.action_({slid.value.postln;}); //this is the callback: the function is called whenever you move the slider. action_ means to set up the slider object to use the function passed in as its argument.
w.front;
)
Note how the default slider range is from 0.0 to 1.0
We might not want to create numbers from 0.0 to 1.0, but remap the value to other ranges. 0.0 to 1.0 is a very useful starting point, though. Try:
1.0.rand //create a random number from 0.0 to 1.0
1.0.rand*50 //create a random number from 0.0 to 1.0, and multiply it by 50 to get a new range from 0.0 to 50.0
1.0.rand*50+14.7 //create a random number from 0.0 to 1.0, multiply it by 50, then add 14.7, to get a new range from 14.7 to 64.7
1.0.rand.linlin(0.0,1.0,14.7,64.71) //create a random number from 0.0 to 1.0, and use a built in function to remap it to the output range 14.7 to 64.71
1.0.rand.linexp(0.0,1.0,14.7,64.71) //create a random number from 0.0 to 1.0, and use a built in function to remap it to the output range 14.7 to 64.71 with an exponential function, which tends to spend longer over lower values
Rather than doing these remappings yourself, an alternative is to take advantage of a ControlSpec, a helpful class which can be used to turn input into any desired range through various available precanned mappings
(
var w, slid, cs;
w=Window("My Window", Rect(100,500,200,100));
//A 200 by 200 window appears at screen co-ordinates (100, 500)
slid=Slider(w,Rect(10,10,180,40));
//arguments minimum value, maximum value, warp (mapping function), stepsize, starting value
cs= ControlSpec(20, 20000, \exponential, 10, 1000);
slid.action_({cs.map(slid.value).postln;}); //map to the desired range
w.front;
)
Given the action function for a GUI component, we can plug through to sound synthesis. Here we use the set command to modulate the control arguments of a running synth.
//Demo of using 2D-Slider for synthesis
(
SynthDef(\filterme,{arg freq=1000, rq=0.5; //make sure there are control arguments to affect!
Out.ar(0,Pan2.ar(
BPF.ar(Impulse.ar(LFNoise0.kr(15,500,1000),0.1, WhiteNoise.ar(2)),freq,rq)
))
}).add;
)
(
var w, slid2d, syn;
w=Window("My Window", Rect(100,300,200,200));
slid2d= Slider2D(w,Rect(5,5,175,175));
syn=Synth(\filterme); //create synth
slid2d.action_({
[slid2d.x, slid2d.y].postln;
syn.set(\freq,100+(10000*slid2d.y),\rq,0.01+(0.09*slid2d.x)); //I'm doing my own linear mapping here rather than use a ControlSpec
});
w.front;
w.onClose={syn.free;}; //action which stops running synth when the window close button is pressed
)
If you want to arrange a bank of dials, for instance, you might use a helper class (a 'decorator') for arranging views on screen:
Note:
10@10 //is the Point (10,10), an (x,y) co-ordinate position
(
w= Window("decoration",Rect(200,200,400,500));
//set up decorator. FlowLayout needs to know the size of the parent window, the outer borders (10 pixels in on horizontal and vertical here) and the standard gap to space GUI views (20 in x, 20 in y)
w.view.decorator= FlowLayout(w.view.bounds, 10@10, 20@20);
//now, when GUI views are added to the main view, they are automatically arranged, and you only need to say how big each view is
k= Array.fill(10,{Knob(w.view,100@100).background_(Color.rand)});
w.front; //make GUI appear
)
//they were stored in an array, held in global variable k, so we can access them all easily via one variable
k[3].background_(Color.rand)
However, maximum precision will come from specifying positions yourself. Make use of SuperCollider as a programming language to do this:
(
w= Window("programming it directly ourselves",Rect(200,200,400,400));
//now, when GUI views are added to the main view, they are automatically arranged, and you only need to say how big each view is
k= Array.fill(16,{|i| Knob(w,Rect((i%4)*100+10,i.div(4)*100+10,80,80)).background_(Color.rand)});
//if worried by the use of % for modulo and .div for integer division, try the code in isolation:
//i.e., try out 5%4, and 5.div(4) as opposed to 5/4. How does this give the different grid positions as
//argument i goes from 0 to 15?
w.front; //make GUI appear
)
You can dynamically add and remove views from a window.
Run these one at a time:
w=Window();
w.front; //window appears
Slider(w,Rect(10,10,100,100)); //slider appears straight away
w.view.children //slider should be in the list, even though we didn't store any reference to the slider object in a global variable (like w) ourselves
w.view.children[0].remove //nothing happens visually immediately
w.refresh; //refresh updates the appearance of the window and the slider disappears
For further explorations:
For demos of Drag and Drop and other UI facilities see the examples/GUI examples folder
e.g.,
Document.open("examples/GUI examples/GUI_examples1.scd"); //on a Mac with SC3.5 or earlier this should open it straight away
Document.open("examples/GUI examples/GUI_examples2.scd");
Else for the ScIDE (3.6):
ScIDE.open("examples/GUI examples/GUI_examples1.scd");
ScIDE.open("examples/GUI examples/GUI_examples2.scd");
Warning: some examples may not use the more recent implementation free formulation, and involve different calls.
Drag and Drop demo:
(
// create a GUI window with some NumberBoxes.
// You can command click on a control to drag its value to another control for cocoa, or control click for swing
var w, n, f, s;
w = Window("number box test", Rect(128, 64, 260, 80));
w.view.decorator = f = FlowLayout(w.view.bounds);
n = NumberBox(w, Rect(0,0,80,24));
n.value = 123;
n = NumberBox(w, Rect(0,0,80,24));
n.value = 456;
n = DragBoth(w, Rect(0,0,80,24));
n.object = 789;
f.nextLine;
s = Slider(w, Rect(0,0,240,24));
w.front;
)
There are also interesting help files for many other GUI objects. Note that there may be differences between 3.5 and 3.6 in what is available.
(credit: many of the GUI objects were introduced into SuperCollider by Jan Trutzschler. for OS X originally)
[MultiSliderView] //call the Help file
[EnvelopeView]
and some media viewing objects
[SoundFileView]
(
w = Window.new("soundfile test", Rect(200, 200, 800, 400));
a = SoundFileView(w, Rect(20,20, 700, 60));
f = SoundFile.new;
if(Main.versionAtLeast(3,5),{
f.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");
},{
f.openRead("sounds/a11wlk01.wav");
});
a.soundfile_(f);
a.read(0, f.numFrames);
a.gridOn_(false);
w.front;
)
There is a MovieView too, but it doesn't work particularly well.
There are screen drawing facilities using the Pen class
(
var w, h = 400, v = 400, seed = Date.seed, run = true;
w = Window("subdiv", Rect(40, 40, h, v));
w.view.background = Color.rand;
w.onClose = { run = false };
w.front;
//for SC3.4 or earlier, use drawHook
w.drawFunc = { var done, nextx, nexty, yellowness, penwidth;
nextx=0;
nexty=0;
yellowness=rrand(0.0,1.0);
penwidth=rrand(0.5,1.5);
//done=0;
Pen.use {
200.do({arg i;
var lastx,lasty;
lastx=nextx;
lasty=nexty;
nextx=nextx+rrand(1,20);
nexty=nexty+rrand(1,40);
if(nextx>=h, {nextx=nextx%h});
if(nexty>=v, {nexty=nexty%v});
penwidth=(penwidth+(0.2.rand2))%8.0;
Pen.width= penwidth;
yellowness= (yellowness+(0.1.rand2))%2.0;
Color.yellow(yellowness).set;
Pen.beginPath;
Pen.line(Point(lastx,lasty),Point(nextx,nexty));
Pen.rotate(rand(i%40));
Pen.line(Point(lastx,lasty),Point(nextx,nexty));
Pen.rotate(rand(i%40));
Pen.line(Point(lastx,lasty),Point(nextx,nexty));
Pen.rotate(rand(i%40));
Pen.line(Point(lastx,lasty),Point(nextx,nexty));
Pen.stroke;
//Pen.fillRect(Rect(h.rand,v.rand,rrand(1,50),rrand(1,50)))
});
};
};
//{ while { run } { w.refresh; 3.wait; } }.fork(AppClock)
)
Which you could use for text manipulation as well...
(
var linetext, drawletter;
var w, h = 800, v = 60, seed = Date.seed, run = true;
var time, name, sourcestring;
var yellowness, penwidth;
//name=[\s,\u,\p,\e,\r,\c,\o,\l,\l,\i,\d,\e,\r];
//sourcestring= "any lower case text";
sourcestring= "welcome to supercollider";
name=Array.fill(sourcestring.size,{arg i; sourcestring[i].asSymbol});
time=0;
linetext= (
'a':[[[0,1],[0.5,0]],[[0.5,0],[1,1]],[[0.25,0.5],[0.75,0.5]]],
'b':[[[0,1],[0,0]],[[0,1],[1,1]],[[0,0],[1,0]],[[0,0.5],[0.75,0.5]],[[0.75,0.5],[1,0.75]],[[0.75,0.5],[1,0.25]],[[1,0.75],[1,1]],[[1,0.25],[1,0]]],
'c':[[[0,1],[0,0]],[[0,0],[1,0]],[[0,1],[1,1]]],
'd':[[[0,1],[0,0]],[[0,0],[0.75,0]],[[0,1],[0.75,1]],[[0.75,1],[1,0.75]],[[0.75,0],[1,0.25]],[[1,0.25],[1,0.75]]],
'e':[[[0,0],[0,1]],[[0,0],[1,0]],[[0,1],[1,1]],[[0,0.5],[1,0.5]]],
'f':[[[0,0],[0,1]],[[0,0],[1,0]],[[0,0.5],[1,0.5]]],
'g':[[[0,1],[0,0]],[[0,0],[1,0]],[[0,1],[1,1]],[[1,1],[1,0.5]],[[0.5,0.5],[1,0.5]]],
'h':[[[0,1],[0,0]],[[0,0.5],[1,0.5]],[[1,1],[1,0]]],
'i':[[[0,0],[1,0]],[[0.5,0],[0.5,1]],[[0,1],[1,1]]],
'j':[[[0,0],[1,0]],[[0.5,0],[0.5,1]],[[0,1],[0.5,1]]],
'k':[[[0,1],[0,0]],[[0,0.5],[1,1]],[[0,0.5],[1,0]]],
'l':[[[0,1],[0,0]],[[0,1],[1,1]]],
'm':[[[0,1],[0,0]],[[0,0],[0.5,0.5]],[[0.5,0.5],[1,0]],[[1,0],[1,1]]],
'n':[[[0,1],[0,0]],[[0,0],[1,1]],[[1,1],[1,0]]],
'o':[[[0,1],[0,0]],[[0,0],[1,0]],[[0,1],[1,1]],[[1,0],[1,1]]],
'p':[[[0,0],[0,1]],[[0,0],[1,0]],[[0,0.5],[1,0.5]],[[1,0],[1,0.5]]],
'q':[[[0,0],[0,0.75]],[[0,0],[0.75,0]],[[0,0.75],[0.75,0.75]],[[0.75,0],[0.75,0.75]],[[0.5,0.5],[1,1]]],
'r':[[[0,0],[0,1]],[[0,0],[1,0]],[[0,0.5],[1,0.5]],[[1,0],[1,0.5]],[[0,0.5],[1,1]]],
's':[[[0,0],[0,0.5]],[[0,0],[1,0]],[[0,1],[1,1]],[[0,0.5],[1,0.5]],[[1,0.5],[1,1]]],
't':[[[0,0],[1,0]],[[0.5,0],[0.5,1]]],
'u':[[[0,1],[0,0]],[[0,1],[1,1]],[[1,0],[1,1]]],
'v':[[[0,0],[0.5,1]],[[0.5,1],[1,0]]],
'w':[[[0,0],[0.25,1]],[[0.25,1],[0.5,0.5]],[[0.5,0.5],[0.75,1]],[[0.75,1],[1,0]]],
'x':[[[0,0],[1,1]],[[0,1],[1,0]]],
'y':[[[0,0],[0.5,0.5]],[[0.5,0.5],[1,0]],[[0.5,0.5],[0.5,1]]],
'z':[[[0,1],[1,0]],[[0,0],[1,0]],[[0,1],[1,1]]],
(" ".asSymbol):[[[0,1],[1,1]],[[0,0.8],[0,1]],[[1,0.8],[1,1]]]
);
w = Window("welcome", Rect(40, 500, h, v));
w.view.background = Color.blue(0.5);
w.onClose = { run = false };
w.front;
drawletter={arg which, startx, starty, xscale=100, yscale,prop=1.0;
var data;
yscale= yscale ? xscale;
data= linetext[which];
prop=(round((data.size)*prop).asInteger).max(1);
prop.do({arg i;
var val=data[i];
Pen.beginPath;
Pen.line(Point(startx+(xscale*val[0][0]),starty+(yscale*val[0][1])),Point(startx+(xscale*val[1][0]),starty+(yscale*val[1][1])));
Pen.stroke;
});
};
yellowness=rrand(0.7,0.9);
penwidth=rrand(2,3);
w.drawFunc = {
Pen.use {var xoscil, xsizoscil,yoscil, todraw, usedtime;
Pen.width= penwidth;
Color.yellow(yellowness).set;
usedtime=time.min(1.0);
todraw=(round((name.size)*usedtime).asInteger).max(1);
todraw.do({arg j;
xoscil= sin(2*pi*time+(j*pi*0.13))*140/(1+(10*time));
yoscil= sin(2*pi*time+(j*pi*0.03))*200/(1+(200*time));
xsizoscil= time*5+5;
drawletter.value(name[j],50+(25*j)+(xoscil),10+yoscil,xsizoscil,xsizoscil,usedtime);
});
};
};
{ while { time<2.0 } {
w.refresh;
time=(time+0.025); //%2.0;
0.05.wait; } }.fork(AppClock)
)
If you're on OS X before SC3.6, also see SCImage for many image processing capabilities, such as placing an image into the background of your GUI window, or adding a logo.