qambi
Version:
MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio
860 lines (759 loc) • 27.5 kB
JavaScript
'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;
}();