├── .gitignore ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #midi-ports 2 | 3 | Wraps the MIDIAccess object and builds an object of midi devices with name, inputID, outputID, and manufacturer allowing a more semantic way of interacting with midi devices in the browser. 4 | 5 | For use with the Web MIDI API, check browser support here: http://caniuse.com/#feat=midi 6 | 7 | ##install 8 | ```javascript 9 | npm install midi-ports --save 10 | ``` 11 | ##usage 12 | ```javascript 13 | import midiPorts from 'midi-ports' 14 | 15 | let midi, ports; 16 | 17 | navigator.requestmidiAccess({sysex: true}) // set to true if you need to send sysex messages 18 | .then((midiAccess) => { 19 | midi = midiAccess 20 | ports = midiPorts(midi [,options]) 21 | }) 22 | 23 | //internally builds a ports object: 24 | ports('ports') 25 | /* => 26 | { 27 | 'k-mix-audio-control': { 28 | name: 'K-Mix Audio Control', 29 | inputID: '-543473823', 30 | outputID: '586923498', 31 | manufacturer: 'keith-mcmillen-instruments' 32 | }, 33 | 'k-mix-control-surface': { 34 | name: 'K-Mix Control Surface', 35 | inputID: '-543345892', 36 | outputID: '654298746', 37 | manufacturer: 'keith-mcmillen-instruments' 38 | } 39 | } 40 | */ 41 | ``` 42 | Which enables port access by name: 43 | 44 | ```javascript 45 | // get an input and listen for messages 46 | let kmixInput = ports('k-mix-control-surface').get('input') 47 | kmixInput.onmidimessage = (e) => console.log('data', e.data) 48 | 49 | // or, get an output and send messages 50 | let kmixOutput = ports('k-mix-control-surface').get('output') 51 | kmixOutput.send([176, 1, 64]) 52 | ``` 53 | 54 | ##Grouped Devices 55 | If you want to group ports by device, you can pass in an object as the second param which allows you to get a device:port by name. 56 | 57 | The grouped devices object should be formatted: 58 | 59 | ```javascript 60 | let grouped = { 61 | 'device': { 62 | 'port-name': { }, // empty object which gets populated with name, inputID, outputID, and manufacturer 63 | 'other-info': 'this can be any info you want to reference' 64 | } 65 | } 66 | ``` 67 | 68 | *Port names must be lowercase and hyphen-separated. 69 | 70 | ```javascript 71 | let grouped = { 72 | 'k-mix': { 73 | 'k-mix-audio-control': { }, 74 | 'k-mix-control-surface': { }, 75 | 'icon': 'https://files.keithmcmillen.com/products/k-mix/icons/k-mix.svg', 76 | 'manufacturer': 'Keith McMillen Instruments' 77 | }, 78 | 'k-board':{ 79 | 'k-board': { }, 80 | 'icon': 'https://files.keithmcmillen.com/products/k-board/icons/k-board.svg', 81 | 'manufacturer': 'Keith McMillen Instruments' 82 | } 83 | } 84 | //... 85 | let devices = midiPorts(midi, grouped) 86 | /* 87 | internally builds a devices.ports object: 88 | 89 | devices('devices') => 90 | { 91 | 'k-mix': { 92 | 'k-mix-audio-control': { 93 | name: 'K-Mix Audio Control', 94 | inputID: '-543473823', 95 | outputID: '586923498', 96 | manufacturer: 'keith-mcmillen-instruments' 97 | }, 98 | 'k-mix-control-surface': { 99 | 'name': 'K-Mix Control Surface', 100 | inputID: '-543345892', 101 | outputID: '654298746', 102 | manufacturer: 'keith-mcmillen-instruments' 103 | }, 104 | 'icon': 'https://files.keithmcmillen.com/products/k-mix/icons/k-mix.svg', 105 | 'manufacturer': 'Keith McMillen Instruments' 106 | }, 107 | 'k-board':{ 108 | 'k-board': { 109 | name:'K-Board', 110 | inputID:'1852960744', 111 | outputID:'-162522465', 112 | manufacturer:'kesumo-llc' 113 | }, 114 | 'icon': 'https://files.keithmcmillen.com/products/k-board/icons/k-board.svg', 115 | 'manufacturer': 'Keith McMillen Instruments' 116 | } 117 | } 118 | */ 119 | ``` 120 | To access grouped devices, use the 'device:port-name' format 121 | 122 | ```javascript 123 | let kmixOutput = devices('k-mix:k-mix-audio-control').get('output') 124 | 125 | kmixOutput.send([240, 126, 127, 6, 1, 247]) 126 | ``` 127 | If you want to group a device:port with only one port and its name is the same as the device, you can use a shorthand for getting that port. 128 | 129 | ```javascript 130 | // you can use either 131 | devices('k-board:k-board').get('input') // => midiInput 132 | 133 | // or, the shorter 134 | devices('k-board').get('input') // => midiInput 135 | ``` 136 | 137 | ##Accessing midiAccess The Ports Object, and the Devices Object 138 | You can get direct access to the midiAccess object from within midi-ports, for example, to set a statechange handler, or loop over the 'ports' or 'devices' objects. 139 | 140 | If you're not passing in an grouped devices object, 'ports' and 'devices' reference the same object, otherwise, 'devices' is in the format of grouped devices, 'device.port-name'. 141 | 142 | The 'ports' object is always available and includes *_ALL_* attached ports. 143 | 144 | ```javascript 145 | let devices = midiPorts(midi, grouped) 146 | 147 | // get ports object, a la midi-ports v.1.x 148 | devices('ports') // => {'port-name': { ... }} 149 | 150 | // get device object, a la midi-ports v.1.x 151 | devices('devices') // => {'device': {'port-name': { ... }}} 152 | 153 | // get midiAccess object 154 | devices('access') // => midiAccess 155 | 156 | // get midiAccess inputs / outputs iterator 157 | // ** when getting inputs / outputs from the midi Access object you must use the 'midi' param 158 | devices('midi').get('inputs') // => midiInputMap 159 | 160 | // if using grouped devices object and a device isn't found ** see Error Handling below 161 | devices('notfound') // => array of notfound ports ['k-mix-audio-control','k-mix-control-surface'] 162 | // otherwise returns false 163 | ``` 164 | 165 | ##Setting / Getting data 166 | You can also set arbitrary port-specific data using the set method. Set and Get can be chained. 167 | 168 | ```javascript 169 | let devices = midiPorts(midi) 170 | devices('k-board').set('quality', 'great!') 171 | 172 | // get custom data 173 | devices('k-board').get('quality') // => great! 174 | 175 | // chain set and get 176 | devices('k-board').set('price', '$99').set('review', 'awesome!').get('review') // => 'awesome!' 177 | ``` 178 | 179 | ##Error Handling 180 | 181 | If you're passing in an 'grouped devices' object and that device is not connected/found, midi-ports will add a each not-found port to an internal list, allowing for easier error handling. Fallback ports can be setup by using the 'ports' object if desired. For example, you could loop over 'ports' and build a select menu to allow the user to choose an alternate port. 182 | 183 | In the case above, if 'k-mix' is not connected/found, querying 'notfound' will return a list like this: 184 | 185 | ```javascript 186 | devices('notfound') // => ['k-mix-audio-control','k-mix-control-surface'] 187 | 188 | devices('devices') 189 | /* => { 190 | 'k-board': { 191 | 'k-board': { 192 | name: "K-Board", 193 | inputID: "1852960744", 194 | outputID: "-162522465", 195 | manufacturer: "kesumo-llc" 196 | } 197 | } 198 | } 199 | */ 200 | devices('ports') 201 | /* => { 202 | 'k-board': { 203 | name: "K-Board", 204 | inputID: "1852960744", 205 | outputID: "-162522465", 206 | manufacturer: "kesumo-llc" 207 | } 208 | */ 209 | ``` 210 | Which makes it easy to use alternative ports if your desired port isn't connected/found: 211 | 212 | ```javascript 213 | if(!!ports('notfound')){ 214 | console.warn('device ' + ...devices('notfound') + ' not found') 215 | 216 | // use an alternate port from devices('ports') 217 | // map devices('ports') keys to build select menu 218 | } 219 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIDI device port (input & output) properties [name, ID, manufacturer] 3 | * @function 4 | * @param {Object} midi - MIDI access object 5 | * @returns {Object} Port map of device properties' port (input & output) properties [name, ID, manufacturer] 6 | */ 7 | function getPorts(midi) { 8 | let portMap = {} 9 | 10 | midi.inputs.forEach(function(device) { 11 | portMap[format(device.name)] = { 12 | 'name': device.name, 13 | 'inputID': format(device.id), 14 | 'manufacturer': format(device.manufacturer) 15 | } 16 | }) 17 | 18 | midi.outputs.forEach(function(device) { 19 | if (portMap[format(device.name)]) portMap[format(device.name)]['outputID'] = format(device.id) 20 | }) 21 | 22 | return portMap; 23 | } 24 | 25 | /** 26 | * MIDI device ports allowed for this App. 27 | * @function 28 | * @param {Array} deviceNames - array of MIDI devices derived from source 29 | * @param {Object} ports - Port map of device properties 30 | * @param {Object} source - chosen MIDI device list 31 | * @returns {Object} devices with port data 32 | */ 33 | function buildDevices(deviceNames, ports, source) { 34 | let props = ['name', 'inputID', 'outputID', 'manufacturer'] 35 | 36 | if (!Object.keys(source).length) source = Object.assign({}, ports) 37 | /** 38 | * @let {Array} devices - copied source devices 39 | */ 40 | let devices = Object.assign({}, source), 41 | notfound = [] 42 | 43 | // build devices 44 | deviceNames.forEach(function(device) { 45 | Object.keys(ports).forEach(function(port) { 46 | if (devices[device][port]) { 47 | props.forEach(function(prop) { 48 | devices[device][port][prop] = ports[port][prop] 49 | }) 50 | } 51 | }) 52 | // populate notfound with port 53 | Object.keys(devices[device]).forEach(function(port) { 54 | if (!Object.keys(devices[device][port]).length) { 55 | notfound.push(port) 56 | } 57 | }) 58 | }) 59 | // remove notfound ports from devices 60 | if (notfound.length) { 61 | Object.keys(devices).forEach(function(device) { 62 | Object.keys(devices[device]).forEach(function(port){ 63 | if(notfound.includes(port)) delete devices[device] 64 | }) 65 | }) 66 | } 67 | 68 | return { 69 | devices: devices, 70 | notfound: notfound 71 | } 72 | } 73 | 74 | /** 75 | * Builds MIDI devices object with all port (input & output) properties [name, ID, manufacturer] 76 | * @function 77 | * @param {Object} midi - MIDI access object 78 | * @param {Object} sourceDevices - initial MIDI devices 79 | * @returns {Function} Allowing access to midi access object, ports, device object, and notfound 80 | */ 81 | function midiPorts(midi, source = {}) { 82 | let collections = { 83 | 'access': {}, 84 | 'ports': {}, 85 | 'devices' :{}, 86 | 'notfound' : [] 87 | } 88 | 89 | /** 90 | * @let {Array} deviceNames - desired device names 91 | */ 92 | let deviceNames = Object.keys(source) 93 | 94 | /** 95 | * {Object} midiAccess - midi access object from successful request 96 | */ 97 | collections.access = midi 98 | 99 | /** 100 | * {Object} ports - device ports (input & output) with properties [name, ID, manufacturer] 101 | */ 102 | collections.ports = getPorts(midi) 103 | 104 | /** 105 | * @let {Object} built - All MIDI devices with port [name, ID, manufacturer] properties, notfound devices 106 | */ 107 | let built = buildDevices(deviceNames, collections.ports, source) 108 | collections.devices = built.devices 109 | collections.notfound = built.notfound 110 | 111 | /** 112 | * Returns midi access object, ports, device object, and notfound 113 | * @Function 114 | * param {String} 'access', 'midi', 'notfound', or 'device:port' to get props from 115 | * @example 116 | * // returns device object 117 | * ports('devices') 118 | * @example 119 | * // returns full midi access object 120 | * ports('access') 121 | * @example 122 | * // returns midi inputs Iterator 123 | * ports('midi').get('inputs') 124 | * @example 125 | * // returns Boolean if desired / allowed ports are not found 126 | * ports('notfound') 127 | */ 128 | 129 | return function(device = 'midi') { 130 | // tests 131 | let isAccess = (device === 'access'.toLowerCase()) ? true : false, 132 | isMIDI = (device === 'midi'.toLowerCase()) ? true : false, 133 | isDevices = (device === 'devices'.toLowerCase()) ? true : false, 134 | isPorts = (device === 'ports'.toLowerCase()) ? true : false, 135 | isNotFound = (device === 'notfound'.toLowerCase()) ? true : false, 136 | notFound = (collections.notfound.length) ? true : false, 137 | isDevicePort = (!isAccess && device.toLowerCase().includes(':')) ? true : false, 138 | inDevices = (device in collections.ports || device in collections.devices) ? true : false; 139 | // device:port 140 | let dvc = (isDevicePort) ? device.split(':') : device, 141 | inDevicesPort = (isDevicePort && collections.devices[dvc[0]] && dvc[1] in collections.devices[dvc[0]]) ? true : false; 142 | 143 | if (isPorts) { 144 | return collections.ports 145 | } else if (isDevices) { 146 | return collections.devices 147 | } else if (isAccess) { 148 | return collections.access 149 | } else if(isNotFound){ 150 | if(notFound){ 151 | return collections.notfound 152 | } else { 153 | return false 154 | } 155 | } 156 | /** 157 | * Returns object with get and set methods, set supports chaining 158 | * @Object 159 | */ 160 | return { 161 | /** 162 | * param {String} property from device, midi, or device object 163 | * @example 164 | * // returns device input object 165 | * ports('k-board').get('input') 166 | * @example 167 | * // sets property on device object 168 | * ports('k-board').set('quality', 'great!') 169 | * @example 170 | * // returns property value [name, manufacturer, custom] 171 | * ports('k-board').get('quality')) 172 | */ 173 | get: function get(property) { 174 | if (!property) return 175 | 176 | let prop, 177 | isIO = (property.includes('input') || property.includes('output')), 178 | inputType = (isIO && property.includes('input')) ? 'inputs' : 'outputs', 179 | suffix = (isIO && !isMIDI) ? 'ID' : ''; 180 | 181 | if (isMIDI && !!collections.access[property]) { 182 | prop = collections.access[property] 183 | } else if (isDevicePort && inDevicesPort) { // device:port 184 | if (isIO) { 185 | prop = collections.access[inputType].get(collections.devices[dvc[0]][dvc[1]][property + suffix]) 186 | } else { 187 | prop = collections.devices[dvc[0]][dvc[1]][property] 188 | } 189 | } else if (!isDevicePort && inDevices) { // 'port' 190 | if (isIO) { 191 | if (!!collections.devices[dvc] && !!collections.devices[dvc][dvc]) { // device:device exists 192 | prop = collections.access[inputType].get(collections.devices[dvc][dvc][property + suffix]) 193 | } else if(!!collections.devices[dvc]){ 194 | prop = collections.access[inputType].get(collections.devices[dvc][property + suffix]) 195 | } else if(!!collections.ports[dvc]){ 196 | prop = collections.access[inputType].get(collections.ports[dvc][property + suffix]) 197 | } else { 198 | console.warn('port '+ device +' not found') 199 | } 200 | } else { 201 | if (!!collections.devices[dvc][dvc]) { 202 | prop = collections.devices[dvc][dvc][property] 203 | } else { 204 | prop = collections.devices[dvc][property] 205 | } 206 | } 207 | } else { 208 | console.warn('port '+ device +' not found') 209 | } 210 | 211 | return prop 212 | }, 213 | /** 214 | * param {String} property to be set 215 | * param {String} property value to be set 216 | * @example 217 | * // sets property on device object 218 | * ports('k-board').set('quality', 'great!') 219 | * @example 220 | * // returns property value [name, manufacturer, custom] 221 | * ports('k-board').get('quality')) 222 | */ 223 | set: function set(property, value) { 224 | if(isDevicePort && !!collections.devices[dvc[0]][dvc[1]]){ 225 | collections.devices[dvc[0]][dvc[1]][property] = value 226 | } else if(inDevices){ 227 | if(!!collections.devices[device][device]){ 228 | collections.devices[device][device][property] = value 229 | } else { 230 | collections.devices[device][property] = value 231 | } 232 | } else { 233 | console.warn('port '+ device +' not found') 234 | } 235 | return this 236 | } 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * Formats string to remove spaces and convert to lowercase 243 | * @function 244 | * @param {String} string to format 245 | * @returns {String} formatted string 246 | */ 247 | function format(string) { 248 | return string.toLowerCase().replace(/\s/g, '-').replace(',', '') 249 | } 250 | 251 | module.exports = midiPorts -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midi-ports", 3 | "version": "2.1.0", 4 | "description": "Wraps the MIDIAccess object and builds an object of midi devices with name, inputID, outputID, and manufacturer allowing a more semantic way of interacting with midi devices in the browser.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/AndrejHronco/midi-ports.git" 9 | }, 10 | "homepage": "https://github.com/AndrejHronco/midi-ports", 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "Andrej Hronco", 15 | "license": "MIT", 16 | "keywords": [ 17 | "webmidi", 18 | "midiports", 19 | "midi", 20 | "ports" 21 | ], 22 | "devDependencies": {}, 23 | "dependencies": {} 24 | } 25 | --------------------------------------------------------------------------------