349 lines
11 KiB
JavaScript
349 lines
11 KiB
JavaScript
|
/*jshint es5: true, bitwise: true */
|
||
|
/*globals DOMLoader, MidiFile, App */
|
||
|
|
||
|
/**
|
||
|
* file.js
|
||
|
*
|
||
|
* Read MIDI files
|
||
|
*/
|
||
|
(function () {
|
||
|
'use strict';
|
||
|
|
||
|
/**
|
||
|
* Note
|
||
|
*
|
||
|
* Represent a note object
|
||
|
*/
|
||
|
function Note(options) {
|
||
|
options = options || {};
|
||
|
|
||
|
this.note = options.note || 0;
|
||
|
this.channel = options.channel || 0;
|
||
|
this.start = options.start || 0;
|
||
|
this.length = options.length || 1;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Channel
|
||
|
*
|
||
|
* Represent a channel object
|
||
|
*/
|
||
|
function Channel(options) {
|
||
|
options = options || {};
|
||
|
|
||
|
this.id = options.id || 0;
|
||
|
this.meta = options.meta || {};
|
||
|
this.program = options.program || 0;
|
||
|
this.muted = options.muted || false;
|
||
|
this.solo = options.solo || false;
|
||
|
this.pending = options.pending || {};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert stained data
|
||
|
*
|
||
|
* @param {data: string}
|
||
|
* @return string
|
||
|
*/
|
||
|
function convertStained(data) {
|
||
|
var converted, length, i;
|
||
|
|
||
|
converted = [];
|
||
|
length = data.length;
|
||
|
|
||
|
for (i = 0; i < length; i += 1) {
|
||
|
converted[i] = String.fromCharCode(
|
||
|
data.charCodeAt(i) & 255
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return converted.join("");
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Push note on pending notes stack
|
||
|
*
|
||
|
* @param {meta: Object} Meta object for this song
|
||
|
* @param {event: Object} Event describing noteOn
|
||
|
* @param {time: int} Starting time
|
||
|
*/
|
||
|
function pushNote(meta, event, time) {
|
||
|
var channel = event.channel,
|
||
|
note = event.noteNumber,
|
||
|
data = {
|
||
|
start: time,
|
||
|
channel: channel,
|
||
|
note: note
|
||
|
};
|
||
|
|
||
|
meta.channels[channel].pending[note] = data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Pop note from given stack and add it to notes list
|
||
|
*
|
||
|
* @param {meta: Object} Meta object for this song
|
||
|
* @param {event: Object} Event describing noteOff
|
||
|
* @param {time: int} Ending time
|
||
|
*/
|
||
|
function popNote(meta, event, time) {
|
||
|
var channel = event.channel,
|
||
|
note = event.noteNumber,
|
||
|
data;
|
||
|
|
||
|
data = meta.channels[channel].pending[note];
|
||
|
|
||
|
if (data === undefined) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
data.length = time - data.start;
|
||
|
|
||
|
meta.length = Math.max(
|
||
|
meta.length,
|
||
|
time
|
||
|
);
|
||
|
|
||
|
meta.notes.push(data);
|
||
|
delete meta.channels[channel].pending[note];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load file
|
||
|
*
|
||
|
* @param {url: string} Path to file
|
||
|
* @return Promise
|
||
|
*/
|
||
|
function loadFile(url) {
|
||
|
return new window.Promise(function (resolve, reject) {
|
||
|
var fetch = new XMLHttpRequest();
|
||
|
|
||
|
fetch.open('GET', url);
|
||
|
fetch.overrideMimeType('text/plain; charset=x-user-defined');
|
||
|
|
||
|
fetch.onreadystatechange = function () {
|
||
|
if (this.readyState === this.DONE) {
|
||
|
// wait for next tick before resolving, since
|
||
|
// not found errors are triggered after readyState
|
||
|
// change event
|
||
|
window.setImmediate(function () {
|
||
|
resolve(fetch.responseText);
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
fetch.onerror = function () {
|
||
|
reject(new Error(
|
||
|
'Le fichier demandé est introuvable.'
|
||
|
));
|
||
|
};
|
||
|
|
||
|
fetch.send();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Import given MIDI file and translate
|
||
|
* MIDI data object
|
||
|
*
|
||
|
* @param {url: string} Path to MIDI file
|
||
|
* @return Promise
|
||
|
*/
|
||
|
function parseData(data) {
|
||
|
return new window.Promise(function (resolve, reject) {
|
||
|
var tracks, events, tracksLength, eventsLength, length,
|
||
|
event, helpLink, i, j,
|
||
|
// parsing data
|
||
|
timeline = [], meta = {},
|
||
|
channelPrefix = null, metaObject,
|
||
|
// timing
|
||
|
time, bpm = 120, tpb,
|
||
|
firstNote = +Infinity, offset;
|
||
|
|
||
|
// check MIDI structure and load events
|
||
|
data = convertStained(data);
|
||
|
|
||
|
try {
|
||
|
data = new MidiFile(data);
|
||
|
} catch (err) {
|
||
|
helpLink = 'http://fr.wikipedia.org/wiki/Fichier_midi';
|
||
|
|
||
|
if (err === 'Bad .mid file - header not found') {
|
||
|
reject(new Error(
|
||
|
'Le fichier n\'est pas un fichier MIDI ' +
|
||
|
'valide. Ce logiciel ne prend en charge ' +
|
||
|
'que les fichiers de type ' +
|
||
|
'<a href="' + helpLink + '">MIDI</a>.'
|
||
|
));
|
||
|
} else {
|
||
|
reject(new Error(
|
||
|
'Le fichier MIDI n\'est pas correctement ' +
|
||
|
'formé. Il contient une erreur qui ' +
|
||
|
'empêche le logiciel de le lire ' +
|
||
|
'correctement. Veuillez rapporter ' +
|
||
|
'ce problème aux auteurs du fichier.'
|
||
|
));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
tracks = data.tracks;
|
||
|
tracksLength = tracks.length;
|
||
|
tpb = data.header.ticksPerBeat;
|
||
|
|
||
|
// prepare meta object
|
||
|
meta = {
|
||
|
names: [],
|
||
|
instruments: [],
|
||
|
channels: [],
|
||
|
notes: [],
|
||
|
markers: [],
|
||
|
copyright: '',
|
||
|
lyrics: '',
|
||
|
length: 0
|
||
|
};
|
||
|
|
||
|
// init channels
|
||
|
length = 16;
|
||
|
|
||
|
for (i = 0; i < length; i += 1) {
|
||
|
meta.channels[i] = new Channel({
|
||
|
id: i
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// parse all events
|
||
|
for (i = 0; i < tracksLength; i += 1) {
|
||
|
time = 0;
|
||
|
events = tracks[i];
|
||
|
eventsLength = events.length;
|
||
|
|
||
|
for (j = 0; j < eventsLength; j += 1) {
|
||
|
event = events[j];
|
||
|
|
||
|
// time of each event is relative from last track event
|
||
|
if (event.deltaTime) {
|
||
|
time += event.deltaTime / tpb / (bpm / 60);
|
||
|
}
|
||
|
|
||
|
if (event.type === 'meta') {
|
||
|
if (event.subtype === 'midiChannelPrefix') {
|
||
|
channelPrefix = event.channel;
|
||
|
} else {
|
||
|
if (channelPrefix !== null) {
|
||
|
metaObject =
|
||
|
meta.channels[channelPrefix];
|
||
|
} else {
|
||
|
metaObject = meta;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (event.subtype) {
|
||
|
case 'trackName':
|
||
|
metaObject.names[i] = event.text;
|
||
|
break;
|
||
|
case 'copyrightNotice':
|
||
|
metaObject.copyright = event.text;
|
||
|
break;
|
||
|
case 'lyrics':
|
||
|
metaObject.lyrics = event.text;
|
||
|
break;
|
||
|
case 'instrumentName':
|
||
|
metaObject.instruments[i] = event.text;
|
||
|
break;
|
||
|
case 'marker':
|
||
|
metaObject.markers.push({
|
||
|
time: time,
|
||
|
text: event.text
|
||
|
});
|
||
|
break;
|
||
|
case 'setTempo':
|
||
|
bpm = 60000000 / event.microsecondsPerBeat;
|
||
|
break;
|
||
|
}
|
||
|
} else if (event.type === 'channel') {
|
||
|
channelPrefix = null;
|
||
|
|
||
|
switch (event.subtype) {
|
||
|
case 'noteOn':
|
||
|
firstNote = Math.min(firstNote, time);
|
||
|
pushNote(meta, event, time);
|
||
|
timeline.push({
|
||
|
type: 'noteOn',
|
||
|
time: time,
|
||
|
note: event.noteNumber,
|
||
|
channel: event.channel,
|
||
|
velocity: event.velocity
|
||
|
});
|
||
|
break;
|
||
|
case 'noteOff':
|
||
|
popNote(meta, event, time);
|
||
|
timeline.push({
|
||
|
type: 'noteOff',
|
||
|
time: time,
|
||
|
note: event.noteNumber,
|
||
|
channel: event.channel
|
||
|
});
|
||
|
break;
|
||
|
case 'noteAftertouch':
|
||
|
timeline.push({
|
||
|
type: 'noteAftertouch',
|
||
|
time: time,
|
||
|
note: event.noteNumber,
|
||
|
channel: event.channel,
|
||
|
amount: event.amount
|
||
|
});
|
||
|
break;
|
||
|
case 'controller':
|
||
|
timeline.push({
|
||
|
type: 'controller',
|
||
|
time: time,
|
||
|
subtype: event.controllerType,
|
||
|
value: event.value,
|
||
|
channel: event.channel
|
||
|
});
|
||
|
break;
|
||
|
case 'programChange':
|
||
|
meta.channels[event.channel].program =
|
||
|
event.programNumber;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// normalize starting time: always have 1s delay
|
||
|
if (firstNote < +Infinity) {
|
||
|
offset = 1 - firstNote;
|
||
|
}
|
||
|
|
||
|
meta.length += offset;
|
||
|
length = timeline.length;
|
||
|
|
||
|
for (i = 0; i < length; i += 1) {
|
||
|
event = timeline[i];
|
||
|
|
||
|
if (event.time + offset < 0) {
|
||
|
event.time = 0;
|
||
|
} else {
|
||
|
event.time += offset;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
length = meta.notes.length;
|
||
|
|
||
|
for (i = 0; i < length; i += 1) {
|
||
|
meta.notes[i].start += offset;
|
||
|
}
|
||
|
|
||
|
resolve({
|
||
|
meta: meta,
|
||
|
timeline: timeline
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
App.MIDI.file = {
|
||
|
load: loadFile,
|
||
|
parse: parseData
|
||
|
};
|
||
|
}());
|