UNPKG

webdaw-modules

Version:

a set of modules for building a web-based DAW

516 lines (458 loc) 15.9 kB
/* parse method is based on: https://github.com/gasman/jasmid adapted to work with heartbeatjs' type MidiEvent and Track */ function midiFile() { "use strict"; var // import parseUrl, // defined in util.js base64ToBinary, // defined in util.js typeString, // defined in util.js ajax, // defined in util.js findItem, // defined in util.js storeItem, // defined in util.js deleteItem, // defined in util.js parseMidiFile, // defined in midi_parse.js createTrack, // defined in track.js createPart, // defined in part.js createMidiEvent, // defined in midi_event.js index = 0, MidiFile; function cleanup(midifile, callback) { midifile = undefined; if (callback) { callback(false); } } function parse(midifile, buffer, callback) { //console.time('parse midi'); var trackMapping = []; var rawEvents = []; var data, i, j, numEvents, part, track, numTracks, events, event, ticks, tmpTicks, channel, parsed, timeEvents, noteNumber, bpm, lastNoteOn, lastNoteOff, ppqFactor, type, lastType, lastData1, lastData2, numNoteOn, numNoteOff, numOther, noteOns, noteOffs; // buffer is ArrayBuffer, so convert it buffer = new Uint8Array(buffer); data = parseMidiFile(buffer); // console.log(data); //console.log(data.header.ticksPerBeat); // save some memory midifile.base64 = ""; midifile.numTracks = 0; i = 0; numTracks = data.tracks.length; if (sequencer.overrulePPQ === true && isNaN(sequencer.defaultPPQ) === false && sequencer.defaultPPQ > 0) { ppqFactor = sequencer.defaultPPQ / data.header.ticksPerBeat; midifile.ppq = sequencer.defaultPPQ; } else { ppqFactor = 1; midifile.ppq = data.header.ticksPerBeat; } timeEvents = []; midifile.tracks = []; //console.log(ppqFactor, midifile.ppq, sequencer.overrulePPQ, sequencer.defaultPPQ); while (i < numTracks) { events = data.tracks[i]; numEvents = events.length; ticks = 0; tmpTicks = 0; channel = -1; part = createPart(); track = createTrack(); parsed = []; j = 0; numNoteOn = 0; numNoteOff = 0; numOther = 0; noteOns = {}; noteOffs = {}; for (j = 0; j < numEvents; j++) { event = events[j]; tmpTicks += event.deltaTime * ppqFactor; //console.log(event.subtype, event.deltaTime, tmpTicks); if (channel === -1 && event.channel !== undefined) { channel = event.channel; track.channel = channel; } type = event.subtype; if (type === "noteOn") { numNoteOn++; } else if (type === "noteOff") { numNoteOff++; } else { numOther++; } switch (event.subtype) { case "trackName": track.name = event.text; //console.log('name', track.name, numTracks); break; case "instrumentName": if (event.text) { track.instrumentName = event.text; } break; case "noteOn": //track.isUseful = true; /* noteNumber = event.noteNumber; if(tmpTicks === ticks && lastType === type && noteNumber === lastNoteOn){ if(sequencer.debug >= 3){ console.info('note on events on the same tick', j, tmpTicks, noteNumber, lastNoteOn, numTracks, parsed.length); } //parsed.pop(); } lastNoteOn = noteNumber; parsed.push(createMidiEvent(tmpTicks, 0x90, noteNumber, event.velocity)); */ /* noteNumber = event.noteNumber; if(noteOns[noteNumber] === undefined){ noteOns[noteNumber] = []; } noteOns[noteNumber].push(event); */ parsed.push(createMidiEvent(tmpTicks, 0x90, event.noteNumber, event.velocity)); break; case "noteOff": //track.isUseful = true; /* noteNumber = event.noteNumber; if(tmpTicks === ticks && lastType === type && noteNumber === lastNoteOff){ if(sequencer.debug >= 3){ console.info('note off events on the same tick', j, tmpTicks, noteNumber, lastNoteOff, numTracks, parsed.length); } //parsed.pop(); } lastNoteOff = noteNumber; parsed.push(createMidiEvent(tmpTicks, 0x80, noteNumber, event.velocity)); */ /* noteNumber = event.noteNumber; if(noteOffs[noteNumber] === undefined){ noteOffs[noteNumber] = []; } noteOns[noteNumber].push(event); */ parsed.push(createMidiEvent(tmpTicks, 0x80, event.noteNumber, event.velocity)); break; case "endOfTrack": //console.log(track.name, '0x2F', tmpTicks); //parsed.push(createMidiEvent(tmpTicks,0x2F)); break; case "setTempo": //sometimes 2 tempo events have the same position in ticks //→ we use the last in these cases (same as Cubase) bpm = 60000000 / event.microsecondsPerBeat; //console.log('setTempo',bpm,event.microsecondsPerBeat); if (tmpTicks === ticks && lastType === type) { if (sequencer.debug >= 3) { console.info("tempo events on the same tick", j, tmpTicks, bpm); } timeEvents.pop(); } if (midifile.bpm === undefined) { midifile.bpm = bpm; // }else{ // timeEvents.push(createMidiEvent(tmpTicks, 0x51, bpm)); } timeEvents.push(createMidiEvent(tmpTicks, 0x51, bpm)); break; case "timeSignature": //see comment above ↑ if (tmpTicks === ticks && lastType === type) { if (sequencer.debug >= 3) { console.info("time signature events on the same tick", j, tmpTicks, event.numerator, event.denominator); } timeEvents.pop(); } if (midifile.nominator === undefined) { midifile.nominator = event.numerator; midifile.denominator = event.denominator; // }else{ // //console.log('timeSignature', event.numerator, event.denominator, event.metronome, event.thirtyseconds); // timeEvents.push(createMidiEvent(tmpTicks, 0x58, event.numerator, event.denominator)); } timeEvents.push(createMidiEvent(tmpTicks, 0x58, event.numerator, event.denominator)); break; case "controller": //track.isUseful = true; /* if( tmpTicks === ticks && event.controllerType === lastData1 && event.value === lastData2 && lastData1 !== undefined && lastData2 !== undefined ){ if(sequencer.debug >= 3){ console.warn('double controller events on the same tick', j, tmpTicks, event.controllerType, event.value); } }else{ parsed.push(createMidiEvent(tmpTicks, 0xB0, event.controllerType, event.value)); } lastData1 = event.controllerType; lastData2 = event.value; */ parsed.push(createMidiEvent(tmpTicks, 0xb0, event.controllerType, event.value)); //console.log('controller:', tmpTicks, event.type, event.controllerType, event.value); break; case "programChange": //track.isUseful = true; parsed.push(createMidiEvent(tmpTicks, 0xc0, event.programNumber)); //console.log(event.type,event.controllerType); break; case "channelAftertouch": parsed.push(createMidiEvent(tmpTicks, 0xd0, event.amount)); break; case "pitchBend": parsed.push(createMidiEvent(tmpTicks, 0xe0, event.value)); break; default: //console.log(track.name, event.type); } lastType = type; ticks = tmpTicks; } //console.log('NOTE ON', numNoteOn, 'NOTE OFF', numNoteOff, 'OTHER', numOther); // console.log('PARSED', parsed); var originalName = data.trackNames[i]; if (parsed.length > 0) { track.addPart(part); part.addEvents(parsed); midifile.tracks.push(track); midifile.numTracks++; trackMapping.push({ name: track.name, id: track.id, index: i, usedInSong: true }); } else { trackMapping.push({ name: originalName, id: track.id, index: i, usedInSong: false }); } rawEvents.push(events); i++; } // midifile.tracks.reverse(); // console.log(midifile.tracks); midifile.timeEvents = timeEvents; midifile.autoSize = true; midifile.trackMapping = trackMapping; midifile.rawEvents = rawEvents; //console.timeEnd('parse midi'); // console.log(trackMapping); midifile.loaded = true; callback(midifile); } function load(midifile, callback) { if (midifile.base64 !== undefined) { parse(midifile, base64ToBinary(midifile.base64), callback); return; } else if (midifile.arraybuffer !== undefined) { parse(midifile, midifile.arraybuffer, callback); return; } ajax({ url: midifile.url, responseType: midifile.responseType, onError: function() { cleanup(midifile, callback); }, onSuccess: function(data) { if (midifile.responseType === "json") { // if the json data is corrupt (for instance because of a trailing comma) data will be null if (data === null) { callback(false); return; } if (data.base64 === undefined) { cleanup(midifile, callback); if (sequencer.debug) { console.warn("no base64 data"); } return; } if (data.name !== undefined && midifile.name === undefined) { midifile.name = data.name; } if (data.folder !== undefined && midifile.folder === undefined) { midifile.folder = data.folder; } if (midifile.name === undefined) { midifile.name = parseUrl(midifile.url).name; } midifile.localPath = midifile.folder !== undefined ? midifile.folder + "/" + midifile.name : midifile.name; parse(midifile, base64ToBinary(data.base64), callback); } else { if (midifile.name === undefined) { midifile.name = parseUrl(midifile.url).name; } midifile.localPath = midifile.folder !== undefined ? midifile.folder + "/" + midifile.name : midifile.name; parse(midifile, data, callback); } }, }); } function store(midifile) { var occupied = findItem(midifile.localPath, sequencer.storage.midi, true), action = midifile.action; //console.log(occupied); if (occupied && occupied.className === "MidiFile" && action !== "overwrite") { if (sequencer.debug >= 2) { console.warn("there is already a midifile at", midifile.localPath); cleanup(midifile); } } else { storeItem(midifile, midifile.localPath, sequencer.storage.midi); } } MidiFile = function(config) { this.id = "MF" + index++ + new Date().getTime(); this.className = "MidiFile"; this.url = config.url; this.json = config.json; this.base64 = config.base64; this.arraybuffer = config.arraybuffer; this.name = config.name; this.folder = config.folder; if (this.url !== undefined) { this.responseType = this.url.indexOf(".json") === this.url.lastIndexOf(".") ? "json" : "arraybuffer"; } else { if (this.name === undefined && this.folder === undefined) { this.name = this.id; this.localPath = this.id; } else { this.localPath = this.folder !== undefined ? this.folder + "/" + this.name : this.name; } } }; sequencer.addMidiFile = function(config, callback) { var type = typeString(config), midifile, json, name, folder; if (type !== "object") { if (sequencer.debug >= 2) { console.warn("can't create a MidiFile with this data", config); } return false; } if (config.json) { json = config.json; name = config.name; folder = config.folder; if (typeString(json) === "string") { try { json = JSON.parse(json); } catch (e) { if (sequencer.debug >= 2) { console.warn("can't create a MidiFile with this data", config); } return false; } } if (json.base64 === undefined) { if (sequencer.debug >= 2) { console.warn("can't create a MidiFile with this data", config); } return false; } config = { base64: json.base64, name: name === undefined ? json.name : name, folder: folder === undefined ? json.folder : folder, }; //console.log('config', name, folder, json.name, json.folder); } midifile = new MidiFile(config); sequencer.addTask( { type: "load midifile", method: load, params: midifile, }, function() { //console.log(midifile); store(midifile); if (callback) { callback(midifile); } } ); sequencer.startTaskQueue(); /* load(midifile, function(){ //console.log(midifile); store(midifile); if(callback){ callback(midifile); } }); */ }; function MidiFile2(config) { var reader = new FileReader(); function executor(resolve, reject) { reader.addEventListener("loadend", function() { // reader.result contains the contents of blob as a typed array parse({}, reader.result, function(midifile) { resolve(midifile); }); }); reader.addEventListener("error", function(e) { reject(e); }); if (config.blob !== undefined) { reader.readAsArrayBuffer(config.blob); } else if (config.arraybuffer !== undefined) { parse({}, config.arraybuffer, function(midifile) { resolve(midifile); }); } else if (config.base64 !== undefined) { parse({}, base64ToBinary(config.base64), function(midifile) { resolve(midifile); }); } } this._promise = new Promise(executor); } sequencer.createMidiFile = function(config) { var mf = new MidiFile2(config); return mf._promise; }; sequencer.protectedScope.addInitMethod(function() { ajax = sequencer.protectedScope.ajax; findItem = sequencer.protectedScope.findItem; storeItem = sequencer.protectedScope.storeItem; deleteItem = sequencer.protectedScope.deleteItem; parseUrl = sequencer.protectedScope.parseUrl; typeString = sequencer.protectedScope.typeString; parseMidiFile = sequencer.protectedScope.parseMidiFile; base64ToBinary = sequencer.protectedScope.base64ToBinary; createPart = sequencer.createPart; createTrack = sequencer.createTrack; createMidiEvent = sequencer.createMidiEvent; }); }