qambi
Version:
MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio
405 lines (341 loc) • 13 kB
JavaScript
import {getMIDIOutputById, getMIDIOutputs} from './init_midi'
import {context} from './init_audio'
import {MIDIEvent} from './midi_event'
import {sortEvents} from './util' // millis
import {dispatchEvent} from './eventlistener' // millis
export default class Scheduler {
constructor(song) {
this.song = song
this.notes = new Map()
this.bufferTime = song.bufferTime
}
init(millis) {
this.songCurrentMillis = millis
this.songStartMillis = millis
this.events = this.song._allEvents
this.numEvents = this.events.length
this.index = 0
this.maxtime = 0
this.prevMaxtime = 0
this.beyondLoop = false // tells us if the playhead has already passed the looped section
this.precountingDone = false
this.looped = false
this.setIndex(this.songStartMillis)
}
updateSong() {
//this.songCurrentMillis = this.song._currentMillis
this.events = this.song._allEvents
this.numEvents = this.events.length
this.index = 0
this.maxtime = 0
//this.precountingDone = false
this.setIndex(this.song._currentMillis)
}
setTimeStamp(timeStamp) {
this.timeStamp = timeStamp // timestamp WebAudio context -> for internal instruments
this.timeStamp2 = performance.now() // timestamp since opening webpage -> for external instruments
}
// get the index of the event that has its millis value at or right after the provided millis value
setIndex(millis) {
let i = 0
let event
for (event of this.events) {
if (event.millis >= millis) {
this.index = i;
break;
}
i++;
}
this.beyondLoop = millis > this.song._rightLocator.millis
// this.notes = new Map()
//this.looped = false
this.precountingDone = false
}
getEvents() {
let events = []
if (this.song._loop === true && this.song._loopDuration < this.bufferTime) {
this.maxtime = this.songStartMillis + this.song._loopDuration - 1
//console.log(this.maxtime, this.song.loopDuration);
}
if (this.song._loop === true) {
if (this.maxtime >= this.song._rightLocator.millis && this.beyondLoop === false) {
//console.log('LOOP', this.maxtime, this.song._rightLocator.millis)
let diff = this.maxtime - this.song._rightLocator.millis
this.maxtime = this.song._leftLocator.millis + diff
//console.log('-------LOOPED', this.maxtime, diff, this.song._leftLocator.millis, this.song._rightLocator.millis);
if (this.looped === false) {
this.looped = true;
let leftMillis = this.song._leftLocator.millis
let rightMillis = this.song._rightLocator.millis
for (let i = this.index; i < this.numEvents; i++) {
let event = this.events[i];
//console.log(event)
if (event.millis < rightMillis) {
event.time = this.timeStamp + event.millis - this.songStartMillis
event.time2 = this.timeStamp2 + event.millis - this.songStartMillis
events.push(event)
if (event.type === 144) {
this.notes.set(event.midiNoteId, event.midiNote)
}
//console.log(event.midiNoteId, event.type)
this.index++
} else {
break
}
}
// stop overflowing notes-> add a new note off event at the position of the right locator (end of the loop)
let endTicks = this.song._rightLocator.ticks - 1
let endMillis = this.song.calculatePosition({type: 'ticks', target: endTicks, result: 'millis'}).millis
for (let note of this.notes.values()) {
let noteOn = note.noteOn
let noteOff = note.noteOff
if (noteOff.millis <= rightMillis) {
continue
}
let event = new MIDIEvent(endTicks, 128, noteOn.data1, 0)
event.millis = endMillis
event._part = noteOn._part
event._track = noteOn._track
event.midiNote = note
event.midiNoteId = note.id
event.time = this.timeStamp + event.millis - this.songStartMillis
event.time2 = this.timeStamp2 + event.millis - this.songStartMillis
//console.log('added', event)
events.push(event)
}
/*
// stop overflowing audio samples
for(i in this.scheduledAudioEvents){
if(this.scheduledAudioEvents.hasOwnProperty(i)){
audioEvent = this.scheduledAudioEvents[i];
if(audioEvent.endMillis > this.song.loopEnd){
audioEvent.stopSample(this.song.loopEnd/1000);
delete this.scheduledAudioEvents[i];
//console.log('stopping audio event', i);
}
}
}
*/
this.notes = new Map()
this.setIndex(leftMillis)
this.timeStamp += this.song._loopDuration
this.songCurrentMillis -= this.song._loopDuration
//console.log(events.length)
// get the audio events that start before song.loopStart
//this.getDanglingAudioEvents(this.song.loopStart, events);
}
} else {
this.looped = false
}
}
//console.log('scheduler', this.looped)
// main loop
for (let i = this.index; i < this.numEvents; i++) {
let event = this.events[i];
//console.log(event.millis, this.maxtime)
if (event.millis < this.maxtime) {
//event.time = this.timeStamp + event.millis - this.songStartMillis;
if (event.type === 'audio') {
// to be implemented
} else {
event.time = (this.timeStamp + event.millis - this.songStartMillis)
event.time2 = (this.timeStamp2 + event.millis - this.songStartMillis)
events.push(event);
}
this.index++;
} else {
break;
}
}
return events;
}
update(diff) {
var i,
event,
numEvents,
track,
events
this.prevMaxtime = this.maxtime
if (this.song.precounting) {
this.songCurrentMillis += diff
this.maxtime = this.songCurrentMillis + this.bufferTime
//console.log(this.songCurrentMillis)
events = this.song._metronome.getPrecountEvents(this.maxtime)
// if(events.length > 0){
// console.log(context.currentTime * 1000)
// console.log(events)
// }
if (this.maxtime > this.song._metronome.endMillis && this.precountingDone === false) {
this.precountingDone = true
this.timeStamp += this.song._precountDuration
// start scheduling events of the song -> add the first events of the song
this.songCurrentMillis = this.songStartMillis
//console.log('---->', this.songCurrentMillis)
this.songCurrentMillis += diff
this.maxtime = this.songCurrentMillis + this.bufferTime
events.push(...this.getEvents())
//console.log(events)
}
} else {
this.songCurrentMillis += diff
this.maxtime = this.songCurrentMillis + this.bufferTime
events = this.getEvents()
//events = this.song._getEvents2(this.maxtime, (this.timeStamp - this.songStartMillis))
//events = this.getEvents2(this.maxtime, (this.timeStamp - this.songStartMillis))
//console.log('done', this.songCurrentMillis, diff, this.index, events.length)
}
// if(this.song.useMetronome === true){
// let metronomeEvents = this.song._metronome.getEvents2(this.maxtime, (this.timeStamp - this.songStartMillis))
// // if(metronomeEvents.length > 0){
// // console.log(this.maxtime, metronomeEvents)
// // }
// // metronomeEvents.forEach(e => {
// // e.time = (this.timeStamp + e.millis - this.songStartMillis)
// // })
// events.push(...metronomeEvents)
// }
numEvents = events.length
// if(numEvents > 5){
// console.log(numEvents)
// }
//console.log(this.maxtime, this.song._currentMillis, '[diff]', this.maxtime - this.prevMaxtime)
for (i = 0; i < numEvents; i++) {
event = events[i]
track = event._track
// console.log(this.maxtime, this.prevMaxtime, event.millis)
// if(event.millis > this.maxtime){
// // skip events that were harvest accidently while jumping the playhead -> should happen very rarely if ever
// console.log('skip', event)
// continue
// }
if (event._part === null || track === null) {
console.log(event)
this.notes.set(event.midiNoteId, event.midiNote)
continue
}
if (event._part.muted === true || track.muted === true || event.muted === true) {
continue
}
if ((event.type === 144 || event.type === 128) && typeof event.midiNote === 'undefined') {
// this is usually caused by the same note on the same ticks value, which is probably a bug in the midi file
//console.info('no midiNoteId', event)
continue
}
// /console.log(event.ticks, event.time, event.millis, event.type, event._track.name)
if (event.type === 'audio') {
// to be implemented
} else {
track.processMIDIEvent(event)
if (track.name === `${this.song.id}_metronome` && this.song.useMetronome) {
dispatchEvent({
type: 'metronome',
data: event,
});
}
//console.log(context.currentTime * 1000, event.time, this.index)
if (event.type === 144) {
this.notes.set(event.midiNoteId, event.midiNote)
} else if (event.type === 128) {
this.notes.delete(event.midiNoteId)
}
// if(this.notes.size > 0){
// console.log(this.notes)
// }
}
}
//console.log(this.index, this.numEvents)
//return this.index >= 10
return this.index >= this.numEvents // last event of song
}
/*
unschedule(){
let min = this.song._currentMillis
let max = min + (bufferTime * 1000)
//console.log('reschedule', this.notes.size)
this.notes.forEach((note, id) => {
// console.log(note)
// console.log(note.noteOn.millis, note.noteOff.millis, min, max)
if(typeof note === 'undefined' || note.state === 'removed'){
//sample.unschedule(0, unscheduleCallback);
//console.log('NOTE IS UNDEFINED')
//sample.stop(0)
this.notes.delete(id)
}else if((note.noteOn.millis >= min || note.noteOff.millis < max) === false){
//sample.stop(0)
let noteOn = note.noteOn
let noteOff = new MIDIEvent(0, 128, noteOn.data1, 0)
noteOff.midiNoteId = note.id
noteOff.time = 0//context.currentTime + min
note._track.processMIDIEvent(noteOff)
this.notes.delete(id)
console.log('STOPPING', id, note._track.name)
}
})
//console.log('NOTES', this.notes.size)
//this.notes.clear()
}
*/
allNotesOff() {
let timeStamp = performance.now()
let outputs = getMIDIOutputs()
outputs.forEach((output) => {
output.send([0xB0, 0x7B, 0x00], timeStamp + this.bufferTime) // stop all notes
output.send([0xB0, 0x79, 0x00], timeStamp + this.bufferTime) // reset all controllers
})
}
}
/*
getEvents2(maxtime, timestamp){
let loop = true
let event
let result = []
//console.log(this.timeEventsIndex, this.songEventsIndex, this.metronomeEventsIndex)
while(loop){
let stop = false
if(this.timeEventsIndex < this.numTimeEvents){
event = this.timeEvents[this.timeEventsIndex]
if(event.millis < maxtime){
this.millisPerTick = event.millisPerTick
//console.log(this.millisPerTick)
this.timeEventsIndex++
}else{
stop = true
}
}
if(this.songEventsIndex < this.numSongEvents){
event = this.songEvents[this.songEventsIndex]
if(event.type === 0x2F){
loop = false
break
}
let millis = event.ticks * this.millisPerTick
if(millis < maxtime){
event.time = millis + timestamp
event.millis = millis
result.push(event)
this.songEventsIndex++
}else{
stop = true
}
}
if(this.song.useMetronome === true && this.metronomeEventsIndex < this.numMetronomeEvents){
event = this.metronomeEvents[this.metronomeEventsIndex]
let millis = event.ticks * this.millisPerTick
if(millis < maxtime){
event.time = millis + timestamp
event.millis = millis
result.push(event)
this.metronomeEventsIndex++
}else{
stop = true
}
}
if(stop){
loop = false
break
}
}
sortEvents(result)
return result
}
*/