UNPKG

web-midi-test

Version:

Fake Web MIDI API for testing Web MIDI applications

575 lines (540 loc) 18.9 kB
(function(global, factory) { if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define('WMT', [], factory); } else { if (!global) global = window; global.WMT = factory(); } })(this, function() { function _readonly(obj, name, val) { Object.defineProperty(obj, name, { get: function() { return val; }, enumerable: true }); } function _changed(target, data) { if (target.onstatechange) target.onstatechange(new MIDIConnectionEvent(data)); } function _noop() {} var _time = Date.now || function () { return new Date().getTime(); }; var _startTime = _time(); var _now = typeof performance != 'undefined' && performance.now ? function() { return performance.now(); } : function() { return _time() - _startTime; }; var _Acc = []; var _Src = {}; var _Dst = {}; var _heap = []; var _interval; function _insert(x) { var k; var n = _heap.length; _heap.push(x); while(n) { k = Math.floor((n - 1) / 2); if (_heap[k][0] > x[0]) { _heap[n] = _heap[k]; _heap[k] = x; n = k; } else break; } if (_heap.length == 1) { _interval = setInterval(_tick, 1); } } function _remove(i) { var n = _heap.length - 1; var x = _heap[n]; _heap.length = n; if (i < n) { _heap[i] = x; while (true) { n = i * 2 + 2; if (n < _heap.length && _heap[n][0] < _heap[n - 1][0] && _heap[n][0] < _heap[i][0]) { _heap[i] = _heap[n]; _heap[n] = x; i = n; } else { n--; if (n < _heap.length && _heap[n][0] < _heap[i][0]) { _heap[i] = _heap[n]; _heap[n] = x; i = n; } else break; } } } if (!_heap.length) clearInterval(_interval); } function _tick() { while (_heap.length && _heap[0][0] <= _now()) { if (_heap[0][2].connected) _heap[0][2].receive(_heap[0][3]) _remove(0); } } function MIDIMessageEvent(data) { this.data = new Uint8Array(data); this.timeStamp = _now(); Object.freeze(this); } function _split(q) { var i, k; while (q.length) { for (i = 0; i < q.length; i++) if (q[i] == parseInt(q[i]) && q[i] >= 0x80 && q[i] <= 0xff && q[i] != 0xf7) break; q.splice(0, i); if (!q.length) return; if (q[0] == 0xf0) { for (i = 1; i < q.length; i++) if (q[i] == 0xf7) break; if (i == q.length) return; return q.splice(0, i + 1); } else { k = _datalen(q[0]) + 1; if (k > q.length) return; for (i = 1; i < k; i++) if (q[i] != parseInt(q[i]) || q[i] < 0 || q[i] >= 0x80) break; if (i == k) return q.splice(0, i); else q.splice(0, i); } } } function MidiSrc(name, man, ver) { if (!(this instanceof MidiSrc)) return new MidiSrc(name, man, ver); _readonly(this, 'name', '' + name); _readonly(this, 'manufacturer', '' + man); _readonly(this, 'version', '' + ver); var id; var _queue = []; for (var n = 0; true; n++) { id = this.name + '/' + n; if (!_Src[id]) { _readonly(this, 'id', id); _Src[id] = { port: this, connected: false, busy: false, ports: [], pending: [] }; break; } } this.emit = function(arr) { var i, p, msg; _queue = _queue.concat(arr); for (msg = _split(_queue); msg; msg = _split(_queue)) { for (i = 0; i < _Acc.length; i++) { if (msg[0] != 0xf0 || _Acc[i].sysexEnabled) { p = _Acc[i].inputs.get(id); if (p.onmidimessage && p.connection == 'open') p.onmidimessage(new MIDIMessageEvent(msg)); } } } }; Object.defineProperty(this, 'connected', { get: function() { return _Src[id].connected; }, enumerable: true }); Object.defineProperty(this, 'busy', { get: function() { return _Src[id].busy; }, set: function(b) { if (_Src[id].busy != !!b) { _Src[id].busy = !!b; if (_Src[id].busy) for (var i = 0; i < _Src[id].ports.length; i++) _Src[id].ports[i].close().then(_noop, _noop); } }, enumerable: true }); Object.freeze(this); } MidiSrc.prototype.connect = function() { if (!_Src[this.id].connected) { var i; _Src[this.id].connected = true; for (i = 0; i < _Src[this.id].ports.length; i++) _changed(_Src[this.id].ports[i], _Src[this.id].ports[i]); for (i = 0; i < _Acc.length; i++) _changed(_Acc[i], _Acc[i].inputs.get(this.id)); if (_Src[this.id].busy) { for (i = 0; i < _Src[this.id].pending.length; i++) { _Src[this.id].pending[i][2](new DOMException('InvalidAccessError', 'Port is not available', 15)); } } else { for (i = 0; i < _Src[this.id].pending.length; i++) { _Src[this.id].pending[i][1](_Src[this.id].pending[i][0]); } } _Src[this.id].pending = []; } } MidiSrc.prototype.disconnect = function() { if (_Src[this.id].connected) { var i; var x = []; for (i = 0; i < _Acc.length; i++) { x.push([_Acc[i], _Acc[i].inputs.get(this.id)]); } _Src[this.id].connected = false; for (i = 0; i < _Src[this.id].ports.length; i++) _changed(_Src[this.id].ports[i], _Src[this.id].ports[i]); for (i = 0; i < x.length; i++) _changed(x[i][0], x[i][1]); } } function MidiDst(name, man, ver) { if (!(this instanceof MidiDst)) return new MidiDst(name, man, ver); _readonly(this, 'name', '' + name); _readonly(this, 'manufacturer', '' + man); _readonly(this, 'version', '' + ver); var id; for (var n = 0; true; n++) { id = this.name + '/' + n; if (!_Dst[id]) { _readonly(this, 'id', id); _Dst[id] = { port: this, connected: false, busy: false, ports: [], pending: [] }; break; } } this.receive = _noop; Object.defineProperty(this, 'connected', { get: function() { return _Dst[id].connected; }, enumerable: true }); Object.defineProperty(this, 'busy', { get: function() { return _Dst[id].busy; }, set: function(b) { if (_Dst[id].busy != !!b) { _Dst[id].busy = !!b; if (_Dst[id].busy) for (var i = 0; i < _Dst[id].ports.length; i++) _Dst[id].ports[i].close().then(_noop, _noop); } }, enumerable: true }); Object.seal(this); } MidiDst.prototype.connect = function() { if (!_Dst[this.id].connected) { var i; _Dst[this.id].connected = true; for (i = 0; i < _Dst[this.id].ports.length; i++) _changed(_Dst[this.id].ports[i], _Dst[this.id].ports[i]); for (i = 0; i < _Acc.length; i++) _changed(_Acc[i], _Acc[i].outputs.get(this.id)); if (_Dst[this.id].busy) { for (i = 0; i < _Dst[this.id].pending.length; i++) { _Dst[this.id].pending[i][2](new DOMException('InvalidAccessError', 'Port is not available', 15)); } } else { for (i = 0; i < _Dst[this.id].pending.length; i++) { _Dst[this.id].pending[i][1](_Dst[this.id].pending[i][0]); } } _Dst[this.id].pending = []; } } MidiDst.prototype.disconnect = function() { if (_Dst[this.id].connected) { var i; var x = []; for (i = 0; i < _Acc.length; i++) { x.push([_Acc[i], _Acc[i].outputs.get(this.id)]); } _Dst[this.id].connected = false; for (i = 0; i < _Dst[this.id].ports.length; i++) _changed(_Dst[this.id].ports[i], _Dst[this.id].ports[i]); for (i = 0; i < x.length; i++) _changed(x[i][0], x[i][1]); } } function MIDIConnectionEvent(port) { this.port = port; Object.freeze(this); } function MIDIInput(access, port) { var self = this; var _open = false; var _onmidimessage = null; var _onstatechange = null; _readonly(this, 'type', 'input'); _readonly(this, 'id', port.id); _readonly(this, 'name', port.name); _readonly(this, 'manufacturer', port.manufacturer); _readonly(this, 'version', port.version); this.open = function() { return new Promise((resolve, reject) => { if (port.connected) { if (!port.busy) { if (!_open) { _open = true; _changed(self, self); _changed(access, self); } resolve(self); } else { reject(new DOMException('InvalidAccessError', 'Port is not available', 15)); } } else { if (!_open) { _open = true; _changed(self, self); _changed(access, self); } _Src[port.id].pending.push([self, resolve, reject]); } }); }; this.close = function() { return new Promise((resolve, reject) => { var arr = []; for (var i = 0; i < _Src[port.id].pending.length; i++) { if (_Src[port.id].pending[i][0] != self) arr.push(_Src[port.id].pending[i]); else _Src[port.id].pending[i][2](new DOMException('InvalidAccessError', 'Port is closed', 15)); } _Src[port.id].pending = arr; if (_open) { _open = false; _changed(self, self); _changed(access, self); } resolve(self); }); }; Object.defineProperty(this, 'state', { get: function() { return port.connected ? 'connected' : 'disconnected'; }, enumerable: true }); Object.defineProperty(this, 'connection', { get: function() { return port.connected ? _open && !port.busy ? 'open' : 'closed' : _open ? 'pending' : 'closed'; }, enumerable: true }); Object.defineProperty(this, 'onmidimessage', { get: function() { return _onmidimessage; }, set: function(f) { if (!_open) self.open().then(_noop, _noop); _onmidimessage = (typeof f== 'function') ? f : null; }, enumerable: true }); Object.defineProperty(this, 'onstatechange', { get: function() { return _onstatechange; }, set: function(f) { _onstatechange = (typeof f== 'function') ? f : null; }, enumerable: true }); Object.freeze(this); } function _datalen(x) { if (x >= 0x80 && x <= 0xbf || x >= 0xe0 && x <= 0xef || x == 0xf2) return 2; if (x >= 0xc0 && x <= 0xdf || x == 0xf1 || x == 0xf3) return 1; return 0; } var _epr = "Failed to execute 'send' on 'MIDIOutput': "; function _validate(arr, sysex) { var i, k; var msg; var data = []; for (i = 0; i < arr.length; i++) { if (arr[i] != parseInt(arr[i]) || arr[i] < 0 || arr[i] > 255) throw TypeError(_epr + arr[i] + ' is not a UInt8 value.'); } k = 0; for (i = 0; i < arr.length; i++) { if (!k) { if (arr[i] < 0x80) throw TypeError(_epr + 'Running status is not allowed at index ' + i + ' (' + arr[i] + ').'); if (arr[i] == 0xf7) throw TypeError(_epr + 'Unexpected end of system exclusive message at index ' + i + ' (' + arr[i] + ').'); msg = [arr[i]]; data.push(msg); if (arr[i] == 0xf0) { if (!sysex) throw new DOMException('InvalidAccessError', _epr + 'System exclusive messag is not allowed at index ' + i + ' (' + arr[i] + ').', 15); k = -1; for (; i < arr.length; i++) { msg.push(arr[i]); if (arr[i] == 0xf7) { k = 0; break; } } } else { k = _datalen(arr[i]); } } else { if (arr[i] > 0x7f) throw TypeError(_epr + 'Unexpected status byte at index ' + i + ' (' + arr[i] + ').'); msg.push(arr[i]); k--; } } if (k) throw TypeError(_epr + 'Message is incomplete'); return data; } function MIDIOutput(access, port) { var self = this; var _open = false; var _onstatechange = null; _readonly(this, 'type', 'output'); _readonly(this, 'id', port.id); _readonly(this, 'name', port.name); _readonly(this, 'manufacturer', port.manufacturer); _readonly(this, 'version', port.version); this.send = function(arr, t) { var data = _validate(arr, access.sysexEnabled); if (port.busy) return; if (!port.connected) throw new DOMException('InvalidStateError', 'Port is not connected', 11); if (!_open) self.open().then(_noop, _noop); var i; if (t > _now()) for (i = 0; i < data.length; i++) _insert([t, this, port, data[i]]); else for (i = 0; i < data.length; i++) port.receive(data[i]); }; this.open = function() { return new Promise((resolve, reject) => { if (port.connected) { if (!port.busy) { if (!_open) { _open = true; _changed(self, self); _changed(access, self); } resolve(self); } else { reject(new DOMException('InvalidAccessError', 'Port is not available', 15)); } } else { if (!_open) { _open = true; _changed(self, self); _changed(access, self); } _Dst[port.id].pending.push([self, resolve, reject]); } }); }; this.close = function() { this.clear(); return new Promise((resolve, reject) => { var arr = []; for (var i = 0; i < _Dst[port.id].pending.length; i++) { if (_Dst[port.id].pending[i][0] != self) arr.push(_Dst[port.id].pending[i]); else _Dst[port.id].pending[i][2](new DOMException('InvalidAccessError', 'Port is closed', 15)); } _Dst[port.id].pending = arr; if (_open) { _open = false; _changed(self, self); _changed(access, self); } resolve(self); }); }; this.clear = function() { for (var i = _heap.length - 1; i >= 0; i--) if (_heap[i][1] == this) _remove(i); }; Object.defineProperty(this, 'state', { get: function() { return port.connected ? 'connected' : 'disconnected'; }, enumerable: true }); Object.defineProperty(this, 'connection', { get: function() { return port.connected ? _open && !port.busy ? 'open' : 'closed' : _open ? 'pending' : 'closed'; }, enumerable: true }); Object.defineProperty(this, 'onstatechange', { get: function() { return _onstatechange; }, set: function(f) { _onstatechange = (typeof f== 'function') ? f : null; }, enumerable: true }); Object.freeze(this); } function _Maplike(data) { this.has = function(id) { return data.hasOwnProperty(id) && data[id].connected; }; this.keys = function() { var m = new Map(); for (var id in data) if (this.has(id)) m.set(id, this.get(id)); return m.keys(); }; this.values = function() { var m = new Map(); for (var id in data) if (this.has(id)) m.set(id, this.get(id)); return m.values(); }; this.entries = function() { var m = new Map(); for (var id in data) if (this.has(id)) m.set(id, this.get(id)); return m.entries(); }; this.forEach = function(fun, self) { if (typeof self == 'undefined') self = this; for (var id in data) if (this.has(id)) fun.call(self, this.get(id), id, this); }; Object.defineProperty(this, 'size', { get: function() { var len = 0; for (var id in data) if (this.has(id)) len++; return len; }, enumerable: true }); } function MIDIInputMap(_access, _inputs) { this.get = function(id) { if (_Src.hasOwnProperty(id) && _Src[id].connected) { if (!_inputs[id]) { _inputs[id] = new MIDIInput(_access, _Src[id].port); _Src[id].ports.push(_inputs[id]); } return _inputs[id]; } }; Object.freeze(this); } MIDIInputMap.prototype = new _Maplike(_Src); MIDIInputMap.prototype.constructor = MIDIInputMap; function MIDIOutputMap(_access, _outputs) { this.get = function(id) { if (_Dst.hasOwnProperty(id) && _Dst[id].connected) { if (!_outputs[id]) { _outputs[id] = new MIDIOutput(_access, _Dst[id].port); _Dst[id].ports.push(_outputs[id]); } return _outputs[id]; } }; Object.freeze(this); } MIDIOutputMap.prototype = new _Maplike(_Dst); MIDIOutputMap.prototype.constructor = MIDIOutputMap; function MIDIAccess(sysex) { var _inputs = {}; var _outputs = {}; var _onstatechange = null; var self = this; this.sysexEnabled = sysex; this.inputs = new MIDIInputMap(self, _inputs); this.outputs = new MIDIOutputMap(self, _outputs); Object.defineProperty(this, 'onstatechange', { get: function() { return _onstatechange; }, set: function(f) { _onstatechange = (typeof f== 'function') ? f : null; }, enumerable: true }); Object.freeze(this); _Acc.push(this); } function DOMException(name, message, code) { this.name = name; this.message = message; this.code = code; Object.freeze(this); } var _midi = true; var _sysex = true; function requestMIDIAccess(arg) { var sysex = !!(arg && arg.sysex); return new Promise((resolve, reject) => { if (_midi) { if (sysex && !_sysex) { reject(new DOMException('SecurityError', 'Sysex is not allowed', 18)); } else { resolve(new MIDIAccess(sysex)); } } else { reject(new DOMException('SecurityError', 'MIDI is not allowed', 18)); } }); } var WMT = { MidiSrc: MidiSrc, MidiDst: MidiDst, requestMIDIAccess: requestMIDIAccess }; Object.defineProperty(WMT, 'midi', { get: function() { return _midi; }, set: function(b) { _midi = !!b; }, enumerable: true }); Object.defineProperty(WMT, 'sysex', { get: function() { return _sysex; }, set: function(b) { _sysex = !!b; }, enumerable: true }); Object.defineProperty(WMT, 'now', { get: function() { return _now; }, set: function(f) { _now = f; }, enumerable: true }); Object.freeze(WMT); return WMT; });