From 0d914c2ddd2ba9494770193c6fd77940e9009aaa Mon Sep 17 00:00:00 2001 From: gauthiier Date: Mon, 24 Oct 2016 06:31:50 +0200 Subject: [PATCH] haha! --- .gitignore | 44 ++++++++ README | 9 ++ bin/lpd8 | 135 +++++++++++++++++++++++ lib/lpd8-spacebrew.js | 60 ++++++++++ lib/lpd8.js | 201 ++++++++++++++++++++++++++++++++++ package.json | 21 ++++ util/third_octave_midi_map.js | 15 +++ 7 files changed, 485 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100755 bin/lpd8 create mode 100644 lib/lpd8-spacebrew.js create mode 100644 lib/lpd8.js create mode 100644 package.json create mode 100644 util/third_octave_midi_map.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc7fc55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz diff --git a/README b/README new file mode 100644 index 0000000..83e39c6 --- /dev/null +++ b/README @@ -0,0 +1,9 @@ + akai lpd8 midi forwarder/router + -- + Usage: lpd8 [options] + + Options: + + -h, --help output usage information + -V, --version output the version number + -c, --conf [value] LPD8 config file \ No newline at end of file diff --git a/bin/lpd8 b/bin/lpd8 new file mode 100755 index 0000000..d2c14b8 --- /dev/null +++ b/bin/lpd8 @@ -0,0 +1,135 @@ +#! /usr/local/bin/node +'use strict'; + +// straight forward + +var args = require('commander'); + +args + .version('0.1') + .option('-c, --conf [value]', 'LPD8 config file') + .parse(process.argv); + + +var lpd8 = require('../lib/lpd8.js') +var lpd8sb = require('../lib/lpd8-spacebrew.js') + +var midi = require('midi'); +var input = new midi.input(); + +var fs = require('fs'); + +/* - - - + LIST MIDI PORTS +- - - - */ + +function list_ports() { + var ports = [] + for (var i = 0; i < input.getPortCount(); i++) + ports.push(input.getPortName(i)); + return ports; +} + +var input_ports = list_ports(); + +if (input_ports.length < 1){ + console.log('no midi port detected...'); + process.exit(); +} + +var menu = require("terminal-menu")({width:35}); +menu.reset(); +menu.write('AKAI LPD8 midi forward\n'); +menu.write('----------------------\n'); + +for(var i = 0; i < input_ports.length; i++) { + menu.add(input_ports[i]); +} + +menu.add('exit'); + +menu.on('select', function(item, index) { + if (item == 'exit') { + menu.reset(); + menu.close(); + process.exit(); + } + else { + menu.reset(); + menu.close(); + init_lpd8(); + open_midi_port(item, index); + } +}); + +menu.on('close', function () { + process.stdin.setRawMode(false); + process.stdin.end(); +}); + +process.stdin.pipe(menu.createStream()).pipe(process.stdout); +process.stdin.setRawMode(true); + +process.stdin.setEncoding( 'utf8' ); + +process.stdin.on( 'data', function( key ){ + if ( key === '\u001B' ) { + quit(); + process.exit(); + } +}); + +function quit() { + menu.reset(); + menu.close(); + input.closePort(); +} + +/* - - - + LPD8 +- - - - */ + +var LPD8 = null; +var LPD8SB = null; + +function init_lpd8() { + + LPD8 = new lpd8.LPD8(args.conf); + LPD8SB = new lpd8sb.LPD8_SB(LPD8); + + LPD8.set_pad_cb(function (...params) { + LPD8SB.signal(params); + }); + + LPD8.set_kontrol_cb(function (...params) { + LPD8SB.signal(params); + }); + +} + +/* - - - + MIDI +- - - - */ + +input.on('message', function(deltaTime, message) { + if(LPD8) LPD8.midi_signal(message, deltaTime); +}); + +function open_midi_port(port_name, index) { + console.log('opening port: ' + port_name); + input.openPort(index); +} + +function close_midi_port() { + input.closePort() +} + + + + + + + + + + diff --git a/lib/lpd8-spacebrew.js b/lib/lpd8-spacebrew.js new file mode 100644 index 0000000..9ca9ccb --- /dev/null +++ b/lib/lpd8-spacebrew.js @@ -0,0 +1,60 @@ +'use strict'; + +var lpd8 = require('./lpd8.js'); +var sb = require('spacebrew'); + +// this should config file +const server = 'localhost'; +const name = 'lpd8'; +const desc = 'lpd8 midi controller'; + +class LPD8_SB { + + constructor(lpd8) { + + this.lpd8 = lpd8; + this.client = new sb.Client(server, name, desc); + + var i; + for(i in lpd8.PADS) { + let pad = lpd8.PADS[i]; + let padname = 'PAD' + pad.nbr; + this.client.addPublish(padname, 'boolean', false); + } + + for(i in lpd8.KONTROLS) { + let k = lpd8.KONTROLS[i]; + let kname = 'K' + k.nbr; + this.client.addPublish(kname, 'range', 0); + } + + this.client.connect(); + + } + + // rest params + signal(...params) { + + // weird.. params = [[]] + params = params[0]; + + switch(params[0]) + { + case 'pad': + // 'pad', this.nbr, this.note, this.state, this.vel, this.dt + let padname = 'PAD' + params[1]; + this.client.send(padname, 'boolean', params[3]); + break; + case 'k': + // 'k', this.nbr, this.cc, this.kv, this.dt + let kname = 'K' + params[1]; + this.client.send(kname, 'range', params[3]); + break; + default: + break; + } + + } +} + +module.exports.LPD8_SB = LPD8_SB; diff --git a/lib/lpd8.js b/lib/lpd8.js new file mode 100644 index 0000000..bd7b3b8 --- /dev/null +++ b/lib/lpd8.js @@ -0,0 +1,201 @@ + +'use strict'; + +var fs = require('fs'); + +// midi +const NOTEOFF = 0x80; +const NOTEON = 0x90; +const KCHANGE = 0xB0; + +// akai's third ocatave config +var lpd8_midi_notes = { 0x0: "C-2", 0x1: "C#-2", 0x2: "D-2", 0x3: "D#-2", 0x4: "E-2", 0x5: "F-2", + 0x6: "F#-2", 0x7: "G-2", 0x8: "G#-2", 0x9: "A-2", 0xA: "A#-2", 0xB: "B-2", + 0xC: "C-1", 0xD: "C#-1", 0xE: "D-1", 0xF: "D#-1", 0x10: "E-1", 0x11: "F-1", + 0x12: "F#-1", 0x13: "G-1", 0x14: "G#-1", 0x15: "A-1", 0x16: "A#-1", 0x17: "B-1", + 0x18: "C0", 0x19: "C#0", 0x1A: "D0", 0x1B: "D#0", 0x1C: "E0", 0x1D: "F0", + 0x1E: "F#0", 0x1F: "G0", 0x20: "G#0", 0x21: "A0", 0x22: "A#0", 0x23: "B0", + 0x24: "C1", 0x25: "C#1", 0x26: "D1", 0x27: "D#1", 0x28: "E1", 0x29: "F1", + 0x2A: "F#1", 0x2B: "G1", 0x2C: "G#1", 0x2D: "A1", 0x2E: "A#1", 0x2F: "B1", + 0x30: "C2", 0x31: "C#2", 0x32: "D2", 0x33: "D#2", 0x34: "E2", 0x35: "F2", + 0x36: "F#2", 0x37: "G2", 0x38: "G#2", 0x39: "A2", 0x3A: "A#2", 0x3B: "B2", + 0x3C: "C3", 0x3D: "C#3", 0x3E: "D3", 0x3F: "D#3", 0x40: "E3", 0x41: "F3", + 0x42: "F#3", 0x43: "G3", 0x44: "G#3", 0x45: "A3", 0x46: "A#3", 0x47: "B3", + 0x48: "C4", 0x49: "C#4", 0x4A: "D4", 0x4B: "D#4", 0x4C: "E4", 0x4D: "F4", + 0x4E: "F#4", 0x4F: "G4", 0x50: "G#4", 0x51: "A4", 0x52: "A#4", 0x53: "B4", + 0x54: "C5", 0x55: "C#5", 0x56: "D5", 0x57: "D#5", 0x58: "E5", 0x59: "F5", + 0x5A: "F#5", 0x5B: "G5", 0x5C: "G#5", 0x5D: "A5", 0x5E: "A#5", 0x5F: "B5", + 0x60: "C6", 0x61: "C#6", 0x62: "D6", 0x63: "D#6", 0x64: "E6", 0x65: "F6", + 0x66: "F#6", 0x67: "G6", 0x68: "G#6", 0x69: "A6", 0x6A: "A#6", 0x6B: "B6", + 0x6C: "C7", 0x6D: "C#7", 0x6E: "D7", 0x6F: "D#7", 0x70: "E7", 0x71: "F7", + 0x72: "F#7", 0x73: "G7", 0x74: "G#7", 0x75: "A7", 0x76: "A#7", 0x77: "B7", + 0x78: "C8", 0x79: "C#8", 0x7A: "D8", 0x7B: "D#8", 0x7C: "E8", 0x7D: "F8", + 0x7E: "F#8", 0x7F: "G8" + }; + +// default conf +var conf = { + 'CHANNEL': 0, + 'PADS': [ + {'note': 0x24, 'pc': 0, 'cc': 1, 'channel': 0}, + {'note': 0x25, 'pc': 1, 'cc': 2, 'channel': 0}, + {'note': 0x26, 'pc': 2, 'cc': 3, 'channel': 0}, + {'note': 0x27, 'pc': 3, 'cc': 4, 'channel': 0}, + {'note': 0x28, 'pc': 4, 'cc': 5, 'channel': 0}, + {'note': 0x29, 'pc': 5, 'cc': 6, 'channel': 0}, + {'note': 0x2A, 'pc': 6, 'cc': 8, 'channel': 0}, + {'note': 0x2B, 'pc': 7, 'cc': 9, 'channel': 0} + ], + 'KONTROLS': [ + {'cc': 1, 'low': 0, 'hi': 127}, + {'cc': 2, 'low': 0, 'hi': 127}, + {'cc': 3, 'low': 0, 'hi': 127}, + {'cc': 4, 'low': 0, 'hi': 127}, + {'cc': 5, 'low': 0, 'hi': 127}, + {'cc': 6, 'low': 0, 'hi': 127}, + {'cc': 7, 'low': 0, 'hi': 127}, + {'cc': 8, 'low': 0, 'hi': 127} + ] +}; + + +class PAD { + + constructor(pad_nbr, note, pc, cc, channel) { + this.nbr = pad_nbr; + this.note = note; + this.pc = pc; + this.cc = cc; + this.channel = channel; + this.state = false; + this.vel = 0; + this.dt = 0; + } + + on(status_byte, velocity, delta_time, signal) { + if (this.channel == (status_byte & 0x0f)) { + this.state = ((status_byte & 0xf0) == NOTEON); + this.dt = delta_time; + if (signal) + signal('pad', this.nbr, this.note, this.state, this.vel, this.dt); + } + } +} + +class Kontrol { + + constructor(k_nbr, cc, low, hi) { + this.nbr = k_nbr; + this.cc = cc; + this.low = low; + this.hi = hi; + this.kv = 0; + this.dt = 0; + } + + on(value, delta_time, signal) { + this.kv = value; + this.dt = delta_time; + if(signal) + signal('k', this.nbr, this.cc, this.kv, this.dt); + } +} + +class LPD8 { + + constructor(config_file_path) { + + this.conf = parse_lpd8_config(config_file_path) || conf; + this.CHANNEL = this.conf.CHANNEL; + this.PADS = {}; + this.KONTROLS = {}; + + var i = 0; + for(i = 0; i < this.conf.PADS.length; i++) { + let pad = this.conf.PADS[i]; + this.PADS[pad.note] = new PAD(i + 1, pad.note, pad.pc, pad.cc, pad.channel); + } + + for(i = 0; i < this.conf.KONTROLS.length; i++) { + let k = this.conf.KONTROLS[i]; + this.KONTROLS[k.cc] = new Kontrol(i + 1, k.cc, k.low, k.hi); + } + } + + set_pad_cb(callback) { + this.pad_cb = callback; + } + + set_kontrol_cb(callback) { + this.k_cb = callback; + } + + // based on node-midi (not raw midi) + midi_signal(message, dt) { + + var cmd = message[0] & 0xf0; + var channel = message[0] & 0x0f; + + if(this.CHANNEL !== channel) return; + + if(cmd == NOTEOFF || cmd == NOTEON) { + this.PADS[message[1]].on(message[0], message[2], dt, this.pad_cb); + } else if(cmd == KCHANGE) { + this.KONTROLS[message[1]].on(message[2], dt, this.k_cb); + } + } +} + + + +function parse_lpd8_config(config_file_path) { + + try { + fs.accessSync(config_file_path, fs.R_OK) + } catch (e) { + console.log('error reading lpd8 config file\n' + e); + return null; + } + + var data = fs.readFileSync(config_file_path, 'utf8'); + var lpd8_conf = {'PADS': [], 'KONTROLS' : []} + + var tok = data.split(' '); + var channel = parseInt(tok[8], 10); + + lpd8_conf.CHANNEL = channel; + + var i; + for (i = 9; i < (4 * 8) + 9; i += 4) { + lpd8_conf.PADS.push({'note': parseInt(tok[i], 10), + 'pc': parseInt(tok[i + 1], 10), + 'cc': parseInt(tok[i + 2], 10), + 'channel': channel}); + } + + for(; i < (3 * 8) + 41; i += 3) { + lpd8_conf.KONTROLS.push({'cc': parseInt(tok[i], 10), + 'low': parseInt(tok[i + 1], 10), + 'hi': parseInt(tok[i + 2], 10), + 'channel': channel}); + } + + return lpd8_conf; + +} + +module.exports.PAD = PAD; +module.exports.Kontrol = Kontrol; +module.exports.LPD8 = LPD8; + + + + + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..b9eeecd --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "lpd8", + "version": "0.1", + "description": "akai lpd8 midi forwarder/router", + "main": "index.js", + "scripts": { + "test": "echo \"Errorrrr: no no no test specified\" && exit 1" + }, + "keywords": [ + "lpd8", + "midi" + ], + "author": "gauthiier", + "license": "MIT", + "dependencies": { + "commander": "^2.9.0", + "midi": "^0.9.5", + "spacebrew": "0.0.2", + "terminal-menu": "^2.1.1" + } +} diff --git a/util/third_octave_midi_map.js b/util/third_octave_midi_map.js new file mode 100644 index 0000000..5c8d1a0 --- /dev/null +++ b/util/third_octave_midi_map.js @@ -0,0 +1,15 @@ +// for akai lpd8 config [C-2..G8] -- third octave (re: C0 on third octave) +// see /lib/lpd8.js +var oct = ['-2', '-1', '0', '1', '2', '3', '4', '5', '6', '7', '8']; +var notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; +var i = 0, j, k; +var midi_map = {}; + +for(j = 0; j < oct.length; j++) { + for(k = 0; k < notes.length; k++) { + process.stdout.write('0x' + i.toString(16).toUpperCase() + ': "' + notes[k] + oct[j] + '", '); + if (k == 5) process.stdout.write('\n'); + i++; + } + process.stdout.write('\n'); +} \ No newline at end of file