UNPKG

webdaw-modules

Version:

a set of modules for building a web-based DAW

1,622 lines (1,415 loc) 58.1 kB
function song() { "use strict"; var slice = Array.prototype.slice, //import createMidiEvent, // → defined in midi_event.js createPlayhead, // → defined in playhead.js createFollowEvent, // → defined in song_follow_event.js createScheduler, // → defined in scheduler.js createMetronome, // → defined in metronome.js followEvent, // → defined in follow_event_song.js masterGainNode, // -> defined in open_module.js context, // -> defined in open_module.js timedTasks, // -> defined in open_module.js repetitiveTasks, // -> defined in open_module.js initMidi, // defined in midi_system.js addMidiEventListener, // defined in midi_system.js removeMidiEventListener, // defined in midi_system.js setMidiInput, // defined in midi_system.js setMidiOutput, // defined in midi_system.js getMidiInputs, // defined in midi_system.js getMidiOutputs, // defined in midi_system.js getMidiPortsAsDropdown, // defined in midi_system.js getPosition, // → defined in position.js millisToTicks, // → defined in position.js ticksToMillis, // → defined in position.js ticksToBars, // → defined in position.js millisToBars, // → defined in position.js barsToTicks, // → defined in position.js barsToMillis, // → defined in position.js addEventListener, // → defined in song_event_listener.js removeEventListener, // → defined in song_event_listener.js dispatchEvent, // → defined in song_event_listener.js update, // → defined in song_update.js checkDuration, // → defined in song_update.js addMetronomeEvents, // → defined in song_update.js gridToSong, // → defined in song_grid.js noteToGrid, // → defined in song_grid.js eventToGrid, // → defined in song_grid.js positionToGrid, // → defined in song_grid.js //createTrack, // → defined in track.js typeString, // → defined in util.js removeFromArray, // → defined in util.js removeFromArray2, // → defined in util.js getNoteLengthName, // → defined in util.js getStats, // → defined in event_statistics.js findEvent, // → defined in find_event.js findNote, // → defined in find_event.js objectForEach, // → defined in util.js addSong, // → defined in sequencer.js getTimeDiff, // → defined in open_module.js //private _removeTracks, pulse, getArguments, getTrack, addTracks, getPart, getParts, getTimeEvents, setRecordingStatus, _getRecordingPerTrack, songIndex = 0, //protected createGrid, //public Song; Song = function(config) { //Object.defineProperty(this,'tracks',{value: []}); //Object.defineProperty(this, 'events', {value: 'val'}); config = config || {}; this.id = "S" + songIndex++ + "" + new Date().getTime(); this.name = config.name || this.id; this.className = "Song"; addSong(this); this.midiInputs = {}; this.midiOutputs = {}; initMidi(this); this.bpm = config.bpm || 120; this.ppq = config.ppq || sequencer.defaultPPQ; this.bars = config.bars || 30; //default song duration is 30 bars @ 120 bpm is 1 minute this.lastBar = this.bars; this.lowestNote = config.lowestNote || 0; this.highestNote = config.highestNote || 127; this.pitchRange = this.highestNote - this.lowestNote + 1; this.nominator = config.nominator || 4; this.denominator = config.denominator || 4; this.factor = 4 / this.denominator; this.ticksPerBeat = this.ppq * this.factor; this.ticksPerBar = this.ticksPerBeat * this.nominator; this.millisPerTick = 60000 / this.bpm / this.ppq; this.quantizeValue = config.quantizeValue || "8"; this.fixedLengthValue = config.fixedLengthValue || false; this.positionType = config.positionType || "all"; this.useMetronome = config.useMetronome; this.autoSize = config.autoSize === undefined ? true : config.autoSize === true; this.playbackSpeed = 1; this.defaultInstrument = config.defaultInstrument || sequencer.defaultInstrument; this.recordId = -1; this.autoQuantize = false; this.loop = config.loop || false; this.doLoop = false; this.illegalLoop = true; this.loopStart = 0; this.loopEnd = 0; this.loopDuration = 0; this.audioRecordingLatency = 0; //console.log('PPQ song', this.ppq) if (this.useMetronome !== true && this.useMetronome !== false) { this.useMetronome = false; } //console.log(this.useMetronome); this.grid = undefined; if (config.timeEvents && config.timeEvents.length > 0) { this.timeEvents = [].concat(config.timeEvents); this.tempoEvent = getTimeEvents(sequencer.TEMPO, this)[0]; this.timeSignatureEvent = getTimeEvents(sequencer.TIME_SIGNATURE, this)[0]; if (this.tempoEvent === undefined) { this.tempoEvent = createMidiEvent(0, sequencer.TEMPO, this.bpm); this.timeEvents.unshift(this.tempoEvent); } else { this.bpm = this.tempoEvent.bpm; } if (this.timeSignatureEvent === undefined) { this.timeSignatureEvent = createMidiEvent(0, sequencer.TIME_SIGNATURE, this.nominator, this.denominator); this.timeEvents.unshift(this.timeSignatureEvent); } else { this.nominator = this.timeSignatureEvent.nominator; this.denominator = this.timeSignatureEvent.denominator; } //console.log(1, this.nominator, this.denominator, this.bpm); } else { // there has to be a tempo and time signature event at ticks 0, otherwise the position can't be calculated, and moreover, it is dictated by the MIDI standard this.tempoEvent = createMidiEvent(0, sequencer.TEMPO, this.bpm); this.timeSignatureEvent = createMidiEvent(0, sequencer.TIME_SIGNATURE, this.nominator, this.denominator); this.timeEvents = [this.tempoEvent, this.timeSignatureEvent]; } // TODO: A value for bpm, nominator and denominator in the config overrules the time events specified in the config -> maybe this should be the other way round // if a value for bpm is set in the config, and this value is different from the bpm value of the first // tempo event, all tempo events will be updated to the bpm value in the config. if (config.timeEvents !== undefined && config.bpm !== undefined) { if (this.bpm !== config.bpm) { this.setTempo(config.bpm, false); } } // if a value for nominator and/or denominator is set in the config, and this/these value(s) is/are different from the values // of the first time signature event, all time signature events will be updated to the values in the config. // @TODO: maybe only the first time signature event should be updated? if (config.timeEvents !== undefined && (config.nominator !== undefined || config.denominator !== undefined)) { if (this.nominator !== config.nominator || this.denominator !== config.denominator) { this.setTimeSignature(config.nominator || this.nominator, config.denominator || this.denominator, false); } } //console.log(2, this.nominator, this.denominator, this.bpm); this.tracks = []; this.parts = []; this.notes = []; this.events = []; //.concat(this.timeEvents); this.allEvents = []; // all events plus metronome ticks this.tracksById = {}; this.tracksByName = {}; this.partsById = {}; this.notesById = {}; this.eventsById = {}; this.activeEvents = null; this.activeNotes = null; this.activeParts = null; this.recordedNotes = []; this.recordedEvents = []; this.recordingNotes = {}; // notes that don't have their note off events yet this.numTracks = 0; this.numParts = 0; this.numNotes = 0; this.numEvents = 0; this.instruments = []; this.playing = false; this.paused = false; this.stopped = true; this.recording = false; this.prerolling = false; this.precounting = false; this.preroll = true; this.precount = 0; this.listeners = {}; this.playhead = createPlayhead(this, this.positionType, this.id, this); //, this.position); this.playheadRecording = createPlayhead(this, "all", this.id + "_recording"); this.scheduler = createScheduler(this); this.followEvent = createFollowEvent(this); this.volume = 1; this.gainNode = context.createGainNode(); this.gainNode.gain.value = this.volume; this.metronome = createMetronome(this, dispatchEvent); this.connect(); if (config.className === "MidiFile" && config.loaded === false) { if (sequencer.debug) { console.warn("midifile", config.name, "has not yet been loaded!"); } } //if(config.tracks && config.tracks.length > 0){ if (config.tracks) { // console.log("TRACKS", config.tracks); this.addTracks(config.tracks); } if (config.parts) { this.addParts(config.parts); } if (config.events) { this.addEvents(config.events); } if (config.events || config.parts || config.tracks) { //console.log(config.events, config.parts, config.tracks) // the length of the song will be determined by the events, parts and/or tracks that are added to the song if (config.bars === undefined) { this.lastBar = 0; } this.lastEvent = createMidiEvent([this.lastBar, sequencer.END_OF_TRACK]); } else { this.lastEvent = createMidiEvent([this.bars * this.ticksPerBar, sequencer.END_OF_TRACK]); } //console.log('update'); this.update(true); this.numTimeEvents = this.timeEvents.length; this.playhead.set("ticks", 0); this.midiEventListeners = {}; //console.log(this.timeEvents); }; getPart = function(data, song) { var part = false; if (data === undefined) { part = false; } else if (part.className === "Part") { part = data; } else if (typeString(data) === "string") { part = song.partsById[data]; } else if (isNaN(data) === false) { part = song.parts[data]; } return part; }; getTrack = function(data, song) { var track = false; //console.log(data); if (data === undefined) { track = false; } else if (data.className === "Track") { track = data; } else if (typeString(data) === "string") { track = song.tracksById[data]; if (track === undefined) { track = song.tracksByName[data]; // objectForEach(song.tracksById, function(t){ // if(t.name === data){ // track = t; // } // }); } } else if (isNaN(data) === false) { track = song.tracks[data]; } if (track === undefined) { track = false; } return track; }; addTracks = function(newTracks, song) { //console.log('addTracks'); var tracksById = song.tracksById, tracksByName = song.tracksByName, addedIds = [], i, part, track; // for (i = newTracks.length - 1; i >= 0; i--) { for (i = 0; i < newTracks.length; i++) { track = getTrack(newTracks[i]); if (track === false) { continue; } //console.log(track.song); if (track.song !== undefined && track.song !== null) { track = track.copy(); } track.song = song; track.instrument.song = song; track.quantizeValue = song.quantizeValue; track.connect(song.gainNode); /* // -> not possible because of the endless midi feedback loop with IAC virtual midi ports on OSX //console.log(song.midiInputs); objectForEach(song.midiInputs, function(port){ //console.log(port.id); track.setMidiInput(port.id, true); }); */ track.state = "new"; track.needsUpdate = true; tracksById[track.id] = track; tracksByName[track.name] = track; addedIds.push(track.id); objectForEach(track.partsById, function(part) { part.state = "new"; }); /* for(j in track.partsById){ if(track.partsById.hasOwnProperty(j)){ //console.log('addTracks, part', part); part = track.partsById[j]; //part.song = song; part.state = 'new'; } } */ } return addedIds; }; _removeTracks = function(tobeRemoved) { var i, track, removed = []; for (i = tobeRemoved.length - 1; i >= 0; i--) { track = getTrack(tobeRemoved[i]); if (track === false) { continue; } //console.log(track); if (track.song !== undefined && track.song !== this) { console.warn("can't remove: this track belongs to song", track.song.id); continue; } track.state = "removed"; track.disconnect(this.gainNode); track.reset(); delete this.tracksById[track.id]; delete this.tracksByName[track.name]; removed.push(track); } return removed; }; /* getParts = function(args, song){ var part, i, result = []; for(i = args.length - 1; i >= 0; i--){ part = getPart(args[i], song); if(part){ result.push(part); } } return result; }; */ getParts = function(args) { var part, i, result = []; for (i = args.length - 1; i >= 0; i--) { part = getPart(args[i], this); if (part) { result.push(part); } } return result; }; function getEvents(args, song) { var result = []; args = slice.call(args); //console.log(args); function loop(data, i, maxi) { var arg, type, event; for (i = 0; i < maxi; i++) { arg = data[i]; type = typeString(arg); if (type === "array") { loop(arg, 0, arg.length); } else if (type === "string") { event = song.eventsById[arg]; if (event !== undefined) { result.push(arg); } } else if (arg.className === "MidiEvent") { result.push(arg); } } } loop(args, 0, args.length); return result; } getTimeEvents = function(type, song) { var events = []; song.timeEvents.forEach(function(event) { if (event.type === type) { events.push(event); } }); return events; }; pulse = function(song) { var //now = window.performance.now(), now = sequencer.getTime() * 1000, diff = now - song.timeStamp, millis = song.millis + diff; song.diff = diff; //console.log(diff); //console.log(now, song.recordTimestamp, song.eventsMidiAudioMetronome[0].time); song.timeStamp = now; if (song.precounting === true) { song.metronome.millis += diff; song.scheduler.update(diff); // now return otherwise the position of the song gets updated return; } // is this comment still valid? // put followEvent and scheduler before playhead.update(), otherwise followEvent will miss the first event (scheduler could come after playhead.update) song.prevMillis = song.millis; //song.playhead.update('millis', diff); // song.followEvent.update(); // song.scheduler.update(); //console.log(song.millis, diff, song.loopEnd); //console.log(song.doLoop, song.scheduler.looped, song.millis > song.loopEnd); //console.log(song.scheduler.prevMaxtime, song.loopEnd); if (song.doLoop && song.scheduler.looped && millis >= song.loopEnd) { // && song.jump !== true){ //console.log(song.prevMillis, song.millis); //song.scheduler.looped = false; song.followEvent.resetAllListeners(); song.playhead.set("millis", song.loopStart + (millis - song.loopEnd)); song.followEvent.update(); //console.log('-->', song.millis); song.scheduler.update(); dispatchEvent(song, "loop"); //song.startTime += (song.loopEnd - song.loopStart); } else if (millis >= song.durationMillis) { song.playhead.update("millis", song.durationMillis - song.millis); song.followEvent.update(); song.pause(); song.endOfSong = true; dispatchEvent(song, "end"); } else { song.playhead.update("millis", diff); song.followEvent.update(); song.scheduler.update(); } song.jump = false; //console.log(now, sequencer.getTime()); //console.log(song.barsAsString); //console.log('pulse', song.playhead.barsAsString, song.playhead.millis); //console.log(song.millis); }; Song.prototype.remove = function() { console.warn("Song.remove() is deprecated, please use sequencer.deleteSong()"); sequencer.deleteSong(this); }; Song.prototype.play = function() { sequencer.unlockWebAudio(); var song, playstart; //console.log(this.playing); if (this.playing) { this.pause(); return; } // tell the scheduler to schedule the audio events that start before the current position of the playhead this.scheduler.firstRun = true; // only loop when the loop is legal and this.loop is set to true this.doLoop = this.illegalLoop === false && this.loop === true; //console.log('play', this.doLoop, this.illegalLoop, this.loop); // or should I move to loopStart here if loop is enabled? if (this.endOfSong) { this.followEvent.resetAllListeners(); this.playhead.set("millis", 0); this.scheduler.setIndex(0); } // timeStamp is used for calculating the diff in time of every consecutive frame this.timeStamp = sequencer.getTime() * 1000; this.startTime = this.timeStamp; try { this.startTime2 = window.performance.now(); //this.startTime2 = undefined; } catch (e) { if (sequencer.debug) { console.log("window.performance.now() not supported"); } } if (this.precounting) { this.metronome.startTime = this.startTime; this.metronome.startTime2 = this.startTime2; this.startTime += this.metronome.precountDurationInMillis; this.startTime2 += this.metronome.precountDurationInMillis; //console.log(this.metronome.startTime, this.recordTimestamp); song = this; playstart = this.startTime / 1000; //console.log(this.startTime, playstart, this.recordTimestamp/1000 - playstart); repetitiveTasks.playAfterPrecount = function() { if (sequencer.getTime() >= playstart) { song.precounting = false; song.prerolling = false; song.recording = true; song.playing = true; dispatchEvent(song, "record_start"); dispatchEvent(song, "play"); //console.log('playAfterPrecount', sequencer.getTime(), playstart, song.metronome.precountDurationInMillis); repetitiveTasks.playAfterPrecount = undefined; delete repetitiveTasks.playAfterPrecount; } }; } // this value will be deducted from the millis value of the event as soon as the event get scheduled this.startMillis = this.millis; //console.log(this.startMillis); // make first call right after setting a time stamp to avoid delay //pulse(this); song = this; // fixes bug: when an event listener is added to a midi note, the listener sometimes misses the first note song.playhead.update("millis", 0); song.followEvent.update(); repetitiveTasks[this.id] = function() { pulse(song); }; this.paused = false; this.stopped = false; this.endOfSong = false; if (this.precounting !== true) { this.playing = true; dispatchEvent(this, "play"); } }; Song.prototype.pause = function() { if (this.recording === true || this.precounting === true) { this.stop(); return; } if (this.stopped || this.paused) { this.play(); return; } delete repetitiveTasks[this.id]; this.allNotesOff(); this.playing = false; this.paused = true; dispatchEvent(this, "pause"); }; Song.prototype.stop = function() { if (this.stopped) { // is this necessary? this.followEvent.resetAllListeners(); this.playhead.set("millis", 0); this.scheduler.setIndex(0); return; } if (this.recording === true || this.precounting === true) { this.stopRecording(); } delete repetitiveTasks[this.id]; // remove unschedule callback of all samples objectForEach(timedTasks, function(task, id) { //console.log(id); if (id.indexOf("unschedule_") === 0 || id.indexOf("event_") === 0) { task = null; delete timedTasks[id]; } }); this.allNotesOff(); this.playing = false; this.paused = false; this.stopped = true; this.endOfSong = false; this.followEvent.resetAllListeners(); this.playhead.set("millis", 0); this.scheduler.setIndex(0); dispatchEvent(this, "stop"); }; Song.prototype.adjustLatencyForAllRecordings = function(value) { // @todo: add callback here! this.audioRecordingLatency = value; this.tracks.forEach(function(track) { track.setAudioRecordingLatency(value); }); }; Song.prototype.setAudioRecordingLatency = function(recordId, value, callback) { var i, event, sampleId; for (i = this.audioEvents.length - 1; i >= 0; i--) { event = this.audioEvents[i]; sampleId = event.sampleId; if (sampleId === undefined) { continue; } if (recordId === sampleId) { break; } } //console.log(recordId, value, callback); event.track.setAudioRecordingLatency(recordId, value, callback); }; Song.prototype.startRecording = Song.prototype.record = function(precount) { //console.log(this.recording, this.precounting, precount); if (this.recording === true || this.precounting === true) { this.stop(); return; } var userFeedback = false, audioRecording = false, i, track, self = this; this.metronome.precountDurationInMillis = 0; // allow to start a recording while playing if (this.playing) { this.precount = 0; this.recordStartMillis = this.millis; } else { if (precount === undefined) { this.precount = 0; this.recordStartMillis = this.millis; } else { // a recording with a precount always starts at the beginning of a bar this.setPlayhead("barsbeats", this.bar); this.metronome.createPrecountEvents(precount); this.precount = precount; this.recordStartMillis = this.millis - this.metronome.precountDurationInMillis; //console.log(this.metronome.precountDurationInMillis); } /* if(this.preroll === true){ // TODO: improve this -> leave it, preroll is always on unless the user sets it to false //this.preroll = (this.bar - this.precount) > 0; } */ } //console.log('preroll', this.preroll); //console.log('precount', this.precount); //console.log('precountDurationInMillis', this.metronome.precountDurationInMillis); //console.log('recordStartMillis', this.recordStartMillis); this.recordTimestampTicks = this.ticks; this.recordId = "REC" + new Date().getTime(); this.recordedNotes = []; this.recordedEvents = []; this.recordingNotes = {}; this.recordingAudio = false; if (this.keyEditor !== undefined) { this.keyEditor.prepareForRecording(this.recordId); } for (i = this.numTracks - 1; i >= 0; i--) { track = this.tracks[i]; if (track.recordEnabled === "audio") { this.recordingAudio = true; } //console.log(track.name, track.index); if (track.recordEnabled === "audio") { audioRecording = true; track.prepareForRecording(this.recordId, function() { if (userFeedback === false) { userFeedback = true; setRecordingStatus.call(self); } }); } else { track.prepareForRecording(this.recordId); } } if (audioRecording === false) { setRecordingStatus.call(this); } return this.recordId; }; setRecordingStatus = function() { this.recordTimestamp = context.currentTime * 1000; // millis if (this.playing === false) { if (this.precount > 0) { // recording with precount always starts at the beginning of a bar //this.setPlayhead('barsbeats', this.bar); this.precounting = true; this.prerolling = this.preroll; if (this.prerolling) { dispatchEvent(this, "record_preroll"); } else { dispatchEvent(this, "record_precount"); } } else { this.recording = true; dispatchEvent(this, "record_start"); } this.play(); } else { this.recording = true; this.precounting = false; dispatchEvent(this, "record_start"); } }; _getRecordingPerTrack = function(index, recordingHistory, callback) { var track, scope = this; if (index < this.numTracks) { track = this.tracks[index]; track.stopRecording(this.recordId, function(events) { if (events !== undefined) { recordingHistory[track.name] = events; } index++; _getRecordingPerTrack.call(scope, index, recordingHistory, callback); }); } else { callback(recordingHistory); } }; Song.prototype.stopRecording = function() { if (this.recording === false) { return; } this.recording = false; this.prerolling = false; this.precounting = false; //repetitiveTasks.playAfterPrecount = undefined; delete repetitiveTasks.playAfterPrecount; var scope = this; _getRecordingPerTrack.call(this, 0, {}, function(history) { scope.update(); dispatchEvent(scope, "recorded_events", history); }); // perform update immediately for midi recordings this.update(); dispatchEvent(this, "record_stop"); return this.recordId; }; Song.prototype.undoRecording = function(history) { var i, tracksByName; if (history === undefined) { for (i = this.numTracks - 1; i >= 0; i--) { this.tracks[i].undoRecording(this.recordId); } } else { tracksByName = this.tracksByName; objectForEach(history, function(events, name) { var track = tracksByName[name]; track.undoRecording(events); }); } //this.update(); }; Song.prototype.getAudioRecordingData = function(recordId) { var i, event, sampleId; for (i = this.audioEvents.length - 1; i >= 0; i--) { event = this.audioEvents[i]; sampleId = event.sampleId; if (sampleId === undefined) { continue; } if (recordId === sampleId) { break; } } if (event === undefined) { return false; } return event.track.getAudioRecordingData(recordId); }; // non-mandatory arguments: quantize value, history object Song.prototype.quantize = function() { var i, track, arg, type, args = slice.call(arguments), numArgs = args.length, value, historyObject = {}; //console.log(arguments); for (i = 0; i < numArgs; i++) { arg = args[i]; type = typeString(arg); //console.log(arg, type); if (type === "string" || type === "number") { // overrule the quantize values of all tracks in this song, but the song's quantizeValue doesn't change value = arg; } else if (type === "object") { historyObject = arg; } } //console.log(value, historyObject) for (i = this.numTracks - 1; i >= 0; i--) { track = this.tracks[i]; // if no value is specified, use the value of the track if (value === undefined) { value = track.quantizeValue; } sequencer.quantize(track.events, value, this.ppq, historyObject); } return historyObject; //this.update(); }; Song.prototype.undoQuantize = function(history) { if (history === undefined) { if (sequencer.debug >= 2) { console.warn("please pass a quantize history object"); } return; } var i, track; for (i = this.numTracks - 1; i >= 0; i--) { track = this.tracks[i]; track.undoQuantize(history); } }; Song.prototype.quantizeRecording = function(value) { var i, track; for (i = this.numTracks - 1; i >= 0; i--) { track = this.tracks[i]; if (track.recordId === this.recordId) { track.quantizeRecording(value); } } //this.update(); }; // left: song position >= left locator Song.prototype.setLeftLocator = function() { //var pos = getPosition(this, [].concat(type, value)); //this.leftLocator = AP.slice.call(arguments); var pos = getPosition(this, slice.call(arguments)); if (pos !== undefined) { this.loopStartPosition = pos; this.loopStart = pos.millis; this.loopStartTicks = pos.ticks; } this.illegalLoop = this.loopStart >= this.loopEnd; this.doLoop = this.illegalLoop === false && this.loop === true; this.loopDuration = this.illegalLoop === true ? 0 : this.loopEnd - this.loopStart; // if(this.doLoop === false && this.loop === true){ // dispatchEvent('loop_off', this); // } //console.log('left', this.doLoop, this.illegalLoop, this.loop); //console.log(pos.millis, pos.millis, pos.ticks); //console.log('l', this.loopStartPosition, pos); }; // right: song position < right locator Song.prototype.setRightLocator = function() { //(value){ //var pos = getPosition(this, [].concat(type, value)); //this.rightLocator = AP.slice.call(arguments); var pos = getPosition(this, slice.call(arguments)), previousState = this.illegalLoop; //var pos = getPosition(this, value); if (pos !== undefined) { this.loopEndPosition = pos; this.loopEnd = pos.millis; this.loopEndTicks = pos.ticks; } //console.log(this.loopEnd); this.illegalLoop = this.loopEnd <= this.loopStart; this.doLoop = this.illegalLoop === false && this.loop === true; this.loopDuration = this.illegalLoop === true ? 0 : this.loopEnd - this.loopStart; // if(previousState !== false && this.loop === true){ // dispatchEvent('loop_off', this); // } //console.log('right', this.doLoop, this.illegalLoop, this.loop); //console.log(pos.millis, pos.millis, pos.ticks); //console.log('r', this.loopEndPosition); }; Song.prototype.setLoop = function(flag) { if (flag === undefined) { this.loop = !this.loop; } else if (flag === true || flag === false) { this.loop = flag; } else { if (sequencer.debug >= 1) { console.error('pass "true", "false" or no value'); } return; } this.doLoop = this.illegalLoop === false && this.loop === true; }; Song.prototype.setPlayhead = function() { //console.log('setPlayhead'); this.jump = true; this.scheduler.looped = false; this.scheduler.firstRun = true; this.timeStamp = sequencer.getTime() * 1000; this.startTime = this.timeStamp; if (this.playing) { this.allNotesOff(); } //console.log(slice.call(arguments)); var pos = getPosition(this, slice.call(arguments)), millis = pos.millis; this.startMillis = millis; this.playhead.set("millis", millis); this.scheduler.setIndex(millis); //console.log(pos.bar, this.bar); //console.log(this.playhead.activeEvents); }; Song.prototype.addEventListener = function() { return addEventListener.apply(this, arguments); }; Song.prototype.removeEventListener = function() { removeEventListener.apply(this, arguments); }; Song.prototype.addEvent = Song.prototype.addEvents = function() { var track, part; track = this.tracks[0]; if (track === undefined) { track = sequencer.createTrack(); this.addTrack(track); } // we need to find the first part on the track, so update the track if necessary if (track.needsUpdate) { track.update(); } part = track.parts[0]; if (part === undefined) { part = sequencer.createPart(); track.addPart(part); } part.addEvents.apply(part, arguments); //console.log(part.needsUpdate); return this; }; Song.prototype.addPart = Song.prototype.addParts = function() { var track = this.tracks[0]; if (track === undefined) { //console.log('-> create track for parts') track = sequencer.createTrack(); this.addTrack(track); } //console.log(arguments); track.addParts.apply(track, arguments); return this; }; Song.prototype.addTrack = function() { var args = getArguments(arguments), arg0 = args[0], numArgs = args.length; if (typeString(arg0) === "array" || numArgs > 1) { console.warn("please use addTracks() if you want to get more that one tracks"); args = [arg0]; } return addTracks(args, this)[0]; }; Song.prototype.addTracks = function() { //console.log(arguments, getArguments(arguments)); return addTracks(getArguments(arguments), this); }; Song.prototype.getTrack = function(arg) { return getTrack(arg, this); }; Song.prototype.getTracks = function() { var args = getArguments(arguments), track, i, result = []; for (i = args.length - 1; i >= 0; i--) { track = getTrack(args[i], this); if (track) { result.push(track); } } return result; }; Song.prototype.getPart = function() { var args = getArguments(arguments); if (args.length > 1) { console.warn("please use getParts() if you want to get more that one part"); } //@TODO: check if a call is faster //return getParts(args, this)[0]; return getParts.call(this, args)[0]; }; Song.prototype.getParts = function() { var args = getArguments(arguments); //return getParts(args, this); return getParts.call(this, args); }; Song.prototype.removeTrack = function() { var args = getArguments(arguments); //var args = getArguments.apply(null, arguments); if (args.length > 1) { console.warn("please use removeTracks() if you want to remove more that one tracks"); } //return _removeTracks(args, this)[0]; return _removeTracks.call(this, args)[0]; }; Song.prototype.removeTracks = function() { return _removeTracks.call(this, getArguments(arguments)); }; Song.prototype.setPlaybackSpeed = function(speed) { if (speed < 0.01 || speed > 100) { console.error("playback speed has to be > 0.01 and < 100"); return; } var ticks = this.ticks, startLoop, endLoop, newPos; this.playbackSpeed = speed; //console.log('setPlaybackSpeed -> update()'); this.update(true); // get the new position of the locators after the update if (this.loopStartTicks !== undefined) { startLoop = this.getPosition("ticks", this.loopStartTicks); this.loopStart = startLoop.millis; this.loopStartTicks = startLoop.ticks; } if (this.loopEndTicks !== undefined) { endLoop = this.getPosition("ticks", this.loopEndTicks); this.loopEnd = endLoop.millis; this.loopEndTicks = endLoop.ticks; } // get the new position of the playhead after the update newPos = this.getPosition("ticks", ticks); this.setPlayhead("ticks", newPos.ticks); }; Song.prototype.gridToSong = function(x, y, width, height) { return gridToSong(this, x, y, width, height); }; Song.prototype.noteToGrid = function(note, height) { return noteToGrid(note, height, this); }; Song.prototype.eventToGrid = function(event, width, height) { return eventToGrid(event, width, height, this); }; Song.prototype.positionToGrid = function(position, width) { return positionToGrid(position, width, this); }; Song.prototype.getPosition = function() { //console.log(slice.call(arguments)); return getPosition(this, slice.call(arguments)); }; Song.prototype.ticksToMillis = function(ticks, beyondEndOfSong) { return ticksToMillis(this, ticks, beyondEndOfSong); }; Song.prototype.millisToTicks = function(millis, beyondEndOfSong) { return millisToTicks(this, millis, beyondEndOfSong); }; Song.prototype.ticksToBars = function(ticks, beyondEndOfSong) { return ticksToBars(this, ticks, beyondEndOfSong); }; Song.prototype.millisToBars = function(millis, beyondEndOfSong) { return millisToBars(this, millis, beyondEndOfSong); }; Song.prototype.barsToTicks = function() { return barsToTicks(this, slice.call(arguments)); }; Song.prototype.barsToMillis = function() { return barsToMillis(this, slice.call(arguments)); }; Song.prototype.findEvent = Song.prototype.findEvents = function(pattern) { return findEvent(this, pattern); }; Song.prototype.findNote = Song.prototype.findNotes = function(pattern) { return findNote(this, pattern); }; Song.prototype.getStats = function(pattern) { return getStats(this, pattern); }; Song.prototype.createGrid = function(config) { if (this.grid === undefined) { this.grid = createGrid(this, config); } else { this.grid.update(config); } return this.grid; }; Song.prototype.update = function(updateTimeEvents) { //console.log('Song.update()'); update(this, updateTimeEvents); }; Song.prototype.updateGrid = function(config) { this.grid.update(config); return this.grid; }; Song.prototype.updateTempoEvent = function(event, bpm) { if (event.type !== sequencer.TEMPO) { if (sequencer.debug >= 4) { console.error("this is not a tempo event"); } return; } if (event.song !== this) { if (sequencer.debug >= 4) { console.error("this event has not been added to this song yet"); } return; } var ticks = this.ticks, percentage = this.percentage; event.bpm = bpm; this.update(true); this.updatePlayheadAndLocators(ticks); }; Song.prototype.updateTimeSignatureEvent = function(event, nominator, denominator) { if (event.type !== sequencer.TIME_SIGNATURE) { if (sequencer.debug >= 4) { console.error("this is not a time signature event"); } return; } if (event.song !== this) { if (sequencer.debug >= 4) { console.error("this event has not been added to this song yet"); } return; } var ticks = this.ticks, percentage = this.percentage; event.nominator = nominator || event.nominator; event.denominator = denominator || event.denominator; this.update(true); this.updatePlayheadAndLocators(ticks); }; Song.prototype.getTempoEvents = function() { return getTimeEvents(sequencer.TEMPO, this); }; Song.prototype.getTimeSignatureEvents = function() { return getTimeEvents(sequencer.TIME_SIGNATURE, this); }; Song.prototype.updatePlayheadAndLocators = function(ticks) { var newStartPos, newEndPos, startPos = this.loopStartPosition, endPos = this.loopEndPosition, newPos; // get the new position of the locators after the update if (startPos !== undefined) { /* newStartPos = this.getPosition('barsbeats', startPos.bar, startPos.beat, startPos.sixteenth, startPos.tick); if(newStartPos.ticks > this.durationTicks || newStartPos.bar > this.bars + 1){ newStartPos = this.getPosition('barsbeats', 1, 1, 1, 0); console.log('start', newStartPos.barsAsString); } */ newStartPos = this.getPosition("ticks", startPos.ticks); this.loopStart = newStartPos.millis; this.loopStartTicks = newStartPos.ticks; this.loopStartPosition = newStartPos; } if (endPos !== undefined) { /* newEndPos = this.getPosition('barsbeats', endPos.bar, endPos.beat, endPos.sixteenth, endPos.tick); if(newEndPos.ticks > this.durationTicks || newEndPos.bar > this.bars + 1){ newEndPos = this.getPosition('barsbeats', this.bars, 1, 1, 0); console.log('end', newEndPos.barsAsString); } */ //console.log('right locator', endPos.barsAsString, endPos.ticks); newEndPos = this.getPosition("ticks", endPos.ticks); if (newEndPos.ticks > this.durationTicks) { //console.log('right locator beyond end of song'); newEndPos = this.getPosition("ticks", this.bars); } this.loopEnd = newEndPos.millis; this.loopEndTicks = newEndPos.ticks; this.loopEndPosition = newEndPos; //console.log('right locator', newEndPos.barsAsString, newEndPos.ticks); } //console.log('l', this.loopStartPosition, 'r', this.loopEndPosition); // get the new position of the playhead after the update /* newPos = this.getPosition('ticks', ticks); if(newPos.ticks > this.durationTicks || newPos.bar > this.bars + 1){ newPos = this.getPosition('barsbeats', 1, 1, 1, 0); //console.log('playhead', newPos.barsAsString); } */ newPos = this.getPosition("ticks", ticks); if (this.doLoop && newPos.ticks > this.durationTicks) { //console.log('playhead beyond end of song'); this.setPlayhead("ticks", 0); } else { this.setPlayhead("ticks", newPos.ticks); } this.loopDuration = this.illegalLoop === true ? 0 : this.loopEnd - this.loopStart; /* console.log(percentage); newPos = this.getPosition('percentage', percentage); this.setPlayhead('ticks', newPos.ticks); */ }; Song.prototype.setTempo = function(bpm, update) { var timeEvents = getTimeEvents(sequencer.TEMPO, this), i, event, ticks = this.ticks, percentage = this.percentage, ratio = bpm / timeEvents[0].bpm; for (i = timeEvents.length - 1; i >= 0; i--) { event = timeEvents[i]; event.bpm = ratio * event.bpm; } this.bpm = bpm; if (update === false) { return; } //console.log('setTempo -> update()'); this.update(true); this.updatePlayheadAndLocators(ticks); }; Song.prototype.setTimeSignature = function(nominator, denominator, update) { var timeEvents = getTimeEvents(sequencer.TIME_SIGNATURE, this), i, event, percentage = this.percentage, ticks = this.ticks; for (i = timeEvents.length - 1; i >= 0; i--) { event = timeEvents[i]; event.nominator = nominator; event.denominator = denominator; } this.nominator = nominator; this.denominator = denominator; if (update === false) { return; } //console.log('setTimeSignature -> update()'); this.update(true); this.updatePlayheadAndLocators(ticks); }; Song.prototype.resetTempo = function(bpm) { var firstTempoEvent = getTimeEvents(sequencer.TEMPO, this)[0], timeEvents = this.timeEvents; firstTempoEvent.bpm = bpm; timeEvents = removeFromArray2(timeEvents, function(event) { if (event.type === 0x51) { return true; } return false; }); this.numTimeEvents = timeEvents.length; this.update(true); }; Song.prototype.resetTimeSignature = function(nominator, denominator) { var firstTimeSignatureEvent = getTimeEvents(sequencer.TIME_SIGNATURE, this)[0], timeEvents = this.timeEvents, ticks = this.ticks; firstTimeSignatureEvent.nominator = nominator; firstTimeSignatureEvent.denominator = denominator; timeEvents = removeFromArray2(timeEvents, function(event) { if (event.type === 0x58) { return true; } return false; }); this.numTimeEvents = timeEvents.length; this.update(true); this.updatePlayheadAndLocators(ticks); }; Song.prototype.addTimeEvent = Song.prototype.addTimeEvents = function() { var events = getArguments(arguments), ticks = this.ticks, i, event, position; // console.log(events); for (i = events.length - 1; i >= 0; i--) { event = events[i]; if (event.className === "MidiEvent") { if (event.type === sequencer.TEMPO) { this.timeEvents.push(event); } else if (event.type === sequencer.TIME_SIGNATURE) { /* A time signature event can only be positioned at the beginning of a bar, so we look for the nearest bar and put the event there. */ position = this.getPosition("ticks", event.ticks); if (position.beat > position.nominator / 2) { position = this.getPosition("barsbeats", position.bar + 1); } else { position = this.getPosition("barsbeats", position.bar); } event.ticks = position.ticks; this.timeEvents.push(event); } } } this.numTimeEvents = this.timeEvents.length; this.update(true); //console.log('addTimeEvents', this.timeEvents); this.updatePlayheadAndLocators(ticks); }; /* Song.prototype.addTimeEvent = function(){ var events = getArguments(arguments), arg0 = events[0], numArgs = events.length; if(typeString(arg0) === 'array' || numArgs > 1){ console.warn('please use addTimeEvents() if you want to add more that one time event'); events = [arg0]; } addTimeEvents(events, this); }; */ Song.prototype.removeTimeEvent = Song.prototype.removeTimeEvents = function() { var tmp = getArguments(arguments), i, maxi = tmp.length, event, ticks = this.ticks, events = []; for (i = 0; i < maxi; i++) { event = tmp[i]; if (event !== this.tempoEvent && event !== this.timeSignatureEvent) { events.push(event); } } //console.log(events); this.timeEvents = removeFromArray(events, this.timeEvents); this.numTimeEvents = this.timeEvents.length; this.update(true); this.updatePlayheadAndLocators(ticks); }; Song.prototype.removeDoubleTimeEvents = function() { var events = [], i, event, ticks, type, eventsByTicks = { "81": {}, "88": {}, }; //console.log('before', this.timeEvents); for (i = this.timeEvents.length - 1; i >= 0; i--) { event = this.timeEvents[i]; if (eventsByTicks[event.type][event.ticks] !== undefined) { continue; } type = event.type; ticks = event.ticks; eventsByTicks[type][ticks] = event; if (ticks === 0) { if (type === 0x51) { this.tempoEvent = event; } else if (type === 0x58) { this.timeSignatureEvent = event; } } } objectForEach(eventsByTicks["81"], function(event) { events.push(event); }); objectForEach(eventsByTicks["88"], function(event) { events.push(event); }); this.timeEvents = events; this.update(true); //console.log('after', this.timeEvents); //this.timeEvents.forEach(function(event){ // console.log(event.barsAsString, event.bpm, event.nominator, event.denominator); //}); //console.log('tempo', this.tempoEvent.bpm, this.tempoEvent.nominator, this.tempoEvent.denominator, this.tempoEvent.barsAsString); //console.log('time signature', this.timeSignatureEvent.bpm, this.timeSignatureEvent.nominator, this.timeSignatureEvent.denominator, this.timeSignatureEvent.barsAsString); }; Song.prototype.setPitchRange = function(min, max) { var me = this; me.lowestNote = min; me.highestNote = max; me.numNotes = me.pitchRange = me.highestNote - me.lowestNote + 1; }; Song.prototype.trim = function() { checkDuration(this, true); }; Song.prototype.setDurationInBars = function(bars) { var me = this, removedEvents = me.findEvent("bar > " + bars), removedParts = [], changedTracks = [], changedParts = [], dirtyTracks = {}, dirtyParts = {}; //console.log(removedEvents); removedEvents.forEach(function(event) { var trackId = event.trackId, partId = event.partId; if (dirtyTracks[trackId] === undefined) { dirtyTracks[trackId] = []; } dirtyTracks[trackId].push(event); if (dirtyParts[partId] === undefined) { dirtyParts[partId] = event.part; //console.log(me.getPart(partId)); } }); objectForEach(dirtyTracks, function(events, trackId) { var track = me.getTrack(trackId); //console.log(track.name) track.removeEvents(events); changedTracks.push(track); }); objectForEach(dirtyParts, function(part, partId) { if (part.events.length === 0) { //console.log(partId, 'has no events'); part.track.removePart(part); removedParts.push(part); } else { changedParts.push(part); } }); me.bars = bars; me.lastBar = bars; // user needs to call song.update() after setDurationInBars()! //checkDuration(this); //console.log(this.ticks); return { removedEvents: removedEvents, removedParts: removedParts, changedTracks: changedTracks, changedParts: changedParts, }; }; Song.prototype.addEffect = function() {}; Song.prototype.removeEffect = function() {}; Song.prototype.setVolume = function() { // value, Track, Track, Track, etc. in any order var args = slice.call(arguments), numArgs = args.length, tracks = [], value, i; function loop(data, i, maxi) { for (i = 0; i < maxi; i++) { var arg = data[i],