Commission Music

Commission Music
Bespoke Noise!!

Tuesday, 3 October 2006

HID, Joysticks and SuperCollider

So, game controllers and joysticks and the like are called Human Interface Devices (HID). There is some HID stuff built into SuperCollider, but it seemed sort of half implemented. They let you specify things, but not do much with it. So I wrote an extension yesterday, which maybe I should submit as something official or a replacement, although I didn't bother checking first if anybody else had already done anything. No internet access at home sucks.

To use it, download HID.sc and put it in ~/Library/Application\ Support/SuperCollider/Extensions. Plug in your joystick or controller. Start SuperCollider.

First, you want to know some specifications for your device, so execute:


HIDDeviceServiceExt.buildDeviceList;

  
(
HIDDeviceServiceExt.devices.do({arg dev;
  [dev.manufacturer, dev.product, dev.vendorID, dev.productID, dev.locID].postln;
  dev.elements.do({arg ele;
   [ele.type, ele.usage, ele.cookie, ele.min, ele.max].postln;
  });
});
)

I got back:

  [ , USB Joystick STD, 1539, 26742, 454033408 ]
  [ Collection, Joystick, 1, 0, 0 ]
  [ Collection, Pointer, 2, 0, 0 ]
  [ Button Input, Button #5, 3, 0, 1 ]
  [ Button Input, Button #6, 4, 0, 1 ]
  [ Button Input, Button #7, 5, 0, 1 ]
  [ Button Input, Button #8, 6, 0, 1 ]
  [ Button Input, Button #1, 7, 0, 1 ]
  [ Button Input, Button #2, 8, 0, 1 ]
  [ Button Input, Button #3, 9, 0, 1 ]
  [ Button Input, Button #4, 10, 0, 1 ]
  [ Miscellaneous Input, Hatswitch, 11, 0, 3 ]
  [ Miscellaneous Input, X-Axis, 12, 0, 127 ]
  [ Miscellaneous Input, Y-Axis, 13, 0, 127 ]
  [ Miscellaneous Input, Z-Rotation, 14, 0, 127 ]
  [ Miscellaneous Input, Slider, 15, 0, 127 ]
  [ a HIDDevice ]

Ok, so I know the name of the device and something about all the buttons and ranges it claims to have. (Some of them are lies!) So, I want to assign it to a variable. I use the name.

j = HIDDeviceServiceExt.deviceDict.at('USB Joystick STD');

Ok, I can set the action for the whole joystick to tell me what it's doing, or I can use the cookie to set the action for each individual element. I want to print the results. In either case, the action would look like:

x.action = { arg value, cookie, vendorID, productID, locID, val;
 [ value, cookie, vendorID, productID, locID, val].postln;
};

You need to queue the device and start the polling loop before the action will execute.

HIDDeviceServiceExt.queueDeviceByName('USB Joystick STD');
HIDDeviceServiceExt.runEventLoop;

It will start printing a bunch of stuff as you move the controls. When you want it to stop, you can stop the loop.

 HIDDeviceServiceExt.stopEventLoop; 

Ok, so what is all that data. The first is the output of the device scaled to be between 0-1 according to the min and max range it reported by the specification. The next is very important. It's the cookie. The cookie is the ID number for each element. You can use that to figure out which button and widget is which. Once you know this information, you can come up with an IdentityDictionary to refer to every element by a name that you want. Mine looks like:

  j.deviceSpec = IdentityDictionary[
   // buttons
   \a->7, \left->9, \right->10, \down->8, \hat->11,
   // stick
   \x->12, \y->13,
   \wheel->14,
   \throttle->15
  ]; 

The last argument in our action function was the raw data actually produced by the device. You can use all your printed data to figure out actual ranges of highs and lows, or you can set the action to an empty function and then restart the loop. Push each control as far in every direction that it will go. Then stop the loop. Each element will recall the minimum and maximum numbers actually recorded.

(
j.elements.do({arg ele;
   [ele.type, ele.usage, ele.cookie, ele.min, ele.max, ele.minFound, ele.maxFound].postln;
  });
)

This gives us back a table like the one we saw earlier but with two more numbers, the actual min and the actual max. This should improve scaling quite a bit, if you set the elements min and max to those values.   ele.min = ele.minFound;   Speaking of scaling, there are times when you want non-linear or within a different range, so you can set a ControlSpec for every element. Using my identity dictionary:

j.get(\throttle).spec = ControlSpec(-1, 1, step: 0, default: 0);

Every element can have an action, as can the device as a whole and the HIDService as a whole. You can also create HIDElementGroup 's which contain one or more elements and have an action. For example, you could have an action specific for buttons. (Note that if you have an action for an individual button, one for a button group and one for the device, three actions will be evaluated when push the button.) Currently, an element can only belong to one group, although that might change if somebody gives me a good reason.

This is a lot of stuff to configure, so luckily HIDDeviceExt has a config method that looks a lot like make in the conductor class. HIDDeviceExt.config takes a function as an argument. The function you write has as many arguments as you want. The first refers to the HIDDeviceExt that you are configuring. The subsequent ones refer to HIDElementGroup 's. The HIDDeviceExt will create the groups for you and you can manipulate them in your function. You can also set min and max ranges for an entire group and apply a ControlSpec to an entire group. My config is below.

 j.config ({arg thisHID, simpleButtons, buttons, stick;
  
  thisHID.deviceSpec = IdentityDictionary[
   // buttons
   \a->7, \left->9, \right->10, \down->8, \hat->11,
   // stick
   \x->12, \y->13,
   \wheel->14,
   \throttle->15
  ]; 
  
  
  simpleButtons.add(thisHID.get(\a));
  simpleButtons.add(thisHID.get(\left));
  simpleButtons.add(thisHID.get(\right));
  simpleButtons.add(thisHID.get(\down));
  
  buttons.add(simpleButtons);
  buttons.add(thisHID.get(\hat));
  
  stick.add(thisHID.get(\x));
  stick.add(thisHID.get(\y));
  
  // make sure each element is only lsted once in the nodes list, or else it
  // will run it's action once for each time a value arrives for it
  
  thisHID.nodes = [ buttons, stick, thisHID.get(\wheel), thisHID.get(\throttle)];
  
  // When I was testing my joystick, I noticed that it lied a bit about ranges
  
  thisHID.get(\hat).min = -1;  // it gives back -12 for center, but -1 is close enough
  
  // none of the analog inputs ever went below 60
  
  
  thisHID.get(\wheel).min = 60;
  thisHID.get(\throttle).min = 60;
  // can set all stick elements at once
  stick.min = 60;
      
  // ok, now set some ControlSpecs
  
  thisHID.get(\hat).spec = ControlSpec(0, 4, setp:1, default: 0);
  // set all binary buttons at once
  simpleButtons.spec = ControlSpec(0, 1, step: 1, default: 1);
  
  // all stick elements at once
  stick.spec = ControlSpec(-1, 1, step: 0, default: 0);
  thisHID.get(\wheel).spec = ControlSpec(-1, 1, step: 0, default: 0);
  thisHID.get(\throttle).spec = ControlSpec(-1, 1, step: 0, default: 0);
  
  thisHID.action = {arg value, cookie, vendorID, productID, locID, val;
  
   [value, cookie, vendorID, productID, locID, val].postln;
  };
   
 });

I wrote all of this yesterday, so this is very demo/pre-release. If something doesn't work, let me know and I'll fix it. If people have interest, I'll post better versions in the future.

Tags: , ,

No comments: