rechtbank.org

Controlling SuperCollider with the Google Stadia controller

Published on

Using the following code it is possible to control a basic ring modulated signal. It is far from finished and mainly shows how it is possible to control parameters in SuperCollider using HIDs, such as game controllers.

Connecting the controller

When I initially connected the controller I also turned it on by pressing the Stadia button until the controller vibrated. This meant it was connected with both USB and Bluetooth to my Macbook. SuperCollider showed both devices and I could connect to at least the USB one (possibly also the bluetooth one), but no signals were received. When I turned off the controller and solely relied on the USB connection, the signals came in.

Code

Boot the server and show some HID related information. Booting the server, opening the controller and storing the connection in ~stadia cannot be left out.

s.boot;

HID.findAvailable;

HID.postAvailable;

~stadia = HID.open(6353, 37888);

HIDFunc.trace( true );

HID.openDevices;

HID.closeAll;

~stadia.postElements

Here is an overview of the different inputs and its behavior I found (might not be 100% accurate):

/*
i = element index (~stadia.elements[i])
ui = usage index

i  ui   usage name
18  1 = b1           = A (0-1) (rest: 0)
17  2 = b2           = B (0-1; rest: 0)
16  4 = b4           = X (0-1; rest: 0)
15  5 = b5           = Y (0-1; rest: 0)
14  7 = b7           = left shoulder (0-1; rest: 0)
13  8 = b8           = right shoulder (0-1; rest: 0)
10 11 = b11          = ... button (upper left) (0-1; rest: 0)
 9 12 = b12          = hamburger button (upper right) (0-1; rest: 0)
 8 13 = b13          = stadia button (middle) (0-1; rest: 0)
12 14 = b14          = push left joystick (0-1; rest: 0)
11 15 = b15          = push right joystick (0-1; rest: 0)
 5 17 = b17          = function button left (0-1; rest: 0)
 4 18 = b18          = function/screenshot button right (0-1; rest: 0)
 0 48 = X            = left joystick (together with index: 49) (0..1; rest: 0.5)
 1 49 = Y            = left joystick (together with index: 48) (0..1; rest 0.5) => I think position?
 2 50 = Z            = right joystick (together with index: 53) (0..1; rest: 0.5) => I think also position?
 3 53 = Rz           = right joystick (together with index: 50) (0..1; rest: 0.5)
22 57 = Hat_switch   = dpad (up(0):0.0; up/right(1):0.1428; right(2):0.2857; right/down(3):0.4285;
                       down(4):0.5714; down/left(5):0.7142; left(6):0.8571; up/left(7):1.0; rest(8):1.1428)
24 196 = Accelerator = right trigger (0..1) (rest: 0)
23 197 = Brake       = left trigger (0..1) (rest: 0)
*/

The following code block defines some global variables and what to do when inputs are given:

(
~synths = Array.newClear(197);
~activeSynth = 0;


// start or select synth
HIDdef.usage( \A, {
	arg value, rawValue, usage;
	if (~synths[usage].isNil) {
		~synths[usage] = Synth.new(\ringmod);
	};
	~activeSynth = usage;
}, \b1);

// free selected synth
HIDdef.usage( \StadiaButton, {
	arg value, rawValue, usage;
	~synths[~activeSynth].free;
	~synths[~activeSynth] = nil;
	~activeSynth = 0;
}, \b13);

Here ~synths is an array that will contain all started synths. The idea is that under each A/B/X/Y-button a synth can be started. Currently it only works for the A-button, but it supports multiple already. The index to the array is the usage argument, which contains the usage index defined on the device itself (see mapping above).

~activeSynth contains the usage value of the active synth. It is 0 when there is no active synth.

The first HIDdef starts and sets the synth as active. If it has already been started, it is only set as active.

The second HIDdef frees the active synth and resets relevant values.

HIDdef.usage( \Rvel, {
	arg value;
	if (~activeSynth > 0) {
		~synths[~activeSynth].set(\velY, value);
	};
}, \Z);

HIDdef.usage( \Rpos, {
	arg value;
	if (~activeSynth > 0) {
		~synths[~activeSynth].set(\posY, value);
	};
}, \Rz);

HIDdef.usage( \Lvel, {
	arg value;
	if (~activeSynth > 0) {
		~synths[~activeSynth].set(\velX, value);
	};
}, \X);

HIDdef.usage( \Lpos, {
	arg value;
	if (~activeSynth > 0) {
		~synths[~activeSynth].set(\posX, value);
	};
}, \Y);

HIDdef.usage( \Rtrig, {
	arg value;
	if (~activeSynth > 0) {
		~synths[~activeSynth].set(\delay, value);
	};
}, \Accelerator);

HIDdef.usage( \Ltrig, {
	arg value;
	if (~activeSynth > 0) {
		~synths[~activeSynth].set(\reverb, value);
	};
}, \Brake);
)

All the HIDdef above send a value to the active synth by calling set() on it.

The synth itself looks like this:

(
SynthDef("ringmod", {
	arg out=0, amp=0.3, posX=0.5, posY=0.5, velX=0.5, velY=0.5, delay=0, reverb=0;
	var mod, car, snd, env, modFreq, carFreq;

	modFreq = LinExp.kr(posX*velX, 0.0, 1.0, 1, 2000);
	carFreq = LinExp.kr(posY*velY, 0.0, 1.0, 200, 1500);

	mod = Pulse.ar(modFreq);
	car = Pulse.ar(carFreq);

	snd = car*mod * amp;
	
	delay = (1-delay)*0.01;
	snd = CombC.ar(snd, 1, delay);
	
	reverb = reverb;
	snd = FreeVerb.ar(snd, reverb, reverb);
	
	Out.ar(out, snd!2);
}).add;
)