├── .gitignore ├── .npmignore ├── .travis.yml ├── .eslintrc.json ├── package.json ├── LICENSE ├── README.md ├── parser.js └── test └── parser.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .travis.yml 3 | .eslintrc.json 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "indent": [ 18 | "error", 19 | 2 20 | ], 21 | "linebreak-style": [ 22 | "error", 23 | "unix" 24 | ], 25 | "quotes": [ 26 | "error", 27 | "single" 28 | ], 29 | "semi": [ 30 | "error", 31 | "never" 32 | ], 33 | "prefer-const": "error", 34 | "no-var": "error", 35 | "padded-blocks":["error", "never"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midi-parser", 3 | "version": "1.0.1", 4 | "description": "A node js midi data parser that blissfully ignores all hardware and networking.", 5 | "main": "parser.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "lint": "eslint parser.js test/parser.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/reconbot/midi-parser.git" 13 | }, 14 | "keywords": [ 15 | "midi", 16 | "midi-parser" 17 | ], 18 | "author": "Francis Gulotta ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/reconbot/midi-parser/issues" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^6.6.0", 25 | "mocha": "^6.2.2", 26 | "sinon": "^7.5.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Francis Gulotta 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Midi Parser 2 | 3 | [![Build Status](https://travis-ci.org/reconbot/midi-parser.png?branch=master)](https://travis-ci.org/reconbot/midi-parser) 4 | 5 | ## Why 6 | I needed to decode midi data for [FirmataPi](https://github.com/reconbot/firmata-pi). I learned a lot from [node-firmata](https://github.com/jgautier/firmata), [Essentials of the MIDI protocol](https://ccrma.stanford.edu/~craig/articles/linuxmidi/misc/essenmidi.html) and [Control Systems for Live Entertainment](http://www.amazon.com/Control-Systems-Live-Entertainment-Huntington/dp/0240809378) which is a great book, and this nice little C library [Miby: MIDI Byte-stream Parser](https://code.google.com/p/miby/) based off the MIDI 1.0 spec. 7 | 8 | The midi-parser library is a node event emitter. You write midi commands in buffers and it emits `midi` and `sysex` commands as events. We avoid releasing zalgo by always emitting events immediately. 9 | 10 | The SysEx commands are unwrapped of their header and footer bytes and provided on the `sysex` event with the command, and data. Since any multibyte data (eg strings) or values over 127 need to be "14 bit encoded". The class methods `decodeString` and `encodeString` are available to assist. 11 | 12 | The Midi command are emitted on the `midi` event with command, channel (or null if N/A), and an array of data bytes. 13 | 14 | ## Feature Completeness 15 | - Robust error handling (drop anything that doesn't make sense) which is great because MIDI is self synchronizing 16 | - All standard midi commands are handled 17 | - All `system realtime` commands are handled. 18 | - All `Channel Voice` Commands are handled. 19 | - `SysEx` commands are handled. 20 | - `Running status` is currently not implemented. 21 | - `SysEx realtime` commands are not handled specially - I'm not clear if they can occur at anytime. If they occur during normal message flow (eg, not in the middle of something else) they'll work fine. 22 | 23 | ## Contributing 24 | This is MIT licensed work. Feel free to use it, abuse it, complain about performance, bugs, etc. Patches that add core parser or extensibility features are very welcome. The libraries intention is to decode the messages and make them available to another program, not to decode the meanings of these messages for humans. All Patches are welcome with that in mind. 25 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events') 2 | 3 | function channelCmd(byt) { 4 | return byt >= 0x80 && byt <= 0xEF 5 | } 6 | 7 | function dataLength(cmd) { 8 | if (channelCmd(cmd)) { 9 | cmd = cmd & 0xF0 10 | } 11 | let length = msgLength[cmd] 12 | // if we don't know how many data bytes we need assume 2 13 | if (length === undefined) { 14 | length = 2 15 | } 16 | return length 17 | } 18 | 19 | function systemRealTimeByte(byt) { 20 | return byt >= 0xF8 && byt <= 0xFF 21 | } 22 | 23 | function commandByte(byt) { 24 | return byt >= 128 25 | } 26 | 27 | class Parser extends EventEmitter { 28 | constructor() { 29 | super() 30 | this.buffer = [] 31 | } 32 | 33 | write(data) { 34 | for (const byte of data) { 35 | this.writeByte(byte) 36 | } 37 | } 38 | 39 | writeByte(byte) { 40 | if (systemRealTimeByte(byte)) { 41 | return this.emitMidi([byte]) 42 | } 43 | 44 | // if were not in a command and we receive data we've probably lost 45 | // it someplace and we should wait for the next command 46 | if (this.buffer.length === 0 && !commandByte(byte)) { 47 | return 48 | } 49 | 50 | if (this.buffer[0] === msg.startSysex) { 51 | // emit commands 52 | if (byte === msg.endSysex) { 53 | this.emitSysEx(this.buffer.slice(1)) 54 | this.buffer = [] 55 | return 56 | } 57 | 58 | // Store data 59 | if (!commandByte(byte)) { 60 | return this.buffer.push(byte) 61 | } 62 | 63 | 64 | // Clear the buffer if another non realtime command was started 65 | if (commandByte(byte)) { 66 | this.buffer = [] 67 | } 68 | } 69 | 70 | this.buffer.push(byte) 71 | 72 | // once we have enough data bytes emit the cmd 73 | if (dataLength(this.buffer[0]) === (this.buffer.length - 1)) { 74 | this.emitMidi(this.buffer.slice()) 75 | this.buffer = [] 76 | return 77 | } 78 | } 79 | 80 | emitMidi(bytes) { 81 | if (channelCmd(bytes[0])) { 82 | const cmd = bytes[0] & 0xF0 83 | const channel = bytes[0] & 0x0F 84 | return this.emit('midi', cmd, channel, bytes.slice(1)) 85 | } 86 | this.emit('midi', bytes[0], null, bytes.slice(1)) 87 | } 88 | 89 | emitSysEx(bytes) { 90 | this.emit('sysex', bytes[0], bytes.slice(1)) 91 | } 92 | } 93 | 94 | module.exports = Parser 95 | 96 | // Commands that have names that we care about 97 | const msg = Parser.msg = { 98 | noteOff: 0x80, 99 | noteOn: 0x90, // 144 100 | polyAT: 0xA0, // 160 101 | ctrlChg: 0xB0, // 176 102 | progChg: 0xC0, // 192 103 | chanPressure: 0xD0, // 208 104 | pitchBnd: 0xE0, 105 | startSysex: 0xF0, // 240 106 | endSysex: 0xF7, // 247 107 | timeCode: 0xF1, // 241 108 | songPos: 0xF2, 109 | songSel: 0xF3, 110 | tuneReq: 0xF6 111 | } 112 | 113 | // Commands that have a specified lengths for their data 114 | // I wish there were actual rules around this 115 | const msgLength = Parser.msgLength = {} 116 | msgLength[msg.timeCode] = 1 117 | msgLength[msg.songPos] = 2 118 | msgLength[msg.songSel] = 1 119 | msgLength[msg.tuneReq] = 0 120 | msgLength[msg.noteOff] = 2 121 | msgLength[msg.noteOn] = 2 122 | msgLength[msg.polyAT] = 2 123 | msgLength[msg.ctrlChg] = 2 124 | msgLength[msg.progChg] = 1 125 | msgLength[msg.chanPressure] = 1 126 | msgLength[msg.pitchBnd] = 2 127 | 128 | Parser.encodeValue = function (buffer) { 129 | const encoded = [] 130 | for (let i = 0; i < buffer.length; i += 1) { 131 | encoded.push(buffer[i] & 0x7F) // The bottom 7 bits of the byte LSB 132 | encoded.push(buffer[i] >> 7 & 0x7F) // The top 1 bit of the byte MSB 133 | } 134 | 135 | return Buffer.from(encoded) 136 | } 137 | 138 | Parser.encodeString = function (buffer) { 139 | if (typeof buffer === 'string') { 140 | buffer = Buffer.from(buffer, 'ascii') 141 | } 142 | return Parser.encodeValue(buffer) 143 | } 144 | 145 | Parser.decodeValue = function (buffer) { 146 | const decoded = [] 147 | for (let i = 0; i < buffer.length - 1; i += 2) { 148 | const _char = (buffer[i] & 0x7F) | (buffer[i + 1] << 7) 149 | decoded.push(_char) 150 | } 151 | return Buffer.from(decoded) 152 | } 153 | 154 | Parser.decodeString = function (buffer) { 155 | return Parser.decodeValue(buffer).toString('ascii') 156 | } 157 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const Parser = require('../parser') 3 | const sinon = require('sinon') 4 | const msg = Parser.msg 5 | 6 | const soundOff = [188, 120, 0] // All Sound Off - parsed it looks like (176, 12, [120, 0]) 7 | const systemReset = [0xFF] // System Realtime Command 8 | 9 | describe('midi-parser', () => { 10 | let parser 11 | beforeEach(() => { 12 | parser = new Parser() 13 | }) 14 | 15 | it('#emitMidi', () => { 16 | const spy = sinon.spy() 17 | parser.on('midi', spy) 18 | parser.emitMidi(soundOff) 19 | const called = spy.calledWith(176, 12, [120, 0]) 20 | assert.ok(called, 'emits the midi command') 21 | }) 22 | 23 | it('#emitSysEx', () => { 24 | const spy = sinon.spy() 25 | parser.on('sysex', spy) 26 | const message = [99, 0, 0] 27 | parser.emitSysEx(message) 28 | const called = spy.calledWith(99, [0, 0]) 29 | assert.ok(called, 'emits the sysex command') 30 | }) 31 | 32 | it('sysex command', () => { 33 | const message = [msg.startSysex, 99, msg.endSysex] 34 | const spy = sinon.spy() 35 | parser.on('sysex', spy) 36 | parser.write(message) 37 | assert.ok(spy.calledWith(99, []), 'sysex command emitted') 38 | }) 39 | 40 | it('sysex command during command', () => { 41 | const message = [msg.startSysex, msg.startSysex, 99, msg.endSysex] 42 | const spy = sinon.spy() 43 | parser.on('sysex', spy) 44 | parser.write(message) 45 | assert.ok(spy.calledWith(99, []), 'sysex command emitted') 46 | }) 47 | 48 | it('midi command', () => { 49 | const spy = sinon.spy() 50 | parser.on('midi', spy) 51 | parser.write(soundOff) 52 | assert.ok(spy.calledWith(176, 12, [120, 0]), 'midi command emitted') 53 | }) 54 | 55 | it('Command during a Sysex Command clears current Sysex', () => { 56 | const spy = sinon.spy() 57 | parser.on('midi', spy) 58 | parser.write([msg.startSysex]) 59 | parser.write(soundOff) 60 | assert.ok(spy.calledWith(176, 12, [120, 0]), 'midi command emitted') 61 | }) 62 | 63 | it('midi System Realtime Commands emit immediately', () => { 64 | const spy = sinon.spy() 65 | parser.on('midi', spy) 66 | parser.write(systemReset) 67 | assert.ok(spy.calledWith(systemReset[0], null, []), 'realtime command emited') 68 | }) 69 | 70 | it('midi System Realtime Commands emit during sysex', () => { 71 | const spy = sinon.spy() 72 | parser.on('midi', spy) 73 | parser.on('sysex', spy) 74 | parser.write([msg.startSysex, systemReset[0], 99, msg.endSysex]) 75 | assert.ok(spy.getCall(0).calledWith(systemReset[0], null, []), 'realtime command emited') 76 | assert.ok(spy.getCall(1).calledWith(99, []), 'sysex command emited') 77 | }) 78 | 79 | it('midi System Realtime Commands emit during midi channel', () => { 80 | const spy = sinon.spy() 81 | const mixed = [188, 0xFF, 120, 0] 82 | parser.on('midi', spy) 83 | parser.write(mixed) 84 | assert.ok(spy.getCall(0).calledWith(systemReset[0], null, []), 'realtime command emited') 85 | assert.ok(spy.getCall(1).calledWith(176, 12, [120, 0]), 'midi command emited') 86 | }) 87 | 88 | it('midi single byte commands', () => { 89 | const spy = sinon.spy() 90 | parser.on('midi', spy) 91 | parser.write([msg.tuneReq]) // [0xF6] 92 | assert.ok(spy.calledWith(msg.tuneReq, null, []), 'Single byte command emitted') 93 | }) 94 | 95 | it('midi channel voice messages', () => { 96 | const noteOnChan2 = msg.noteOn + 2 97 | const packet = [noteOnChan2, 0, 0] 98 | const spy = sinon.spy() 99 | parser.on('midi', spy) 100 | parser.write(packet) 101 | assert.ok(spy.calledWith(msg.noteOn, 2, [0, 0]), 'Channel voice message') 102 | }) 103 | 104 | it('midi channel voice messages with 1 data byte', () => { 105 | const chanPressure2 = msg.chanPressure + 2 106 | const packet = [chanPressure2, 0] 107 | const spy = sinon.spy() 108 | parser.on('midi', spy) 109 | parser.write(packet) 110 | assert.ok(spy.calledWith(msg.chanPressure, 2, [0]), 'Channel voice message') 111 | }) 112 | 113 | it('.encodeValue', () => { 114 | const message = Buffer.from([245, 0, 1, 128]) 115 | const encodedMessage = Buffer.from([117, 1, 0, 0, 1, 0, 0, 1]) 116 | assert.deepEqual(Parser.encodeValue(message), encodedMessage) 117 | }) 118 | 119 | it('.encodeString', () => { 120 | const message = 'abc' 121 | const encodedMessage = Buffer.from([97, 0, 98, 0, 99, 0]) 122 | assert.deepEqual(Parser.encodeString(message), encodedMessage) 123 | }) 124 | 125 | it('.decodeValue', () => { 126 | const message = Buffer.from([120, 121, 122, 254]) 127 | const encodedMessage = Buffer.from([120, 0, 121, 0, 122, 0, 126, 1]) 128 | assert.deepEqual(Parser.decodeValue(encodedMessage), message) 129 | }) 130 | 131 | it('.decodeString', () => { 132 | const message = 'xyz' 133 | const encodedMessage = Buffer.from([120, 0, 121, 0, 122, 0]) 134 | assert.deepEqual(Parser.decodeString(encodedMessage), message) 135 | }) 136 | }) 137 | --------------------------------------------------------------------------------