UNPKG

qambi

Version:

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

810 lines (697 loc) 25.9 kB
'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; }();