UNPKG

qambi

Version:

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

677 lines (588 loc) 18.4 kB
import { Part } from './part' import { MIDIEvent } from './midi_event' import { MIDINote } from './midi_note' import { getMIDIInputById, getMIDIOutputById } from './init_midi' import { sortEvents } from './util' import { context } from './init_audio' import { MIDIEventTypes } from './qambi' import { dispatchEvent } from './eventlistener' const zeroValue = 0.00000000000000001 let instanceIndex = 0 export class Track { constructor(settings = {}) { this.id = `${this.constructor.name}_${instanceIndex++}_${new Date().getTime()}`; ({ name: this.name = this.id, channel: this.channel = 0, muted: this.muted = false, volume: this.volume = 0.5, } = settings); //console.log(this.name, this.channel, this.muted, this.volume) this._panner = context.createPanner() this._panner.panningModel = 'equalpower' this._panner.setPosition(zeroValue, zeroValue, zeroValue) this._gainNode = context.createGain() this._gainNode.gain.value = this.volume this._panner.connect(this._gainNode) //this._gainNode.connect(this._panner) this._midiInputs = new Map() this._midiOutputs = new Map() this._song = null this._parts = [] this._partsById = new Map() this._events = [] this._eventsById = new Map() this._needsUpdate = false this._createEventArray = false this._instrument = null this._tmpRecordedNotes = new Map() this._recordedEvents = [] this.scheduledSamples = new Map() this.sustainedSamples = [] this.sustainPedalDown = false this.monitor = false this._songGainNode = null this._effects = [] this._numEffects = 0 let { parts, instrument } = settings if (typeof parts !== 'undefined') { this.addParts(...parts) } if (typeof instrument !== 'undefined') { this.setInstrument(instrument) } } setInstrument(instrument = null) { if (instrument !== null // check if the mandatory functions of an instrument are present (Interface Instrument) && typeof instrument.connect === 'function' && typeof instrument.disconnect === 'function' && typeof instrument.processMIDIEvent === 'function' && typeof instrument.allNotesOff === 'function' && typeof instrument.unschedule === 'function' ) { this.removeInstrument() this._instrument = instrument this._instrument.connect(this._panner) } else if (instrument === null) { // if you pass null as argument the current instrument will be removed, same as removeInstrument this.removeInstrument() } else { console.log('Invalid instrument, and instrument should have the methods "connect", "disconnect", "processMIDIEvent", "unschedule" and "allNotesOff"') } } removeInstrument() { if (this._instrument !== null) { this._instrument.allNotesOff() this._instrument.disconnect() this._instrument = null } } getInstrument() { return this._instrument } connectMIDIOutputs(...outputs) { //console.log(outputs) outputs.forEach(output => { if (typeof output === 'string') { output = getMIDIOutputById(output) } // if (output instanceof MIDIOutput) { if (output.type === 'output') { this._midiOutputs.set(output.id, output) } }) //console.log(this._midiOutputs) } disconnectMIDIOutputs(...outputs) { //console.log(outputs) if (outputs.length === 0) { this._midiOutputs.clear() } outputs.forEach(port => { // if (port instanceof MIDIOutput) { if (port.type === 'output') { port = port.id } if (this._midiOutputs.has(port)) { //console.log('removing', this._midiOutputs.get(port).name) this._midiOutputs.delete(port) } }) //this._midiOutputs = this._midiOutputs.filter(...outputs) //console.log(this._midiOutputs) } connectMIDIInputs(...inputs) { //console.log(Object.getPrototypeOf(MIDIInput)); inputs.forEach(input => { if (typeof input === 'string') { input = getMIDIInputById(input) } // if (input instanceof MIDIInput) { if (input.type === 'input') { this._midiInputs.set(input.id, input) input.onmidimessage = e => { if (this.monitor === true) { //console.log(...e.data) this._preprocessMIDIEvent(new MIDIEvent(this._song._ticks, ...e.data)) } } } }) //console.log(this._midiInputs) } // you can pass both port and port ids disconnectMIDIInputs(...inputs) { if (inputs.length === 0) { this._midiInputs.forEach(port => { port.onmidimessage = null }) this._midiInputs.clear() return } inputs.forEach(port => { // if (port instanceof MIDIInput) { if (port.type === 'input') { port = port.id } if (this._midiInputs.has(port)) { this._midiInputs.get(port).onmidimessage = null this._midiInputs.delete(port) } }) //this._midiOutputs = this._midiOutputs.filter(...outputs) //console.log(this._midiInputs) } getMIDIInputs() { return Array.from(this._midiInputs.values()) } getMIDIOutputs() { return Array.from(this._midiOutputs.values()) } setRecordEnabled(type) { // 'midi', 'audio', empty or anything will disable recording this._recordEnabled = type } _startRecording(recordId) { if (this._recordEnabled === 'midi') { //console.log(recordId) this._recordId = recordId this._recordedEvents = [] this._recordPart = new Part(this._recordId) } } _stopRecording(recordId) { if (this._recordId !== recordId) { return } if (this._recordedEvents.length === 0) { return } this._recordPart.addEvents(...this._recordedEvents) //this._song._newEvents.push(...this._recordedEvents) this.addParts(this._recordPart) } undoRecording(recordId) { if (this._recordId !== recordId) { return } this.removeParts(this._recordPart) //this._song._removedEvents.push(...this._recordedEvents) } redoRecording(recordId) { if (this._recordId !== recordId) { return } this.addParts(this._recordPart) } copy() { let t = new Track(this.name + '_copy') // implement getNameOfCopy() in util (see heartbeat) let parts = [] this._parts.forEach(function (part) { let copy = part.copy() console.log(copy) parts.push(copy) }) t.addParts(...parts) t.update() return t } transpose(amount: number) { this._events.forEach((event) => { event.transpose(amount) }) } addParts(...parts) { let song = this._song parts.forEach((part) => { part._track = this this._parts.push(part) this._partsById.set(part.id, part) let events = part._events this._events.push(...events) if (song) { part._song = song song._newParts.push(part) song._newEvents.push(...events) } events.forEach((event) => { event._track = this if (song) { event._song = song } this._eventsById.set(event.id, event) }) }) this._needsUpdate = true } removeParts(...parts) { let song = this._song parts.forEach((part) => { part._track = null this._partsById.delete(part.id, part) let events = part._events if (song) { song._removedParts.push(part) song._removedEvents.push(...events) } events.forEach(event => { event._track = null if (song) { event._song = null } this._eventsById.delete(event.id, event) }) }) this._needsUpdate = true this._createEventArray = true } getParts() { if (this._needsUpdate) { this._parts = Array.from(this._partsById.values()) this._events = Array.from(this._eventsById.values()) this._needsUpdate = false } return [...this._parts] } transposeParts(amount: number, ...parts) { parts.forEach(function (part) { part.transpose(amount) }) } moveParts(ticks: number, ...parts) { parts.forEach(function (part) { part.move(ticks) }) } movePartsTo(ticks: number, ...parts) { parts.forEach(function (part) { part.moveTo(ticks) }) } /* addEvents(...events){ let p = new Part() p.addEvents(...events) this.addParts(p) } */ removeEvents(...events) { let parts = new Set() events.forEach((event) => { parts.set(event._part) event._part = null event._track = null event._song = null this._eventsById.delete(event.id) }) if (this._song) { this._song._removedEvents.push(...events) this._song._changedParts.push(...Array.from(parts.entries())) } this._needsUpdate = true this._createEventArray = true } moveEvents(ticks: number, ...events) { let parts = new Set() events.forEach((event) => { event.move(ticks) parts.set(event.part) }) if (this._song) { this._song._movedEvents.push(...events) this._song._changedParts.push(...Array.from(parts.entries())) } } moveEventsTo(ticks: number, ...events) { let parts = new Set() events.forEach((event) => { event.moveTo(ticks) parts.set(event.part) }) if (this._song) { this._song._movedEvents.push(...events) this._song._changedParts.push(...Array.from(parts.entries())) } } getEvents(filter: string[] = null) { // can be use as findEvents if (this._needsUpdate) { this.update() } return [...this._events] //@TODO implement filter -> filterEvents() should be a utility function (not a class method) } mute(flag: boolean = null) { if (flag) { this._muted = flag } else { this._muted = !this._muted } } update() { // you should only use this in huge songs (>100 tracks) if (this._createEventArray) { this._events = Array.from(this._eventsById.values()) this._createEventArray = false } sortEvents(this._events) this._needsUpdate = false } _checkEffect(effect) { if (effect.input instanceof AudioNode === false || effect.output instanceof AudioNode === false) { console.log('A channel fx should have an input and an output implementing the interface AudioNode') return false } return true } // routing: audiosource -> panning -> track output -> [...effect] -> song input insertEffect(effect) { if (this._checkEffect(effect) === false) { return } let prevEffect if (this._numEffects === 0) { this._gainNode.disconnect(this._songGainNode) this._gainNode.connect(effect.input) effect.output.connect(this._songGainNode) } else { prevEffect = this._effects[this._numEffects - 1] try { prevEffect.output.disconnect(this._songGainNode) } catch (e) { //Chrome throws an error here which is wrong } prevEffect.output.connect(effect.input) effect.output.connect(this._songGainNode) } this._effects.push(effect) this._numEffects++ } insertEffectAt(effect, index: number) { if (this._checkEffect(effect) === false) { return } let prevEffect = this._effects[index - 1] let nextEffect if (index === this._numEffects) { prevEffect.output.disconnect(this._songGainNode) prevEffect.output.connect(effect.input) effect.input.connect(this._songGainNode) } else { nextEffect = this._effects[index] prevEffect.output.disconnect(nextEffect.input) prevEffect.output.connect(effect.input) effect.output.connect(nextEffect.input) } this._effects.splice(index, 0, effect) this._numEffects++ } //removeEffect(effect: Effect){ removeEffect(effect) { if (this._checkEffect(effect) === false) { return } let i for (i = 0; i < this._numEffects; i++) { let fx = this._effects[i] if (effect === fx) { break } } this.removeEffectAt(i) } removeEffectAt(index: number) { if (isNaN(index) || this._numEffects === 0 || index >= this._numEffects) { return } let effect = this._effects[index] let nextEffect let prevEffect //console.log(index, this._effects) if (index === 0) { // we remove the first effect, so disconnect from output of track this._gainNode.disconnect(effect.input) if (this._numEffects === 1) { // no effects anymore, so connect output of track to input of the song try { effect.output.disconnect(this._songGainNode) } catch (e) { //Chrome throws an error here which is wrong } this._gainNode.connect(this._songGainNode) } else { // disconnect the removed effect from the next effect in the chain, this is now the first effect in the chain... nextEffect = this._effects[index + 1] try { effect.output.disconnect(nextEffect.input) } catch (e) { //Chrome throws an error here which is wrong } // ... so connect the output of the track to the input of this effect this._gainNode.connect(nextEffect.input) } } else { prevEffect = this._effects[index - 1] //console.log(prevEffect) // disconnect the removed effect from the previous effect in the chain try { prevEffect.output.disconnect(effect.input) } catch (e) { //Chrome throws an error here which is wrong } if (index === this._numEffects - 1) { // we remove the last effect in the chain, so disconnect from the input of the song try { effect.output.disconnect(this._songGainNode) } catch (e) { //Chrome throws an error here which is wrong } // the previous effect is now the last effect to connect it to the input of the song prevEffect.output.connect(this._songGainNode) } else { // disconnect the effect from the next effect in the chain nextEffect = this._effects[index] effect.output.disconnect(nextEffect.input) // connect the previous effect to the next effect prevEffect.output.connect(nextEffect.input) } } this._effects.splice(index, 1) this._numEffects-- } getEffects() { return [...this._effects] } getEffectAt(index: number) { if (isNaN(index)) { return null } return this._effects[index] } getOutput() { return this._gainNode } getInput() { return this._songGainNode } // method is called when a MIDI events is send by an external or on-screen keyboard _preprocessMIDIEvent(midiEvent) { let time = context.currentTime * 1000 midiEvent.time = time midiEvent.time2 = 0//performance.now() -> passing 0 has the same effect as performance.now() so we choose the former midiEvent.recordMillis = time let note if (midiEvent.type === MIDIEventTypes.NOTE_ON) { note = new MIDINote(midiEvent) this._tmpRecordedNotes.set(midiEvent.data1, note) dispatchEvent({ type: 'noteOn', data: midiEvent }) } else if (midiEvent.type === MIDIEventTypes.NOTE_OFF) { note = this._tmpRecordedNotes.get(midiEvent.data1) if (typeof note === 'undefined') { return } note.addNoteOff(midiEvent) this._tmpRecordedNotes.delete(midiEvent.data1) dispatchEvent({ type: 'noteOff', data: midiEvent }) } if (this._recordEnabled === 'midi' && this._song.recording === true) { this._recordedEvents.push(midiEvent) } this.processMIDIEvent(midiEvent) } // method is called by scheduler during playback processMIDIEvent(event) { if (typeof event.time === 'undefined') { this._preprocessMIDIEvent(event) return } // send to javascript instrument if (this._instrument !== null) { //console.log(this.name, event) this._instrument.processMIDIEvent(event) } // send to external hardware or software instrument this._sendToExternalMIDIOutputs(event) } _sendToExternalMIDIOutputs(event) { //console.log(event.time, event.millis) for (let port of this._midiOutputs.values()) { if (port) { if (event.data2 !== -1) { port.send([event.type + this.channel, event.data1, event.data2], event.time2) } else { port.send([event.type + this.channel, event.data1], event.time2) } // if(event.type === 128 || event.type === 144 || event.type === 176){ // port.send([event.type + this.channel, event.data1, event.data2], event.time + latency) // }else if(event.type === 192 || event.type === 224){ // port.send([event.type, event.data1], event.time + latency) // } } } } unschedule(midiEvent) { if (this._instrument !== null) { this._instrument.unschedule(midiEvent) } if (this._midiOutputs.size === 0) { return } if (midiEvent.type === 144) { let midiNote = midiEvent.midiNote let noteOff = new MIDIEvent(0, 128, midiEvent.data1, 0) noteOff.midiNoteId = midiNote.id noteOff.time = context.currentTime this._sendToExternalMIDIOutputs(noteOff, true) } } allNotesOff() { if (this._instrument !== null) { this._instrument.allNotesOff() } // let timeStamp = (context.currentTime * 1000) + this.latency // for(let output of this._midiOutputs.values()){ // output.send([0xB0, 0x7B, 0x00], timeStamp) // stop all notes // output.send([0xB0, 0x79, 0x00], timeStamp) // reset all controllers // } } setPanning(value) { if (value < -1 || value > 1) { console.log('Track.setPanning() accepts a value between -1 (full left) and 1 (full right), you entered:', value) return } let x = value let y = 0 let z = 1 - Math.abs(x) x = x === 0 ? zeroValue : x y = y === 0 ? zeroValue : y z = z === 0 ? zeroValue : z this._panner.setPosition(x, y, z) this._panningValue = value } getPanning() { return this._panningValue } }