UNPKG

qambi

Version:

MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio

860 lines (759 loc) 27.5 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.Track = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _part = require('./part'); var _midi_event = require('./midi_event'); var _midi_note = require('./midi_note'); var _init_midi = require('./init_midi'); var _util = require('./util'); var _init_audio = require('./init_audio'); var _qambi = require('./qambi'); var _eventlistener = require('./eventlistener'); function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var zeroValue = 0.00000000000000001; var instanceIndex = 0; var Track = exports.Track = function () { function Track() { var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _classCallCheck(this, Track); this.id = this.constructor.name + '_' + instanceIndex++ + '_' + new Date().getTime(); //console.log(this.name, this.channel, this.muted, this.volume) var _settings$name = settings.name; this.name = _settings$name === undefined ? this.id : _settings$name; var _settings$channel = settings.channel; this.channel = _settings$channel === undefined ? 0 : _settings$channel; var _settings$muted = settings.muted; this.muted = _settings$muted === undefined ? false : _settings$muted; var _settings$volume = settings.volume; this.volume = _settings$volume === undefined ? 0.5 : _settings$volume; this._panner = _init_audio.context.createPanner(); this._panner.panningModel = 'equalpower'; this._panner.setPosition(zeroValue, zeroValue, zeroValue); this._gainNode = _init_audio.context.createGain(); this._gainNode.gain.value = this.volume; this._panner.connect(this._gainNode); //this._gainNode.connect(this._panner) this._midiInputs = new Map(); this._midiOutputs = new Map(); this._song = null; this._parts = []; this._partsById = new Map(); this._events = []; this._eventsById = new Map(); this._needsUpdate = false; this._createEventArray = false; this._instrument = null; this._tmpRecordedNotes = new Map(); this._recordedEvents = []; this.scheduledSamples = new Map(); this.sustainedSamples = []; this.sustainPedalDown = false; this.monitor = false; this._songGainNode = null; this._effects = []; this._numEffects = 0; var parts = settings.parts, instrument = settings.instrument; if (typeof parts !== 'undefined') { this.addParts.apply(this, _toConsumableArray(parts)); } if (typeof instrument !== 'undefined') { this.setInstrument(instrument); } } _createClass(Track, [{ key: 'setInstrument', value: function setInstrument() { var instrument = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; if (instrument !== null // check if the mandatory functions of an instrument are present (Interface Instrument) && typeof instrument.connect === 'function' && typeof instrument.disconnect === 'function' && typeof instrument.processMIDIEvent === 'function' && typeof instrument.allNotesOff === 'function' && typeof instrument.unschedule === 'function') { this.removeInstrument(); this._instrument = instrument; this._instrument.connect(this._panner); } else if (instrument === null) { // if you pass null as argument the current instrument will be removed, same as removeInstrument this.removeInstrument(); } else { console.log('Invalid instrument, and instrument should have the methods "connect", "disconnect", "processMIDIEvent", "unschedule" and "allNotesOff"'); } } }, { key: 'removeInstrument', value: function removeInstrument() { if (this._instrument !== null) { this._instrument.allNotesOff(); this._instrument.disconnect(); this._instrument = null; } } }, { key: 'getInstrument', value: function getInstrument() { return this._instrument; } }, { key: 'connectMIDIOutputs', value: function connectMIDIOutputs() { var _this = this; for (var _len = arguments.length, outputs = Array(_len), _key = 0; _key < _len; _key++) { outputs[_key] = arguments[_key]; } //console.log(outputs) outputs.forEach(function (output) { if (typeof output === 'string') { output = (0, _init_midi.getMIDIOutputById)(output); } // if (output instanceof MIDIOutput) { if (output.type === 'output') { _this._midiOutputs.set(output.id, output); } }); //console.log(this._midiOutputs) } }, { key: 'disconnectMIDIOutputs', value: function disconnectMIDIOutputs() { var _this2 = this; for (var _len2 = arguments.length, outputs = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { outputs[_key2] = arguments[_key2]; } //console.log(outputs) if (outputs.length === 0) { this._midiOutputs.clear(); } outputs.forEach(function (port) { // if (port instanceof MIDIOutput) { if (port.type === 'output') { port = port.id; } if (_this2._midiOutputs.has(port)) { //console.log('removing', this._midiOutputs.get(port).name) _this2._midiOutputs.delete(port); } }); //this._midiOutputs = this._midiOutputs.filter(...outputs) //console.log(this._midiOutputs) } }, { key: 'connectMIDIInputs', value: function connectMIDIInputs() { var _this3 = this; for (var _len3 = arguments.length, inputs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { inputs[_key3] = arguments[_key3]; } //console.log(Object.getPrototypeOf(MIDIInput)); inputs.forEach(function (input) { if (typeof input === 'string') { input = (0, _init_midi.getMIDIInputById)(input); } // if (input instanceof MIDIInput) { if (input.type === 'input') { _this3._midiInputs.set(input.id, input); input.onmidimessage = function (e) { if (_this3.monitor === true) { //console.log(...e.data) _this3._preprocessMIDIEvent(new (Function.prototype.bind.apply(_midi_event.MIDIEvent, [null].concat([_this3._song._ticks], _toConsumableArray(e.data))))()); } }; } }); //console.log(this._midiInputs) } // you can pass both port and port ids }, { key: 'disconnectMIDIInputs', value: function disconnectMIDIInputs() { var _this4 = this; for (var _len4 = arguments.length, inputs = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { inputs[_key4] = arguments[_key4]; } if (inputs.length === 0) { this._midiInputs.forEach(function (port) { port.onmidimessage = null; }); this._midiInputs.clear(); return; } inputs.forEach(function (port) { // if (port instanceof MIDIInput) { if (port.type === 'input') { port = port.id; } if (_this4._midiInputs.has(port)) { _this4._midiInputs.get(port).onmidimessage = null; _this4._midiInputs.delete(port); } }); //this._midiOutputs = this._midiOutputs.filter(...outputs) //console.log(this._midiInputs) } }, { key: 'getMIDIInputs', value: function getMIDIInputs() { return Array.from(this._midiInputs.values()); } }, { key: 'getMIDIOutputs', value: function getMIDIOutputs() { return Array.from(this._midiOutputs.values()); } }, { key: 'setRecordEnabled', value: function setRecordEnabled(type) { // 'midi', 'audio', empty or anything will disable recording this._recordEnabled = type; } }, { key: '_startRecording', value: function _startRecording(recordId) { if (this._recordEnabled === 'midi') { //console.log(recordId) this._recordId = recordId; this._recordedEvents = []; this._recordPart = new _part.Part(this._recordId); } } }, { key: '_stopRecording', value: function _stopRecording(recordId) { var _recordPart; if (this._recordId !== recordId) { return; } if (this._recordedEvents.length === 0) { return; } (_recordPart = this._recordPart).addEvents.apply(_recordPart, _toConsumableArray(this._recordedEvents)); //this._song._newEvents.push(...this._recordedEvents) this.addParts(this._recordPart); } }, { key: 'undoRecording', value: function undoRecording(recordId) { if (this._recordId !== recordId) { return; } this.removeParts(this._recordPart); //this._song._removedEvents.push(...this._recordedEvents) } }, { key: 'redoRecording', value: function redoRecording(recordId) { if (this._recordId !== recordId) { return; } this.addParts(this._recordPart); } }, { key: 'copy', value: function copy() { var t = new Track(this.name + '_copy'); // implement getNameOfCopy() in util (see heartbeat) var parts = []; this._parts.forEach(function (part) { var copy = part.copy(); console.log(copy); parts.push(copy); }); t.addParts.apply(t, parts); t.update(); return t; } }, { key: 'transpose', value: function transpose(amount) { this._events.forEach(function (event) { event.transpose(amount); }); } }, { key: 'addParts', value: function addParts() { var _this5 = this; var song = this._song; for (var _len5 = arguments.length, parts = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { parts[_key5] = arguments[_key5]; } parts.forEach(function (part) { var _events; part._track = _this5; _this5._parts.push(part); _this5._partsById.set(part.id, part); var events = part._events; (_events = _this5._events).push.apply(_events, _toConsumableArray(events)); if (song) { var _song$_newEvents; part._song = song; song._newParts.push(part); (_song$_newEvents = song._newEvents).push.apply(_song$_newEvents, _toConsumableArray(events)); } events.forEach(function (event) { event._track = _this5; if (song) { event._song = song; } _this5._eventsById.set(event.id, event); }); }); this._needsUpdate = true; } }, { key: 'removeParts', value: function removeParts() { var _this6 = this; var song = this._song; for (var _len6 = arguments.length, parts = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { parts[_key6] = arguments[_key6]; } parts.forEach(function (part) { part._track = null; _this6._partsById.delete(part.id, part); var events = part._events; if (song) { var _song$_removedEvents; song._removedParts.push(part); (_song$_removedEvents = song._removedEvents).push.apply(_song$_removedEvents, _toConsumableArray(events)); } events.forEach(function (event) { event._track = null; if (song) { event._song = null; } _this6._eventsById.delete(event.id, event); }); }); this._needsUpdate = true; this._createEventArray = true; } }, { key: 'getParts', value: function getParts() { if (this._needsUpdate) { this._parts = Array.from(this._partsById.values()); this._events = Array.from(this._eventsById.values()); this._needsUpdate = false; } return [].concat(_toConsumableArray(this._parts)); } }, { key: 'transposeParts', value: function transposeParts(amount) { for (var _len7 = arguments.length, parts = Array(_len7 > 1 ? _len7 - 1 : 0), _key7 = 1; _key7 < _len7; _key7++) { parts[_key7 - 1] = arguments[_key7]; } parts.forEach(function (part) { part.transpose(amount); }); } }, { key: 'moveParts', value: function moveParts(ticks) { for (var _len8 = arguments.length, parts = Array(_len8 > 1 ? _len8 - 1 : 0), _key8 = 1; _key8 < _len8; _key8++) { parts[_key8 - 1] = arguments[_key8]; } parts.forEach(function (part) { part.move(ticks); }); } }, { key: 'movePartsTo', value: function movePartsTo(ticks) { for (var _len9 = arguments.length, parts = Array(_len9 > 1 ? _len9 - 1 : 0), _key9 = 1; _key9 < _len9; _key9++) { parts[_key9 - 1] = arguments[_key9]; } parts.forEach(function (part) { part.moveTo(ticks); }); } /* addEvents(...events){ let p = new Part() p.addEvents(...events) this.addParts(p) } */ }, { key: 'removeEvents', value: function removeEvents() { var _this7 = this; var parts = new Set(); for (var _len10 = arguments.length, events = Array(_len10), _key10 = 0; _key10 < _len10; _key10++) { events[_key10] = arguments[_key10]; } events.forEach(function (event) { parts.set(event._part); event._part = null; event._track = null; event._song = null; _this7._eventsById.delete(event.id); }); if (this._song) { var _song$_removedEvents2, _song$_changedParts; (_song$_removedEvents2 = this._song._removedEvents).push.apply(_song$_removedEvents2, events); (_song$_changedParts = this._song._changedParts).push.apply(_song$_changedParts, _toConsumableArray(Array.from(parts.entries()))); } this._needsUpdate = true; this._createEventArray = true; } }, { key: 'moveEvents', value: function moveEvents(ticks) { var parts = new Set(); for (var _len11 = arguments.length, events = Array(_len11 > 1 ? _len11 - 1 : 0), _key11 = 1; _key11 < _len11; _key11++) { events[_key11 - 1] = arguments[_key11]; } events.forEach(function (event) { event.move(ticks); parts.set(event.part); }); if (this._song) { var _song$_movedEvents, _song$_changedParts2; (_song$_movedEvents = this._song._movedEvents).push.apply(_song$_movedEvents, events); (_song$_changedParts2 = this._song._changedParts).push.apply(_song$_changedParts2, _toConsumableArray(Array.from(parts.entries()))); } } }, { key: 'moveEventsTo', value: function moveEventsTo(ticks) { var parts = new Set(); for (var _len12 = arguments.length, events = Array(_len12 > 1 ? _len12 - 1 : 0), _key12 = 1; _key12 < _len12; _key12++) { events[_key12 - 1] = arguments[_key12]; } events.forEach(function (event) { event.moveTo(ticks); parts.set(event.part); }); if (this._song) { var _song$_movedEvents2, _song$_changedParts3; (_song$_movedEvents2 = this._song._movedEvents).push.apply(_song$_movedEvents2, events); (_song$_changedParts3 = this._song._changedParts).push.apply(_song$_changedParts3, _toConsumableArray(Array.from(parts.entries()))); } } }, { key: 'getEvents', value: function getEvents() { var filter = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; // can be use as findEvents if (this._needsUpdate) { this.update(); } return [].concat(_toConsumableArray(this._events)); //@TODO implement filter -> filterEvents() should be a utility function (not a class method) } }, { key: 'mute', value: function mute() { var flag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; if (flag) { this._muted = flag; } else { this._muted = !this._muted; } } }, { key: 'update', value: function update() { // you should only use this in huge songs (>100 tracks) if (this._createEventArray) { this._events = Array.from(this._eventsById.values()); this._createEventArray = false; } (0, _util.sortEvents)(this._events); this._needsUpdate = false; } }, { key: '_checkEffect', value: function _checkEffect(effect) { if (effect.input instanceof AudioNode === false || effect.output instanceof AudioNode === false) { console.log('A channel fx should have an input and an output implementing the interface AudioNode'); return false; } return true; } // routing: audiosource -> panning -> track output -> [...effect] -> song input }, { key: 'insertEffect', value: function insertEffect(effect) { if (this._checkEffect(effect) === false) { return; } var prevEffect = void 0; if (this._numEffects === 0) { this._gainNode.disconnect(this._songGainNode); this._gainNode.connect(effect.input); effect.output.connect(this._songGainNode); } else { prevEffect = this._effects[this._numEffects - 1]; try { prevEffect.output.disconnect(this._songGainNode); } catch (e) { //Chrome throws an error here which is wrong } prevEffect.output.connect(effect.input); effect.output.connect(this._songGainNode); } this._effects.push(effect); this._numEffects++; } }, { key: 'insertEffectAt', value: function insertEffectAt(effect, index) { if (this._checkEffect(effect) === false) { return; } var prevEffect = this._effects[index - 1]; var nextEffect = void 0; if (index === this._numEffects) { prevEffect.output.disconnect(this._songGainNode); prevEffect.output.connect(effect.input); effect.input.connect(this._songGainNode); } else { nextEffect = this._effects[index]; prevEffect.output.disconnect(nextEffect.input); prevEffect.output.connect(effect.input); effect.output.connect(nextEffect.input); } this._effects.splice(index, 0, effect); this._numEffects++; } //removeEffect(effect: Effect){ }, { key: 'removeEffect', value: function removeEffect(effect) { if (this._checkEffect(effect) === false) { return; } var i = void 0; for (i = 0; i < this._numEffects; i++) { var fx = this._effects[i]; if (effect === fx) { break; } } this.removeEffectAt(i); } }, { key: 'removeEffectAt', value: function removeEffectAt(index) { if (isNaN(index) || this._numEffects === 0 || index >= this._numEffects) { return; } var effect = this._effects[index]; var nextEffect = void 0; var prevEffect = void 0; //console.log(index, this._effects) if (index === 0) { // we remove the first effect, so disconnect from output of track this._gainNode.disconnect(effect.input); if (this._numEffects === 1) { // no effects anymore, so connect output of track to input of the song try { effect.output.disconnect(this._songGainNode); } catch (e) { //Chrome throws an error here which is wrong } this._gainNode.connect(this._songGainNode); } else { // disconnect the removed effect from the next effect in the chain, this is now the first effect in the chain... nextEffect = this._effects[index + 1]; try { effect.output.disconnect(nextEffect.input); } catch (e) {} //Chrome throws an error here which is wrong // ... so connect the output of the track to the input of this effect this._gainNode.connect(nextEffect.input); } } else { prevEffect = this._effects[index - 1]; //console.log(prevEffect) // disconnect the removed effect from the previous effect in the chain try { prevEffect.output.disconnect(effect.input); } catch (e) { //Chrome throws an error here which is wrong } if (index === this._numEffects - 1) { // we remove the last effect in the chain, so disconnect from the input of the song try { effect.output.disconnect(this._songGainNode); } catch (e) {} //Chrome throws an error here which is wrong // the previous effect is now the last effect to connect it to the input of the song prevEffect.output.connect(this._songGainNode); } else { // disconnect the effect from the next effect in the chain nextEffect = this._effects[index]; effect.output.disconnect(nextEffect.input); // connect the previous effect to the next effect prevEffect.output.connect(nextEffect.input); } } this._effects.splice(index, 1); this._numEffects--; } }, { key: 'getEffects', value: function getEffects() { return [].concat(_toConsumableArray(this._effects)); } }, { key: 'getEffectAt', value: function getEffectAt(index) { if (isNaN(index)) { return null; } return this._effects[index]; } }, { key: 'getOutput', value: function getOutput() { return this._gainNode; } }, { key: 'getInput', value: function getInput() { return this._songGainNode; } // method is called when a MIDI events is send by an external or on-screen keyboard }, { key: '_preprocessMIDIEvent', value: function _preprocessMIDIEvent(midiEvent) { var time = _init_audio.context.currentTime * 1000; midiEvent.time = time; midiEvent.time2 = 0; //performance.now() -> passing 0 has the same effect as performance.now() so we choose the former midiEvent.recordMillis = time; var note = void 0; if (midiEvent.type === _qambi.MIDIEventTypes.NOTE_ON) { note = new _midi_note.MIDINote(midiEvent); this._tmpRecordedNotes.set(midiEvent.data1, note); (0, _eventlistener.dispatchEvent)({ type: 'noteOn', data: midiEvent }); } else if (midiEvent.type === _qambi.MIDIEventTypes.NOTE_OFF) { note = this._tmpRecordedNotes.get(midiEvent.data1); if (typeof note === 'undefined') { return; } note.addNoteOff(midiEvent); this._tmpRecordedNotes.delete(midiEvent.data1); (0, _eventlistener.dispatchEvent)({ type: 'noteOff', data: midiEvent }); } if (this._recordEnabled === 'midi' && this._song.recording === true) { this._recordedEvents.push(midiEvent); } this.processMIDIEvent(midiEvent); } // method is called by scheduler during playback }, { key: 'processMIDIEvent', value: function processMIDIEvent(event) { if (typeof event.time === 'undefined') { this._preprocessMIDIEvent(event); return; } // send to javascript instrument if (this._instrument !== null) { //console.log(this.name, event) this._instrument.processMIDIEvent(event); } // send to external hardware or software instrument this._sendToExternalMIDIOutputs(event); } }, { key: '_sendToExternalMIDIOutputs', value: function _sendToExternalMIDIOutputs(event) { //console.log(event.time, event.millis) var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = this._midiOutputs.values()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var port = _step.value; if (port) { if (event.data2 !== -1) { port.send([event.type + this.channel, event.data1, event.data2], event.time2); } else { port.send([event.type + this.channel, event.data1], event.time2); } // if(event.type === 128 || event.type === 144 || event.type === 176){ // port.send([event.type + this.channel, event.data1, event.data2], event.time + latency) // }else if(event.type === 192 || event.type === 224){ // port.send([event.type, event.data1], event.time + latency) // } } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } }, { key: 'unschedule', value: function unschedule(midiEvent) { if (this._instrument !== null) { this._instrument.unschedule(midiEvent); } if (this._midiOutputs.size === 0) { return; } if (midiEvent.type === 144) { var midiNote = midiEvent.midiNote; var noteOff = new _midi_event.MIDIEvent(0, 128, midiEvent.data1, 0); noteOff.midiNoteId = midiNote.id; noteOff.time = _init_audio.context.currentTime; this._sendToExternalMIDIOutputs(noteOff, true); } } }, { key: 'allNotesOff', value: function allNotesOff() { if (this._instrument !== null) { this._instrument.allNotesOff(); } // let timeStamp = (context.currentTime * 1000) + this.latency // for(let output of this._midiOutputs.values()){ // output.send([0xB0, 0x7B, 0x00], timeStamp) // stop all notes // output.send([0xB0, 0x79, 0x00], timeStamp) // reset all controllers // } } }, { key: 'setPanning', value: function setPanning(value) { if (value < -1 || value > 1) { console.log('Track.setPanning() accepts a value between -1 (full left) and 1 (full right), you entered:', value); return; } var x = value; var y = 0; var z = 1 - Math.abs(x); x = x === 0 ? zeroValue : x; y = y === 0 ? zeroValue : y; z = z === 0 ? zeroValue : z; this._panner.setPosition(x, y, z); this._panningValue = value; } }, { key: 'getPanning', value: function getPanning() { return this._panningValue; } }]); return Track; }();