piano/js/midi/file.js

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
};
}());