UNPKG

webaudiofont

Version:

Soundfonts for web. About of 2000 musical instruments. GM MIDI compatible.

1,385 lines (1,327 loc) 45.2 kB
var UTF8 = { // non UTF8 encoding detection (cf README file for details) 'isNotUTF8': function (bytes, byteOffset, byteLength) { try { UTF8.getStringFromBytes(bytes, byteOffset, byteLength, true); } catch (e) { return true; } return false; }, // UTF8 decoding functions 'getCharLength': function (theByte) { // 4 bytes encoded char (mask 11110000) if (0xF0 == (theByte & 0xF0)) { return 4; // 3 bytes encoded char (mask 11100000) } else if (0xE0 == (theByte & 0xE0)) { return 3; // 2 bytes encoded char (mask 11000000) } else if (0xC0 == (theByte & 0xC0)) { return 2; // 1 bytes encoded char } else if (theByte == (theByte & 0x7F)) { return 1; } return 0; }, 'getCharCode': function (bytes, byteOffset, charLength) { var charCode = 0, mask = ''; byteOffset = byteOffset || 0; // validate that the array has at least one byte in it if (bytes.length - byteOffset <= 0) { throw new Error('No more characters remaining in array.'); } // Retrieve charLength if not given charLength = charLength || UTF8.getCharLength(bytes[byteOffset]); if (charLength == 0) { throw new Error(bytes[byteOffset].toString(2) + ' is not a significative' + ' byte (offset:' + byteOffset + ').'); } // Return byte value if charlength is 1 if (1 === charLength) { return bytes[byteOffset]; } // validate that the array has enough bytes to make up this character if (bytes.length - byteOffset < charLength) { throw new Error('Expected at least ' + charLength + ' bytes remaining in array.'); } // Test UTF8 integrity mask = '00000000'.slice(0, charLength) + 1 + '00000000'.slice(charLength + 1); if (bytes[byteOffset] & (parseInt(mask, 2))) { throw Error('Index ' + byteOffset + ': A ' + charLength + ' bytes' + ' encoded char' + ' cannot encode the ' + (charLength + 1) + 'th rank bit to 1.'); } // Reading the first byte mask = '0000'.slice(0, charLength + 1) + '11111111'.slice(charLength + 1); charCode += (bytes[byteOffset] & parseInt(mask, 2)) << ((--charLength) * 6); // Reading the next bytes while (charLength) { if (0x80 !== (bytes[byteOffset + 1] & 0x80) || 0x40 === (bytes[byteOffset + 1] & 0x40)) { throw Error('Index ' + (byteOffset + 1) + ': Next bytes of encoded char' + ' must begin with a "10" bit sequence.'); } charCode += ((bytes[++byteOffset] & 0x3F) << ((--charLength) * 6)); } return charCode; }, 'getStringFromBytes': function (bytes, byteOffset, byteLength, strict) { var charLength, chars = []; byteOffset = byteOffset | 0; byteLength = ('number' === typeof byteLength ? byteLength : bytes.byteLength || bytes.length); for (; byteOffset < byteLength; byteOffset++) { charLength = UTF8.getCharLength(bytes[byteOffset]); if (byteOffset + charLength > byteLength) { if (strict) { throw Error('Index ' + byteOffset + ': Found a ' + charLength + ' bytes encoded char declaration but only ' + (byteLength - byteOffset) + ' bytes are available.'); } } else { chars.push(String.fromCodePoint( UTF8.getCharCode(bytes, byteOffset, charLength, strict))); } byteOffset += charLength - 1; } return chars.join(''); }, // UTF8 encoding functions 'getBytesForCharCode': function (charCode) { if (charCode < 128) { return 1; } else if (charCode < 2048) { return 2; } else if (charCode < 65536) { return 3; } else if (charCode < 2097152) { return 4; } throw new Error('CharCode ' + charCode + ' cannot be encoded with UTF8.'); }, 'setBytesFromCharCode': function (charCode, bytes, byteOffset, neededBytes) { charCode = charCode | 0; bytes = bytes || []; byteOffset = byteOffset | 0; neededBytes = neededBytes || UTF8.getBytesForCharCode(charCode); // Setting the charCode as it to bytes if the byte length is 1 if (1 == neededBytes) { bytes[byteOffset] = charCode; } else { // Computing the first byte bytes[byteOffset++] = (parseInt('1111'.slice(0, neededBytes), 2) << 8 - neededBytes) + (charCode >>> ((--neededBytes) * 6)); // Computing next bytes for (; neededBytes > 0; ) { bytes[byteOffset++] = ((charCode >>> ((--neededBytes) * 6)) & 0x3F) | 0x80; } } return bytes; }, 'setBytesFromString': function (string, bytes, byteOffset, byteLength, strict) { string = string || ''; bytes = bytes || []; byteOffset = byteOffset | 0; byteLength = ('number' === typeof byteLength ? byteLength : bytes.byteLength || Infinity); for (var i = 0, j = string.length; i < j; i++) { var neededBytes = UTF8.getBytesForCharCode(string[i].codePointAt(0)); if (strict && byteOffset + neededBytes > byteLength) { throw new Error('Not enought bytes to encode the char "' + string[i] + '" at the offset "' + byteOffset + '".'); } UTF8.setBytesFromCharCode(string[i].codePointAt(0), bytes, byteOffset, neededBytes, strict); byteOffset += neededBytes; } return bytes; } }; ///.................................................................. // MIDIEvents : Read and edit events from various sources (ArrayBuffer, Stream) function MIDIEvents() { throw new Error('MIDIEvents function not intended to be run.'); } // Static constants // Event types MIDIEvents.EVENT_META = 0xFF; MIDIEvents.EVENT_SYSEX = 0xF0; MIDIEvents.EVENT_DIVSYSEX = 0xF7; MIDIEvents.EVENT_MIDI = 0x8; // Meta event types MIDIEvents.EVENT_META_SEQUENCE_NUMBER = 0x00; MIDIEvents.EVENT_META_TEXT = 0x01; MIDIEvents.EVENT_META_COPYRIGHT_NOTICE = 0x02; MIDIEvents.EVENT_META_TRACK_NAME = 0x03; MIDIEvents.EVENT_META_INSTRUMENT_NAME = 0x04; MIDIEvents.EVENT_META_LYRICS = 0x05; MIDIEvents.EVENT_META_MARKER = 0x06; MIDIEvents.EVENT_META_CUE_POINT = 0x07; MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX = 0x20; MIDIEvents.EVENT_META_END_OF_TRACK = 0x2F; MIDIEvents.EVENT_META_SET_TEMPO = 0x51; MIDIEvents.EVENT_META_SMTPE_OFFSET = 0x54; MIDIEvents.EVENT_META_TIME_SIGNATURE = 0x58; MIDIEvents.EVENT_META_KEY_SIGNATURE = 0x59; MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC = 0x7F; // MIDI event types MIDIEvents.EVENT_MIDI_NOTE_OFF = 0x8; MIDIEvents.EVENT_MIDI_NOTE_ON = 0x9; MIDIEvents.EVENT_MIDI_NOTE_AFTERTOUCH = 0xA; MIDIEvents.EVENT_MIDI_CONTROLLER = 0xB; MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE = 0xC; MIDIEvents.EVENT_MIDI_CHANNEL_AFTERTOUCH = 0xD; MIDIEvents.EVENT_MIDI_PITCH_BEND = 0xE; // MIDI event sizes MIDIEvents.MIDI_1PARAM_EVENTS = [ MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE, MIDIEvents.EVENT_MIDI_CHANNEL_AFTERTOUCH, ]; MIDIEvents.MIDI_2PARAMS_EVENTS = [ MIDIEvents.EVENT_MIDI_NOTE_OFF, MIDIEvents.EVENT_MIDI_NOTE_ON, MIDIEvents.EVENT_MIDI_NOTE_AFTERTOUCH, MIDIEvents.EVENT_MIDI_CONTROLLER, MIDIEvents.EVENT_MIDI_PITCH_BEND, ]; // Create an event stream parser MIDIEvents.createParser = function midiEventsCreateParser(stream, startAt, strictMode) { // Private vars // Common vars var eventTypeByte; var event; // MIDI events vars var MIDIEventType; var MIDIEventChannel; var MIDIEventParam1; // Wrap DataView into a data stream if (stream instanceof DataView) { stream = { position: startAt || 0, buffer: stream, readUint8: function () { return this.buffer.getUint8(this.position++); }, readUint16: function () { var v = this.buffer.getUint16(this.position); this.position = this.position + 2; return v; }, readUint32: function () { var v = this.buffer.getUint16(this.position); this.position = this.position + 2; return v; }, readVarInt: function () { var v = 0; var i = 0; var b; while (4 > i++) { b = this.readUint8(); if (b & 0x80) { v += (b & 0x7f); v <<= 7; } else { return v + b; } } throw new Error('0x' + this.position.toString(16) + ':' + ' Variable integer length cannot exceed 4 bytes'); }, readBytes: function (length) { var bytes = []; for (; 0 < length; length--) { bytes.push(this.readUint8()); } return bytes; }, pos: function () { return '0x' + (this.buffer.byteOffset + this.position).toString(16); }, end: function () { return this.position === this.buffer.byteLength; }, }; startAt = 0; } // Consume stream till not at start index if (0 < startAt) { while (startAt--) { stream.readUint8(); } } // creating the parser object return { // Read the next event next: function () { // Check available datas if (stream.end()) { return null; } // Creating the event event = { // Memoize the event index index: stream.pos(), // Read the delta time delta: stream.readVarInt(), }; // Read the eventTypeByte eventTypeByte = stream.readUint8(); if (0xF0 === (eventTypeByte & 0xF0)) { // Meta events if (eventTypeByte === MIDIEvents.EVENT_META) { event.type = MIDIEvents.EVENT_META; event.subtype = stream.readUint8(); event.length = stream.readVarInt(); switch (event.subtype) { case MIDIEvents.EVENT_META_SEQUENCE_NUMBER: if (strictMode && 2 !== event.length) { throw new Error(stream.pos() + ' Bad metaevent length.'); } event.msb = stream.readUint8(); event.lsb = stream.readUint8(); return event; case MIDIEvents.EVENT_META_TEXT: case MIDIEvents.EVENT_META_COPYRIGHT_NOTICE: case MIDIEvents.EVENT_META_TRACK_NAME: case MIDIEvents.EVENT_META_INSTRUMENT_NAME: case MIDIEvents.EVENT_META_LYRICS: case MIDIEvents.EVENT_META_MARKER: case MIDIEvents.EVENT_META_CUE_POINT: event.data = stream.readBytes(event.length); return event; case MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX: if (strictMode && 1 !== event.length) { throw new Error(stream.pos() + ' Bad metaevent length.'); } event.prefix = stream.readUint8(); return event; case MIDIEvents.EVENT_META_END_OF_TRACK: if (strictMode && 0 !== event.length) { throw new Error(stream.pos() + ' Bad metaevent length.'); } return event; case MIDIEvents.EVENT_META_SET_TEMPO: if (strictMode && 3 !== event.length) { throw new Error(stream.pos() + ' Tempo meta event length must be 3.'); } event.tempo = ( (stream.readUint8() << 16) + (stream.readUint8() << 8) + stream.readUint8()); event.tempoBPM = 60000000 / event.tempo; return event; case MIDIEvents.EVENT_META_SMTPE_OFFSET: if (strictMode && 5 !== event.length) { throw new Error(stream.pos() + ' Bad metaevent length.'); } event.hour = stream.readUint8(); if (strictMode && 23 < event.hour) { throw new Error(stream.pos() + ' SMTPE offset hour value must' + ' be part of 0-23.'); } event.minutes = stream.readUint8(); if (strictMode && 59 < event.minutes) { throw new Error(stream.pos() + ' SMTPE offset minutes value' + ' must be part of 0-59.'); } event.seconds = stream.readUint8(); if (strictMode && 59 < event.seconds) { throw new Error(stream.pos() + ' SMTPE offset seconds value' + ' must be part of 0-59.'); } event.frames = stream.readUint8(); if (strictMode && 30 < event.frames) { throw new Error(stream.pos() + ' SMTPE offset frames value must' + ' be part of 0-30.'); } event.subframes = stream.readUint8(); if (strictMode && 99 < event.subframes) { throw new Error(stream.pos() + ' SMTPE offset subframes value' + ' must be part of 0-99.'); } return event; case MIDIEvents.EVENT_META_KEY_SIGNATURE: if (strictMode && 2 !== event.length) { throw new Error(stream.pos() + ' Bad metaevent length.'); } event.key = stream.readUint8(); if (strictMode && (-7 > event.key || 7 < event.key)) { throw new Error(stream.pos() + ' Bad metaevent length.'); } event.scale = stream.readUint8(); if (strictMode && 0 !== event.scale && 1 !== event.scale) { throw new Error(stream.pos() + ' Key signature scale value must' + ' be 0 or 1.'); } return event; case MIDIEvents.EVENT_META_TIME_SIGNATURE: if (strictMode && 4 !== event.length) { throw new Error(stream.pos() + ' Bad metaevent length.'); } event.data = stream.readBytes(event.length); event.param1 = event.data[0]; event.param2 = event.data[1]; event.param3 = event.data[2]; event.param4 = event.data[3]; return event; case MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC: event.data = stream.readBytes(event.length); return event; default: if (strictMode) { throw new Error(stream.pos() + ' Unknown meta event type ' + '(' + event.subtype.toString(16) + ').'); } event.data = stream.readBytes(event.length); return event; } // System events } else if (eventTypeByte === MIDIEvents.EVENT_SYSEX || eventTypeByte === MIDIEvents.EVENT_DIVSYSEX) { event.type = eventTypeByte; event.length = stream.readVarInt(); event.data = stream.readBytes(event.length); return event; // Unknown event, assuming it's system like event } else { if (strictMode) { throw new Error(stream.pos() + ' Unknown event type ' + eventTypeByte.toString(16) + ', Delta: ' + event.delta + '.'); } event.type = eventTypeByte; event.badsubtype = stream.readVarInt(); event.length = stream.readUint8(); event.data = stream.readBytes(event.length); return event; } // MIDI eventsdestination[index++] } else { // running status if (0 === (eventTypeByte & 0x80)) { if (!(MIDIEventType)) { throw new Error(stream.pos() + ' Running status without previous event'); } MIDIEventParam1 = eventTypeByte; } else { MIDIEventType = eventTypeByte >> 4; MIDIEventChannel = eventTypeByte & 0x0F; MIDIEventParam1 = stream.readUint8(); } event.type = MIDIEvents.EVENT_MIDI; event.subtype = MIDIEventType; event.channel = MIDIEventChannel; event.param1 = MIDIEventParam1; switch (MIDIEventType) { case MIDIEvents.EVENT_MIDI_NOTE_OFF: event.param2 = stream.readUint8(); return event; case MIDIEvents.EVENT_MIDI_NOTE_ON: event.param2 = stream.readUint8(); // If velocity is 0, it's a note off event in fact if (!event.param2) { event.subtype = MIDIEvents.EVENT_MIDI_NOTE_OFF; event.param2 = 127; // Find a standard telling what to do here } return event; case MIDIEvents.EVENT_MIDI_NOTE_AFTERTOUCH: event.param2 = stream.readUint8(); return event; case MIDIEvents.EVENT_MIDI_CONTROLLER: event.param2 = stream.readUint8(); return event; case MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE: return event; case MIDIEvents.EVENT_MIDI_CHANNEL_AFTERTOUCH: return event; case MIDIEvents.EVENT_MIDI_PITCH_BEND: event.param2 = stream.readUint8(); return event; default: if (strictMode) { throw new Error(stream.pos() + ' Unknown MIDI event type ' + '(' + MIDIEventType.toString(16) + ').'); } return event; } } }, }; }; // Return the buffer length needed to encode the given events MIDIEvents.writeToTrack = function midiEventsWriteToTrack(events, destination, strictMode) { var index = 0; var i; var j; var k; var l; // Converting each event to binary MIDI datas for (i = 0, j = events.length; i < j; i++) { // Writing delta value if (events[i].delta >>> 28) { throw Error('Event #' + i + ': Maximum delta time value reached (' + events[i].delta + '/134217728 max)'); } if (events[i].delta >>> 21) { destination[index++] = ((events[i].delta >>> 21) & 0x7F) | 0x80; } if (events[i].delta >>> 14) { destination[index++] = ((events[i].delta >>> 14) & 0x7F) | 0x80; } if (events[i].delta >>> 7) { destination[index++] = ((events[i].delta >>> 7) & 0x7F) | 0x80; } destination[index++] = (events[i].delta & 0x7F); // MIDI Events encoding if (events[i].type === MIDIEvents.EVENT_MIDI) { // Adding the byte of subtype + channel destination[index++] = (events[i].subtype << 4) + events[i].channel; // Adding the byte of the first params destination[index++] = events[i].param1; // Adding a byte for the optionnal second param if (-1 !== MIDIEvents.MIDI_2PARAMS_EVENTS.indexOf(events[i].subtype)) { destination[index++] = events[i].param2; } // META / SYSEX events encoding } else { // Adding the event type byte destination[index++] = events[i].type; // Adding the META event subtype byte if (events[i].type === MIDIEvents.EVENT_META) { destination[index++] = events[i].subtype; } // Writing the event length bytes if (events[i].length >>> 28) { throw Error('Event #' + i + ': Maximum length reached (' + events[i].length + '/134217728 max)'); } if (events[i].length >>> 21) { destination[index++] = ((events[i].length >>> 21) & 0x7F) | 0x80; } if (events[i].length >>> 14) { destination[index++] = ((events[i].length >>> 14) & 0x7F) | 0x80; } if (events[i].length >>> 7) { destination[index++] = ((events[i].length >>> 7) & 0x7F) | 0x80; } destination[index++] = (events[i].length & 0x7F); if (events[i].type === MIDIEvents.EVENT_META) { switch (events[i].subtype) { case MIDIEvents.EVENT_META_SEQUENCE_NUMBER: destination[index++] = events[i].msb; destination[index++] = events[i].lsb; break; case MIDIEvents.EVENT_META_TEXT: case MIDIEvents.EVENT_META_COPYRIGHT_NOTICE: case MIDIEvents.EVENT_META_TRACK_NAME: case MIDIEvents.EVENT_META_INSTRUMENT_NAME: case MIDIEvents.EVENT_META_LYRICS: case MIDIEvents.EVENT_META_MARKER: case MIDIEvents.EVENT_META_CUE_POINT: for (k = 0, l = events[i].length; k < l; k++) { destination[index++] = events[i].data[k]; } break; case MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX: destination[index++] = events[i].prefix; break; case MIDIEvents.EVENT_META_END_OF_TRACK: break; case MIDIEvents.EVENT_META_SET_TEMPO: destination[index++] = (events[i].tempo >> 16); destination[index++] = (events[i].tempo >> 8) & 0xFF; destination[index++] = events[i].tempo & 0xFF; break; case MIDIEvents.EVENT_META_SMTPE_OFFSET: if (strictMode && 23 < events[i].hour) { throw new Error('Event #' + i + ': SMTPE offset hour value must be' + ' part of 0-23.'); } destination[index++] = events[i].hour; if (strictMode && 59 < events[i].minutes) { throw new Error('Event #' + i + ': SMTPE offset minutes value must' + ' be part of 0-59.'); } destination[index++] = events[i].minutes; if (strictMode && 59 < events[i].seconds) { throw new Error('Event #' + i + ': SMTPE offset seconds value must' + ' be part of 0-59.'); } destination[index++] = events[i].seconds; if (strictMode && 30 < events[i].frames) { throw new Error('Event #' + i + ': SMTPE offset frames amount must' + ' be part of 0-30.'); } destination[index++] = events[i].frames; if (strictMode && 99 < events[i].subframes) { throw new Error('Event #' + i + ': SMTPE offset subframes amount' + ' must be part of 0-99.'); } destination[index++] = events[i].subframes; break; case MIDIEvents.EVENT_META_KEY_SIGNATURE: if ('number' != typeof events[i].key || -7 > events[i].key || 7 < events[i].scale) { throw new Error('Event #' + i + ':The key signature key must be' + ' between -7 and 7'); } if ('number' !== typeof events[i].scale || 0 > events[i].scale || 1 < events[i].scale) { throw new Error('Event #' + i + ':' + 'The key signature scale must be 0 or 1'); } destination[index++] = events[i].key; destination[index++] = events[i].scale; break; // Not implemented case MIDIEvents.EVENT_META_TIME_SIGNATURE: case MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC: default: for (k = 0, l = events[i].length; k < l; k++) { destination[index++] = events[i].data[k]; } break; } // Adding bytes corresponding to the sysex event datas } else { for (k = 0, l = events[i].length; k < l; k++) { destination[index++] = events[i].data[k]; } } } } }; // Return the buffer length needed to encode the given events MIDIEvents.getRequiredBufferLength = function (events) { var bufferLength = 0; var i = 0; var j; // Calculating the track size by adding events lengths for (i = 0, j = events.length; i < j; i++) { // Computing necessary bytes to encode the delta value bufferLength += events[i].delta >>> 21 ? 4 : events[i].delta >>> 14 ? 3 : events[i].delta >>> 7 ? 2 : 1; // MIDI Events have various fixed lengths if (events[i].type === MIDIEvents.EVENT_MIDI) { // Adding a byte for subtype + channel bufferLength++; // Adding a byte for the first params bufferLength++; // Adding a byte for the optionnal second param if (-1 !== MIDIEvents.MIDI_2PARAMS_EVENTS.indexOf(events[i].subtype)) { bufferLength++; } // META / SYSEX events lengths are self defined } else { // Adding a byte for the event type bufferLength++; // Adding a byte for META events subtype if (events[i].type === MIDIEvents.EVENT_META) { bufferLength++; } // Adding necessary bytes to encode the length bufferLength += events[i].length >>> 21 ? 4 : events[i].length >>> 14 ? 3 : events[i].length >>> 7 ? 2 : 1; // Adding bytes corresponding to the event length bufferLength += events[i].length; } } return bufferLength; }; ///........................................... // MIDIFileHeader : Read and edit a MIDI header chunk in a given ArrayBuffer function MIDIFileHeader(buffer) { let a; // No buffer creating him if (!buffer) { a = new Uint8Array(MIDIFileHeader.HEADER_LENGTH); // Adding the header id (MThd) a[0] = 0x4D; a[1] = 0x54; a[2] = 0x68; a[3] = 0x64; // Adding the header chunk size a[4] = 0x00; a[5] = 0x00; a[6] = 0x00; a[7] = 0x06; // Adding the file format (1 here cause it's the most commonly used) a[8] = 0x00; a[9] = 0x01; // Adding the track count (1 cause it's a new file) a[10] = 0x00; a[11] = 0x01; // Adding the time division (192 ticks per beat) a[12] = 0x00; a[13] = 0xC0; // saving the buffer this.datas = new DataView(a.buffer, 0, MIDIFileHeader.HEADER_LENGTH); // Parsing the given buffer } else { if (!(buffer instanceof ArrayBuffer)) { throw Error('Invalid buffer received.'); } this.datas = new DataView(buffer, 0, MIDIFileHeader.HEADER_LENGTH); // Reading MIDI header chunk if (!( 'M' === String.fromCharCode(this.datas.getUint8(0)) && 'T' === String.fromCharCode(this.datas.getUint8(1)) && 'h' === String.fromCharCode(this.datas.getUint8(2)) && 'd' === String.fromCharCode(this.datas.getUint8(3)))) { throw new Error('Invalid MIDIFileHeader : MThd prefix not found'); } // Reading chunk length if (6 !== this.datas.getUint32(4)) { throw new Error('Invalid MIDIFileHeader : Chunk length must be 6'); } } } // Static constants MIDIFileHeader.HEADER_LENGTH = 14; MIDIFileHeader.FRAMES_PER_SECONDS = 1; MIDIFileHeader.TICKS_PER_BEAT = 2; // MIDI file format MIDIFileHeader.prototype.getFormat = function () { const format = this.datas.getUint16(8); if (0 !== format && 1 !== format && 2 !== format) { throw new Error('Invalid MIDI file : MIDI format (' + format + '),' + ' format can be 0, 1 or 2 only.'); } return format; }; MIDIFileHeader.prototype.setFormat = function (format) { if (0 !== format && 1 !== format && 2 !== format) { throw new Error('Invalid MIDI format given (' + format + '),' + ' format can be 0, 1 or 2 only.'); } this.datas.setUint16(8, format); }; // Number of tracks MIDIFileHeader.prototype.getTracksCount = function () { return this.datas.getUint16(10); }; MIDIFileHeader.prototype.setTracksCount = function (n) { return this.datas.setUint16(10, n); }; // Tick compute MIDIFileHeader.prototype.getTickResolution = function (tempo) { // Frames per seconds if (this.datas.getUint16(12) & 0x8000) { return 1000000 / (this.getSMPTEFrames() * this.getTicksPerFrame()); // Ticks per beat } // Default MIDI tempo is 120bpm, 500ms per beat tempo = tempo || 500000; return tempo / this.getTicksPerBeat(); }; // Time division type MIDIFileHeader.prototype.getTimeDivision = function () { if (this.datas.getUint16(12) & 0x8000) { return MIDIFileHeader.FRAMES_PER_SECONDS; } return MIDIFileHeader.TICKS_PER_BEAT; }; // Ticks per beat MIDIFileHeader.prototype.getTicksPerBeat = function () { var divisionWord = this.datas.getUint16(12); if (divisionWord & 0x8000) { throw new Error('Time division is not expressed as ticks per beat.'); } return divisionWord; }; MIDIFileHeader.prototype.setTicksPerBeat = function (ticksPerBeat) { this.datas.setUint16(12, ticksPerBeat & 0x7FFF); }; // Frames per seconds MIDIFileHeader.prototype.getSMPTEFrames = function () { const divisionWord = this.datas.getUint16(12); let smpteFrames; if (!(divisionWord & 0x8000)) { throw new Error('Time division is not expressed as frames per seconds.'); } smpteFrames = divisionWord & 0x7F00; if (-1 === [24, 25, 29, 30].indexOf(smpteFrames)) { throw new Error('Invalid SMPTE frames value (' + smpteFrames + ').'); } return 29 === smpteFrames ? 29.97 : smpteFrames; }; MIDIFileHeader.prototype.getTicksPerFrame = function () { const divisionWord = this.datas.getUint16(12); if (!(divisionWord & 0x8000)) { throw new Error('Time division is not expressed as frames per seconds.'); } return divisionWord & 0x00FF; }; MIDIFileHeader.prototype.setSMTPEDivision = function (smpteFrames, ticksPerFrame) { if (29.97 === smpteFrames) { smpteFrames = 29; } if (-1 === [24, 25, 29, 30].indexOf(smpteFrames)) { throw new Error('Invalid SMPTE frames value given (' + smpteFrames + ').'); } if (0 > ticksPerFrame || 0xFF < ticksPerFrame) { throw new Error('Invalid ticks per frame value given (' + smpteFrames + ').'); } this.datas.setUint8(12, 0x80 | smpteFrames); this.datas.setUint8(13, ticksPerFrame); }; ///........................................... // MIDIFileTrack : Read and edit a MIDI track chunk in a given ArrayBuffer function MIDIFileTrack(buffer, start) { let a; let trackLength; // no buffer, creating him if (!buffer) { a = new Uint8Array(12); // Adding the empty track header (MTrk) a[0] = 0x4D; a[1] = 0x54; a[2] = 0x72; a[3] = 0x6B; // Adding the empty track size (4) a[4] = 0x00; a[5] = 0x00; a[6] = 0x00; a[7] = 0x04; // Adding the track end event a[8] = 0x00; a[9] = 0xFF; a[10] = 0x2F; a[11] = 0x00; // Saving the buffer this.datas = new DataView(a.buffer, 0, MIDIFileTrack.HDR_LENGTH + 4); // parsing the given buffer } else { if (!(buffer instanceof ArrayBuffer)) { throw new Error('Invalid buffer received.'); } // Buffer length must size at least like an empty track (8+3bytes) if (12 > buffer.byteLength - start) { throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' + ' Buffer length must size at least 12bytes'); } // Creating a temporary view to read the track header this.datas = new DataView(buffer, start, MIDIFileTrack.HDR_LENGTH); // Reading MIDI track header chunk if (!( 'M' === String.fromCharCode(this.datas.getUint8(0)) && 'T' === String.fromCharCode(this.datas.getUint8(1)) && 'r' === String.fromCharCode(this.datas.getUint8(2)) && 'k' === String.fromCharCode(this.datas.getUint8(3)))) { throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' + ' MTrk prefix not found'); } // Reading the track length trackLength = this.getTrackLength(); if (buffer.byteLength - start < trackLength) { throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' + ' The track size exceed the buffer length.'); } // Creating the final DataView this.datas = new DataView(buffer, start, MIDIFileTrack.HDR_LENGTH + trackLength); // Trying to find the end of track event if (!( 0xFF === this.datas.getUint8(MIDIFileTrack.HDR_LENGTH + (trackLength - 3)) && 0x2F === this.datas.getUint8(MIDIFileTrack.HDR_LENGTH + (trackLength - 2)) && 0x00 === this.datas.getUint8(MIDIFileTrack.HDR_LENGTH + (trackLength - 1)))) { throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' + ' No track end event found at the expected index' + ' (' + (MIDIFileTrack.HDR_LENGTH + (trackLength - 1)).toString(16) + ').'); } } } // Static constants MIDIFileTrack.HDR_LENGTH = 8; // Track length MIDIFileTrack.prototype.getTrackLength = function () { return this.datas.getUint32(4); }; MIDIFileTrack.prototype.setTrackLength = function (trackLength) { return this.datas.setUint32(4, trackLength); }; // Read track contents MIDIFileTrack.prototype.getTrackContent = function () { return new DataView(this.datas.buffer, this.datas.byteOffset + MIDIFileTrack.HDR_LENGTH, this.datas.byteLength - MIDIFileTrack.HDR_LENGTH); }; // Set track content MIDIFileTrack.prototype.setTrackContent = function (dataView) { let origin; let destination; let i; let j; // Calculating the track length const trackLength = dataView.byteLength - dataView.byteOffset; // Track length must size at least like an empty track (4bytes) if (4 > trackLength) { throw new Error('Invalid track length, must size at least 4bytes'); } this.datas = new DataView( new Uint8Array(MIDIFileTrack.HDR_LENGTH + trackLength).buffer); // Adding the track header (MTrk) this.datas.setUint8(0, 0x4D); // M this.datas.setUint8(1, 0x54); // T this.datas.setUint8(2, 0x72); // r this.datas.setUint8(3, 0x6B); // k // Adding the track size this.datas.setUint32(4, trackLength); // Copying the content origin = new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength); destination = new Uint8Array(this.datas.buffer, MIDIFileTrack.HDR_LENGTH, trackLength); for (i = 0, j = origin.length; i < j; i++) { destination[i] = origin[i]; } }; ///........................................... // MIDIFile : Read (and soon edit) a MIDI file in a given ArrayBuffer function ensureArrayBuffer(buf) { if (buf) { if (buf instanceof ArrayBuffer) { return buf; } if (buf instanceof Uint8Array) { // Copy/convert to standard Uint8Array, because derived classes like // node.js Buffers might have unexpected data in the .buffer property. return new Uint8Array(buf).buffer; } } throw new Error('Unsupported buffer type, need ArrayBuffer or Uint8Array'); } // Constructor function MIDIFile(buffer, strictMode) { var track; var curIndex; var i; var j; // If not buffer given, creating a new MIDI file if (!buffer) { // Creating the content this.header = new MIDIFileHeader(); this.tracks = [new MIDIFileTrack()]; // if a buffer is provided, parsing him } else { buffer = ensureArrayBuffer(buffer); // Minimum MIDI file size is a headerChunk size (14bytes) // and an empty track (8+3bytes) if (25 > buffer.byteLength) { throw new Error('A buffer of a valid MIDI file must have, at least, a' + ' size of 25bytes.'); } // Reading header this.header = new MIDIFileHeader(buffer, strictMode); this.tracks = []; curIndex = MIDIFileHeader.HEADER_LENGTH; // Reading tracks for (i = 0, j = this.header.getTracksCount(); i < j; i++) { // Testing the buffer length if (strictMode && curIndex >= buffer.byteLength - 1) { throw new Error('Couldn\'t find datas corresponding to the track #' + i + '.'); } // Creating the track object track = new MIDIFileTrack(buffer, curIndex, strictMode); this.tracks.push(track); // Updating index to the track end curIndex += track.getTrackLength() + 8; } // Testing integrity : curIndex should be at the end of the buffer if (strictMode && curIndex !== buffer.byteLength) { throw new Error('It seems that the buffer contains too much datas.'); } } } MIDIFile.prototype.startNote = function (event, song) { var track = this.takeTrack(event.channel, song); track.notes.push({ when: event.playTime / 1000, pitch: event.param1, duration: 0.0000001, slides: [] }); } MIDIFile.prototype.closeNote = function (event, song) { var track = this.takeTrack(event.channel, song); for (var i = 0; i < track.notes.length; i++) { if (track.notes[i].duration == 0.0000001 // && track.notes[i].pitch == event.param1 // && track.notes[i].when < event.playTime / 1000) { track.notes[i].duration = event.playTime / 1000 - track.notes[i].when; break; } } } MIDIFile.prototype.addSlide = function (event, song) { var track = this.takeTrack(event.channel, song); for (var i = 0; i < track.notes.length; i++) { if (track.notes[i].duration == 0.0000001 // && track.notes[i].when < event.playTime / 1000) { //if (Math.abs(track.notes[i].shift) < Math.abs(event.param2 - 64) / 6) { //track.notes[i].shift = (event.param2 - 64) / 6; //console.log(event.param2-64); //} track.notes[i].slides.push({ //pitch: track.notes[i].pitch + (event.param2 - 64) / 6, delta: (event.param2 - 64) / 6, when: event.playTime / 1000-track.notes[i].when }); } } } MIDIFile.prototype.startDrum = function (event, song) { var beat = this.takeBeat(event.param1, song); beat.notes.push({ when: event.playTime / 1000 }); } MIDIFile.prototype.takeTrack = function (n, song) { for (var i = 0; i < song.tracks.length; i++) { if (song.tracks[i].n == n) { return song.tracks[i]; } } var track = { n: n, notes: [], volume: 1, program: 0 }; song.tracks.push(track); return track; } MIDIFile.prototype.takeBeat = function (n, song) { for (var i = 0; i < song.beats.length; i++) { if (song.beats[i].n == n) { return song.beats[i]; } } var beat = { n: n, notes: [], volume: 1 }; song.beats.push(beat); return beat; } MIDIFile.prototype.parseSong = function () { var song = { duration: 0, tracks: [], beats: [] }; var events = this.getMidiEvents(); console.log(events); for (var i = 0; i < events.length; i++) { //console.log(' next',events[i]); if (song.duration < events[i].playTime / 1000) { song.duration = events[i].playTime / 1000; } if (events[i].subtype == MIDIEvents.EVENT_MIDI_NOTE_ON) { if (events[i].channel == 9) { if (events[i].param1 >= 35 && events[i].param1 <= 81) { this.startDrum(events[i], song); } else { console.log('wrong drum', events[i]); } } else { if (events[i].param1 >= 0 && events[i].param1 <= 127) { //console.log('start', events[i].param1); this.startNote(events[i], song); } else { console.log('wrong tone', events[i]); } } } else { if (events[i].subtype == MIDIEvents.EVENT_MIDI_NOTE_OFF) { if (events[i].channel != 9) { this.closeNote(events[i], song); //console.log('close', events[i].param1); } } else { if (events[i].subtype == MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE) { if (events[i].channel != 9) { var track = this.takeTrack(events[i].channel, song); track.program = events[i].param1; } else { console.log('skip program for drums'); } } else { if (events[i].subtype == MIDIEvents.EVENT_MIDI_CONTROLLER) { if (events[i].param1 == 7) { if (events[i].channel != 9) { var track = this.takeTrack(events[i].channel, song); track.volume = events[i].param2/127||0.000001; //console.log('volume', track.volume,'for',events[i].channel); } } else { //console.log('controller', events[i]); } } else { if (events[i].subtype == MIDIEvents.EVENT_MIDI_PITCH_BEND) { //console.log(' bend', events[i].channel, events[i].param1, events[i].param2); this.addSlide(events[i], song); } else { console.log('unknown', events[i].channel, events[i]); }; } } } } } return song; } // Events reading helpers MIDIFile.prototype.getEvents = function (type, subtype) { var events; var event; var playTime = 0; var filteredEvents = []; var format = this.header.getFormat(); var tickResolution = this.header.getTickResolution(); var i; var j; var trackParsers; var smallestDelta; // Reading events // if the read is sequential if (1 !== format || 1 === this.tracks.length) { for (i = 0, j = this.tracks.length; i < j; i++) { // reset playtime if format is 2 playTime = (2 === format && playTime ? playTime : 0); events = MIDIEvents.createParser(this.tracks[i].getTrackContent(), 0, false); // loooping through events event = events.next(); while (event) { playTime += event.delta ? (event.delta * tickResolution) / 1000 : 0; if (event.type === MIDIEvents.EVENT_META) { // tempo change events if (event.subtype === MIDIEvents.EVENT_META_SET_TEMPO) { tickResolution = this.header.getTickResolution(event.tempo); } } // push the asked events if (((!type) || event.type === type) && ((!subtype) || (event.subtype && event.subtype === subtype))) { event.playTime = playTime; filteredEvents.push(event); } event = events.next(); } } // the read is concurrent } else { trackParsers = []; smallestDelta = -1; // Creating parsers for (i = 0, j = this.tracks.length; i < j; i++) { trackParsers[i] = {}; trackParsers[i].parser = MIDIEvents.createParser( this.tracks[i].getTrackContent(), 0, false); trackParsers[i].curEvent = trackParsers[i].parser.next(); } // Filling events do { smallestDelta = -1; // finding the smallest event for (i = 0, j = trackParsers.length; i < j; i++) { if (trackParsers[i].curEvent) { if (-1 === smallestDelta || trackParsers[i].curEvent.delta < trackParsers[smallestDelta].curEvent.delta) { smallestDelta = i; } } } if (-1 !== smallestDelta) { // removing the delta of previous events for (i = 0, j = trackParsers.length; i < j; i++) { if (i !== smallestDelta && trackParsers[i].curEvent) { trackParsers[i].curEvent.delta -= trackParsers[smallestDelta].curEvent.delta; } } // filling values event = trackParsers[smallestDelta].curEvent; playTime += (event.delta ? (event.delta * tickResolution) / 1000 : 0); if (event.type === MIDIEvents.EVENT_META) { // tempo change events if (event.subtype === MIDIEvents.EVENT_META_SET_TEMPO) { tickResolution = this.header.getTickResolution(event.tempo); } } // push midi events if (((!type) || event.type === type) && ((!subtype) || (event.subtype && event.subtype === subtype))) { event.playTime = playTime; event.track = smallestDelta; filteredEvents.push(event); } // getting next event trackParsers[smallestDelta].curEvent = trackParsers[smallestDelta].parser.next(); } } while (-1 !== smallestDelta); } return filteredEvents; }; MIDIFile.prototype.getMidiEvents = function () { return this.getEvents(MIDIEvents.EVENT_MIDI); }; MIDIFile.prototype.getLyrics = function () { var events = this.getEvents(MIDIEvents.EVENT_META); var texts = []; var lyrics = []; var event; var i; var j; for (i = 0, j = events.length; i < j; i++) { event = events[i]; // Lyrics if (event.subtype === MIDIEvents.EVENT_META_LYRICS) { lyrics.push(event); // Texts } else if (event.subtype === MIDIEvents.EVENT_META_TEXT) { // Ignore special texts if ('@' === String.fromCharCode(event.data[0])) { if ('T' === String.fromCharCode(event.data[1])) { // console.log('Title : ' + event.text.substring(2)); } else if ('I' === String.fromCharCode(event.data[1])) { // console.log('Info : ' + event.text.substring(2)); } else if ('L' === String.fromCharCode(event.data[1])) { // console.log('Lang : ' + event.text.substring(2)); } // karaoke text follows, remove all previous text } else if (0 === String.fromCharCode.apply(String, event.data).indexOf('words')) { texts.length = 0; // console.log('Word marker found'); // Karaoke texts // If playtime is greater than 0 } else if (0 !== event.playTime) { texts.push(event); } } } // Choosing the right lyrics if (2 < lyrics.length) { texts = lyrics; } else if (!texts.length) { texts = []; } // Convert texts and detect encoding try { texts.forEach(function (event) { event.text = UTF8.getStringFromBytes(event.data, 0, event.length, true); }); } catch (e) { texts.forEach(function (event) { event.text = event.data.map(function (c) { return String.fromCharCode(c); }).join(''); }); } return texts; }; // Basic events reading MIDIFile.prototype.getTrackEvents = function (index) { var event; var events = []; var parser; if (index > this.tracks.length || 0 > index) { throw Error('Invalid track index (' + index + ')'); } parser = MIDIEvents.createParser( this.tracks[index].getTrackContent(), 0, false); event = parser.next(); do { events.push(event); event = parser.next(); } while (event); return events; }; // Basic events writting MIDIFile.prototype.setTrackEvents = function (index, events) { var bufferLength; var destination; if (index > this.tracks.length || 0 > index) { throw Error('Invalid track index (' + index + ')'); } if ((!events) || (!events.length)) { throw Error('A track must contain at least one event, none given.'); } bufferLength = MIDIEvents.getRequiredBufferLength(events); destination = new Uint8Array(bufferLength); MIDIEvents.writeToTrack(events, destination); this.tracks[index].setTrackContent(destination); }; // Remove a track MIDIFile.prototype.deleteTrack = function (index) { if (index > this.tracks.length || 0 > index) { throw Error('Invalid track index (' + index + ')'); } this.tracks.splice(index, 1); this.header.setTracksCount(this.tracks.length); }; // Add a track MIDIFile.prototype.addTrack = function (index) { var track; if (index > this.tracks.length || 0 > index) { throw Error('Invalid track index (' + index + ')'); } track = new MIDIFileTrack(); if (index === this.tracks.length) { this.tracks.push(track); } else { this.tracks.splice(index, 0, track); } this.header.setTracksCount(this.tracks.length); }; // Retrieve the content in a buffer MIDIFile.prototype.getContent = function () { var bufferLength; var destination; var origin; var i; var j; var k; var l; var m; var n; // Calculating the buffer content // - initialize with the header length bufferLength = MIDIFileHeader.HEADER_LENGTH; // - add tracks length for (i = 0, j = this.tracks.length; i < j; i++) { bufferLength += this.tracks[i].getTrackLength() + 8; } // Creating the destination buffer destination = new Uint8Array(bufferLength); // Adding header origin = new Uint8Array(this.header.datas.buffer, this.header.datas.byteOffset, MIDIFileHeader.HEADER_LENGTH); for (i = 0, j = MIDIFileHeader.HEADER_LENGTH; i < j; i++) { destination[i] = origin[i]; } // Adding tracks for (k = 0, l = this.tracks.length; k < l; k++) { origin = new Uint8Array(this.tracks[k].datas.buffer, this.tracks[k].datas.byteOffset, this.tracks[k].datas.byteLength); for (m = 0, n = this.tracks[k].datas.byteLength; m < n; m++) { destination[i++] = origin[m]; } } return destination.buffer; }; // Exports Track/Header constructors MIDIFile.Header = MIDIFileHeader; MIDIFile.Track = MIDIFileTrack;