Členové projektu

Vratislav Kalenda, Studia nových médií, e-mail: v.kalenda@gmail.com

Marta Kolárová, Studia nových médií, e-mail: martavkolarova@gmail.com

Motivace vzniku projektu a jeho význam

Původním záměrem bylo pomocí leap motion naprogramovat světelné diody - ovládané pohyby. Následně jsme se rozhodli využít leap motion i pro vytváření a modulování zvuku, tj. vytvořit pohyby ovládaný virtuální hudební nástroj v ideálním případě napojený na knihovnu samplovaných zvuků. A ještě kombinovaný s barevnými světelnými diodami, které informují o hraných tónech. Cílem byla zároveň možnost spoluhry více hráčů a vytváření harmonií.

V první fázi byl zvuk generován přímo počítačem (bylo potřeba nastavit temperované ladění a rozmezí oktáv), v závěrečné fázi se ale podařilo systém propojit s knihovnou zvuků GarageBand pomocí protokolu MIDI – v současné době je k dispozici využití veškerých nástrojů GarageBand.

Vlastní zdrojový kód

Upozornění! Vzhledem k omezení syntaxe wikisofia kopírujte prosím zdrojový kód přímo z editace zdroje stránky (nebo využijte možnosti si jej stáhnout), nikoliv z podoby, do jaké je intepretována při zobrazení na stránce. Přislušný úsek je v souladu se syntaxí české wikipedie umístěn mezi značkou <tt> a </tt>.

<tt> var lifx = require('./lifx'); var util = require('util'); var packet = require('./packet'); var leap = require('leapjs'); var midi = require('midi'); var lx = lifx.init(); lx.on('bulbstate', function(b) { //console.log('Bulb state: ' + util.inspect(b)); }); lx.on('bulbonoff', function(b) { //console.log('Bulb on/off: ' + util.inspect(b)); }); Number.prototype.map = function ( in_min , in_max , out_min , out_max ) { return ( this - in_min ) * ( out_max - out_min ) / ( in_max - in_min ) + out_min; } // Set up a new output. var output = new midi.output(); // Count the available output ports. var count = output.getPortCount(); console.log("Available ports: " + count) // Get the name of a specified output port. output.getPortName(0); // Open the first available output port. output.openPort(0); // Send a MIDI message. output.sendMessage([176,22,1]); var damperPedal = true; var toneMap = { 'c' : [ 60,72,84], //'cis' : [277.18,554.36], 'd' : [ 62,74], //'dis' : [ 311.13,622.26], 'e' : [ 64,76], 'f' : [ 65,77], //'fis' : [ 369.00,739.98], 'g' : [67,79], //'gis' : [ 415.00,830.60], 'a' : [69,81], //'ais' : [233.08,466.17], 'h' : [71,83] } var dateDiff = function(start,end){ return (end.getTime() - start.getTime())/1000; } var findTone = function(freq){ var diff = 9999999999; var tone = null; for(var toneName in toneMap){ //console.log("looking in " + toneName) for(var i = 0; i < toneMap[toneName].length; i++){ var newDiff = Math.abs(freq- toneMap[toneName][i]); //console.log("diff " + newDiff) if(newDiff < diff){ diff = newDiff; tone = {name : toneName, freq : toneMap[toneName][i]} //console.log("Found " + toneName + i) } } } if(tone != null){ //console.log("Found " + tone.name + " " + tone.freq + "hz") }else{ //console.log("No tone for " + freq) } return tone; } var midiMessages = { "noteOn" : parseInt('10010000', 2), "noteOff" : parseInt('10000000',2), "mode" : parseInt('10110000',2), "damper" : parseInt('01000000',2), "pedalOff" : 127, "pedalOn" : 0 } var findToneByMidi = function(step){ var diff = 9999999999; var tone = null; var stepIndex = 0; var i = 0; for(i = 0; i< 3; i++){ for(var toneName in toneMap){ //console.log("looking in " + toneName) if(toneMap[toneName].length > i){ var newDiff = Math.abs(step-stepIndex); //console.log("aaa" + newDiff) //console.log("diff " + newDiff) if(newDiff < diff){ diff = newDiff; tone = {name : toneName, freq : toneMap[toneName][i]} //console.log("Found " + toneName + i) } stepIndex++; } } } if(tone != null){ //console.log("Found " + tone.name + " " + tone.freq + "hz") }else{ //console.log("No tone for " + freq) } return tone; } var InterpolatedVal = function(){ this.data = []; this.windowSize = 5; this.add = function(val){ this.data.unshift(val); //console.log("Pusging value " + val + " win size " + this.windowSize) if(this.data.length > this.windowSize){ this.data.pop(val) } //console.log("CData lenght " + this.data.length) } this.getValue = function(){ var result = 0.0; for(var i = 0; i < this.data.length; i++){ result += this.data[i]; } //console.log("value " + result / this.data.length) return result / this.data.length } }; var Hand = function(){ this.grip = new InterpolatedVal(); this.grip.windowSize = 30; this.rotation = new InterpolatedVal(); this.rotation.windowSize = 10; this.bulb = null; this.note = new InterpolatedVal(); }; var leftHand = new Hand(); var rightHand = new Hand(); var baudio = require('baudio'); var leftTone = 440; var rightTone = 440; var curLeftTone = leftTone; var curRightTone = rightTone; var note = null; var n = 0; // var b = baudio({rate: 704000},function (t) { // //console.log(t) // //console.log(leftTone) // var x = Math.sin(2* (rightTone+0.01) * Math.PI * t) ;//+ Math.sin(n)); // var next_x = Math.sin(2* (curLeftTone+0.02) * Math.PI * t); // var a = Math.sin(2* (leftTone+0.01) * Math.PI * t) ; // // if(Math.abs(x) < 0.001 && x < next_x) { // // curLeftTone = leftTone // // x = 0; // // } // // if(Math.abs(a) < 0.001) { // // curRightTone = rightTone // // a = 0; // // } // //n += Math.sin(t); // return (x * rightHand.grip.getValue()) + (a * leftHand.grip.getValue()); // }); // b.play(); // var baudio = require('baudio'), tune = require('tune'); // var blackholesun = tune(String( // 'A4 E4 A5 D5 A5 E4 A4 C4 E4 A5 D5 . . . ' + // 'G3 D4 G4 D5 G4 D4 E3 F#3 C#4 F#4 C#5 . . . ' + // 'E3 F3 C4 F4 A#5 F4 C4 F3 E3 D4 E4 B5 . . .' // ).split(' ')); // var b = baudio(function(t) { // // black hole sun + some fx // return blackholesun(t) + Math.sin(2 * Math.PI * t); // }); // b.play(); leftBulb = {}; leftBulb.isOn = false; rightBulb = {}; rightBulb.isOn = false; lx.on('bulb', function(b) { console.log('New bulb found: ' + b.name + " : " + b.addr.toString("hex")); addr = b.addr.toString("hex"); if(addr == "d073d5001d70"){ console.log("Assigning left bulb"); leftBulb.bulb = lx.bulbs[addr]; leftHand.bulb = leftBulb }else if(addr == "d073d50139d0"){ console.log("Assigning right bulb"); rightHand.bulb = rightBulb rightBulb.bulb = lx.bulbs[addr]; } }); lx.on('gateway', function(g) { console.log('New gateway found: ' + g.ip); }); lx.on('packet', function(p) { // Show informational packets switch (p.packetTypeShortName) { case 'powerState': case 'wifiInfo': case 'wifiFirmwareState': case 'wifiState': case 'accessPoint': case 'bulbLabel': case 'tags': case 'tagLabels': //case 'lightStatus': case 'timeState': case 'resetSwitchState': case 'meshInfo': case 'meshFirmware': case 'versionState': case 'infoState': case 'mcuRailVoltage': console.log(p.packetTypeName + " - " + p.preamble.bulbAddress.toString('hex') + " - " + util.inspect(p.payload)); break; default: break; } }); console.log("Keys:"); console.log("Press 1 to turn the lights on"); console.log("Press 2 to turn the lights off"); console.log("Press 3 to turn the lights a dim red"); console.log("Press 4 to turn the lights a dim purple"); console.log("Press 5 to turn the lights a bright white"); console.log("Press 6 to cycle forwards through colours"); console.log("Press 7 to cycle backwards through colours"); console.log("Press 8 to show debug messages including network traffic"); console.log("Press 9 to hide debug messages including network traffic"); console.log("Press letters a-m to request various status fields"); var stdin = process.openStdin(); process.stdin.setRawMode(true); process.stdin.resume(); var cycledColour = 0; var exit = false; stdin.on('data', function (key) { //process.stdout.write('Got key ' + util.inspect(key) + '\n'); switch (key[0]) { case 0x20: console.log("hit") if(note != null){ sendMidi(null,note.freq,127); } case 0x31: // 1 console.log("Lights on"); // lx.lightsOn('Bedroom Lamp'); // Can specify one bulb by name var b = lx.bulbs['d073d5014163']; lx.lightsOn(b); // lx.lightsOn(); break; case 0x32: // 2 console.log("Lights off"); var b = lx.bulbs['d073d5014163']; lx.lightsOff(b); // lx.lightsOff(); break; case 0x33: // 3 console.log("Dim red"); // BB8 7D0 lx.lightsColour(0x0000, 0xffff, 1000, 0, 0); break; case 0x34: // 4 console.log("Dim purple"); //lx.lightsColour(0x0000, 0xffff, 500, 0, 0); lx.lightsColour(0xcc15, 0xffff, 500, 0, 0); break; case 0x35: // 5 console.log("Bright white"); lx.lightsColour(0x0000, 0x0000, 0x8000, 0x0af0, 0x0513); break; case 0x36: // 6 cycledColour = (cycledColour+100) & 0xffff; console.log("Colour value is " + cycledColour); lx.lightsColour(cycledColour, 0xffff, 0x0200, 0x0000, 0x0000); break; case 0x37: // 7 cycledColour = (cycledColour-100) & 0xffff; console.log("Colour value is " + cycledColour); lx.lightsColour(cycledColour, 0xffff, 0x0200, 0x0000, 0x0000); break; case 0x38: // 8 console.log("Enabling debug"); lifx.setDebug(true); break; case 0x39: // 9 console.log("Disabling debug"); lifx.setDebug(false); break; case 0x61: // a console.log("Requesting voltage"); var message = packet.getMcuRailVoltage(); lx.sendToAll(message); break; case 0x62: // b console.log("Requesting power state"); var message = packet.getPowerState(); lx.sendToAll(message); break; case 0x63: // c console.log("hit") if(note != null){ sendMidi(midiMessages['noteOn'],note.freq,127); } break; case 0x64: // d console.log("Requesting wifi firmware state"); var message = packet.getWifiFirmwareState(); lx.sendToAll(message); break; case 0x65: // e console.log("Requesting wifi state"); var message = packet.getWifiState({interface:2}); lx.sendToAll(message); break; case 0x66: // f console.log("Requesting bulb label"); var message = packet.getBulbLabel(); lx.sendToAll(message); break; case 0x67: // g console.log("Requesting tags"); var message = packet.getTags(); lx.sendToAll(message); break; case 0x68: // h console.log("Requesting tag label for tag 1"); var message = packet.getTagLabels({tags:new Buffer([1,0,0,0,0,0,0,0])}); lx.sendToAll(message); break; case 0x69: // i console.log("Requesting time"); var message = packet.getTime(); lx.sendToAll(message); break; case 0x6a: // j console.log("Requesting info"); var message = packet.getInfo(); lx.sendToAll(message); break; case 0x6b: // k console.log("Requesting reset switch state"); var message = packet.getResetSwitchState(); lx.sendToAll(message); break; case 0x6c: // l console.log("Requesting mesh info"); var message = packet.getMeshInfo(); lx.sendToAll(message); break; case 0x6d: // m console.log("Requesting mesh firmware"); var message = packet.getMeshFirmware(); lx.sendToAll(message); break; case 0x03: // ctrl-c console.log("Closing..."); lx.close(); // Close the port when done. output.closePort(); process.stdin.pause(); exit = true; process.exit(); break; } }); var lastMidiMessage = new Date(); function sendMidi(c,b1,b2){ var diff = dateDiff(lastMidiMessage,new Date()); if(c == null){ c = midiMessages['noteOn'] } console.log("Diff is " + diff); if(diff > 0.10){ lastMidiMessage = new Date(); console.log("Sending c:"+c + " n:" + b1 + " v:" + b2) output.sendMessage([c,b1,b2]); if(c == midiMessages['noteOn']){ setTimeout(function(){ console.log("Sending OFF c:"+midiMessages['noteOff'] + " n:" + b1 + " v:" + b2) output.sendMessage([midiMessages['noteOff'],b1,b2]); }, 20); } } } function vectorToString(vector, digits) { if (typeof digits === "undefined") { digits = 1; } return "(" + vector[0].toFixed(digits) + ", " + vector[1].toFixed(digits) + ", " + vector[2].toFixed(digits) + ")"; } var lightsOn = false; var lastNote = null; var previousFrame = null; gripInterpol = new InterpolatedVal toneInterpol = new InterpolatedVal leap.loop(function(frame){ if (frame.hands.length > 0) { for (var i = 0; i < frame.hands.length; i++) { var hand = frame.hands[i]; var handString = ""; handString += "<div style='width:300px; float:left; padding:5px'>"; handString += "Hand ID: " + hand.id + "<br />"; handString += "Type: " + hand.type + " hand" + "<br />"; handString += "Direction: " + vectorToString(hand.direction, 2) + "<br />"; handString += "Palm position: " + vectorToString(hand.palmPosition) + " mm<br />"; handString += "Grab strength: " + hand.grabStrength + "<br />"; handString += "Pinch strength: " + hand.pinchStrength + "<br />"; handString += "Confidence: " + hand.confidence + "<br />"; handString += "Arm direction: " + vectorToString(hand.arm.direction()) + "<br />"; handString += "Arm center: " + vectorToString(hand.arm.center()) + "<br />"; handString += "Arm up vector: " + vectorToString(hand.arm.basis[1]) + "<br />"; //console.log(tone) //console.log(handString); var handModel = null; if(hand.type == "right"){ handModel = rightHand; }else{ handModel = leftHand; console.log(handModel.grip.getValue()); } handModel.grip.add(hand.grabStrength); handModel.note.add(hand.palmPosition[1]) var value = handModel.note.getValue(); value = Math.max(value,60); value = Math.min(value,500); value = value.map(60,500,0,14) note = findToneByMidi(value); if(handModel.grip.getValue() > 0.8 && damperPedal == true){ console.log("gripping " + handModel.grip.getValue()) //sendMidi(midiMessages["mode"],parseInt('00000001',2),80) sendMidi(midiMessages["mode"],midiMessages["damper"],midiMessages["pedalOff"]) damperPedal = false } else if(damperPedal == false && handModel.grip.getValue() < 0.8){ damperPedal = true console.log("letting go") sendMidi(midiMessages["mode"],midiMessages["damper"],midiMessages["pedalOn"]) //sendMidi(midiMessages["mode"],parseInt('00000001',2),0) } if(lastNote != note.freq){ lastNote = note.freq; console.log(note.freq) } if(note != null && hand.type == "right"){ // Send a MIDI message. //sendMidi(midiMessages['noteOn'],note.freq,127); //rightTone = note.freq; //console.log("assigning right tone " + rightTone) }else if(note != null){ //leftTone = note.freq; //sendMidi(midiMessages['noteOn'],note.freq,127); //console.log("assigning left tone " + leftTone) } if (previousFrame && previousFrame.valid) { var translation = hand.translation(previousFrame); //handString += "Translation: " + vectorToString(translation) + " mm<br />"; var rotationAxis = hand.rotationAxis(previousFrame, 2); var rotationAngle = hand.rotationAngle(previousFrame); handModel.rotation.add(rotationAxis[0]); //console.log('rotation ' + handModel.rotation.getValue()) //handString += "Rotation axis: " + vectorToString(rotationAxis) + "<br />"; //console.log(handString) } // Store frame for motion functions previousFrame = frame; // if(handModel.grip.getValue() >= 1 && handModel.bulb.isOn == false){ // lx.lightsOn(handModel.bulb.bulb); // handModel.bulb.isOn = true; // }else if(handModel.grip.getValue() <= 0.2 && handModel.bulb.isOn == true){ // lx.lightsOff(handModel.bulb.bulb); // handModel.bulb.isOn = false; // } } } }); </tt>

Popis vývoje a konečné verze vlastního produktu

Virtuální hudební nástroj napojený na knihovnu zvuků GarageBand pomocí protokolu MIDI ovládaný pomocí leap motion. Pohyby nahoru a dolů se hráč pohybuje po tónech stupnice (zatím C-Dur) a gesty je možné spouštět doplňkové funkce (např sevřením pěsti se tón prodlouží – podobně jako po sešlápnutí pedálu na pianu). Hra je určena pro jednoho až dva hráče (každý využívá vlastní PC).

Zajímavá je možnost propojení s MIDI knihovnou a díky tomu rozšíření na prakticky neomezený počet zvuků. Virtuální nástroj lze poměrně snadno nainstalovat (zatím jen pro Mac OS) a při hraní více hráčů vytvořit virtuální kapelu. Vytvoření virtuálního nástroje, který je možné ovládat pomocí gest, je přínosné nejen jako zábavný prostředek pro vytváření hudby, ale lze využít i jako základ pro výukové hudební aplikace.

Příklad využití pro výukovou hru

Výuka intervalů a melodií – správný tón pomáhají určit barevné diody (ukazují, kterým směrem je potřeba postupovat).

Virtuální hudební nástroj s výukou melodií

1. level
a. PC vygeneruje náhodný tón z vybrané stupnice (zatím C-Dur)
b. Hráč má za úkol tón najít
c. Vloží ruku nad snímač, uslyší tón, na kterém se zrovna nachází a snaží se k tónu doladit
d. Diody budou signalizovat, jestli je příliš vysoko nebo nízko
d1. Pravá dioda signalizuje že má jít výš
d2. Levá, že má jít níž (vložím ruku a jsem příliš vysoko, rozsvítí se červená na levé diodě, při pohybu rukou svítí červeně vždy dioda, která určuje směr, při správném určení tónu se obě rozsvítí zeleně)
d3. Po stisknutí vybrané klávesy lze vyvolat nápovědu (tón bude znít znovu a když bude nápovědu držet, může se doladit live)
d4. Na obrazovce se objevuje název tónu

2. level
a. Melodie složená ze dvou tónů, vygenerováno náhodně PC = výuka intervalů
b. Cílem je zopakovat správně interval (melodii složenou ze dvou tónů)
c. K prvnímu tónu se hráč doladí jako v 1. levelu, druhý tón funguje stejně (signalizace diod, možnost vyvolání nápovědy)
d. (Aby byl úkol splněn, musí pak zahrát posloupnost tónů v čase max. 3 s)
e. Pokud bude možné zobrazovat název tónu, hráč by viděl, jaký hraje interval

3. level: Melodie složená ze tří tónů

4. level: Melodie složená ze čtyř tónů

5. level: Melodie složená z pěti tónů

6. level: Melodie složená ze 6 tónů

7. level: Melodie složená ze 7 tónů

8. level: Melodie složená z 8 tónů

9. level: Jednoduché písničky (mohou být vybírány náhodně nebo ze seznamu)

Fotodokumentace