Grrr: A SuperCollider UI lib for grid based controllers

Grrr is something I’ve been working on for SuperCollider for some time. It’s an UI library loosely based on SuperCollider GUI classes but for grid based controllers. Try it out!

Grrr-Monome-SerialOSCClient-2016-06-15.zip (30.8 KB)

Download and extract the zip in your SuperCollider Extensions folder.

Recompile your class library and:

// Hello World

a=GRMonome64.new; // create a monome
b=GRButton(a, 0@0); // a 1x1 button placed at top left key
b.action = { |view, value| (value.if("Hello", "Goodbye") + "World").postln }; // an action triggered when the button is pressed

The zip includes SerialOSCClient which was discussed in an earlier post

Make sure you only have one SerialOSCClient.sc file in your Extensions folder, otherwise you’ll not be able to recompile the class library.

More UI:

// A tour of widgets...

b.remove; // remove the button created above

a.spawnGui; // spawnGui creates a virtual grid (GRScreenGrid) attached to the same view as the monome. key presses and led state of the monome will be indicated on the virtual grid.

GRCommon.indicateAddedRemovedAttachedDetached = true; // when this global is true views added and removed will be indicated on the monome and virtual grid

b=GRButton(a, 5@0, 3, 3); // create a bigger button in the top-right corner
b.action = { |view, value| ("Big button says:" + value.if("Hello", "Goodbye")).postln };

c=GRHToggle(a, 0@7, 8, 1); // a horizontal 8x1 toggle placed at row 8
c.action = { |view, value| ("Horizontal toggle says:" + value).postln };

d=GRVToggle(a, 0@0, 2, 7); // a vertical 2x7 toggle placed at columns 1 and 2
d.action = { |view, value| ("Vertical toggle says:" + value).postln };

e=GRMultiButtonView(a, 2@0, 3, 7); // a view with 21 buttons
e.action = { |view, value| ("MultiButtonView says:" + value).postln };

f=GRSwitcher(a, 5@3, 3, 3); // a view for paging other views
o=GRMultiButtonView(f, 0@0, 3, 3); // first view of switcher: a group of 3x3 buttons
p=GRMultiToggleView(f, 0@0, 3, 3, 'horizontal'); // second view: a group of 3x1 toggles
q=GRMultiToggleView(f, 0@0, 3, 3); // third view: a group of 1x3 toggles

n=GRHToggle(a, 5@6, 3, 1); // a vertical 3x1 toggle that controls switcher page
n.action = { |view, value| f.value = value };

Let the widgets control sounds…

// now, let some widgets control sound
s.boot; // boot server if not already booted
s.latency=0.05; // lower server latency for leds to update in sync

i = 440; // frequency

(
// let big button toggle a sound
b.action = { |view, value|
	value.if {
		j = { |freq| SinOsc.ar(Lag.kr(freq), mul: (-20).dbamp) ! 2 }.play(args: [\freq, i]);
	} {
		j !? {
			j.release;
			j = nil;
		}
	};
};
)

(
// let vertical toggle determine the frequency of big button's sound
d.action = { |view, value|
	i = \midfreq.asSpec.map([0, view.maximumValue].asSpec.unmap(value));
	j !? { j.set(\freq, i) };
};
d.valuesAreInverted=true;
)

(
// setup the 3x7 button group to toggle individual sounds
k = Group.basicNew(nodeID: 1); // default_group
h = Array2D(3, 7);
e.buttonValueChangedAction = { |view, x, y, value|
	var ugenGraphFunc = { |freq|
		var sig = Pan2.ar(SinOsc.ar(Lag.kr(freq+(y*freq), 1)*SinOsc.ar(freq + ((y/7)*freq)), mul: (-30).dbamp), x-1);
		sig + CombC.ar(sig, 1, (y/7), x);
	};
	value.if {
		h.put(x, y, ugenGraphFunc.play(target: k, args: [\freq, i]));
	} {
		h.at(x, y) !? { |synth|
			synth.release;
			h.put(x, y, nil);
		}
	};
};
e.action = nil;
)

(
// let vertical toggle determine the frequency of both big button sound and button group sounds by sending set(\freq, i) to default_group
d.action = { |view, value|
	i = \midfreq.asSpec.map([0, view.maximumValue].asSpec.unmap(value));
	k.set(\freq, i);
};
)

Let the 3x3 button matrix play a pattern…

(
SynthDef(
	\grrr_test,
	{ |freq, width, gate=1, amp|
		var sig = VarSaw.ar(freq, width: width);
		var amp_env = EnvGen.ar(Env.perc, gate, amp, doneAction: 2);
		var filter_env = EnvGen.ar(Env.perc(0.01, 0.5), gate);
		sig = RLPF.ar(sig, filter_env.linexp(-1, 1, 20, 8000)) * amp_env;
		Out.ar(0, sig ! 2);
	}
).add;
)

(
// let switcher widgets play a 3x3 sequence
l=Pbind(*[
	\instrument, \grrr_test,
	\degree, Prout({
		loop {
			3.do { |y|
				3.do { |x| (if (o.buttonValue(x, y), 7.rand, \r)).yield };
			}
		}
	}),
	\dur, Prout({
		loop {
			3.do { |togglenum| (switch (p.toggleValue(togglenum), 0, {0.25}, 1, {0.35}, 2, {0.45})).yield };
		}
	}),
	\width, Prout({
		loop {
			3.do { |togglenum| (switch (q.toggleValue(togglenum), 0, {0.15}, 1, {0.35}, 2, {0.55})).yield };
		}
	}),
	\flash, Prout(
		{
			loop {
				3.do { |y|
					3.do { |x|
						switch (f.value,
							0, { o.flashButton(x, y, 100) },
							1, { p.flashToggle(x, 100) },
							2, { q.flashToggle(x, 100) }
						);
						1.yield;
					};
				}
			}
		}
	)
]);
m=l.play;
)

Here’s a gist version of above: https://gist.github.com/antonhornquist/4f3f5bc09a695b3f453ff39c4f57c025

Ping @Tate_Carson and @cannc

/Anton

9 Likes

What’s the difference between GRHToggle and GRVToggle? eg: what’s wrong with switching the dimensions GRHToggle(a, 0@7, 1, 8) ?

Good question! Since you can have toggles of varying sizes I chose to differentiate horizontal and vertical orientation that way, ie.

h=GRHToggle.newDetached(4, 4);
v=GRVToggle.newDetached(4, 4);

h.plot; // plot is textual representation of view, the right box show the leds
v.plot;

h.value = 1;
v.value = 1;

h.plot;
v.plot;

h.value = 3;
v.value = 3;

h.plot;
v.plot;

For more basics (newDetached, etc) you can find a Grrr walkthrough here:

In the gist decoupled operation is also described, that is a key press is not coupled to changing the value of the widget. You have to build the logic yourself, ie:

a=GRScreenGrid.new;
b=GRHToggle.newDecoupled(a, 0@0, 8, 1);
s.boot;
(
b.toggleValuePressedAction = { |view, value|
	var playFunc;
	playFunc = { |degree| (degree: degree, sustain: 0.05).play };
	r.stop;
	r = Routine({
		loop {
			(value..0).do { |val|
				0.1.wait;
				playFunc.(val);
				view.value = val;
			};
		};
	}).play;
};
)

Cool. I will have to try this out when I get home from work. Have been playing a lot with Monome and SC lately.

Is there built-in support for pages?

Yeah, kind of, see GRSwitcher.

Any reason you didn’t use the existing MonoM monome classes?

I created the SerialOSCClient library so that’s what I prefer. I elaborated on the differences between SerialOSCClient and MonoM here: SerialOSCClient for SuperCollider

Also note that the GRMonome* classes - the SerialOSCClient based monome classes - are actually not part of Grrr. If you look in the zip they’re in a different folder. The only grid that’s part of Grrr is the virtual one: GRScreenGrid. I tried to design Grrr as controller agnostic. That’s why I refer to it as a UI lib for grid based controllers.

To make your own controller you just subclass GRController. It’s pretty easy for you to create a controller that uses MonoM, if you like.

The core Grrr lib is quite stable.

The GRMonome* classes and SerialOSCClient lib are fairly young projects. There are a lot of unimplemented features I want to add when I get the time. My previous Grrr monome classes were based on the MonomeSerial protocol.

1 Like

super helpful library! thanks for sharing it.

Lovely stuff @jah

Gonna dig it and report back :slight_smile:

Thanks.

Just for the record. There are issues and design tradeoffs in the current Grrr, namely:

  • Currently only possible to instantiate one GRMonome at a time. Using Grrr with monomes via the serialosc protocol is of importance to me so this will be very much improved.
  • No vari-bright support in the framework, only led on/off. I started this project many years ago and I did not own a vari-bright unit back then.
  • Widgets cannot overlap. Framework could be improved to facilitate this, would require some work. However, there are ways of making pages of widgets.
  • Performance: readability of code has so far been prioritized over speed. Do not expect ultra-high refresh rates.

Still, with Grrr you can throw together stuff quickly. Below you can find a step sequencer with four pages of modulation. Evaluate and press top right-most button to play:

(
s.boot;
s.latency = 0.02;
~grid=GRMonome64.new;
~grid.spawnGui;
~playpos=GRHToggle.newDecoupled(~grid, 0@7, 8, 1, nillable: true).value_(nil);
~pages=GRSwitcher(~grid, 0@1, 8, 6);
~pageselect=GRHToggle(~grid, 0@0, 4, 1).action_({ |view, value| ~pages.value = value });
~transport=GRHToggle(~grid, 6@0, 2, 1);
~transport.action = { |view, value| if (value == 1) { ~player.play } { ~player.stop } };
~scramble=GRButton.newDecoupled(~grid, 4@0).value_(true);
~scramble.buttonPressedAction = { 8.do { |togglenum|
	var mtv= ~pages.currentView;
	if (mtv.toggleValue(togglenum).notNil) {
		mtv.setToggleValue(togglenum, 6.rand);
	}
} };
~shuffle=GRButton(~grid, 5@0);

~degrees=GRMultiToggleView(~pages, 0@0, 8, 6, nillable: true);
~degrees.valuesAreInverted = true;
~octaves=GRMultiToggleView(~pages, 0@0, 8, 6);
~octaves.valuesAreInverted = true;
~cutoffs=GRMultiToggleView(~pages, 0@0, 8, 6);
~cutoffs.valuesAreInverted = true;
~variation=GRMultiToggleView(~pages, 0@0, 8, 6);
~variation.valuesAreInverted = true;

8.do { |i| ~octaves.setToggleValue(i, 3) };

SynthDef(
	\grrr_test,
	{ |freq, cutoff_freq, variation, gate=1, amp|
		var sig;
		var osc1 = VarSaw.ar(freq, width: variation.linlin(0, 1, 0, 0.25));
		var osc2 = VarSaw.ar(((freq.cpsmidi)-12).midicps, width: 0);
		var amp_env = EnvGen.ar(Env.perc(0.01, variation.linlin(0, 1, 0.9, 0.1)), gate, amp, doneAction: 2);
		var filter_env = EnvGen.ar(Env.perc(0.01, variation.linlin(0, 1, 0.01, 0.6)), gate);
		sig = RLPF.ar((osc1 * 0.7) + (osc2 * 0.3), cutoff_freq.linexp(0, 1, 20, 4000) + filter_env.linexp(-1, 1, 20, 3000)) * amp_env;
		Out.ar(0, sig ! 2);
	}
).add;

~multi_toggle_view_as_prout = { |view, func|
	Prout({
		loop {
			8.do { |togglenum| func.(view.toggleValue(togglenum)).yield }
		}
	})
};

~pattern=Pbind(*[
	\instrument, \grrr_test,
	\degree, ~multi_toggle_view_as_prout.(~degrees, { |degree| if (degree.notNil, degree, \rest) }),
	\octave, ~multi_toggle_view_as_prout.(~octaves, { |toggle_value| toggle_value+2 }),
	\cutoff_freq, ~multi_toggle_view_as_prout.(~cutoffs, { |toggle_value| toggle_value/6 }),
	\variation, ~multi_toggle_view_as_prout.(~variation, { |toggle_value| toggle_value/6 }),
	\dur, Prout({ loop {
		if (~shuffle.value) { 0.16.yield; 0.1.yield } { 0.13.yield; 0.13.yield }
	}}),
	\playpos, Prout( { loop { 8.do { |playpos| (~playpos.value = playpos).yield } } })
]);
~player=~pattern.asEventStreamPlayer;
)

Same thing as a gist: https://gist.github.com/antonhornquist/82e39ba6a2f332674ceee18f638618fa

Hi,
I was
How do I manipulate elements in a GRMultiToggleView? I want to be able to turn the LEDs off from SC but setToggleValue and setToggleValueAction both fail. I was basically trying to turn this code verticle and have the falling LEDs represent nodes ending but it doesn’t seem to work at all so far:

(
c.togglePressedAction = nil;
c.toggleValuePressedAction = { |view, value| // toggleValuePressedAction is triggered anytime a toggle value is pressed
var playFunc;
playFunc = { |degree| (degree: degree, sustain: 0.05).play };
r.stop;
r = Routine({
loop { //counts down from pressed key and plays descending line
(value…0).do { |val|
0.1.wait;
playFunc.(val);
view.value = val;
};
};
}).play;
};
)

Maybe a general help file for all methods would help?

thanks

Also with a multitoggleview how do you do a b.action = {|value| } and use the value for each index?

Hi,

Yeah I need to write docs.

Most GRMultiToggleView actions include an index that refer to affected toggle. See example below where I’ve also transposed each toggle and included a way to stop cycles. Let me know if this clears things up.

a=GRScreenGrid.new;
c=GRMultiToggleView.newDecoupled(a, 0@0, 8, 8);
c.nillable = true;
c.numToggles.do { |index| c.setToggleValue(index, nil) };
s.boot;
x = Array.newClear(c.numToggles);
(
c.toggleValuePressedAction = { |view, index, value|
    var playFunc;
    playFunc = { |degree, mtranspose| (degree: degree, mtranspose: mtranspose, octave: 4, sustain: 0.05).play };
    x[index].stop;
    if (value == 0) {
        view.setToggleValue(index, nil);
    } {
        x[index] = Routine({
            loop {
                (value..0).do { |val|
                    0.1.wait;
                    playFunc.(val, index*2);
                    view.setToggleValue(index, val);
                };
            };
        }).play;
    }
};
)

You can use toggleValueChangedAction for this:

d=GRScreenGrid.new;
e=GRMultiToggleView.new(d, 0@0, 8, 8);
e.toggleValueChangedAction = { |view, index, value| "toggle % value was changed to %".format(index, value).postln };

Thanks. Where in the classes the index method defined? I looked in the class for GRMultiToggleView and didn’t see it.

Toggle index is included as the second argument to any function acting as a callback to actions that concern individual toggles, ie toggleValuePressedAction and toggleValueChangedAction (not action which is triggered by all toggles). Index is an integer ranging from 0 to number of toggles minus 1. So it’s not a method call.

You typically use that index in your logic. In the GRMultiButtonView example code I posted above you can see that the index argument is used to set value (which in turn means what led to display) for the corresponding toggle with setToggleValue(index, val). The loop cycles a Routine that decrements val and invokes setToggleValue(index, val) every 0.1 seconds.

Oh good.

Another question:
Is there anything for doing momentary buttons, where the LED is on for as long as you’re pressing the button? I’m wanting to use it just to trigger one shot samples, so it only needs one state. I was thinking that button could do this but it seems it’s just on and off. Also if this is possible how would I make a 4 by 4 array of these and use the x and y, or index and value of whatever i press, to map to a parameter?

Hope that makes sense. Thanks for all your help so far, it’s making using the monome with SC much easier!

Default behavior for buttons is to toggle. You can change it:

// assuming b is a GRButton or GRMultiButtonView
b.behavior = \momentary;

For a 4x4 grid of buttons use GRMultiButtonView.

Thanks for the feedback. It’s much appreciated. Let me know if you need any more help on this.

this is awesome. i am glad people are attaching this to SuperCollider. Thank you.

One more thing I can’t seem to figure out. I want to take this code:

~drone = GRButton(a, 0@4, 4, 3);
~drone.action = {|view, value|
if(value,
{Pbindef(\gran, \sndBuf, b[\cuts][rrand(0, ~ssize)], \bpWet, 0.1).play;},
{Pbindef(\gran).stop}
);
};

and be able to access the x and y coordinates to use for mapping, while still keeping the whole square lighting up as it is now. Is that possible? I haven’t been able to figure it out.

thanks again!