UNPKG

qambi

Version:

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

445 lines (360 loc) 11.6 kB
/* Requests MIDI access, queries all inputs and outputs and stores them in alphabetical order */ import {typeString} from './util' import 'jzz' // you can also embed the shim as a stand-alone script in the html, then you can comment this line out let MIDIAccess let initialized = false let inputs = [] let outputs = [] let inputIds = [] let outputIds = [] let inputsById = new Map() let outputsById = new Map() let songMidiEventListener let midiEventListenerId = 0 function getMIDIports() { inputs = Array.from(MIDIAccess.inputs.values()) //sort ports by name ascending inputs.sort((a, b) => a.name.toLowerCase() <= b.name.toLowerCase() ? 1 : -1) for (let port of inputs) { inputsById.set(port.id, port) inputIds.push(port.id) } outputs = Array.from(MIDIAccess.outputs.values()) //sort ports by name ascending outputs.sort((a, b) => a.name.toLowerCase() <= b.name.toLowerCase() ? 1 : -1) //console.log(outputs) for (let port of outputs) { //console.log(port.id, port.name) outputsById.set(port.id, port) outputIds.push(port.id) } //console.log(outputsById) } export function initMIDI() { return new Promise(function executor(resolve, reject) { let jazz = false let midi = false let webmidi = false if (typeof navigator === 'undefined') { initialized = true resolve({midi}) } else if (typeof navigator.requestMIDIAccess !== 'undefined') { navigator.requestMIDIAccess().then( function onFulFilled(midiAccess) { MIDIAccess = midiAccess // @TODO: implement something in webmidiapishim that allows us to detect the Jazz plugin version if (typeof midiAccess._jazzInstances !== 'undefined') { jazz = midiAccess._jazzInstances[0]._Jazz.version console.log('jazz version:', jazz); midi = true } else { webmidi = true midi = true } getMIDIports() // onconnect and ondisconnect are not yet implemented in Chrome and Chromium midiAccess.onconnect = function (e) { console.log('device connected', e) getMIDIports() } midiAccess.ondisconnect = function (e) { console.log('device disconnected', e) getMIDIports() } initialized = true resolve({ jazz, midi, webmidi, inputs, outputs, inputsById, outputsById, }) }, function onReject(e) { //console.log(e) //reject('Something went wrong while requesting MIDIAccess', e) initialized = true resolve({midi, jazz}) } ) // browsers without WebMIDI API } else { initialized = true resolve({midi}) } }) } export let getMIDIAccess = function () { if (initialized === false) { console.warn('please call qambi.init() first') } else { getMIDIAccess = function () { return MIDIAccess } return getMIDIAccess() } return false } export let getMIDIOutputs = function () { if (initialized === false) { console.warn('please call qambi.init() first') } else { getMIDIOutputs = function () { return outputs } return getMIDIOutputs() } return false } export let getMIDIInputs = function () { if (initialized === false) { console.warn('please call qambi.init() first') } else { getMIDIInputs = function () { return inputs } return getMIDIInputs() } return false } export let getMIDIOutputIds = function () { if (initialized === false) { console.warn('please call qambi.init() first') } else { getMIDIOutputIds = function () { return outputIds } return getMIDIOutputIds() } return false } export let getMIDIInputIds = function () { if (initialized === false) { console.warn('please call qambi.init() first') } else { getMIDIInputIds = function () { return inputIds } return getMIDIInputIds() } return false } export let getMIDIOutputById = function (id: string) { if (initialized === false) { console.warn('please call qambi.init() first') } else { getMIDIOutputById = function (_id) { return outputsById.get(_id) } return getMIDIOutputById(id) } return false } export let getMIDIInputById = function (id: string) { if (initialized === false) { console.warn('please call qambi.init() first') } else { getMIDIInputById = function (_id) { return inputsById.get(_id) } return getMIDIInputById(id) } return false } /* export function initMidiSong(song){ songMidiEventListener = function(e){ //console.log(e) handleMidiMessageSong(song, e, this); }; // by default a song listens to all available midi-in ports inputs.forEach(function(port){ port.addEventListener('midimessage', songMidiEventListener); song.midiInputs.set(port.id, port); }); outputs.forEach(function(port){ song.midiOutputs.set(port.id, port); }); } export function setMidiInputSong(song, id, flag){ let input = inputs.get(id); if(input === undefined){ warn('no midi input with id', id, 'found'); return; } if(flag === false){ song.midiInputs.delete(id); input.removeEventListener('midimessage', songMidiEventListener); }else{ song.midiInputs.set(id, input); input.addEventListener('midimessage', songMidiEventListener); } let tracks = song.tracks; for(let track of tracks){ track.setMidiInput(id, flag); } } export function setMidiOutputSong(song, id, flag){ let output = outputs.get(id); if(output === undefined){ warn('no midi output with id', id, 'found'); return; } if(flag === false){ song.midiOutputs.delete(id); let time = song.scheduler.lastEventTime + 100; output.send([0xB0, 0x7B, 0x00], time); // stop all notes output.send([0xB0, 0x79, 0x00], time); // reset all controllers }else{ song.midiOutputs.set(id, output); } let tracks = song.tracks; for(let track of tracks){ track.setMidiOutput(id, flag); } } function handleMidiMessageSong(song, midiMessageEvent, input){ let midiEvent = new MidiEvent(song.ticks, ...midiMessageEvent.data); //console.log(midiMessageEvent.data); let tracks = song.tracks; for(let track of tracks){ //console.log(track.midiInputs, input); //if(midiEvent.channel === track.channel || track.channel === 0 || track.channel === 'any'){ // handleMidiMessageTrack(midiEvent, track); //} // like in Cubase, midi events from all devices, sent on any midi channel are forwarded to all tracks // set track.monitor to false if you don't want to receive midi events on a certain track // note that track.monitor is by default set to false and that track.monitor is automatically set to true // if you are recording on that track //console.log(track.monitor, track.id, input.id); if(track.monitor === true && track.midiInputs.get(input.id) !== undefined){ handleMidiMessageTrack(midiEvent, track, input); } } let listeners = song.midiEventListeners.get(midiEvent.type); if(listeners !== undefined){ for(let listener of listeners){ listener(midiEvent, input); } } } function handleMidiMessageTrack(track, midiEvent, input){ let song = track.song, note, listeners, channel; //data = midiMessageEvent.data, //midiEvent = createMidiEvent(song.ticks, data[0], data[1], data[2]); //midiEvent.source = midiMessageEvent.srcElement.name; //console.log(midiMessageEvent) //console.log('---->', midiEvent.type); // add the exact time of this event so we can calculate its ticks position midiEvent.recordMillis = context.currentTime * 1000; // millis midiEvent.state = 'recorded'; if(midiEvent.type === 144){ note = createMidiNote(midiEvent); track.recordingNotes[midiEvent.data1] = note; //track.song.recordingNotes[note.id] = note; }else if(midiEvent.type === 128){ note = track.recordingNotes[midiEvent.data1]; // check if the note exists: if the user plays notes on her keyboard before the midi system has // been fully initialized, it can happen that the first incoming midi event is a NOTE OFF event if(note === undefined){ return; } note.addNoteOff(midiEvent); delete track.recordingNotes[midiEvent.data1]; //delete track.song.recordingNotes[note.id]; } //console.log(song.preroll, song.recording, track.recordEnabled); if((song.prerolling || song.recording) && track.recordEnabled === 'midi'){ if(midiEvent.type === 144){ track.song.recordedNotes.push(note); } track.recordPart.addEvent(midiEvent); // song.recordedEvents is used in the key editor track.song.recordedEvents.push(midiEvent); }else if(track.enableRetrospectiveRecording){ track.retrospectiveRecording.push(midiEvent); } // call all midi event listeners listeners = track.midiEventListeners[midiEvent.type]; if(listeners !== undefined){ objectForEach(listeners, function(listener){ listener(midiEvent, input); }); } channel = track.channel; if(channel === 'any' || channel === undefined || isNaN(channel) === true){ channel = 0; } objectForEach(track.midiOutputs, function(output){ //console.log('midi out', output, midiEvent.type); if(midiEvent.type === 128 || midiEvent.type === 144 || midiEvent.type === 176){ //console.log(midiEvent.type, midiEvent.data1, midiEvent.data2); output.send([midiEvent.type, midiEvent.data1, midiEvent.data2]); // }else if(midiEvent.type === 192){ // output.send([midiEvent.type + channel, midiEvent.data1]); } //output.send([midiEvent.status + channel, midiEvent.data1, midiEvent.data2]); }); // @TODO: maybe a track should be able to send its event to both a midi-out port and an internal heartbeat song? //console.log(track.routeToMidiOut); if(track.routeToMidiOut === false){ midiEvent.track = track; track.instrument.processEvent(midiEvent); } } function addMidiEventListener(...args){ // caller can be a track or a song let id = midiEventListenerId++; let listener; types = {}, ids = [], loop; // should I inline this? loop = function(args){ for(let arg of args){ let type = typeString(arg); //console.log(type); if(type === 'array'){ loop(arg); }else if(type === 'function'){ listener = arg; }else if(isNaN(arg) === false){ arg = parseInt(arg, 10); if(sequencer.checkEventType(arg) !== false){ types[arg] = arg; } }else if(type === 'string'){ if(sequencer.checkEventType(arg) !== false){ arg = sequencer.midiEventNumberByName(arg); types[arg] = arg; } } } }; loop(args, 0, args.length); //console.log('types', types, 'listener', listener); objectForEach(types, function(type){ //console.log(type); if(obj.midiEventListeners[type] === undefined){ obj.midiEventListeners[type] = {}; } obj.midiEventListeners[type][id] = listener; ids.push(type + '_' + id); }); //console.log(obj.midiEventListeners); return ids.length === 1 ? ids[0] : ids; } function removeMidiEventListener(id, obj){ var type; id = id.split('_'); type = id[0]; id = id[1]; delete obj.midiEventListeners[type][id]; } function removeMidiEventListeners(){ } */