diff --git a/soundfont-generator/inc/jsmidi.js b/soundfont-generator/inc/jsmidi.js new file mode 100644 index 0000000..212fda7 --- /dev/null +++ b/soundfont-generator/inc/jsmidi.js @@ -0,0 +1,508 @@ +/* + + 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); \ No newline at end of file diff --git a/soundfont-generator/node_modules/atob/index.js b/soundfont-generator/node_modules/atob/index.js new file mode 100644 index 0000000..72ddb40 --- /dev/null +++ b/soundfont-generator/node_modules/atob/index.js @@ -0,0 +1,9 @@ +(function () { + "use strict"; + + function atob(str) { + return new Buffer(str, 'base64').toString('utf8'); + } + + module.exports = atob; +}()); diff --git a/soundfont-generator/node_modules/atob/package.json b/soundfont-generator/node_modules/atob/package.json new file mode 100644 index 0000000..79b3c36 --- /dev/null +++ b/soundfont-generator/node_modules/atob/package.json @@ -0,0 +1,18 @@ +{ + "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 (http://coolaj86.info)", + "engines" : { + "node": ">= 0.4.0" + }, + "dependencies" : { + }, + "main" : "index", + "version" : "1.0.0" +} diff --git a/soundfont-generator/node_modules/atob/test.js b/soundfont-generator/node_modules/atob/test.js new file mode 100644 index 0000000..65fb01d --- /dev/null +++ b/soundfont-generator/node_modules/atob/test.js @@ -0,0 +1,14 @@ +(function () { + "use strict"; + + var atob = require('./index') + , expected = "SGVsbG8gV29ybGQ=" + , result + ; + + if (expected !== atob("Hello World")) { + return; + } + + console.log('[PASS] all tests pass'); +}()); diff --git a/soundfont-generator/node_modules/btoa/index.js b/soundfont-generator/node_modules/btoa/index.js new file mode 100644 index 0000000..65bfaf5 --- /dev/null +++ b/soundfont-generator/node_modules/btoa/index.js @@ -0,0 +1,9 @@ +(function () { + "use strict"; + + function atob(str) { + return new Buffer(str, 'utf8').toString('base64'); + } + + module.exports = atob; +}()); diff --git a/soundfont-generator/node_modules/btoa/package.json b/soundfont-generator/node_modules/btoa/package.json new file mode 100644 index 0000000..4ff5854 --- /dev/null +++ b/soundfont-generator/node_modules/btoa/package.json @@ -0,0 +1,18 @@ +{ + "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 (http://coolaj86.info)", + "engines" : { + "node": ">= 0.4.0" + }, + "dependencies" : { + }, + "main" : "index", + "version" : "1.0.0" +} diff --git a/soundfont-generator/node_modules/btoa/test.js b/soundfont-generator/node_modules/btoa/test.js new file mode 100644 index 0000000..65fb01d --- /dev/null +++ b/soundfont-generator/node_modules/btoa/test.js @@ -0,0 +1,14 @@ +(function () { + "use strict"; + + var atob = require('./index') + , expected = "SGVsbG8gV29ybGQ=" + , result + ; + + if (expected !== atob("Hello World")) { + return; + } + + console.log('[PASS] all tests pass'); +}()); diff --git a/soundfont-generator/sf2-midi.sh b/soundfont-generator/sf2-midi.sh new file mode 100755 index 0000000..00c9b0c --- /dev/null +++ b/soundfont-generator/sf2-midi.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# UNIX SETUP +# ------------------------------------ +# base64 - http://josefsson.org/base64/ + +# directory to generate into +MIDIDIR="./build" +if [ ! -d "$MIDIDIR" ]; then + mkdir $MIDIDIR +fi + +# put into the head of each generated .JS file +JSHEADER="{" +JSFOOTER="}" + +# write the headers +echo "{ " > "$MIDIDIR/soundfont-midi.js" + +# from MIDI to WAV to OGG to JS, and beyond! +find $MIDIDIR -name '*.mid' -print0 | while read -d $'\0' file + do + OGGFILE=$file + JSCONTENT="\"`basename \"${file%.mid}\"`\": 'data:audio/midi;base64,`base64 -i \"$OGGFILE\" -o -`'," + echo $JSHEADER > "$OGGFILE.js" + echo $JSCONTENT >> "$OGGFILE.js" + echo $JSFOOTER >> "$OGGFILE.js" + echo "\"`basename \"${file%.mid}\"`\": undefined, " >> "$MIDIDIR/soundfont-midi.js" + done + +# write the footers +echo "}" >> "$MIDIDIR/soundfont-midi.js" \ No newline at end of file diff --git a/soundfont-generator/sf2-piano.js b/soundfont-generator/sf2-piano.js new file mode 100644 index 0000000..e2a3c4c --- /dev/null +++ b/soundfont-generator/sf2-piano.js @@ -0,0 +1,72 @@ +/* + + Soundfont Builder : 0.1 + ------------------------ + + +*/ + +/// +window = {}; // create fake window +atob = require('atob'); +btoa = require('btoa'); +http = require('http'); +fs = require('fs'); +/// +require('./inc/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) {}); + } +} \ No newline at end of file diff --git a/soundfont-generator/sf2-piano.sh b/soundfont-generator/sf2-piano.sh new file mode 100755 index 0000000..13ffe9b --- /dev/null +++ b/soundfont-generator/sf2-piano.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# UNIX SETUP +# ------------------------------------ +# nodejs - http://nodejs.org/ +# gzip - http://www.gzip.org/ +# base64 - http://josefsson.org/base64/ +# fluidsynth - http://www.audiosoftstore.com/downloads.html +# 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 "./sf2-piano.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`; + ./inc/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,`base64 -i \"$OGGFILE\" -o -`\"," + 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"` + ./inc/lame -v -b 8 -B 32 "$file.wav" "$MP3FILE" + # from MP3 to base64 embedded in Javascript + JSCONTENT="\"`basename \"${file%.midi}\"`\": \"data:audio/mpeg;base64,`base64 -i \"$MP3FILE\" -o -`\"," + 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 \ No newline at end of file