qambi
Version:
MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio
810 lines (697 loc) • 25.9 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Song = 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; }; }(); //@ flow
var _constants = require('./constants');
var _parse_events = require('./parse_events');
var _init_audio = require('./init_audio');
var _scheduler = require('./scheduler');
var _scheduler2 = _interopRequireDefault(_scheduler);
var _midi_event = require('./midi_event');
var _song_from_midifile = require('./song_from_midifile');
var _util = require('./util');
var _position = require('./position');
var _playhead = require('./playhead');
var _metronome = require('./metronome');
var _eventlistener = require('./eventlistener');
var _save_midifile = require('./save_midifile');
var _song = require('./song.update');
var _settings = require('./settings');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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 instanceIndex = 0;
var recordingIndex = 0;
/*
type songSettings = {
name: string,
ppq: number,
bpm: number,
bars: number,
lowestNote: number,
highestNote: number,
nominator: number,
denominator: number,
quantizeValue: number,
fixedLengthValue: number,
positionType: string,
useMetronome: boolean,
autoSize: boolean,
loop: boolean,
playbackSpeed: number,
autoQuantize: boolean,
pitch: number,
bufferTime: number,
noteNameMode: string
}
*/
/*
// initialize song with tracks and part so you do not have to create them separately
setup: {
timeEvents: []
tracks: [
parts []
]
}
*/
var Song = exports.Song = function () {
_createClass(Song, null, [{
key: 'fromMIDIFile',
value: function fromMIDIFile(data) {
return (0, _song_from_midifile.songFromMIDIFile)(data);
}
}, {
key: 'fromMIDIFileSync',
value: function fromMIDIFileSync(data) {
return (0, _song_from_midifile.songFromMIDIFileSync)(data);
}
}]);
function Song() {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_classCallCheck(this, Song);
this.id = this.constructor.name + '_' + instanceIndex++ + '_' + new Date().getTime();
var defaultSettings = (0, _settings.getSettings)();
var _settings$name = settings.name;
this.name = _settings$name === undefined ? this.id : _settings$name;
var _settings$ppq = settings.ppq;
this.ppq = _settings$ppq === undefined ? defaultSettings.ppq : _settings$ppq;
var _settings$bpm = settings.bpm;
this.bpm = _settings$bpm === undefined ? defaultSettings.bpm : _settings$bpm;
var _settings$bars = settings.bars;
this.bars = _settings$bars === undefined ? defaultSettings.bars : _settings$bars;
var _settings$nominator = settings.nominator;
this.nominator = _settings$nominator === undefined ? defaultSettings.nominator : _settings$nominator;
var _settings$denominator = settings.denominator;
this.denominator = _settings$denominator === undefined ? defaultSettings.denominator : _settings$denominator;
var _settings$quantizeVal = settings.quantizeValue;
this.quantizeValue = _settings$quantizeVal === undefined ? defaultSettings.quantizeValue : _settings$quantizeVal;
var _settings$fixedLength = settings.fixedLengthValue;
this.fixedLengthValue = _settings$fixedLength === undefined ? defaultSettings.fixedLengthValue : _settings$fixedLength;
var _settings$useMetronom = settings.useMetronome;
this.useMetronome = _settings$useMetronom === undefined ? defaultSettings.useMetronome : _settings$useMetronom;
var _settings$autoSize = settings.autoSize;
this.autoSize = _settings$autoSize === undefined ? defaultSettings.autoSize : _settings$autoSize;
var _settings$playbackSpe = settings.playbackSpeed;
this.playbackSpeed = _settings$playbackSpe === undefined ? defaultSettings.playbackSpeed : _settings$playbackSpe;
var _settings$autoQuantiz = settings.autoQuantize;
this.autoQuantize = _settings$autoQuantiz === undefined ? defaultSettings.autoQuantize : _settings$autoQuantiz;
var _settings$pitch = settings.pitch;
this.pitch = _settings$pitch === undefined ? defaultSettings.pitch : _settings$pitch;
var _settings$bufferTime = settings.bufferTime;
this.bufferTime = _settings$bufferTime === undefined ? defaultSettings.bufferTime : _settings$bufferTime;
var _settings$noteNameMod = settings.noteNameMode;
this.noteNameMode = _settings$noteNameMod === undefined ? defaultSettings.noteNameMode : _settings$noteNameMod;
var _settings$volume = settings.volume;
this.volume = _settings$volume === undefined ? defaultSettings.volume : _settings$volume;
this._timeEvents = [];
this._updateTimeEvents = true;
this._lastEvent = new _midi_event.MIDIEvent(0, _constants.MIDIEventTypes.END_OF_TRACK);
this._tracks = [];
this._tracksById = new Map();
this._parts = [];
this._partsById = new Map();
this._events = [];
this._eventsById = new Map();
this._allEvents = []; // MIDI events and metronome events
this._notes = [];
this._notesById = new Map();
this._newEvents = [];
this._movedEvents = [];
this._removedEvents = [];
this._transposedEvents = [];
this._newParts = [];
this._changedParts = [];
this._removedParts = [];
this._removedTracks = [];
this._currentMillis = 0;
this._scheduler = new _scheduler2.default(this);
this._playhead = new _playhead.Playhead(this);
this.playing = false;
this.paused = false;
this.recording = false;
this.precounting = false;
this.stopped = true;
this.looping = false;
this._gainNode = _init_audio.context.createGain();
this._gainNode.gain.value = this.volume;
this._gainNode.connect(_init_audio.masterGain);
this._metronome = new _metronome.Metronome(this);
this._metronomeEvents = [];
this._updateMetronomeEvents = true;
this._metronome.mute(!this.useMetronome);
this._loop = false;
this._leftLocator = { millis: 0, ticks: 0 };
this._rightLocator = { millis: 0, ticks: 0 };
this._illegalLoop = false;
this._loopDuration = 0;
this._precountBars = 0;
this._endPrecountMillis = 0;
var tracks = settings.tracks,
timeEvents = settings.timeEvents;
//console.log(tracks, timeEvents)
if (typeof timeEvents === 'undefined') {
this._timeEvents = [new _midi_event.MIDIEvent(0, _constants.MIDIEventTypes.TEMPO, this.bpm), new _midi_event.MIDIEvent(0, _constants.MIDIEventTypes.TIME_SIGNATURE, this.nominator, this.denominator)];
} else {
this.addTimeEvents.apply(this, _toConsumableArray(timeEvents));
}
if (typeof tracks !== 'undefined') {
this.addTracks.apply(this, _toConsumableArray(tracks));
}
this.update();
}
_createClass(Song, [{
key: 'addTimeEvents',
value: function addTimeEvents() {
var _this = this;
for (var _len = arguments.length, events = Array(_len), _key = 0; _key < _len; _key++) {
events[_key] = arguments[_key];
}
//@TODO: filter time events on the same tick -> use the lastly added events
events.forEach(function (event) {
if (event.type === _constants.MIDIEventTypes.TIME_SIGNATURE) {
_this._updateMetronomeEvents = true;
}
_this._timeEvents.push(event);
});
this._updateTimeEvents = true;
}
}, {
key: 'addTracks',
value: function addTracks() {
var _this2 = this;
for (var _len2 = arguments.length, tracks = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
tracks[_key2] = arguments[_key2];
}
tracks.forEach(function (track) {
var _newEvents, _newParts;
track._song = _this2;
track._gainNode.connect(_this2._gainNode);
track._songGainNode = _this2._gainNode;
_this2._tracks.push(track);
_this2._tracksById.set(track.id, track);
(_newEvents = _this2._newEvents).push.apply(_newEvents, _toConsumableArray(track._events));
(_newParts = _this2._newParts).push.apply(_newParts, _toConsumableArray(track._parts));
});
}
}, {
key: 'removeTracks',
value: function removeTracks() {
var _removedTracks;
(_removedTracks = this._removedTracks).push.apply(_removedTracks, arguments);
}
}, {
key: 'update',
value: function update() {
_song.update.call(this);
}
}, {
key: 'play',
value: function play(type) {
for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
//unlockWebAudio()
this._play.apply(this, [type].concat(args));
if (this._precountBars > 0) {
(0, _eventlistener.dispatchEvent)({ type: 'precounting', data: this._currentMillis });
} else if (this._preparedForRecording === true) {
(0, _eventlistener.dispatchEvent)({ type: 'start_recording', data: this._currentMillis });
} else {
(0, _eventlistener.dispatchEvent)({ type: 'play', data: this._currentMillis });
}
}
}, {
key: '_play',
value: function _play(type) {
if (typeof type !== 'undefined') {
for (var _len4 = arguments.length, args = Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
args[_key4 - 1] = arguments[_key4];
}
this.setPosition.apply(this, [type].concat(args));
}
if (this.playing) {
return;
}
//console.log(this._currentMillis)
this._reference = this._timeStamp = _init_audio.context.currentTime * 1000;
this._scheduler.setTimeStamp(this._reference);
this._startMillis = this._currentMillis;
if (this._precountBars > 0 && this._preparedForRecording) {
// create precount events, the playhead will be moved to the first beat of the current bar
var position = this.getPosition();
this._metronome.createPrecountEvents(position.bar, position.bar + this._precountBars, this._reference);
this._currentMillis = this._calculatePosition('barsbeats', [position.bar], 'millis').millis;
this._precountDuration = this._metronome.precountDuration;
this._endPrecountMillis = this._currentMillis + this._precountDuration;
// console.group('precount')
// console.log('position', this.getPosition())
// console.log('_currentMillis', this._currentMillis)
// console.log('endPrecountMillis', this._endPrecountMillis)
// console.log('_precountDuration', this._precountDuration)
// console.groupEnd('precount')
//console.log('precountDuration', this._metronome.createPrecountEvents(this._precountBars, this._reference))
this.precounting = true;
} else {
this._endPrecountMillis = 0;
this.playing = true;
this.recording = this._preparedForRecording;
}
//console.log(this._endPrecountMillis)
if (this.paused) {
this.paused = false;
}
this._playhead.set('millis', this._currentMillis);
this._scheduler.init(this._currentMillis);
this._loop = this.looping && this._currentMillis <= this._rightLocator.millis;
this._pulse();
}
}, {
key: '_pulse',
value: function _pulse() {
if (this.playing === false && this.precounting === false) {
return;
}
if (this._performUpdate === true) {
this._performUpdate = false;
//console.log('pulse update', this._currentMillis)
_song._update.call(this);
}
var now = _init_audio.context.currentTime * 1000;
//console.log(now, performance.now())
var diff = now - this._reference;
this._currentMillis += diff;
this._reference = now;
if (this._endPrecountMillis > 0) {
if (this._endPrecountMillis > this._currentMillis) {
this._scheduler.update(diff);
requestAnimationFrame(this._pulse.bind(this));
//return because during precounting only precount metronome events get scheduled
return;
}
this.precounting = false;
this._endPrecountMillis = 0;
this._currentMillis -= this._precountDuration;
if (this._preparedForRecording) {
this.playing = true;
this.recording = true;
} else {
this.playing = true;
(0, _eventlistener.dispatchEvent)({ type: 'play', data: this._startMillis });
//dispatchEvent({type: 'play', data: this._currentMillis})
}
}
if (this._loop && this._currentMillis >= this._rightLocator.millis) {
this._currentMillis -= this._loopDuration;
this._playhead.set('millis', this._currentMillis);
//this._playhead.set('millis', this._leftLocator.millis) // playhead is a bit ahead only during this frame
(0, _eventlistener.dispatchEvent)({
type: 'loop',
data: null
});
} else {
this._playhead.update('millis', diff);
}
this._ticks = this._playhead.get().ticks;
//console.log(this._currentMillis, this._durationMillis)
if (this._currentMillis >= this._durationMillis) {
var _scheduler$events;
if (this.recording !== true || this.autoSize !== true) {
this.stop();
return;
}
// add an extra bar to the size of this song
var _events = this._metronome.addEvents(this.bars, this.bars + 1);
var tobeParsed = [].concat(_toConsumableArray(_events), _toConsumableArray(this._timeEvents));
(0, _util.sortEvents)(tobeParsed);
(0, _parse_events.parseEvents)(tobeParsed);
(_scheduler$events = this._scheduler.events).push.apply(_scheduler$events, _toConsumableArray(_events));
this._scheduler.numEvents += _events.length;
var lastEvent = _events[_events.length - 1];
var extraMillis = lastEvent.ticksPerBar * lastEvent.millisPerTick;
this._lastEvent.ticks += lastEvent.ticksPerBar;
this._lastEvent.millis += extraMillis;
this._durationMillis += extraMillis;
this.bars++;
this._resized = true;
//console.log('length', this._lastEvent.ticks, this._lastEvent.millis, this.bars, lastEvent)
}
this._scheduler.update(diff);
requestAnimationFrame(this._pulse.bind(this));
}
}, {
key: 'pause',
value: function pause() {
this.paused = !this.paused;
this.precounting = false;
if (this.paused) {
this.playing = false;
this.allNotesOff();
(0, _eventlistener.dispatchEvent)({ type: 'pause', data: this.paused });
} else {
this.play();
(0, _eventlistener.dispatchEvent)({ type: 'pause', data: this.paused });
}
}
}, {
key: 'stop',
value: function stop() {
//console.log('STOP')
this.precounting = false;
this.allNotesOff();
if (this.playing || this.paused) {
this.playing = false;
this.paused = false;
}
if (this._currentMillis !== 0) {
this._currentMillis = 0;
this._playhead.set('millis', this._currentMillis);
if (this.recording) {
this.stopRecording();
}
(0, _eventlistener.dispatchEvent)({ type: 'stop' });
}
}
}, {
key: 'startRecording',
value: function startRecording() {
var _this3 = this;
if (this._preparedForRecording === true) {
return;
}
this._recordId = 'recording_' + recordingIndex++ + new Date().getTime();
this._tracks.forEach(function (track) {
track._startRecording(_this3._recordId);
});
this._preparedForRecording = true;
}
}, {
key: 'stopRecording',
value: function stopRecording() {
var _this4 = this;
if (this._preparedForRecording === false) {
return;
}
this._tracks.forEach(function (track) {
track._stopRecording(_this4._recordId);
});
this.update();
this._preparedForRecording = false;
this.recording = false;
(0, _eventlistener.dispatchEvent)({ type: 'stop_recording' });
}
}, {
key: 'undoRecording',
value: function undoRecording() {
var _this5 = this;
this._tracks.forEach(function (track) {
track.undoRecording(_this5._recordId);
});
this.update();
}
}, {
key: 'redoRecording',
value: function redoRecording() {
var _this6 = this;
this._tracks.forEach(function (track) {
track.redoRecording(_this6._recordId);
});
this.update();
}
}, {
key: 'setMetronome',
value: function setMetronome(flag) {
if (typeof flag === 'undefined') {
this.useMetronome = !this.useMetronome;
} else {
this.useMetronome = flag;
}
this._metronome.mute(!this.useMetronome);
}
}, {
key: 'configureMetronome',
value: function configureMetronome(config) {
this._metronome.configure(config);
}
}, {
key: 'configure',
value: function configure(config) {
var _this7 = this;
if (typeof config.pitch !== 'undefined') {
if (config.pitch === this.pitch) {
return;
}
this.pitch = config.pitch;
this._events.forEach(function (event) {
event.updatePitch(_this7.pitch);
});
}
if (typeof config.ppq !== 'undefined') {
if (config.ppq === this.ppq) {
return;
}
var ppqFactor = config.ppq / this.ppq;
this.ppq = config.ppq;
this._allEvents.forEach(function (e) {
e.ticks = event.ticks * ppqFactor;
});
this._updateTimeEvents = true;
this.update();
}
if (typeof config.playbackSpeed !== 'undefined') {
if (config.playbackSpeed === this.playbackSpeed) {
return;
}
this.playbackSpeed = config.playbackSpeed;
}
}
}, {
key: 'allNotesOff',
value: function allNotesOff() {
this._tracks.forEach(function (track) {
track.allNotesOff();
});
this._scheduler.allNotesOff();
this._metronome.allNotesOff();
}
/*
panic(){
return new Promise(resolve => {
this._tracks.forEach((track) => {
track.disconnect(this._gainNode)
})
setTimeout(() => {
this._tracks.forEach((track) => {
track.connect(this._gainNode)
})
resolve()
}, 100)
})
}
*/
}, {
key: 'getTracks',
value: function getTracks() {
return [].concat(_toConsumableArray(this._tracks));
}
}, {
key: 'getParts',
value: function getParts() {
return [].concat(_toConsumableArray(this._parts));
}
}, {
key: 'getEvents',
value: function getEvents() {
return [].concat(_toConsumableArray(this._events));
}
}, {
key: 'getNotes',
value: function getNotes() {
return [].concat(_toConsumableArray(this._notes));
}
}, {
key: 'calculatePosition',
value: function calculatePosition(args) {
return (0, _position.calculatePosition)(this, args);
}
// @args -> see _calculatePosition
}, {
key: 'setPosition',
value: function setPosition(type) {
var wasPlaying = this.playing;
if (this.playing) {
this.playing = false;
this.allNotesOff();
}
for (var _len5 = arguments.length, args = Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {
args[_key5 - 1] = arguments[_key5];
}
var position = this._calculatePosition(type, args, 'all');
//let millis = this._calculatePosition(type, args, 'millis')
if (position === false) {
return;
}
this._currentMillis = position.millis;
//console.log(this._currentMillis)
(0, _eventlistener.dispatchEvent)({
type: 'position',
data: position
});
if (wasPlaying) {
this._play();
} else {
//@todo: get this information from let 'position' -> we have just calculated the position
this._playhead.set('millis', this._currentMillis);
}
//console.log('setPosition', this._currentMillis)
}
}, {
key: 'getPosition',
value: function getPosition() {
return this._playhead.get().position;
}
}, {
key: 'getPlayhead',
value: function getPlayhead() {
return this._playhead.get();
}
// @args -> see _calculatePosition
}, {
key: 'setLeftLocator',
value: function setLeftLocator(type) {
for (var _len6 = arguments.length, args = Array(_len6 > 1 ? _len6 - 1 : 0), _key6 = 1; _key6 < _len6; _key6++) {
args[_key6 - 1] = arguments[_key6];
}
this._leftLocator = this._calculatePosition(type, args, 'all');
if (this._leftLocator === false) {
console.warn('invalid position for locator');
this._leftLocator = { millis: 0, ticks: 0 };
return;
}
}
// @args -> see _calculatePosition
}, {
key: 'setRightLocator',
value: function setRightLocator(type) {
for (var _len7 = arguments.length, args = Array(_len7 > 1 ? _len7 - 1 : 0), _key7 = 1; _key7 < _len7; _key7++) {
args[_key7 - 1] = arguments[_key7];
}
this._rightLocator = this._calculatePosition(type, args, 'all');
if (this._rightLocator === false) {
this._rightLocator = { millis: 0, ticks: 0 };
console.warn('invalid position for locator');
return;
}
}
}, {
key: 'setLoop',
value: function setLoop() {
var flag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
this.looping = flag !== null ? flag : !this._loop;
if (this._rightLocator === false || this._leftLocator === false) {
this._illegalLoop = true;
this._loop = false;
this.looping = false;
return false;
}
// locators can not (yet) be used to jump over a segment
if (this._rightLocator.millis <= this._leftLocator.millis) {
this._illegalLoop = true;
this._loop = false;
this.looping = false;
return false;
}
this._loopDuration = this._rightLocator.millis - this._leftLocator.millis;
//console.log(this._loop, this._loopDuration)
this._scheduler.beyondLoop = this._currentMillis > this._rightLocator.millis;
this._loop = this.looping && this._currentMillis <= this._rightLocator.millis;
//console.log(this._loop, this.looping)
return this.looping;
}
}, {
key: 'setPrecount',
value: function setPrecount() {
var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
this._precountBars = value;
}
/*
helper method: converts user friendly position format to internal format
position:
- 'ticks', 96000
- 'millis', 1234
- 'percentage', 55
- 'barsbeats', 1, 4, 0, 25 -> bar, beat, sixteenth, tick
- 'time', 0, 3, 49, 566 -> hours, minutes, seconds, millis
*/
}, {
key: '_calculatePosition',
value: function _calculatePosition(type, args, resultType) {
var target = void 0;
switch (type) {
case 'ticks':
case 'millis':
case 'percentage':
//target = args[0] || 0
target = args || 0;
break;
case 'time':
case 'barsbeats':
case 'barsandbeats':
target = args;
break;
default:
console.log('unsupported type');
return false;
}
var position = (0, _position.calculatePosition)(this, {
type: type,
target: target,
result: resultType
});
return position;
}
}, {
key: 'addEventListener',
value: function addEventListener(type, callback) {
return (0, _eventlistener.addEventListener)(type, callback);
}
}, {
key: 'removeEventListener',
value: function removeEventListener(type, id) {
(0, _eventlistener.removeEventListener)(type, id);
}
}, {
key: 'saveAsMIDIFile',
value: function saveAsMIDIFile(name) {
(0, _save_midifile.saveAsMIDIFile)(this, name);
}
}, {
key: 'setVolume',
value: function setVolume(value) {
if (value < 0 || value > 1) {
console.log('Song.setVolume() accepts a value between 0 and 1, you entered:', value);
return;
}
this.volume = value;
}
}, {
key: 'getVolume',
value: function getVolume() {
return this.volume;
}
}, {
key: 'setPanning',
value: function setPanning(value) {
if (value < -1 || value > 1) {
console.log('Song.setPanning() accepts a value between -1 (full left) and 1 (full right), you entered:', value);
return;
}
this._tracks.forEach(function (track) {
track.setPanning(value);
});
this._pannerValue = value;
}
}]);
return Song;
}();