qambi
Version:
MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio
289 lines (251 loc) • 8.86 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.saveAsMIDIFile = saveAsMIDIFile;
var _filesaverjs = require('filesaverjs');
var PPQ = 960; /*
This code is based on https://github.com/sergi/jsmidi
info: http://www.deluge.co/?q=midi-tempo-bpm
*/
var HDR_PPQ = str2Bytes(PPQ.toString(16), 2);
var HDR_CHUNKID = ['M'.charCodeAt(0), 'T'.charCodeAt(0), 'h'.charCodeAt(0), 'd'.charCodeAt(0)];
var HDR_CHUNK_SIZE = [0x0, 0x0, 0x0, 0x6]; // Header size for SMF
var HDR_TYPE0 = [0x0, 0x0]; // Midi Type 0 id
var HDR_TYPE1 = [0x0, 0x1]; // Midi Type 1 id
//HDR_PPQ = [0x01, 0xE0] // Defaults to 480 ticks per beat
//HDR_PPQ = [0x00, 0x80] // Defaults to 128 ticks per beat
var TRK_CHUNKID = ['M'.charCodeAt(0), 'T'.charCodeAt(0), 'r'.charCodeAt(0), 'k'.charCodeAt(0)];
// Meta event codes
var META_SEQUENCE = 0x00;
var META_TEXT = 0x01;
var META_COPYRIGHT = 0x02;
var META_TRACK_NAME = 0x03;
var META_INSTRUMENT = 0x04;
var META_LYRIC = 0x05;
var META_MARKER = 0x06;
var META_CUE_POINT = 0x07;
var META_CHANNEL_PREFIX = 0x20;
var META_END_OF_TRACK = 0x2f;
var META_TEMPO = 0x51;
var META_SMPTE = 0x54;
var META_TIME_SIG = 0x58;
var META_KEY_SIG = 0x59;
var META_SEQ_EVENT = 0x7f;
function saveAsMIDIFile(song) {
var fileName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : song.name;
var ppq = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 960;
PPQ = ppq;
HDR_PPQ = str2Bytes(PPQ.toString(16), 2);
var byteArray = [].concat(HDR_CHUNKID, HDR_CHUNK_SIZE, HDR_TYPE1);
var tracks = song.getTracks();
var numTracks = tracks.length + 1;
var i = void 0,
maxi = void 0,
track = void 0,
midiFile = void 0,
destination = void 0,
b64 = void 0;
var arrayBuffer = void 0,
dataView = void 0,
uintArray = void 0;
byteArray = byteArray.concat(str2Bytes(numTracks.toString(16), 2), HDR_PPQ);
//console.log(byteArray);
byteArray = byteArray.concat(trackToBytes(song._timeEvents, song._durationTicks, 'tempo'));
for (i = 0, maxi = tracks.length; i < maxi; i++) {
track = tracks[i];
var instrument = void 0;
if (track._instrument !== null) {
instrument = track._instrument.id;
}
//console.log(track.name, track._events.length, instrument)
byteArray = byteArray.concat(trackToBytes(track._events, song._durationTicks, track.name, instrument));
//byteArray = byteArray.concat(trackToBytes(track._events, song._lastEvent.icks, track.name, instrument))
}
//b64 = btoa(codes2Str(byteArray))
//window.location.assign("data:audio/midi;base64," + b64)
//console.log(b64)// send to server
maxi = byteArray.length;
arrayBuffer = new ArrayBuffer(maxi);
uintArray = new Uint8Array(arrayBuffer);
for (i = 0; i < maxi; i++) {
uintArray[i] = byteArray[i];
}
midiFile = new Blob([uintArray], { type: 'application/x-midi', endings: 'transparent' });
fileName = fileName.replace(/\.midi$/, '');
//let patt = /\.mid[i]{0,1}$/
var patt = /\.mid$/;
var hasExtension = patt.test(fileName);
if (hasExtension === false) {
fileName += '.mid';
}
//console.log(fileName, hasExtension)
(0, _filesaverjs.saveAs)(midiFile, fileName);
//window.location.assign(window.URL.createObjectURL(midiFile))
}
function trackToBytes(events, lastEventTicks, trackName) {
var instrumentName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'no instrument';
var lengthBytes,
i,
maxi,
event,
status,
trackLength,
// number of bytes in track chunk
ticks = 0,
delta = 0,
trackBytes = [];
if (trackName) {
trackBytes.push(0x00);
trackBytes.push(0xFF);
trackBytes.push(0x03);
trackBytes = trackBytes.concat(convertToVLQ(trackName.length));
trackBytes = trackBytes.concat(stringToNumArray(trackName));
}
if (instrumentName) {
trackBytes.push(0x00);
trackBytes.push(0xFF);
trackBytes.push(0x04);
trackBytes = trackBytes.concat(convertToVLQ(instrumentName.length));
trackBytes = trackBytes.concat(stringToNumArray(instrumentName));
}
for (i = 0, maxi = events.length; i < maxi; i++) {
event = events[i];
delta = event.ticks - ticks;
delta = convertToVLQ(delta);
//console.log(delta);
trackBytes = trackBytes.concat(delta);
//trackBytes.push.apply(trackBytes, delta);
if (event.type === 0x80 || event.type === 0x90) {
// note off, note on
//status = parseInt(event.type.toString(16) + event.channel.toString(16), 16);
status = event.type + (event.channel || 0);
trackBytes.push(status);
trackBytes.push(event.data1);
trackBytes.push(event.data2);
} else if (event.type === 0x51) {
// tempo
trackBytes.push(0xFF);
trackBytes.push(0x51);
trackBytes.push(0x03); // length
//trackBytes = trackBytes.concat(convertToVLQ(3));// length
var microSeconds = Math.round(60000000 / event.bpm);
//console.log(event.bpm)
trackBytes = trackBytes.concat(str2Bytes(microSeconds.toString(16), 3));
} else if (event.type === 0x58) {
// time signature
var denom = event.denominator;
if (denom === 2) {
denom = 0x01;
} else if (denom === 4) {
denom = 0x02;
} else if (denom === 8) {
denom = 0x03;
} else if (denom === 16) {
denom = 0x04;
} else if (denom === 32) {
denom = 0x05;
}
//console.log(event.denominator, event.nominator)
trackBytes.push(0xFF);
trackBytes.push(0x58);
trackBytes.push(0x04); // length
//trackBytes = trackBytes.concat(convertToVLQ(4));// length
trackBytes.push(event.nominator);
trackBytes.push(denom);
trackBytes.push(PPQ / event.nominator);
trackBytes.push(0x08); // 32nd notes per crotchet
//console.log(trackName, event.nominator, event.denominator, denom, PPQ/event.nominator);
}
// set the new ticks reference
//console.log(status, event.ticks, ticks);
ticks = event.ticks;
}
delta = lastEventTicks - ticks;
//console.log('d', delta, 't', ticks, 'l', lastEventTicks);
delta = convertToVLQ(delta);
//console.log(trackName, ticks, delta);
trackBytes = trackBytes.concat(delta);
trackBytes.push(0xFF);
trackBytes.push(0x2F);
trackBytes.push(0x00);
//console.log(trackName, trackBytes);
trackLength = trackBytes.length;
lengthBytes = str2Bytes(trackLength.toString(16), 4);
return [].concat(TRK_CHUNKID, lengthBytes, trackBytes);
}
// Helper functions
/*
* Converts an array of bytes to a string of hexadecimal characters. Prepares
* it to be converted into a base64 string.
*
* @param byteArray {Array} array of bytes that will be converted to a string
* @returns hexadecimal string
*/
function codes2Str(byteArray) {
return String.fromCharCode.apply(null, byteArray);
}
/*
* Converts a String of hexadecimal values to an array of bytes. It can also
* add remaining '0' nibbles in order to have enough bytes in the array as the
* |finalBytes| parameter.
*
* @param str {String} string of hexadecimal values e.g. '097B8A'
* @param finalBytes {Integer} Optional. The desired number of bytes that the returned array should contain
* @returns array of nibbles.
*/
function str2Bytes(str, finalBytes) {
if (finalBytes) {
while (str.length / 2 < finalBytes) {
str = '0' + str;
}
}
var bytes = [];
for (var i = str.length - 1; i >= 0; i = i - 2) {
var chars = i === 0 ? str[i] : str[i - 1] + str[i];
bytes.unshift(parseInt(chars, 16));
}
return bytes;
}
/**
* Translates number of ticks to MIDI timestamp format, returning an array of
* bytes with the time values. Midi has a very particular time to express time,
* take a good look at the spec before ever touching this function.
*
* @param ticks {Integer} Number of ticks to be translated
* @returns Array of bytes that form the MIDI time value
*/
function convertToVLQ(ticks) {
var buffer = ticks & 0x7F;
while (ticks = ticks >> 7) {
buffer <<= 8;
buffer |= ticks & 0x7F | 0x80;
}
var bList = [];
while (true) {
bList.push(buffer & 0xff);
if (buffer & 0x80) {
buffer >>= 8;
} else {
break;
}
}
//console.log(ticks, bList);
return bList;
}
/*
* Converts a string into an array of ASCII char codes for every character of
* the string.
*
* @param str {String} String to be converted
* @returns array with the charcode values of the string
*/
var AP = Array.prototype;
function stringToNumArray(str) {
// return str.split().forEach(char => {
// return char.charCodeAt(0)
// })
return AP.map.call(str, function (char) {
return char.charCodeAt(0);
});
}