commit
8be692daa7
|
@ -1,5 +0,0 @@
|
|||
var fs = require('fs');
|
||||
var file = process.argv[2];
|
||||
fs.readFile(file, 'binary', function(err, data) {
|
||||
console.log(new Buffer(data, 'binary').toString('base64'));
|
||||
});
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
Soundfont Builder : 0.1
|
||||
*/
|
||||
|
||||
window = {}; // create fake window
|
||||
atob = require('atob');
|
||||
btoa = require('btoa');
|
||||
http = require('http');
|
||||
fs = require('fs');
|
||||
///
|
||||
require('./jsmidi.js');
|
||||
|
||||
//////////////
|
||||
|
||||
var min = window.noteTable["C2"];
|
||||
var max = window.noteTable["C7"];
|
||||
var absmin = window.noteTable["A0"];
|
||||
var absmax = window.noteTable["C8"] + 1;
|
||||
if (false) {
|
||||
var noteEvents = [];
|
||||
for (var key in window.noteTable) {
|
||||
var id = window.noteTable[key];
|
||||
if (id < absmin || id > absmax) continue;
|
||||
// if (id < min || id > max) continue;
|
||||
var note = {
|
||||
duration: 0,
|
||||
channel: 0,
|
||||
pitch: id,
|
||||
volume: 100
|
||||
};
|
||||
noteEvents.push(window.MidiEvent.noteOn(note));
|
||||
note.duration = 1024 * 0.75 >> 0;
|
||||
noteEvents.push(window.MidiEvent.noteOff(note));
|
||||
}
|
||||
var track = new window.MidiTrack({
|
||||
events: noteEvents
|
||||
});
|
||||
var song = window.MidiWriter({
|
||||
tracks: [track]
|
||||
});
|
||||
var decoded = new Buffer(atob(song.b64), 'binary')
|
||||
fs.writeFile('build/SoundFont.midi', decoded, 'binary', function(err) {});
|
||||
} else {
|
||||
for (var key in window.noteTable) {
|
||||
var id = window.noteTable[key];
|
||||
if (id < absmin || id > absmax) continue;
|
||||
// if (id < min || id > max) continue;
|
||||
var note = {
|
||||
duration: 0,
|
||||
channel: 0,
|
||||
pitch: id,
|
||||
volume: 85
|
||||
};
|
||||
var noteEvents = [];
|
||||
noteEvents.push(window.MidiEvent.noteOn(note));
|
||||
note.duration = 1024 * 0.75 >> 0;
|
||||
noteEvents.push(window.MidiEvent.noteOff(note));
|
||||
var track = new window.MidiTrack({
|
||||
events: noteEvents
|
||||
});
|
||||
var song = window.MidiWriter({
|
||||
tracks: [track]
|
||||
});
|
||||
var decoded = new Buffer(atob(song.b64), 'binary')
|
||||
fs.writeFile('build/' + key + '.midi', decoded, 'binary', function(err) {});
|
||||
}
|
||||
}
|
|
@ -1,506 +0,0 @@
|
|||
/*
|
||||
JSMIDI
|
||||
---------
|
||||
https://github.com/sergi/jsmidi
|
||||
*/
|
||||
|
||||
(function (window) {
|
||||
|
||||
var AP = Array.prototype;
|
||||
|
||||
// Create a mock console object to void undefined errors if the console object
|
||||
// is not defined.
|
||||
if (!window.console || !console.firebug) {
|
||||
var names = ["log", "debug", "info", "warn", "error"];
|
||||
|
||||
window.console = {};
|
||||
for (var i = 0; i < names.length; ++i) {
|
||||
window.console[names[i]] = function() {};
|
||||
}
|
||||
}
|
||||
|
||||
var DEFAULT_VOLUME = 90;
|
||||
var DEFAULT_DURATION = 128;
|
||||
var DEFAULT_CHANNEL = 0;
|
||||
|
||||
// These are the different values that compose a MID header. They are already
|
||||
// expressed in their string form, so no useless conversion has to take place
|
||||
// since they are constants.
|
||||
|
||||
var HDR_CHUNKID = "MThd";
|
||||
var HDR_CHUNK_SIZE = "\x00\x00\x00\x06"; // Header size for SMF
|
||||
var HDR_TYPE0 = "\x00\x00"; // Midi Type 0 id
|
||||
var HDR_TYPE1 = "\x00\x01"; // Midi Type 1 id
|
||||
var HDR_SPEED = "\x00\x80"; // Defaults to 128 ticks per beat
|
||||
|
||||
// Midi event codes
|
||||
var EVT_NOTE_OFF = 0x8;
|
||||
var EVT_NOTE_ON = 0x9;
|
||||
var EVT_AFTER_TOUCH = 0xA;
|
||||
var EVT_CONTROLLER = 0xB;
|
||||
var EVT_PROGRAM_CHANGE = 0xC;
|
||||
var EVT_CHANNEL_AFTERTOUCH = 0xD;
|
||||
var EVT_PITCH_BEND = 0xE;
|
||||
|
||||
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;
|
||||
|
||||
// This is the conversion table from notes to its MIDI number. Provided for
|
||||
// convenience, it is not used in this code.
|
||||
var noteTable = { "G9": 0x7F, "Gb9": 0x7E, "F9": 0x7D, "E9": 0x7C, "Eb9": 0x7B,
|
||||
"D9": 0x7A, "Db9": 0x79, "C9": 0x78, "B8": 0x77, "Bb8": 0x76, "A8": 0x75, "Ab8": 0x74,
|
||||
"G8": 0x73, "Gb8": 0x72, "F8": 0x71, "E8": 0x70, "Eb8": 0x6F, "D8": 0x6E, "Db8": 0x6D,
|
||||
"C8": 0x6C, "B7": 0x6B, "Bb7": 0x6A, "A7": 0x69, "Ab7": 0x68, "G7": 0x67, "Gb7": 0x66,
|
||||
"F7": 0x65, "E7": 0x64, "Eb7": 0x63, "D7": 0x62, "Db7": 0x61, "C7": 0x60, "B6": 0x5F,
|
||||
"Bb6": 0x5E, "A6": 0x5D, "Ab6": 0x5C, "G6": 0x5B, "Gb6": 0x5A, "F6": 0x59, "E6": 0x58,
|
||||
"Eb6": 0x57, "D6": 0x56, "Db6": 0x55, "C6": 0x54, "B5": 0x53, "Bb5": 0x52, "A5": 0x51,
|
||||
"Ab5": 0x50, "G5": 0x4F, "Gb5": 0x4E, "F5": 0x4D, "E5": 0x4C, "Eb5": 0x4B, "D5": 0x4A,
|
||||
"Db5": 0x49, "C5": 0x48, "B4": 0x47, "Bb4": 0x46, "A4": 0x45, "Ab4": 0x44, "G4": 0x43,
|
||||
"Gb4": 0x42, "F4": 0x41, "E4": 0x40, "Eb4": 0x3F, "D4": 0x3E, "Db4": 0x3D, "C4": 0x3C,
|
||||
"B3": 0x3B, "Bb3": 0x3A, "A3": 0x39, "Ab3": 0x38, "G3": 0x37, "Gb3": 0x36, "F3": 0x35,
|
||||
"E3": 0x34, "Eb3": 0x33, "D3": 0x32, "Db3": 0x31, "C3": 0x30, "B2": 0x2F, "Bb2": 0x2E,
|
||||
"A2": 0x2D, "Ab2": 0x2C, "G2": 0x2B, "Gb2": 0x2A, "F2": 0x29, "E2": 0x28, "Eb2": 0x27,
|
||||
"D2": 0x26, "Db2": 0x25, "C2": 0x24, "B1": 0x23, "Bb1": 0x22, "A1": 0x21, "Ab1": 0x20,
|
||||
"G1": 0x1F, "Gb1": 0x1E, "F1": 0x1D, "E1": 0x1C, "Eb1": 0x1B, "D1": 0x1A, "Db1": 0x19,
|
||||
"C1": 0x18, "B0": 0x17, "Bb0": 0x16, "A0": 0x15, "Ab0": 0x14, "G0": 0x13, "Gb0": 0x12,
|
||||
"F0": 0x11, "E0": 0x10, "Eb0": 0x0F, "D0": 0x0E, "Db0": 0x0D, "C0": 0x0C };
|
||||
|
||||
// Helper functions
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
function StringToNumArray(str) {
|
||||
return AP.map.call(str, function(char) {
|
||||
return char.charCodeAt(0);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
function isArray(obj) {
|
||||
return !!(obj && obj.concat && obj.unshift && !obj.callee);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
var translateTickTime = function(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; }
|
||||
}
|
||||
return bList;
|
||||
};
|
||||
|
||||
/*
|
||||
* This is the function that assembles the MIDI file. It writes the
|
||||
* necessary constants for the MIDI header and goes through all the tracks, appending
|
||||
* their data to the final MIDI stream.
|
||||
* It returns an object with the final values in hex and in base64, and with
|
||||
* some useful methods to play an manipulate the resulting MIDI stream.
|
||||
*
|
||||
* @param config {Object} Configuration object. It contains the tracks, tempo
|
||||
* and other values necessary to generate the MIDI stream.
|
||||
*
|
||||
* @returns An object with the hex and base64 resulting streams, as well as
|
||||
* with some useful methods.
|
||||
*/
|
||||
var MidiWriter = function(config) {
|
||||
if (config) {
|
||||
var tracks = config.tracks || [];
|
||||
// Number of tracks in hexadecimal
|
||||
var tracksLength = tracks.length.toString(16);
|
||||
|
||||
// This variable will hold the whole midi stream and we will add every
|
||||
// chunk of MIDI data to it in the next lines.
|
||||
var hexMidi = HDR_CHUNKID + HDR_CHUNK_SIZE + HDR_TYPE0;
|
||||
|
||||
// Appends the number of tracks expressed in 2 bytes, as the MIDI
|
||||
// standard requires.
|
||||
hexMidi += codes2Str(str2Bytes(tracksLength, 2));
|
||||
hexMidi += HDR_SPEED;
|
||||
// Goes through the tracks appending the hex strings that compose them.
|
||||
tracks.forEach(function(trk) { hexMidi += codes2Str(trk.toBytes()); });
|
||||
|
||||
return {
|
||||
b64: btoa(hexMidi),
|
||||
play: function() {
|
||||
if (document) {
|
||||
var embed = document.createElement("embed");
|
||||
embed.setAttribute("src", "data:audio/midi;base64," + this.b64);
|
||||
embed.setAttribute("type", "audio/midi");
|
||||
document.body.appendChild(embed);
|
||||
}
|
||||
},
|
||||
save: function() {
|
||||
window.open("data:audio/midi;base64," + this.b64,
|
||||
"JSMidi generated output",
|
||||
"resizable=yes,scrollbars=no,status=no");
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new Error("No parameters have been passed to MidiWriter.");
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Generic MidiEvent object. This object is used to create standard MIDI events
|
||||
* (note Meta events nor SysEx events). It is passed a |params| object that may
|
||||
* contain the keys time, type, channel, param1 and param2. Note that only the
|
||||
* type, channel and param1 are strictly required. If the time is not provided,
|
||||
* a time of 0 will be assumed.
|
||||
*
|
||||
* @param {object} params Object containing the properties of the event.
|
||||
*/
|
||||
var MidiEvent = function(params) {
|
||||
if (params &&
|
||||
(params.type !== null || params.type !== undefined) &&
|
||||
(params.channel !== null || params.channel !== undefined) &&
|
||||
(params.param1 !== null || params.param1 !== undefined)) {
|
||||
this.setTime(params.time);
|
||||
this.setType(params.type);
|
||||
this.setChannel(params.channel);
|
||||
this.setParam1(params.param1);
|
||||
this.setParam2(params.param2);
|
||||
} else {
|
||||
throw new Error("Not enough parameters to create an event.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the list of events that form a note in MIDI. If the |sustained|
|
||||
* parameter is not specified, it creates the noteOff event, which stops the
|
||||
* note after it has been played, instead of keeping it playing.
|
||||
*
|
||||
* This method accepts two ways of expressing notes. The first one is a string,
|
||||
* which will be looked up in the global |noteTable| but it will take the
|
||||
* default values for pitch, channel, durtion and volume.
|
||||
*
|
||||
* If a note object is passed to the method instead, it should contain the properties
|
||||
* channel, pitch, duration and volume, of which pitch is mandatory. In case the
|
||||
* channel, the duration or the volume are not passed, default values will be
|
||||
* used.
|
||||
*
|
||||
* @param note {object || String} Object with note properties or string
|
||||
* @param sustained {Boolean} Whether the note has to end or keep playing
|
||||
* @returns Array of events, with a maximum of two events (noteOn and noteOff)
|
||||
*/
|
||||
|
||||
MidiEvent.createNote = function(note, sustained) {
|
||||
if (!note) { throw new Error("Note not specified"); }
|
||||
|
||||
if (typeof note === "string") {
|
||||
note = noteTable[note];
|
||||
// The pitch is mandatory if the note object is used.
|
||||
} else if (!note.pitch) {
|
||||
throw new Error("The pitch is required in order to create a note.");
|
||||
}
|
||||
var events = [];
|
||||
events.push(MidiEvent.noteOn(note));
|
||||
|
||||
// If there is a |sustained| parameter, the note will keep playing until
|
||||
// a noteOff event is issued for it.
|
||||
if (!sustained) {
|
||||
// The noteOff event will be the one that is passed the actual duration
|
||||
// value for the note, since it is the one that will stop playing the
|
||||
// note at a particular time. If not specified it takes the default
|
||||
// value for it.
|
||||
// TODO: Is is good to have a default value for it?
|
||||
events.push(MidiEvent.noteOff(note, note.duration || DEFAULT_DURATION));
|
||||
}
|
||||
|
||||
return events;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an event of the type NOTE_ON taking the values passed and falling
|
||||
* back to defaults if they are not specified.
|
||||
*
|
||||
* @param note {Note || String} Note object or string
|
||||
* @param time {Number} Duration of the note in ticks
|
||||
* @returns MIDI event with type NOTE_ON for the note specified
|
||||
*/
|
||||
MidiEvent.noteOn = function(note, duration) {
|
||||
return new MidiEvent({
|
||||
time: note.duration || duration || 0,
|
||||
type: EVT_NOTE_ON,
|
||||
channel: note.channel || DEFAULT_CHANNEL,
|
||||
param1: note.pitch || note,
|
||||
param2: note.volume || DEFAULT_VOLUME
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an event of the type NOTE_OFF taking the values passed and falling
|
||||
* back to defaults if they are not specified.
|
||||
*
|
||||
* @param note {Note || String} Note object or string
|
||||
* @param time {Number} Duration of the note in ticks
|
||||
* @returns MIDI event with type NOTE_OFF for the note specified
|
||||
*/
|
||||
|
||||
MidiEvent.noteOff = function(note, duration) {
|
||||
return new MidiEvent({
|
||||
time: note.duration || duration || 0,
|
||||
type: EVT_NOTE_OFF,
|
||||
channel: note.channel || DEFAULT_CHANNEL,
|
||||
param1: note.pitch || note,
|
||||
param2: note.volume || DEFAULT_VOLUME
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
MidiEvent.prototype = {
|
||||
type: 0,
|
||||
channel: 0,
|
||||
time: 0,
|
||||
setTime: function(ticks) {
|
||||
// The 0x00 byte is always the last one. This is how Midi
|
||||
// interpreters know that the time measure specification ends and the
|
||||
// rest of the event signature starts.
|
||||
|
||||
this.time = translateTickTime(ticks || 0);
|
||||
},
|
||||
setType: function(type) {
|
||||
if (type < EVT_NOTE_OFF || type > EVT_PITCH_BEND) {
|
||||
throw new Error("Trying to set an unknown event: " + type);
|
||||
}
|
||||
|
||||
this.type = type;
|
||||
},
|
||||
setChannel: function(channel) {
|
||||
if (channel < 0 || channel > 15) {
|
||||
throw new Error("Channel is out of bounds.");
|
||||
}
|
||||
|
||||
this.channel = channel;
|
||||
},
|
||||
setParam1: function(p) {
|
||||
this.param1 = p;
|
||||
},
|
||||
setParam2: function(p) {
|
||||
this.param2 = p;
|
||||
},
|
||||
toBytes: function() {
|
||||
var byteArray = [];
|
||||
|
||||
var typeChannelByte =
|
||||
parseInt(this.type.toString(16) + this.channel.toString(16), 16);
|
||||
|
||||
byteArray.push.apply(byteArray, this.time);
|
||||
byteArray.push(typeChannelByte);
|
||||
byteArray.push(this.param1);
|
||||
|
||||
// Some events don't have a second parameter
|
||||
if (this.param2 !== undefined && this.param2 !== null) {
|
||||
byteArray.push(this.param2);
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
};
|
||||
|
||||
var MetaEvent = function(params) {
|
||||
if (params) {
|
||||
this.setType(params.type);
|
||||
this.setData(params.data);
|
||||
}
|
||||
};
|
||||
|
||||
MetaEvent.prototype = {
|
||||
setType: function(t) {
|
||||
this.type = t;
|
||||
},
|
||||
setData: function(d) {
|
||||
this.data = d;
|
||||
},
|
||||
toBytes: function() {
|
||||
if (!this.type || !this.data) {
|
||||
throw new Error("Type or data for meta-event not specified.");
|
||||
}
|
||||
|
||||
var byteArray = [0xff, this.type];
|
||||
|
||||
// If data is an array, we assume that it contains several bytes. We
|
||||
// apend them to byteArray.
|
||||
if (isArray(this.data)) {
|
||||
AP.push.apply(byteArray, this.data);
|
||||
}
|
||||
|
||||
return byteArray;
|
||||
}
|
||||
};
|
||||
|
||||
var MidiTrack = function(cfg) {
|
||||
this.events = [];
|
||||
for (var p in cfg) {
|
||||
if (cfg.hasOwnProperty(p)) {
|
||||
// Get the setter for the property. The property is capitalized.
|
||||
// Probably a try/catch should go here.
|
||||
this["set" + p.charAt(0).toUpperCase() + p.substring(1)](cfg[p]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//"MTrk" Marks the start of the track data
|
||||
MidiTrack.TRACK_START = [0x4d, 0x54, 0x72, 0x6b];
|
||||
MidiTrack.TRACK_END = [0x0, 0xFF, 0x2F, 0x0];
|
||||
|
||||
MidiTrack.prototype = {
|
||||
/*
|
||||
* Adds an event to the track.
|
||||
*
|
||||
* @param event {MidiEvent} Event to add to the track
|
||||
* @returns the track where the event has been added
|
||||
*/
|
||||
addEvent: function(event) {
|
||||
this.events.push(event);
|
||||
return this;
|
||||
},
|
||||
setEvents: function(events) {
|
||||
AP.push.apply(this.events, events);
|
||||
return this;
|
||||
},
|
||||
/*
|
||||
* Adds a text meta-event to the track.
|
||||
*
|
||||
* @param type {Number} type of the text meta-event
|
||||
* @param text {String} Optional. Text of the meta-event.
|
||||
* @returns the track where the event ahs been added
|
||||
*/
|
||||
setText: function(type, text) {
|
||||
// If the param text is not specified, it is assumed that a generic
|
||||
// text is wanted and that the type parameter is the actual text to be
|
||||
// used.
|
||||
if (!text) {
|
||||
type = META_TEXT;
|
||||
text = type;
|
||||
}
|
||||
return this.addEvent(new MetaEvent({ type: type, data: text }));
|
||||
},
|
||||
// The following are setters for different kinds of text in MIDI, they all
|
||||
// use the |setText| method as a proxy.
|
||||
setCopyright: function(text) { return this.setText(META_COPYRIGHT, text); },
|
||||
setTrackName: function(text) { return this.setText(META_TRACK_NAME, text); },
|
||||
setInstrument: function(text) { return this.setText(META_INSTRUMENT, text); },
|
||||
setLyric: function(text) { return this.setText(META_LYRIC, text); },
|
||||
setMarker: function(text) { return this.setText(META_MARKER, text); },
|
||||
setCuePoint: function(text) { return this.setText(META_CUE_POINT, text); },
|
||||
|
||||
setTempo: function(tempo) {
|
||||
this.addEvent(new MetaEvent({ type: META_TEMPO, data: tempo }));
|
||||
},
|
||||
setTimeSig: function() {
|
||||
// TBD
|
||||
},
|
||||
setKeySig: function() {
|
||||
// TBD
|
||||
},
|
||||
|
||||
toBytes: function() {
|
||||
var trackLength = 0;
|
||||
var eventBytes = [];
|
||||
var startBytes = MidiTrack.TRACK_START;
|
||||
var endBytes = MidiTrack.TRACK_END;
|
||||
|
||||
/*
|
||||
* Adds the bytes of an event to the eventBytes array and add the
|
||||
* amount of bytes to |trackLength|.
|
||||
*
|
||||
* @param event {MidiEvent} MIDI event we want the bytes from.
|
||||
*/
|
||||
var addEventBytes = function(event) {
|
||||
var bytes = event.toBytes();
|
||||
trackLength += bytes.length;
|
||||
AP.push.apply(eventBytes, bytes);
|
||||
};
|
||||
|
||||
this.events.forEach(addEventBytes);
|
||||
|
||||
// Add the end-of-track bytes to the sum of bytes for the track, since
|
||||
// they are counted (unlike the start-of-track ones).
|
||||
trackLength += endBytes.length;
|
||||
|
||||
// Makes sure that track length will fill up 4 bytes with 0s in case
|
||||
// the length is less than that (the usual case).
|
||||
var lengthBytes = str2Bytes(trackLength.toString(16), 4);
|
||||
|
||||
return startBytes.concat(lengthBytes, eventBytes, endBytes);
|
||||
}
|
||||
};
|
||||
|
||||
window.MidiWriter = MidiWriter;
|
||||
window.MidiEvent = MidiEvent;
|
||||
window.MetaEvent = MetaEvent;
|
||||
window.MidiTrack = MidiTrack;
|
||||
window.noteTable = noteTable;
|
||||
|
||||
})(window);
|
9
soundfont-generator/node_modules/atob/index.js
generated
vendored
9
soundfont-generator/node_modules/atob/index.js
generated
vendored
|
@ -1,9 +0,0 @@
|
|||
(function () {
|
||||
"use strict";
|
||||
|
||||
function atob(str) {
|
||||
return new Buffer(str, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
module.exports = atob;
|
||||
}());
|
18
soundfont-generator/node_modules/atob/package.json
generated
vendored
18
soundfont-generator/node_modules/atob/package.json
generated
vendored
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name" : "atob",
|
||||
"homepage" : "https://github.com/coolaj86/node-browser-compat",
|
||||
"description" : "atob for Node.JS (it's a one-liner)",
|
||||
"repository" : {
|
||||
"type": "git",
|
||||
"url": "git://github.com/coolaj86/node-browser-compat.git"
|
||||
},
|
||||
"keywords" : ["atob", "browser"],
|
||||
"author" : "AJ ONeal <coolaj86@gmail.com> (http://coolaj86.info)",
|
||||
"engines" : {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"dependencies" : {
|
||||
},
|
||||
"main" : "index",
|
||||
"version" : "1.0.0"
|
||||
}
|
14
soundfont-generator/node_modules/atob/test.js
generated
vendored
14
soundfont-generator/node_modules/atob/test.js
generated
vendored
|
@ -1,14 +0,0 @@
|
|||
(function () {
|
||||
"use strict";
|
||||
|
||||
var atob = require('./index')
|
||||
, expected = "SGVsbG8gV29ybGQ="
|
||||
, result
|
||||
;
|
||||
|
||||
if (expected !== atob("Hello World")) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PASS] all tests pass');
|
||||
}());
|
9
soundfont-generator/node_modules/btoa/index.js
generated
vendored
9
soundfont-generator/node_modules/btoa/index.js
generated
vendored
|
@ -1,9 +0,0 @@
|
|||
(function () {
|
||||
"use strict";
|
||||
|
||||
function atob(str) {
|
||||
return new Buffer(str, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
module.exports = atob;
|
||||
}());
|
18
soundfont-generator/node_modules/btoa/package.json
generated
vendored
18
soundfont-generator/node_modules/btoa/package.json
generated
vendored
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name" : "btoa",
|
||||
"homepage" : "https://github.com/coolaj86/node-browser-compat",
|
||||
"description" : "btoa for Node.JS (it's a one-liner)",
|
||||
"repository" : {
|
||||
"type": "git",
|
||||
"url": "git://github.com/coolaj86/node-browser-compat.git"
|
||||
},
|
||||
"keywords" : ["btoa", "browser"],
|
||||
"author" : "AJ ONeal <coolaj86@gmail.com> (http://coolaj86.info)",
|
||||
"engines" : {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"dependencies" : {
|
||||
},
|
||||
"main" : "index",
|
||||
"version" : "1.0.0"
|
||||
}
|
14
soundfont-generator/node_modules/btoa/test.js
generated
vendored
14
soundfont-generator/node_modules/btoa/test.js
generated
vendored
|
@ -1,14 +0,0 @@
|
|||
(function () {
|
||||
"use strict";
|
||||
|
||||
var atob = require('./index')
|
||||
, expected = "SGVsbG8gV29ybGQ="
|
||||
, result
|
||||
;
|
||||
|
||||
if (expected !== atob("Hello World")) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PASS] all tests pass');
|
||||
}());
|
|
@ -1,117 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# UNIX SETUP
|
||||
# ------------------------------------
|
||||
# nodejs - http://nodejs.org/
|
||||
# gzip - http://www.gzip.org/
|
||||
# fluidsynth - http://sourceforge.net/apps/trac/fluidsynth/
|
||||
# oggenc - http://www.rarewares.org/ogg-oggenc.php
|
||||
# lame - http://lame.sourceforge.net/
|
||||
|
||||
# PROCESSING
|
||||
# ------------------------------------
|
||||
# file types to build
|
||||
MP3=1
|
||||
OGG=1
|
||||
|
||||
# formats to create
|
||||
JS=1
|
||||
JGZ=1
|
||||
|
||||
# operating mode
|
||||
SINGLE=0
|
||||
PACKAGE=1
|
||||
|
||||
# directory to generate into
|
||||
MIDIDIR="./build"
|
||||
if [ ! -d "$MIDIDIR" ]; then
|
||||
mkdir $MIDIDIR
|
||||
fi
|
||||
|
||||
# .SF2 file to map MIDI to (make sure this exists!)
|
||||
SOUNDFONT="./sf2/FluidSynth_1.43.sf2"
|
||||
|
||||
# put into the head of each generated .JS/.JGZ file
|
||||
JSHEADER="{"
|
||||
JSFOOTER="}"
|
||||
|
||||
# create MIDI files for audible notes
|
||||
node "./inc/gen-midi.js"
|
||||
|
||||
# write the headers
|
||||
if [ $OGG -eq 1 ]; then
|
||||
echo $JSHEADER > $MIDIDIR/soundfont-ogg.js
|
||||
fi
|
||||
if [ $MP3 -eq 1 ]; then
|
||||
echo $JSHEADER > $MIDIDIR/soundfont-mp3.js
|
||||
fi
|
||||
|
||||
# from MIDI to WAV to OGG to JS to JGZ, and beyond!
|
||||
find $MIDIDIR -name '*.midi' -print0 | while read -d $'\0' file
|
||||
do
|
||||
# from MIDI to WAV
|
||||
fluidsynth -C 1 -R 1 -g 0.5 -F "$file.wav" "$SOUNDFONT" "$file"
|
||||
# from WAV to OGG
|
||||
if [ $OGG -eq 1 ]; then
|
||||
OGGFILE=`echo ${file%.midi}.ogg`;
|
||||
oggenc -m 32 -M 64 "$file.wav"
|
||||
mv "$file.ogg" "$OGGFILE"
|
||||
# from OGG to base64 embedded in Javascript
|
||||
JSCONTENT="\"`basename \"${file%.midi}\"`\": \"data:audio/ogg;base64,`node ./inc/gen-base64.js \"$OGGFILE\"`\","
|
||||
if [ $SINGLE -eq 1 ]; then
|
||||
echo $JSHEADER > "$OGGFILE.js"
|
||||
echo $JSCONTENT >> "$OGGFILE.js"
|
||||
echo $JSFOOTER >> "$OGGFILE.js"
|
||||
# gzipped version
|
||||
if [ $JGZ -eq 1 ]; then
|
||||
gzip "$OGGFILE.js" -c > "$OGGFILE.jgz"
|
||||
fi
|
||||
fi
|
||||
if [ $PACKAGE -eq 1 ]; then
|
||||
echo "$JSCONTENT" >> "$MIDIDIR/soundfont-ogg.js"
|
||||
fi
|
||||
`rm "$OGGFILE"`
|
||||
fi
|
||||
# from WAV to MP3
|
||||
if [ $MP3 -eq 1 ]; then
|
||||
MP3FILE=`echo "${file%.midi}.mp3"`
|
||||
lame -v -b 8 -B 32 "$file.wav" "$MP3FILE"
|
||||
# from MP3 to base64 embedded in Javascript
|
||||
JSCONTENT="\"`basename \"${file%.midi}\"`\": \"data:audio/mpeg;base64,`node ./inc/gen-base64.js \"$MP3FILE\"`\","
|
||||
if [ $SINGLE -eq 1 ]; then
|
||||
echo $JSHEADER > "$MP3FILE.js"
|
||||
echo $JSCONTENT >> "$MP3FILE.js"
|
||||
echo $JSFOOTER >> "$MP3FILE.js"
|
||||
# gzipped version
|
||||
if [ $JGZ -eq 1 ]; then
|
||||
gzip "$MP3FILE.js" -c > "$MP3FILE.jgz"
|
||||
fi
|
||||
fi
|
||||
if [ $PACKAGE -eq 1 ]; then
|
||||
echo $JSCONTENT >> "$MIDIDIR/soundfont-mp3.js"
|
||||
fi
|
||||
# `rm "$MP3FILE"`
|
||||
fi
|
||||
# cleanup
|
||||
rm "$file"
|
||||
rm "$file.wav"
|
||||
done
|
||||
|
||||
# write the footers
|
||||
if [ $OGG -eq 1 ]; then
|
||||
echo $JSFOOTER >> $MIDIDIR/soundfont-ogg.js
|
||||
fi
|
||||
if [ $MP3 -eq 1 ]; then
|
||||
echo $JSFOOTER >> $MIDIDIR/soundfont-mp3.js
|
||||
fi
|
||||
|
||||
if [ $PACKAGE -eq 1 ]; then
|
||||
if [ $JGZ -eq 1 ]; then
|
||||
if [ $OGG -eq 1 ]; then
|
||||
gzip $MIDIDIR/soundfont-ogg.js -c > $MIDIDIR/soundfont-ogg.jgz
|
||||
fi
|
||||
if [ $MP3 -eq 1 ]; then
|
||||
gzip $MIDIDIR/soundfont-mp3.js -c > $MIDIDIR/soundfont-mp3.jgz
|
||||
fi
|
||||
fi
|
||||
fi
|
205
soundfont-generator/soundfont_builder.rb
Executable file
205
soundfont-generator/soundfont_builder.rb
Executable file
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env ruby
|
||||
#
|
||||
# JavaScript Soundfont Builder for MIDI.js
|
||||
# Author: 0xFE <mohit@muthanna.com>
|
||||
#
|
||||
# Requires:
|
||||
#
|
||||
# FluidSynth
|
||||
# Lame
|
||||
# OggEnc (from vorbis-tools)
|
||||
# Ruby Gem: midilib
|
||||
#
|
||||
# $ brew install fluidsynth vorbis-tools lame (on OSX)
|
||||
# $ gem install midilib
|
||||
#
|
||||
# You'll need to download a GM soundbank to generate audio.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# 1) Install the above dependencies.
|
||||
# 2) Edit BUILD_DIR, SOUNDFONT, and INSTRUMENTS as required.
|
||||
# 3) Run without any argument.
|
||||
|
||||
require 'base64'
|
||||
require 'fileutils'
|
||||
require 'midilib'
|
||||
include FileUtils
|
||||
|
||||
BUILD_DIR = "../soundfont" # Output path
|
||||
SOUNDFONT = "./FluidR3_GM.sf2" # Soundfont file path
|
||||
|
||||
# This script will generate MIDI.js-compatible instrument JS files for
|
||||
# all instruments in the below array. Add or remove as necessary.
|
||||
INSTRUMENTS = [
|
||||
0, # Acoustic Grand Piano
|
||||
24, # Acoustic Guitar (nylon)
|
||||
25, # Acoustic Guitar (steel)
|
||||
26, # Electric Guitar (jazz)
|
||||
30, # Distortion Guitar
|
||||
33, # Electric Bass (finger)
|
||||
34, # Electric Bass (pick)
|
||||
56, # Trumpet
|
||||
61, # Brass Section
|
||||
64, # Soprano Sax
|
||||
65, # Alto Sax
|
||||
66, # Tenor Sax
|
||||
67, # Baritone Sax
|
||||
73, # Flute
|
||||
118 # Synth Drum
|
||||
]
|
||||
|
||||
# The encoders and tools are expected in your PATH. You can supply alternate
|
||||
# paths by changing the constants below.
|
||||
OGGENC = `which oggenc`.chomp
|
||||
LAME = `which lame`.chomp
|
||||
FLUIDSYNTH = `which fluidsynth`.chomp
|
||||
|
||||
puts "Building the following instruments using font: " + SOUNDFONT
|
||||
|
||||
# Display instrument names.
|
||||
INSTRUMENTS.each do |i|
|
||||
puts " #{i}: " + MIDI::GM_PATCH_NAMES[i]
|
||||
end
|
||||
|
||||
puts
|
||||
puts "Using OGG encoder: " + OGGENC
|
||||
puts "Using MP3 encoder: " + LAME
|
||||
puts "Using FluidSynth encoder: " + FLUIDSYNTH
|
||||
puts
|
||||
puts "Sending output to: " + BUILD_DIR
|
||||
puts
|
||||
|
||||
raise "Can't find soundfont: #{SOUNDFONT}" unless File.exists? SOUNDFONT
|
||||
raise "Can't find 'oggenc' command" if OGGENC.empty?
|
||||
raise "Can't find 'lame' command" if LAME.empty?
|
||||
raise "Can't find 'fluidsynth' command" if FLUIDSYNTH.empty?
|
||||
raise "Output directory does not exist: #{BUILD_DIR}" unless File.exists?(BUILD_DIR)
|
||||
|
||||
puts "Hit return to begin."
|
||||
$stdin.readline
|
||||
|
||||
NOTES = {
|
||||
"C" => 0,
|
||||
"Db" => 1,
|
||||
"D" => 2,
|
||||
"Eb" => 3,
|
||||
"E" => 4,
|
||||
"F" => 5,
|
||||
"Gb" => 6,
|
||||
"G" => 7,
|
||||
"Ab" => 8,
|
||||
"A" => 9,
|
||||
"Bb" => 10,
|
||||
"B" => 11
|
||||
}
|
||||
|
||||
MIDI_C0 = 12
|
||||
VELOCITY = 85
|
||||
DURATION = Integer(3200 * 0.75)
|
||||
TEMP_FILE = "#{BUILD_DIR}/temp.midi"
|
||||
|
||||
def note_to_int(note, octave)
|
||||
value = NOTES[note]
|
||||
increment = MIDI_C0 + (octave * 12)
|
||||
return value + increment
|
||||
end
|
||||
|
||||
def int_to_note(value)
|
||||
raise "Bad Value" if value < MIDI_C0
|
||||
reverse_notes = NOTES.invert
|
||||
value -= MIDI_C0
|
||||
octave = value / 12
|
||||
note = value % 12
|
||||
return { key: reverse_notes[note],
|
||||
octave: octave }
|
||||
end
|
||||
|
||||
# Run a quick table validation
|
||||
MIDI_C0.upto(100) do |x|
|
||||
note = int_to_note x
|
||||
raise "Broken table" unless note_to_int(note[:key], note[:octave]) == x
|
||||
end
|
||||
|
||||
def generate_midi(program, note_value, file)
|
||||
include MIDI
|
||||
seq = Sequence.new()
|
||||
track = Track.new(seq)
|
||||
|
||||
seq.tracks << track
|
||||
track.events << ProgramChange.new(0, Integer(program))
|
||||
track.events << NoteOn.new(0, note_value, VELOCITY, 0) # channel, note, velocity, delta
|
||||
track.events << NoteOff.new(0, note_value, VELOCITY, DURATION)
|
||||
|
||||
File.open(file, 'wb') { | file | seq.write(file) }
|
||||
end
|
||||
|
||||
def run_command(cmd)
|
||||
puts "Running: " + cmd
|
||||
`#{cmd}`
|
||||
end
|
||||
|
||||
def midi_to_audio(source, target)
|
||||
run_command "#{FLUIDSYNTH} -C 1 -R 1 -g 0.5 -F #{target} #{SOUNDFONT} #{source}"
|
||||
run_command "#{OGGENC} -m 32 -M 64 #{target}"
|
||||
run_command "#{LAME} -v -b 8 -B 32 #{target}"
|
||||
rm target
|
||||
end
|
||||
|
||||
def open_js_file(instrument_key, type)
|
||||
js_file = File.open("#{BUILD_DIR}/#{instrument_key}-#{type}.js", "w")
|
||||
js_file.write(
|
||||
"""
|
||||
if (typeof(MIDI) === 'undefined') var MIDI = {};
|
||||
if (typeof(MIDI.Soundfont) === 'undefined') MIDI.Soundfont = {};
|
||||
MIDI.Soundfont.#{instrument_key} = {
|
||||
""")
|
||||
return js_file
|
||||
end
|
||||
|
||||
def close_js_file(file)
|
||||
file.write("\n}\n")
|
||||
file.close
|
||||
end
|
||||
|
||||
def base64js(note, file, type)
|
||||
output = '"' + note + '": '
|
||||
output += '"' + "data:audio/#{type};base64,"
|
||||
output += Base64.strict_encode64(File.read(file)) + '"'
|
||||
return output
|
||||
end
|
||||
|
||||
def generate_audio(program)
|
||||
include MIDI
|
||||
instrument = GM_PATCH_NAMES[program]
|
||||
program_key = instrument.downcase.gsub(/[^a-z0-9 ]/, "").gsub(/\s+/, "_")
|
||||
|
||||
puts "Generating audio for: " + instrument + "(#{program_key})"
|
||||
|
||||
mkdir_p "#{BUILD_DIR}/#{program_key}-mp3"
|
||||
ogg_js_file = open_js_file(program_key, "ogg")
|
||||
mp3_js_file = open_js_file(program_key, "mp3")
|
||||
|
||||
note_to_int("A", 0).upto(note_to_int("C", 8)) do |note_value|
|
||||
note = int_to_note(note_value)
|
||||
output_name = "#{note[:key]}#{note[:octave]}"
|
||||
output_path_prefix = BUILD_DIR + "/" + output_name
|
||||
|
||||
puts "Generating: #{output_name}"
|
||||
generate_midi(program, note_value, TEMP_FILE)
|
||||
midi_to_audio(TEMP_FILE, output_path_prefix + ".wav")
|
||||
|
||||
puts "Updating JS files..."
|
||||
ogg_js_file.write(base64js(output_name, output_path_prefix + ".ogg", "ogg") + ",\n")
|
||||
mp3_js_file.write(base64js(output_name, output_path_prefix + ".mp3", "mp3") + ",\n")
|
||||
|
||||
mv output_path_prefix + ".mp3", "#{BUILD_DIR}/#{program_key}-mp3"
|
||||
rm output_path_prefix + ".ogg"
|
||||
rm TEMP_FILE
|
||||
end
|
||||
|
||||
close_js_file(ogg_js_file)
|
||||
close_js_file(mp3_js_file)
|
||||
end
|
||||
|
||||
INSTRUMENTS.each {|i| generate_audio(i)}
|
Loading…
Reference in a new issue