NodeJSでJoyConを電子ドラムにした
なんかWebHID+JoyConができても先が長い気がして、で最終的にやりたかったことのもう半分であるところの、ドラム風のなにかをやってみた。
使ったライブラリ
Windowsで使う場合、device.productが Wireless Gamepad みたいな名前になってしまってうまく検出できなかったので、productId で判別するように改造して使った
あと findControllers を一度呼ぶと無限にポーリングするのがレイテンシにつながりそうなので、これも切った
WebHIDと組み合わせるには扱いにくいこともあり、そのうち似たようなものを自分で書きそうだけど
検出周り
ns-joyconから取れる packet.actualAccelerometer.acc.{x/y/z} を1000倍して、min/max/avgを取りながら振ってみて、なんとなく絶対値40ぐらいあればそんなに誤検知もせずにすむのでは、という感じ
単一のしきい値でやるならこんなもんかなというとろ。正確さを求めるとあれだけど、慣れれば割となんとかなる。
ボタンにも音を割り当てておくと、鳴らせる音の幅が広がる&その場の気分で柔軟にやれる
レイテンシ: MIDI以降のレイテンシを最小化しておけば、曲にあわせて鳴らすとかもいける感じ。
code:joycon-drum.js
// license: CC0
const JoyCon = require('ns-joycon');
const Midi = require('midi');
const hit = () => {
let cont = false;
return v => {
if (Math.abs(v) > 40) return cont ? false : cont=true;
else return cont=false;
};
};
const wait = ms => new Promise(x => setTimeout(x, ms));
const knownDevices = new Set();
const midiOut = new Midi.Output();
midiOut.openPort(midiOut.getPortCount()-1);
const playNote = async (ch, noteNo, velo, ms) => {
await wait(ms);
}
JoyCon.findControllers(devices => {
devices.forEach(async device => {
if (knownDevices.has(device.meta.serialNumber)) return;
knownDevices.add(device.meta.serialNumber);
console.log(device);
const hit_acc_x = hit();
const hit_acc_y = hit();
const hit_acc_z = hit();
const hit_symbal = hit();
device.manageHandler('add', packet => {
if (!packet.actualAccelerometer) return;
const acc_x = Math.floor(packet.actualAccelerometer.acc.x * 1000);
const acc_y = Math.floor(packet.actualAccelerometer.acc.y * 1000);
const acc_z = Math.floor(packet.actualAccelerometer.acc.z * 1000);
const dev = device.meta.productId == 8198 ? "Joy-Con (L)" : "Joy-Con (R)";
if (hit_acc_x(acc_x + (packet.buttonStatus.down || packet.buttonStatus.b ? 100 : 0))) {
console.log(HIT ${dev} X: ${acc_x});
playNote(10, 36, 127, 100); // bass
}
if (hit_acc_y(acc_y + (packet.buttonStatus.left || packet.buttonStatus.y ? 100 : 0))) {
console.log(HIT ${dev} Y: ${acc_y});
playNote(10, 42, 127, 100); // hi-hat
}
if (hit_acc_z(acc_z + (packet.buttonStatus.up || packet.buttonStatus.x ? 100 : 0))) {
console.log(HIT ${dev} Z: ${acc_z});
playNote(10, 38, 127, 100); // snare
}
if (hit_symbal(50 * (packet.buttonStatus.right || packet.buttonStatus.a || packet.buttonStatus.r))) {
playNote(10, 49, 127, 100); // crash symbal
}
});
await device.disableIMU();
await device.enableIMU();
});
});