From 29253e379f746250227a131fdcdc91a702c4fc4a Mon Sep 17 00:00:00 2001 From: Mohit Cheppudira Date: Mon, 21 Jan 2013 17:54:29 -0500 Subject: [PATCH] Replace JS/SH based soundfont builder with soundfont_builder.rb. --- soundfont-generator/inc/gen-base64.js | 5 - soundfont-generator/inc/gen-midi.js | 67 --- soundfont-generator/inc/jsmidi.js | 506 ------------------ .../node_modules/atob/index.js | 9 - .../node_modules/atob/package.json | 18 - soundfont-generator/node_modules/atob/test.js | 14 - .../node_modules/btoa/index.js | 9 - .../node_modules/btoa/package.json | 18 - soundfont-generator/node_modules/btoa/test.js | 14 - soundfont-generator/sf2-piano.sh | 117 ---- soundfont-generator/soundfont_builder.rb | 205 +++++++ 11 files changed, 205 insertions(+), 777 deletions(-) delete mode 100644 soundfont-generator/inc/gen-base64.js delete mode 100644 soundfont-generator/inc/gen-midi.js delete mode 100644 soundfont-generator/inc/jsmidi.js delete mode 100644 soundfont-generator/node_modules/atob/index.js delete mode 100644 soundfont-generator/node_modules/atob/package.json delete mode 100644 soundfont-generator/node_modules/atob/test.js delete mode 100644 soundfont-generator/node_modules/btoa/index.js delete mode 100644 soundfont-generator/node_modules/btoa/package.json delete mode 100644 soundfont-generator/node_modules/btoa/test.js delete mode 100755 soundfont-generator/sf2-piano.sh create mode 100755 soundfont-generator/soundfont_builder.rb diff --git a/soundfont-generator/inc/gen-base64.js b/soundfont-generator/inc/gen-base64.js deleted file mode 100644 index f40e241..0000000 --- a/soundfont-generator/inc/gen-base64.js +++ /dev/null @@ -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')); -}); \ No newline at end of file diff --git a/soundfont-generator/inc/gen-midi.js b/soundfont-generator/inc/gen-midi.js deleted file mode 100644 index f7a7414..0000000 --- a/soundfont-generator/inc/gen-midi.js +++ /dev/null @@ -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) {}); - } -} \ No newline at end of file diff --git a/soundfont-generator/inc/jsmidi.js b/soundfont-generator/inc/jsmidi.js deleted file mode 100644 index dd6e4c3..0000000 --- a/soundfont-generator/inc/jsmidi.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/soundfont-generator/node_modules/atob/index.js b/soundfont-generator/node_modules/atob/index.js deleted file mode 100644 index 72ddb40..0000000 --- a/soundfont-generator/node_modules/atob/index.js +++ /dev/null @@ -1,9 +0,0 @@ -(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 deleted file mode 100644 index 79b3c36..0000000 --- a/soundfont-generator/node_modules/atob/package.json +++ /dev/null @@ -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 (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 deleted file mode 100644 index 65fb01d..0000000 --- a/soundfont-generator/node_modules/atob/test.js +++ /dev/null @@ -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'); -}()); diff --git a/soundfont-generator/node_modules/btoa/index.js b/soundfont-generator/node_modules/btoa/index.js deleted file mode 100644 index 65bfaf5..0000000 --- a/soundfont-generator/node_modules/btoa/index.js +++ /dev/null @@ -1,9 +0,0 @@ -(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 deleted file mode 100644 index 4ff5854..0000000 --- a/soundfont-generator/node_modules/btoa/package.json +++ /dev/null @@ -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 (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 deleted file mode 100644 index 65fb01d..0000000 --- a/soundfont-generator/node_modules/btoa/test.js +++ /dev/null @@ -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'); -}()); diff --git a/soundfont-generator/sf2-piano.sh b/soundfont-generator/sf2-piano.sh deleted file mode 100755 index 7714055..0000000 --- a/soundfont-generator/sf2-piano.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/soundfont-generator/soundfont_builder.rb b/soundfont-generator/soundfont_builder.rb new file mode 100755 index 0000000..e6f8193 --- /dev/null +++ b/soundfont-generator/soundfont_builder.rb @@ -0,0 +1,205 @@ +#!/usr/bin/env ruby +# +# JavaScript Soundfont Builder for MIDI.js +# Author: 0xFE +# +# 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)}