├── .gitignore ├── .jshintignore ├── .jshintrc ├── README.md ├── src ├── Chord.js ├── Controls.js ├── Events.js ├── Instrument.js ├── Note.js ├── Player.js ├── Song.js ├── Track.js ├── instruments │ └── Oscillator.js └── songs │ ├── got.json │ ├── imperial_march.json │ ├── mario_theme.json │ └── test.json └── vendor ├── modernizr.js ├── module.js ├── namespace.js └── sushi └── extend.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore the build directory 11 | /build 12 | 13 | # Ignore Sass' cache 14 | /.sass-cache 15 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | src/Events.js -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "regexdash": true, 3 | "browser": true, 4 | "wsh": false, 5 | "trailing": true, 6 | "sub": true, 7 | "laxcomma": true, 8 | "laxbreak": true, 9 | "eqeqeq": true, 10 | "curly": true, 11 | "boss": true, 12 | "forin": true, 13 | "newcap": false, 14 | "noarg": true, 15 | "quotmark": true, 16 | "maxdepth": 2, 17 | "devel": false, 18 | "jquery": true, 19 | "globals" : { 20 | "Module" : true, 21 | "Namespace" : true, 22 | "SH" : true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RetroJS - a Web Audio API experimental player 2 | 3 | The RetroJS is a music parser and player that uses the Web Audio API. Created 4 | as an experiment (and for lots of fun), it parses a custom musical notation input, 5 | and outputs the sound through the browser. 6 | 7 | Demo: http://eshiota.github.io/retro-audio-js/ 8 | 9 | ## Usage 10 | 11 | Create a `Player` instance: 12 | 13 | ```javascript 14 | var player = new Player(); 15 | ``` 16 | 17 | Load songs either through the `load` method, passing the JSON song as parameter; 18 | or load it through the `loadUrl` method, passing an URL that serves the JSON song. 19 | 20 | ```javascript 21 | // load a locally defined JSON song 22 | var mysong = { 23 | "title" : "Test song", 24 | "tempo" : 60, 25 | "time_signature" : "4/4", 26 | "score" : [ 27 | { 28 | "instrument" : "oscillator-sine", 29 | "volume" : 0.5, 30 | "sheet" : [ "C.4", "D.4", "E.4", "F.4", "G.4", "A.4", "B.4", "C5.4" ] 31 | } 32 | ] 33 | }; 34 | 35 | player.load(mysong); 36 | 37 | // or load an URL serving the JSON song 38 | player.loadUrl("http://www.foo.com/mysong.json"); 39 | ``` 40 | 41 | ## Musical notation 42 | 43 | **WARNING:** This musical notation will change as needed, and I'll try to keep 44 | backwards compatibility whenever possible. But whenever that's not possible, 45 | please be patient, and sorry. =) 46 | 47 | Every song is a JSON object with a title, tempo, time signature, and the score itself. 48 | The tempo always refer to how many quarter notes (crochets) are played per second. 49 | 50 | Please check the provided examples on `/src/songs/`. 51 | 52 | ```javascript 53 | { 54 | "title" : "Test song", 55 | "tempo" : 60, 56 | "time_signature" : "4/4", 57 | "score" : [] 58 | }; 59 | ``` 60 | 61 | ### The score and tracks 62 | 63 | The score is an array of tracks. A track is an object with an instrument 64 | (identified by a string), volume (a float from 0 to 1), and a sheet. A sheet 65 | is an array of notes or chords. All instruments must share a common way of writing a note's 66 | value and key, and some may have their own properties (a drumkit has no keys, for example). 67 | 68 | ```javascript 69 | { 70 | "instrument" : "oscillator-sine", 71 | "volume" : 0.5, 72 | "sheet" : [ "C.4", "D.4", "E.4", "F.4", "G.4", "A.4", "B.4", "C5.4" ] 73 | } 74 | ``` 75 | 76 | ### The notes 77 | 78 | A note is a string with the following format: 79 | 80 | `[key/rest/sample information].[duration information]` 81 | 82 | The information to the left of the dot tells either the note's key (C, D, E...), 83 | or if the note is a rest, or if any other information for a specific instrument. 84 | 85 | The information to the right tells the note's value—given the song's tempo, it 86 | tells what's the duration of the note. 87 | 88 | #### Keys and rests 89 | 90 | A key is a letter from A to G, plus an option accidental, plus an optional 91 | octave information. Some examples: 92 | 93 | * `A`: "A" key (La), on the 4th octave. Whenever octave information is not provided, 94 | the application assumes the 4th. 95 | * `C5`: "C" key (Do), on the 5th octave. 96 | * `C#5`: "C" key (Do), with sharp accidental, on the 5th octave. 97 | * `Db5`: "D" key (Re), with flat accidental, on the 5th octave. Represents the same 98 | note as above. 99 | 100 | A rest is always represented as a dash `-`. If a track has a period of silence, 101 | always use a rest. Every empty space must be filled so that the parser calculates 102 | the correct cycle position for each note. 103 | 104 | #### Values 105 | 106 | The note's value represents the length of a note. The final duration will be 107 | a product of the length of the note, times the tempo of the song. 108 | 109 | More information: http://en.wikipedia.org/wiki/Note_value 110 | 111 | The notation is always related to the fraction of a whole note (semibreve), which 112 | has a value of `1`. So: 113 | 114 | * `1`: Semibreve - 1/1 (duration of four quarter notes) 115 | * `2`: Minim - 1/2 (duration two quarter notes) 116 | * `4`: Crochet - 1/4 (quarter note—value used as base for calculating the song's tempo) 117 | * `8`: Quaver - 1/8 (half of a quarter note) 118 | * `16`: Semiquaver - 1/16 (one-fourth of a quarter note) 119 | * `32`: Demisemiquaver - 1/32 (one-eigth of a quarter note) 120 | * `64`: Hemidemisemiquaver - 1/64 (one-sixteenth of a quarter note) 121 | 122 | The notation also supports a few modifiers: 123 | 124 | * `4D`: Dot. Whenever a value ends with "D", the note has the value before the character, 125 | plus half of it. In this case, the note has the duration of a quarter-note, 126 | plus half of a quarter-note. 127 | * `8T3`: Tuplet. Whenever a value is followed by a "T" with a number, it's part 128 | of a tuplet. A tuplet is a number of notes (the one after the "T") that are 129 | played during the duration of a higher value. In this case, three notes with 130 | `8T3` duration are played during the duration of a quarter note ("4"). 131 | 132 | Whenever a parser finds a tuplet note, the following "n-1" notes are considered 133 | part of it, being "n" the tuplet value. So `["C.8T3", "C.8T3", "C.8T3"]` is the 134 | same as `["C.8T3", "C.8", "C.8"]`. 135 | 136 | ### Chords 137 | 138 | Chords are array of notes inside a sheet. 139 | 140 | ```javascript 141 | "sheet" : [ ["C.4", "E.4", "G.4"], "D.4", "E.4" ] 142 | ``` 143 | 144 | In the example above, the `C.4`, `E.4`, `G.4` notes are played together, 145 | followed by `D.4` and `E.4`. 146 | 147 | The parser always assume that the chord has the duration of the longest note 148 | inside the array. If any of the notes is indicated as part of a tuplet, the 149 | whole chord is considered as part of it. 150 | 151 | ## Instruments 152 | 153 | Currently, these are the supported instruments: 154 | 155 | * "oscillator-sine": Oscillator, with the sine wave 156 | * "oscillator-square": Oscillator, with the square wave 157 | 158 | ## Controls 159 | 160 | The RetroJS player may be controlled through DOM nodes using the `Controls` class. 161 | It uses the following data attributes to bind them to the player: 162 | 163 | * `data-player-controls`: contains all controls 164 | * `data-song-controls`: contains controls for loading and selecting songs 165 | * `data-song-select`: ` 180 | 181 | 182 | 183 | 184 | 185 | 186 |
187 | 188 | 189 | 190 |
191 | 192 |
193 |

Please load a song

194 |
195 | 196 | 197 | ``` 198 | 199 | ## Current limitations 200 | 201 | These are the player's limitations that have no priority to be fixed: 202 | 203 | * There are a few notations and edge cases missing from the parser, like more 204 | exotic tuplets or double/trippled dots. 205 | * The player loop uses 1/64 as a base to calculate cycles, so it currently 206 | does not support notes shorter than that. 207 | * The number of tracks and notes is limited to the browser/device's performance. 208 | I haven't tested how far it goes yet. 209 | * Chords have a very simple logic in order to calculate the correct cycles, 210 | so there's no way to have a note on it with longer duration. This can be 211 | achieved through multiple tracks, though. 212 | * There's no way to have a note longer than a semibreve. 213 | 214 | ## Contributing 215 | 216 | Always check the project's issues to see whether what's left to be fixed, 217 | and what's already been done or dismissed. 218 | 219 | 1. Fork the project 220 | 2. Create a topic branch - git checkout -b my_branch 221 | 3. Push to your branch - git push origin my_branch 222 | 4. Create an Issue with a link to your branch 223 | 5. That's it! 224 | 225 | Respect the project's code standards, and the JSHint options. 226 | 227 | ## License 228 | 229 | (The MIT License) 230 | 231 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 232 | 233 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 234 | 235 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 236 | -------------------------------------------------------------------------------- /src/Chord.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Chord 3 | *****************************************************************************/ 4 | 5 | Module("App.Chord", function (Chord) { 6 | 7 | Chord.fn.initialize = function (notes) { 8 | this.notes = notes.map(function (value) { 9 | return new App.Note(value); 10 | }); 11 | 12 | this.value = this.getValue(this.notes); 13 | }; 14 | 15 | // Gets the longest value (lowest value) among the notes 16 | Chord.fn.getValue = function (notes) { 17 | var tupletNotes = notes.filter(function (note) { return note.belongsToTuplet; }) 18 | , dottedNotes = notes.filter(function (note) { return note.isDotted; }) 19 | ; 20 | 21 | // Assume that if one of the notes is a Tuplet, the entire chord is. 22 | // Deal with it. B-) 23 | if (tupletNotes.length > 0) { 24 | this.belongsToTuplet = true; 25 | this.tupletValue = tupletNotes[0].tupletValue; 26 | return tupletNotes[0].value; 27 | } 28 | 29 | if (dottedNotes.length > 0) { 30 | this.isDotted = true; 31 | return dottedNotes[0].value; 32 | } 33 | 34 | return notes.reduce(function (previous, current) { 35 | var previousDuration = typeof previous === "number" ? previous : previous.value; 36 | 37 | return Math.min(previousDuration, current.value); 38 | }); 39 | }; 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /src/Controls.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Controls 3 | *****************************************************************************/ 4 | 5 | Module("App.Controls", function (Controls) { 6 | 7 | // Turns Controls into an event emmiter 8 | SH.extend(Controls.fn, App.Events); 9 | 10 | Controls.fn.initialize = function () { 11 | this.element = document.querySelector("[data-player-controls]"); 12 | this.songControls = this.element.querySelector("[data-song-controls]"); 13 | this.playbackControls = this.element.querySelector("[data-playback-controls]"); 14 | this.songInfo = this.element.querySelector("[data-song-info]"); 15 | 16 | this._attachEvents(); 17 | }; 18 | 19 | Controls.fn.updateSongInfo = function (title) { 20 | this.songInfo.querySelector("[data-current-song]").textContent = "Current song: " + title; 21 | }; 22 | 23 | Controls.fn.switchToPlayStatus = function () { 24 | this.playbackControls.querySelector("[data-play]").style.display = "none"; 25 | this.playbackControls.querySelector("[data-pause]").style.display = "block"; 26 | this.playbackControls.querySelector("[data-stop]").style.display = "block"; 27 | }; 28 | 29 | Controls.fn.switchToStopStatus = function () { 30 | this.playbackControls.querySelector("[data-play]").style.display = "block"; 31 | this.playbackControls.querySelector("[data-pause]").style.display = "none"; 32 | this.playbackControls.querySelector("[data-stop]").style.display = "block"; 33 | }; 34 | 35 | // Private 36 | // ------- 37 | 38 | Controls.fn._attachEvents = function () { 39 | this.playbackControls.querySelector("[data-play]").addEventListener("click", this._onPlayClick.bind(this)); 40 | this.playbackControls.querySelector("[data-stop]").addEventListener("click", this._onStopClick.bind(this)); 41 | this.playbackControls.querySelector("[data-pause]").addEventListener("click", this._onPauseClick.bind(this)); 42 | this.songControls.querySelector("[data-song-load]").addEventListener("click", this._onLoadClick.bind(this)); 43 | }; 44 | 45 | // Callbacks 46 | // --------- 47 | 48 | Controls.fn._onPlayClick = function (event) { 49 | this.switchToPlayStatus(); 50 | this.trigger("play-click"); 51 | }; 52 | 53 | Controls.fn._onStopClick = function (event) { 54 | this.switchToStopStatus(); 55 | this.trigger("stop-click"); 56 | }; 57 | 58 | Controls.fn._onPauseClick = function (event) { 59 | this.switchToStopStatus(); 60 | this.trigger("pause-click"); 61 | }; 62 | 63 | Controls.fn._onLoadClick = function (event) { 64 | var song = this.songControls.querySelector("[data-song-select]").value; 65 | 66 | this.switchToStopStatus(); 67 | this.trigger("load-click", song); 68 | }; 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /src/Events.js: -------------------------------------------------------------------------------- 1 | // Provides on and off callback functions to objects who extends it. 2 | // Based on Backbone's Events. 3 | 4 | ;(function () { 5 | var eventSplitter = /\s+/; 6 | 7 | Module("App.Events", { 8 | // Bind one or more space separated events, `events`, to a `callback` 9 | // function. Passing `"all"` will bind the callback to all events fired. 10 | on: function(events, callback, context) { 11 | var calls, event, list; 12 | if (!callback) { 13 | return this; 14 | } 15 | 16 | events = events.split(eventSplitter); 17 | calls = this._callbacks || (this._callbacks = {}); 18 | 19 | while (event = events.shift()) { 20 | list = calls[event] || (calls[event] = []); 21 | list.push(callback, context); 22 | } 23 | 24 | return this; 25 | }, 26 | 27 | // Remove one or many callbacks. If `context` is null, removes all callbacks 28 | // with that function. If `callback` is null, removes all callbacks for the 29 | // event. If `events` is null, removes all bound callbacks for all events. 30 | off: function(events, callback, context) { 31 | var event, calls, list, i; 32 | 33 | // No events, or removing *all* events. 34 | if (!(calls = this._callbacks)) { 35 | return this; 36 | } 37 | 38 | if (!(events || callback || context)) { 39 | delete this._callbacks; 40 | return this; 41 | } 42 | 43 | events = events ? events.split(eventSplitter) : Object.keys(calls); 44 | 45 | // Loop through the callback list, splicing where appropriate. 46 | while (event = events.shift()) { 47 | if (!(list = calls[event]) || !(callback || context)) { 48 | delete calls[event]; 49 | continue; 50 | } 51 | 52 | for (i = list.length - 2; i >= 0; i -= 2) { 53 | if (!(callback && list[i] !== callback || context && list[i + 1] !== context)) { 54 | list.splice(i, 2); 55 | } 56 | } 57 | } 58 | 59 | return this; 60 | }, 61 | 62 | // Trigger one or many events, firing all bound callbacks. Callbacks are 63 | // passed the same arguments as `trigger` is, apart from the event name 64 | // (unless you're listening on `"all"`, which will cause your callback to 65 | // receive the true name of the event as the first argument). 66 | trigger: function(events) { 67 | var event, calls, list, i, length, args, all, rest; 68 | if (!(calls = this._callbacks)) { 69 | return this; 70 | } 71 | 72 | rest = []; 73 | events = events.split(eventSplitter); 74 | for (i = 1, length = arguments.length; i < length; i++) { 75 | rest[i - 1] = arguments[i]; 76 | } 77 | 78 | // For each event, walk through the list of callbacks twice, first to 79 | // trigger the event, then to trigger any `"all"` callbacks. 80 | while (event = events.shift()) { 81 | // Copy callback lists to prevent modification. 82 | if (all = calls.all) { 83 | all = all.slice(); 84 | } 85 | 86 | if (list = calls[event]) { 87 | list = list.slice(); 88 | } 89 | 90 | // Execute event callbacks. 91 | if (list) { 92 | for (i = 0, length = list.length; i < length; i += 2) { 93 | list[i].apply(list[i + 1] || this, rest); 94 | } 95 | } 96 | 97 | // Execute "all" callbacks. 98 | if (all) { 99 | args = [event].concat(rest); 100 | for (i = 0, length = all.length; i < length; i += 2) { 101 | all[i].apply(all[i + 1] || this, args); 102 | } 103 | } 104 | } 105 | 106 | return this; 107 | } 108 | }); 109 | })(); 110 | -------------------------------------------------------------------------------- /src/Instrument.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Instrument 3 | * 4 | * TODO: implement better strategy for different types of instruments 5 | *****************************************************************************/ 6 | 7 | Module("App.Instrument", function (Instrument) { 8 | 9 | Instrument.fn.initialize = function (instrument, volume, audioContext) { 10 | this.context = audioContext; 11 | this.volume = volume; 12 | this.init(instrument); 13 | }; 14 | 15 | // Inits the instrument instance. 16 | Instrument.fn.init = function (instrument) { 17 | var createGain = this.context.createGain || this.context.createGainNode; 18 | 19 | this.masterVolume = createGain.call(this.context); 20 | this.masterVolume.gain.value = this.volume; 21 | this.masterVolume.connect(this.context.destination); 22 | 23 | if (instrument === "oscillator-sine") { 24 | this.instrumentVariation = new App.instruments.Oscillator(this.context, this.masterVolume, "sine"); 25 | } 26 | 27 | if (instrument === "oscillator-square") { 28 | this.instrumentVariation = new App.instruments.Oscillator(this.context, this.masterVolume, "square"); 29 | } 30 | }; 31 | 32 | // Plays the instrument at a given frequency for duration in milliseconds 33 | Instrument.fn.play = function (note, duration) { 34 | this.instrumentVariation.play(note, duration); 35 | }; 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /src/Note.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Note 3 | *****************************************************************************/ 4 | 5 | Module("App.Note", function (Note) { 6 | 7 | Note.fn.initialize = function (notation) { 8 | this.isPause = false; 9 | this.parseNote(notation); 10 | }; 11 | 12 | // Maps an bemol accidental to its sustain (or flat) counterpart 13 | Note.accidentalsMap = { 14 | "Ab" : "G#", 15 | "Bb" : "A#", 16 | "Cb" : "B", 17 | "Db" : "C#", 18 | "Eb" : "D#", 19 | "Fb" : "E", 20 | "Gb" : "F#" 21 | }; 22 | 23 | // Parses the a note into frequency and value 24 | Note.fn.parseNote = function (notation) { 25 | var data = notation.split(".") 26 | , note 27 | ; 28 | 29 | if (data.length !== 2) { throw "Invalid note/value format :" + notation; } 30 | 31 | this.key = this.normalizeKey(data[0]); 32 | this.value = this.normalizeValue(data.slice(1).join("")); 33 | 34 | if (data[0] === "-") { 35 | this.isPause = true; 36 | return this; 37 | } 38 | }; 39 | 40 | // Normalizes a key notation 41 | // 42 | // A note may be: 43 | // 44 | // - Flat, no octave: "A" 45 | // - Flat, w/ octave: "A2" 46 | // - Flat, accident: "A#" 47 | // - Flat, accident, w/ octave: "A#2" 48 | Note.fn.normalizeKey = function (note) { 49 | var data = note.split("") 50 | , normalizedNote = [] 51 | , defaultOctave = 4 52 | , key 53 | ; 54 | 55 | if (data[1] && ["#", "B", "b"].indexOf(data[1]) !== -1) { 56 | // If there's an accidental, normalize it and 57 | // convert it to sharp 58 | key = data[0].toUpperCase() + data[1].toLowerCase(); 59 | normalizedNote.push(Note.accidentalsMap[key] || key); 60 | // If there's an octave, add it, otherwise, 61 | // use 1 as default 62 | normalizedNote.push(data[2] || defaultOctave); 63 | } else { 64 | // No accidental, so just normalize the note and add the octave 65 | normalizedNote.push(data[0].toUpperCase()); 66 | normalizedNote.push(data[1] || defaultOctave); 67 | } 68 | 69 | return normalizedNote.join(""); 70 | }; 71 | 72 | // Normalizes a value notation 73 | // 74 | // A value may be: 75 | // 76 | // - The denominator of a fraction of a full note 77 | // (semibreve, which has four 'beats' on a 4/4 bar). 78 | // If the value is 4, for example, its value is 1/4 of a semibreve, 79 | // which is a quarter note. 80 | // - A dotted note, which has the value of the fraction, plus half of it. 81 | // - A Tuplet (http://en.wikipedia.org/wiki/Tuplet). The notation is 82 | // the value of a note, followed by "T", and the number of subdivisions. 83 | // For example, "8T3" means that three 1/8 (minim) notes have the same 84 | // value as a 1/4 (crochet) note. 85 | Note.fn.normalizeValue = function (value) { 86 | var parts = value.split("T"); 87 | 88 | // If is a tuplet, make it so 89 | if (parts.length > 1) { 90 | this.belongsToTuplet = true; 91 | this.tupletValue = parseInt(parts[1], 10); 92 | } 93 | 94 | // If is a dotted note, give it an indication 95 | if (value.indexOf("D", 1) !== -1) { 96 | this.isDotted = true; 97 | } 98 | 99 | // If there's only the pure value part, return it as an integer 100 | return parseInt(parts[0], 10); 101 | }; 102 | 103 | }); 104 | -------------------------------------------------------------------------------- /src/Player.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Player 3 | *****************************************************************************/ 4 | 5 | Module("App.Player", function (Player) { 6 | 7 | Player.fn.initialize = function () { 8 | // Holds the universal audio context 9 | var AudioContext = window.AudioContext || window.webkitAudioContext; 10 | this.context = new AudioContext(); 11 | 12 | this.controls = new App.Controls(); 13 | this.isPlaying = false; 14 | 15 | this._registerInterests(); 16 | }; 17 | 18 | // Loads a song from an URL 19 | Player.fn.loadUrl = function (url) { 20 | var request = new XMLHttpRequest(); 21 | 22 | request.open("GET", url, true); 23 | request.overrideMimeType("application/json"); 24 | 25 | request.onload = (function () { 26 | this.load(JSON.parse(request.response)); 27 | }).bind(this); 28 | 29 | request.send(); 30 | }; 31 | 32 | // Loads a song into the player 33 | Player.fn.load = function (song) { 34 | if (this.song) { this.stop(); } 35 | 36 | this.song = new App.Song(song, this.context); 37 | this.controls.updateSongInfo(song.title); 38 | }; 39 | 40 | // Plays the song from the current position. 41 | // If a cycle is given, plays the song starting on that cycle. 42 | Player.fn.play = function (cycle) { 43 | if (!this.song) { return; } 44 | if (this.isPlaying) { return; } 45 | 46 | this.song.play(cycle); 47 | this.isPlaying = true; 48 | }; 49 | 50 | // Stops the song and resets the position 51 | Player.fn.stop = function () { 52 | if (!this.song) { return; } 53 | 54 | this.song.stop(); 55 | this.isPlaying = false; 56 | }; 57 | 58 | // Stops the song and stores the position 59 | Player.fn.pause = function () { 60 | if (!this.song) { return; } 61 | if (!this.isPlaying) { return; } 62 | 63 | this.song.pause(); 64 | this.isPlaying = false; 65 | }; 66 | 67 | // Private 68 | // ------- 69 | 70 | Player.fn._registerInterests = function () { 71 | this.controls.on("load-click", this.loadUrl, this); 72 | this.controls.on("stop-click", this.stop, this); 73 | this.controls.on("pause-click", this.pause, this); 74 | this.controls.on("play-click", this.play, this); 75 | }; 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /src/Song.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Song 3 | *****************************************************************************/ 4 | 5 | Module("App.Song", function (Song) { 6 | 7 | Song.fn.initialize = function (song, audioContext) { 8 | var signature = song.time_signature.split("/"); 9 | 10 | this.title = song.title; 11 | this.tempo = song.tempo; 12 | this.beatsPerBar = signature[0]; 13 | this.beatDuration = signature[1]; 14 | this.tracks = []; 15 | this.currentCycle = 0; 16 | this.playing = false; 17 | 18 | this.cycleDuration = Song.calculateCycle(this.tempo); 19 | 20 | // Loads all tracks from a song 21 | for (var i = 0, l = song.score.length; i < l; i++) { 22 | this.tracks.push(new App.Track(song.score[i], audioContext)); 23 | } 24 | }; 25 | 26 | // Calculates the cycle used to play the notes. The cycle is the minimum 27 | // possible duration for a note, which will serve as base for playing 28 | // the beats. 29 | // 30 | // The cycle is the duration of a Hemidemisemiquaver (OMG) note, 31 | // which is 1/64 the duration of a whole note (semibreve). 32 | // 33 | // Tried using a Demisemihemidemisemiquaver (OMFG)—a 1/256 note–but that 34 | // didn't work quite well. 35 | Song.calculateCycle = function (tempo) { 36 | var cycleDuration = 60000 / (tempo * 16); 37 | 38 | return cycleDuration; 39 | }; 40 | 41 | // Starts the cycle that plays the note 42 | Song.fn.play = function (cycle) { 43 | if (this.playing) { return; } 44 | 45 | if (cycle) { 46 | this.currentCycle = cycle; 47 | } 48 | 49 | this.playing = true; 50 | this._renderCycle(); 51 | }; 52 | 53 | // Stops the cycle rendering 54 | Song.fn.pause = function () { 55 | this.playing = false; 56 | }; 57 | 58 | // Stops the cycle rendering and reset position to zero 59 | Song.fn.stop = function () { 60 | this.playing = false; 61 | this.currentCycle = 0; 62 | }; 63 | 64 | // Loops the animation, calling itself according to the fps 65 | Song.fn._renderCycle = function () { 66 | if (!this.playing) { return true; } 67 | 68 | for (var i = 0, l = this.tracks.length; i < l; i++) { 69 | this.tracks[i].playNote(this.currentCycle, this.cycleDuration); 70 | } 71 | 72 | this.currentCycle = this.currentCycle + 1; 73 | 74 | setTimeout(this._renderCycle.bind(this), this.cycleDuration); 75 | }; 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /src/Track.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Track 3 | *****************************************************************************/ 4 | 5 | Module("App.Track", function (Track) { 6 | 7 | Track.fn.initialize = function (track, audioContext) { 8 | this.instrument = new App.Instrument(track.instrument, track.volume, audioContext); 9 | this.sheet = Track.parseTrack(track.sheet); 10 | }; 11 | 12 | // Parses a track of notes 13 | Track.parseTrack = function (track) { 14 | var parsedTrack = {} 15 | , currentCycle = 0 16 | , element 17 | , elementCycles // how many cycles does an element lasts 18 | , tupletCycle // stores the tuplet cycle in relation to the current cycle 19 | ; 20 | 21 | for (var i = 0, l = track.length; i < l; i++) { 22 | element = Track.parseTrackElement(track[i]); 23 | 24 | if (element.belongsToTuplet) { 25 | // If element is part of a tuplet, we have to put it into the 26 | // correct cycles. 27 | 28 | // The tuplet lasts for these cycles in total 29 | elementCycles = Math.floor(64 / (element.value / 2)); 30 | 31 | // The tuplet starts at the current cycle 32 | tupletCycle = currentCycle; 33 | parsedTrack[tupletCycle + ""] = element; 34 | tupletCycle = tupletCycle + Math.ceil(elementCycles / element.tupletValue); 35 | 36 | // We iterate through the tuplet notes outside the main 37 | // loop, and then continue with the parsing. 38 | for (var j = 1; j < element.tupletValue; j++) { 39 | i++; 40 | parsedTrack[tupletCycle + ""] = Track.parseTrackElement(track[i]); 41 | 42 | tupletCycle = tupletCycle + Math.ceil(elementCycles / element.tupletValue); 43 | } 44 | } else { 45 | parsedTrack[currentCycle + ""] = element; 46 | 47 | elementCycles = Math.floor(64 / element.value); 48 | } 49 | 50 | if (element.isDotted) { 51 | elementCycles = elementCycles + Math.floor((64 / element.value) / 2); 52 | } 53 | 54 | currentCycle = currentCycle + elementCycles; 55 | } 56 | return parsedTrack; 57 | }; 58 | 59 | // Parses a note/chord of a track 60 | Track.parseTrackElement = function (element) { 61 | if (Array.isArray(element)) { 62 | return new App.Chord(element); 63 | } 64 | 65 | return new App.Note(element); 66 | }; 67 | 68 | // Gets a note for a given cycle. If the cycle has no note, 69 | // do nothing. 70 | // 71 | // * `cycle`: The cycle of the music 72 | // * `cycleDuration`: How many milliseconds a cycle lasts 73 | Track.fn.playNote = function (cycle, cycleDuration) { 74 | var note = this.sheet[cycle] 75 | , duration 76 | ; 77 | 78 | if (note && !note.isPause) { 79 | // The duration of a note is actually a fraction. So the higher the 80 | // value, the shorter the note. 81 | // A whole note (semibreve) has 64 cycles. So a note is 82 | // (whole note / note fraction) * cycle duration. 83 | if (note.belongsToTuplet) { 84 | duration = Math.ceil(((64 / (note.value / 2)) * cycleDuration) / note.tupletValue); 85 | } else { 86 | duration = ((64 / note.value) * cycleDuration); 87 | } 88 | 89 | if (note.isDotted) { 90 | duration = duration + (duration / 2); 91 | } 92 | 93 | // This is actually a chord 94 | if (note.notes) { 95 | note.notes.forEach(function (note) { 96 | this.instrument.play(note, duration); 97 | }, this); 98 | 99 | return true; 100 | } 101 | 102 | this.instrument.play(note, duration); 103 | } 104 | }; 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /src/instruments/Oscillator.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Instrument - Oscillator 3 | *****************************************************************************/ 4 | 5 | Module("App.instruments.Oscillator", function (Oscillator) { 6 | 7 | Oscillator.fn.initialize = function (audioContext, node, type) { 8 | this.type = Oscillator.getWaveType(type || "sine", audioContext); 9 | this.context = audioContext; 10 | this.node = node; 11 | }; 12 | 13 | // Returns the wave type based on the available API 14 | Oscillator.getWaveType = function (type, context) { 15 | // Creates an oscillator just to get the wave type on older implementations 16 | var o = context.createOscillator(); 17 | 18 | return o[type.toUpperCase()] || type; 19 | }; 20 | 21 | // https://gist.github.com/stuartmemo/3766449 22 | // Takes string of Note + Octave 23 | // Example: 24 | // var frequency = Oscillator.getFrequency('C3'); 25 | Oscillator.getFrequency = function (note) { 26 | var notes = ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"] 27 | , octave 28 | , keyNumber 29 | ; 30 | 31 | if (note.length === 3) { 32 | octave = note.charAt(2); 33 | } else { 34 | octave = note.charAt(1); 35 | } 36 | 37 | keyNumber = notes.indexOf(note.slice(0, -1)); 38 | 39 | if (keyNumber < 3) { 40 | keyNumber = keyNumber + 12 + ((octave - 1) * 12) + 1; 41 | } else { 42 | keyNumber = keyNumber + ((octave - 1) * 12) + 1; 43 | } 44 | 45 | // Return frequency of note 46 | return Math.floor(440 * Math.pow(2, (keyNumber - 49) / 12)); 47 | }; 48 | 49 | Oscillator.fn.play = function (note, duration) { 50 | var frequency = Oscillator.getFrequency(note.key) 51 | , oscillator = this.context.createOscillator() 52 | , startMethod = oscillator.start || oscillator.noteOn 53 | , stopMethod = oscillator.stop || oscillator.noteOff 54 | ; 55 | 56 | oscillator.type = this.type; 57 | oscillator.frequency.value = frequency; 58 | oscillator.connect(this.node); 59 | startMethod.call(oscillator, 0); 60 | stopMethod.call(oscillator, this.context.currentTime + (duration / 1000)); 61 | }; 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /src/songs/got.json: -------------------------------------------------------------------------------- 1 | { 2 | "title" : "Game of Thrones Theme", 3 | "tempo" : 86, 4 | "time_signature" : "6/8", 5 | "score" : [ 6 | { 7 | "instrument" : "oscillator-square", 8 | "volume" : 0.4, 9 | "sheet" : [ 10 | "C.1", "C.1", "C.1", 11 | 12 | "-.1", "-.2", "D.8", "G3.8", "Bb3.16", "C.16", "D.8", "G3.8", "Bb3.16", "C.16", "D.8", "G3.8", "Bb3.16", "C.16", "D.8", "G3.8", "Bb3.16", "C.16", 13 | "-.1", "-.2", "C.8", "F3.8", "Ab3.16", "Bb3.16", "C.8", "F3.8", "Ab3.16", "Bb3.16", "C.8", "F3.8", "Ab3.16", "Bb3.16", "C.8", "F3.8", "Ab3.16", "Bb3.16" 14 | ] 15 | }, 16 | { 17 | "instrument" : "oscillator-square", 18 | "volume" : 0.6, 19 | "sheet" : [ 20 | "G.8", "C.8", "Eb.16", "F.16", "G.8", "C.8", "Eb.16", "F.16", "G.8", "C.8", "Eb.16", "F.16", "G.8", "C.8", "Eb.16", "F.16", 21 | "G.8", "C.8", "E.16", "F.16", "G.8", "C.8", "E.16", "F.16","G.8", "C.8", "E.16", "F.16", "G.8", "C.8", "E.16", "F.16", 22 | 23 | ["G.4D", "G3.4D"], "C3.4D", "Eb3.16", "F3.16", "G3.4", "C3.4", "Eb3.16", "F3.16", "D3.1", "D3.2", 24 | "F3.4D", "Bb2.4D", "Eb3.16", "D3.16", "F3.4", "Bb2.4D", "Eb3.16", "D3.16", "C3.1", "C3.2" 25 | ] 26 | }, 27 | { 28 | "instrument" : "oscillator-square", 29 | "volume" : 0.6, 30 | "sheet" : [ 31 | "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", 32 | "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", 33 | 34 | "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", "C2.8D", "-.16", "C2.16", "C2.16", 35 | "G2.8D", "-.16", "G2.16", "G2.16", "G2.8D", "-.16", "G2.16", "G2.16", "G2.8D", "-.16", "G2.16", "G2.16", "G2.8D", "-.16", "G2.16", "G2.16", 36 | 37 | "Bb2.8D", "-.16", "Bb2.16", "Bb2.16", "Bb2.8D", "-.16", "Bb2.16", "Bb2.16", "Bb2.8D", "-.16", "Bb2.16", "Bb2.16", "Bb2.8D", "-.16", "Bb2.16", "Bb2.16", 38 | "F2.8D", "-.16", "F2.16", "F2.16", "F2.8D", "-.16", "F2.16", "F2.16", "F2.8D", "-.16", "F2.16", "F2.16", "F2.8D", "-.16", "F2.16", "F2.16" 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/songs/imperial_march.json: -------------------------------------------------------------------------------- 1 | { 2 | "title" : "Imperial March", 3 | "tempo" : 120, 4 | "time_signature" : "4/4", 5 | "score" : [ 6 | { 7 | "instrument" : "oscillator-square", 8 | "volume" : 0.5, 9 | "sheet" : [ 10 | "G.8D", "-.16", "G.8D", "-.16", "G.8D", "-.16", "Eb.8D", "Bb.16", 11 | "G.8D", "-.16", "Eb.8D", "Bb.16", "G.4", "-.4" 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/songs/mario_theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "title" : "Super Mario Bros Theme", 3 | "tempo" : 200, 4 | "time_signature" : "4/4", 5 | "score" : [ 6 | { 7 | "instrument" : "oscillator-square", 8 | "volume" : 0.5, 9 | "sheet" : [ 10 | ["E5.8", "F#4.8"], ["E5.8", "F#4.8"], "-.8" , ["E5.8", "F#4.8"], "-.8" , ["C5.8", "F#4.8"], ["E5.8", "F#4.8"], "-.8", ["G5.8", "B4.8", "G4.8"], "-.8", "-.4" , "G4.8", "-.8", "-.4", 11 | 12 | ["C5.8", "E4.8"], "-.4" , ["G4.8", "C4.8"], "-.4" , ["E4.8", "G3.8"], "-.4" , ["A4.8", "C4.8"], "-.8", ["B.8", "D4.8"] , "-.8", ["Bb4.8", "Db4.8"], ["A4.8", "C4.8"], "-.8", 13 | ["G4.4T3", "C4.4T3"], ["E5.4T3", "G4.4T3"], ["G5.4T3", "B4.4T3"], ["A5.8", "C5.8"], "-.8", ["F5.8", "A4.8"], ["G5.8", "B4.8"], "-.8", ["E5.8", "G4.8"], "-.8", ["C5.8", "E4.8"], ["D5.8", "F4.8"], ["B4.8", "D4.8"], "-.4", 14 | ["C5.8", "E4.8"], "-.4" , ["G4.8", "C4.8"], "-.4" , ["E4.8", "G3.8"], "-.4" , ["A4.8", "C4.8"], "-.8", ["B.8", "D4.8"] , "-.8", ["Bb4.8", "Db4.8"], ["A4.8", "C4.8"], "-.8", 15 | ["G4.4T3", "C4.4T3"], ["E5.4T3", "G4.4T3"], ["G5.4T3", "B4.4T3"], ["A5.8", "C5.8"], "-.8", ["F5.8", "A4.8"], ["G5.8", "B4.8"], "-.8", ["E5.8", "G4.8"], "-.8", ["C5.8", "E4.8"], ["D5.8", "F4.8"], ["B4.8", "D4.8"], "-.2", 16 | 17 | ["G5.8", "E5.8"], ["Gb5.8", "Eb5.8"], ["F5.8", "D5.8"], ["D#5.8", "B4.8"], "-.8", ["E5.8", "C5.8"], "-.8", ["G#4.8", "D4.8"], ["A4.8", "E4.8"], ["C5.8", "G4.8"], "-.8", ["A4.8", "C4.8"], ["C5.8", "E4.8"], ["D5.8", "F4.8"], "-.4", 18 | ["G5.8", "E5.8"], ["Gb5.8", "Eb5.8"], ["F5.8", "D5.8"], ["D#5.8", "B4.8"], "-.8", ["E5.8", "C5.8"], "-.8", ["C6.8", "G5.8"], "-.8", ["C6.8", "G5.8"], ["C6.8", "G5.8"], "-.8", "-.4", "-.4", 19 | ["G5.8", "E5.8"], ["Gb5.8", "Eb5.8"], ["F5.8", "D5.8"], ["D#5.8", "B4.8"], "-.8", ["E5.8", "C5.8"], "-.8", ["G#4.8", "D4.8"], ["A4.8", "E4.8"], ["C5.8", "G4.8"], "-.8", ["A4.8", "C4.8"], ["C5.8", "E4.8"], ["D5.8", "F4.8"], 20 | "-.4", ["Eb5.8", "Ab4.8"], "-.4", ["D5.8", "F4.8"], "-.4", ["C5.8", "E4.8"], "-.8", "-.4", "-.2", "-.4", 21 | 22 | ["G5.8", "E5.8"], ["Gb5.8", "Eb5.8"], ["F5.8", "D5.8"], ["D#5.8", "B4.8"], "-.8", ["E5.8", "C5.8"], "-.8", ["G#4.8", "D4.8"], ["A4.8", "E4.8"], ["C5.8", "G4.8"], "-.8", ["A4.8", "C4.8"], ["C5.8", "E4.8"], ["D5.8", "F4.8"], "-.4", 23 | ["G5.8", "E5.8"], ["Gb5.8", "Eb5.8"], ["F5.8", "D5.8"], ["D#5.8", "B4.8"], "-.8", ["E5.8", "C5.8"], "-.8", ["C6.8", "G5.8"], "-.8", ["C6.8", "G5.8"], ["C6.8", "G5.8"], "-.8", "-.4", "-.4", 24 | ["G5.8", "E5.8"], ["Gb5.8", "Eb5.8"], ["F5.8", "D5.8"], ["D#5.8", "B4.8"], "-.8", ["E5.8", "C5.8"], "-.8", ["G#4.8", "D4.8"], ["A4.8", "E4.8"], ["C5.8", "G4.8"], "-.8", ["A4.8", "C4.8"], ["C5.8", "E4.8"], ["D5.8", "F4.8"], 25 | "-.4", ["Eb5.8", "Ab4.8"], "-.4", ["D5.8", "F4.8"], "-.4", ["C5.8", "E4.8"], "-.8", "-.4", "-.2", 26 | 27 | ["Ab4.8", "C5.8"], ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], ["Bb4.8", "D5.8"], "-.8", ["G4.8", "E5.8"], ["C5.8", "E4.8"], "-.8", ["A4.8", "E4.8"], ["G4.8", "C4.8"], "-.8", "-.4", 28 | ["Ab4.8", "C5.8"], ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], ["Bb4.8", "D5.8"], ["G4.8", "E5.8"], "-.2", "-.2", 29 | ["Ab4.8", "C5.8"], ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], ["Bb4.8", "D5.8"], "-.8", ["G4.8", "E5.8"], ["C5.8", "E4.8"], "-.8", ["A4.8", "E4.8"], ["G4.8", "C4.8"], "-.8", "-.4", 30 | 31 | ["E5.8", "F#4.8"], ["E5.8", "F#4.8"], "-.8" , ["E5.8", "F#4.8"], "-.8" , ["C5.8", "F#4.8"], ["E5.8", "F#4.8"], "-.8", ["G5.8", "B4.8", "G4.8"], "-.8", "-.4" , "G4.8", "-.8", "-.4", 32 | 33 | ["C5.8", "E4.8"], "-.4" , ["G4.8", "C4.8"], "-.4" , ["E4.8", "G3.8"], "-.4" , ["A4.8", "C4.8"], "-.8", ["B.8", "D4.8"] , "-.8", ["Bb4.8", "Db4.8"], ["A4.8", "C4.8"], "-.8", 34 | ["G4.4T3", "C4.4T3"], ["E5.4T3", "G4.4T3"], ["G5.4T3", "B4.4T3"], ["A5.8", "C5.8"], "-.8", ["F5.8", "A4.8"], ["G5.8", "B4.8"], "-.8", ["E5.8", "G4.8"], "-.8", ["C5.8", "E4.8"], ["D5.8", "F4.8"], ["B4.8", "D4.8"], "-.4", 35 | ["C5.8", "E4.8"], "-.4" , ["G4.8", "C4.8"], "-.4" , ["E4.8", "G3.8"], "-.4" , ["A4.8", "C4.8"], "-.8", ["B.8", "D4.8"] , "-.8", ["Bb4.8", "Db4.8"], ["A4.8", "C4.8"], "-.8", 36 | ["G4.4T3", "C4.4T3"], ["E5.4T3", "G4.4T3"], ["G5.4T3", "B4.4T3"], ["A5.8", "C5.8"], "-.8", ["F5.8", "A4.8"], ["G5.8", "B4.8"], "-.8", ["E5.8", "G4.8"], "-.8", ["C5.8", "E4.8"], ["D5.8", "F4.8"], ["B4.8", "D4.8"], "-.4", 37 | 38 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 39 | ["B4.4T3", "G4.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["E5.4T3", "G5.4T3"], ["D5.4T3", "F5.4T3"], ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["A4.8", "F4.8"], ["G4.8", "E4.8"], "-.8", "-.4", 40 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 41 | ["G4.8", "B4.8"], ["D5.8", "F5.8"], "-.8", ["D5.8", "F5.8"], ["D5.4T3", "F5.4T3"], ["C5.4T3", "E5.4T3"], ["B4.4T3", "D5.4T3"], ["G4.8", "C5.8"], "E4.8", "-.8", "E4.8", "C4.8", "-.8", "-.4", 42 | 43 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 44 | ["B4.4T3", "G4.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["E5.4T3", "G5.4T3"], ["D5.4T3", "F5.4T3"], ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["A4.8", "F4.8"], ["G4.8", "E4.8"], "-.8", "-.4", 45 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 46 | ["G4.8", "B4.8"], ["D5.8", "F5.8"], "-.8", ["D5.8", "F5.8"], ["D5.4T3", "F5.4T3"], ["C5.4T3", "E5.4T3"], ["B4.4T3", "D5.4T3"], ["G4.8", "C5.8"], "E4.8", "-.8", "E4.8", "C4.8", "-.8", "-.4", 47 | 48 | ["Ab4.8", "C5.8"], ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], ["Bb4.8", "D5.8"], "-.8", ["G4.8", "E5.8"], ["C5.8", "E4.8"], "-.8", ["A4.8", "E4.8"], ["G4.8", "C4.8"], "-.8", "-.4", 49 | ["Ab4.8", "C5.8"], ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], ["Bb4.8", "D5.8"], ["G4.8", "E5.8"], "-.2", "-.2", 50 | ["Ab4.8", "C5.8"], ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], "-.8", ["Ab4.8", "C5.8"], ["Bb4.8", "D5.8"], "-.8", ["G4.8", "E5.8"], ["C5.8", "E4.8"], "-.8", ["A4.8", "E4.8"], ["G4.8", "C4.8"], "-.8", "-.4", 51 | 52 | ["E5.8", "F#4.8"], ["E5.8", "F#4.8"], "-.8" , ["E5.8", "F#4.8"], "-.8" , ["C5.8", "F#4.8"], ["E5.8", "F#4.8"], "-.8", ["G5.8", "B4.8", "G4.8"], "-.8", "-.4" , "G4.8", "-.8", "-.4", 53 | 54 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 55 | ["B4.4T3", "G4.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["E5.4T3", "G5.4T3"], ["D5.4T3", "F5.4T3"], ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["A4.8", "F4.8"], ["G4.8", "E4.8"], "-.8", "-.4", 56 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 57 | ["G4.8", "B4.8"], ["D5.8", "F5.8"], "-.8", ["D5.8", "F5.8"], ["D5.4T3", "F5.4T3"], ["C5.4T3", "E5.4T3"], ["B4.4T3", "D5.4T3"], ["G4.8", "C5.8"], "E4.8", "-.8", "E4.8", "C4.8", "-.8", "-.4", 58 | 59 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 60 | ["B4.4T3", "G4.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["F5.4T3", "A5.4T3"], ["E5.4T3", "G5.4T3"], ["D5.4T3", "F5.4T3"], ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["A4.8", "F4.8"], ["G4.8", "E4.8"], "-.8", "-.4", 61 | ["E5.8", "C5.8"], ["C5.8", "A4.8"], "-.8", ["G4.8", "E4.8"], "-.4", ["G#4.8", "E4.8"], "-.8", ["A4.8", "F4.8"], ["C5.8", "F5.8"], "-.8", ["C5.8", "F5.8"], ["A4.8", "F4.8"], "-.8", "-.4", 62 | ["G4.8", "B4.8"], ["D5.8", "F5.8"], "-.8", ["D5.8", "F5.8"], ["D5.4T3", "F5.4T3"], ["C5.4T3", "E5.4T3"], ["B4.4T3", "D5.4T3"], ["G4.8", "C5.8"], "E4.8", "-.8", "E4.8", "C4.8", "-.8", "-.4" 63 | ] 64 | }, 65 | { 66 | "volume" : 0.5, 67 | "instrument" : "oscillator-square", 68 | "sheet" : [ 69 | "D3.8", "D3.8", "-.8", "D3.8", "-.8", "D3.8", "D3.8", "-.8", "G3.8", "-.8", "-.4", "G3.8", "-.8", "-.4", 70 | 71 | "G3.8", "-.4", "E3.8", "-.4", "C3.8", "-.4", "F3.8", "-.8", "G3.8", "-.8", "Gb3.8", "F3.8", "-.8", 72 | "E3.4T3", "C4.4T3", "E4.4T3", "F4.8", "-.8", "D4.8", "E4.8", "-.8", "C4.8", "-.8", "A3.8", "B3.8", "G3.8", "-.4", 73 | "G3.8", "-.4", "E3.8", "-.4", "C3.8", "-.4", "F3.8", "-.8", "G3.8", "-.8", "Gb3.8", "F3.8", "-.8", 74 | "E3.4T3", "C4.4T3", "E4.4T3", "F4.8", "-.8", "D4.8", "E4.8", "-.8", "C4.8", "-.8", "A3.8", "B3.8", "G3.8", "-.4", 75 | 76 | "C3.8", "-.4", "G3.8", "-.4", "C4.8", "-.8", "F3.8", "-.4", "C4.8", "C4.8", "-.8", "F4.8", "-.8", 77 | "C3.8", "-.4", "E3.8", "-.4", "G3.8", "C4.8", "-.2", "-.4", "G3.8", "-.8", 78 | "C3.8", "-.4", "G3.8", "-.4", "C4.8", "-.8", "F3.8", "-.4", "C4.8", "C4.8", "-.8", "F4.8", "-.8", 79 | "C3.8", "-.8", "Ab3.8", "-.4", "Bb3.8", "-.4", "C4.8", "-.4", "G3.8", "G3.8", "-.8", "C3.8", "-.8", 80 | 81 | "C3.8", "-.4", "G3.8", "-.4", "C4.8", "-.8", "F3.8", "-.4", "C4.8", "C4.8", "-.8", "F4.8", "-.8", 82 | "C3.8", "-.4", "E3.8", "-.4", "G3.8", "C4.8", "-.2", "-.4", "G3.8", "-.8", 83 | "C3.8", "-.4", "G3.8", "-.4", "C4.8", "-.8", "F3.8", "-.4", "C4.8", "C4.8", "-.8", "F4.8", "-.8", 84 | "C3.8", "-.8", "Ab3.8", "-.4", "Bb3.8", "-.4", "C4.8", "-.4", "G3.8", "G3.8", "-.8", "C3.8", "-.8", 85 | 86 | "Ab2.8", "-.8", "-.8", "Eb3.8", "-.4", "Ab3.8", "-.8", "G3.8", "-.8", "-.8", "C3.8", "-.4", "G2.8", "-.8", 87 | "Ab2.8", "-.8", "-.8", "Eb3.8", "-.4", "Ab3.8", "-.8", "G3.8", "-.8", "-.8", "C3.8", "-.4", "G2.8", "-.8", 88 | "Ab2.8", "-.8", "-.8", "Eb3.8", "-.4", "Ab3.8", "-.8", "G3.8", "-.8", "-.8", "C3.8", "-.4", "G2.8", "-.8", 89 | 90 | "D3.8", "D3.8", "-.8", "D3.8", "-.8", "D3.8", "D3.8", "-.8", "-.2", "G3.8", "-.8", "-.4", 91 | 92 | "G3.8", "-.4", "E3.8", "-.4", "C3.8", "-.4", "F3.8", "-.8", "G3.8", "-.8", "Gb3.8", "F3.8", "-.8", 93 | "E3.4T3", "C4.4T3", "E4.4T3", "F4.8", "-.8", "D4.8", "E4.8", "-.8", "C4.8", "-.8", "A3.8", "B3.8", "G3.8", "-.4", 94 | "G3.8", "-.4", "E3.8", "-.4", "C3.8", "-.4", "F3.8", "-.8", "G3.8", "-.8", "Gb3.8", "F3.8", "-.8", 95 | "E3.4T3", "C4.4T3", "E4.4T3", "F4.8", "-.8", "D4.8", "E4.8", "-.8", "C4.8", "-.8", "A3.8", "B3.8", "G3.8", "-.4", 96 | 97 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 98 | "D3.8", "-.8", "-.8", "F3.8", "G3.8", "-.8", "B3.8", "-.8", "G3.8", "-.8", "G3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 99 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 100 | "G3.8", "-.8", "-.8", "G3.8", "G3.4T3", "A3.4T3", "B3.4T3", "C4.8", "-.8", "G3.8", "-.8", "C3.8", "-.8", "-.4", 101 | 102 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 103 | "D3.8", "-.8", "-.8", "F3.8", "G3.8", "-.8", "B3.8", "-.8", "G3.8", "-.8", "G3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 104 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 105 | "G3.8", "-.8", "-.8", "G3.8", "G3.4T3", "A3.4T3", "B3.4T3", "C4.8", "-.8", "G3.8", "-.8", "C3.8", "-.8", "-.4", 106 | 107 | "Ab2.8", "-.8", "-.8", "Eb3.8", "-.4", "Ab3.8", "-.8", "G3.8", "-.8", "-.8", "C3.8", "-.4", "G2.8", "-.8", 108 | "Ab2.8", "-.8", "-.8", "Eb3.8", "-.4", "Ab3.8", "-.8", "G3.8", "-.8", "-.8", "C3.8", "-.4", "G2.8", "-.8", 109 | "Ab2.8", "-.8", "-.8", "Eb3.8", "-.4", "Ab3.8", "-.8", "G3.8", "-.8", "-.8", "C3.8", "-.4", "G2.8", "-.8", 110 | 111 | "D3.8", "D3.8", "-.8", "D3.8", "-.8", "D3.8", "D3.8", "-.8", "-.2", "G3.8", "-.8", "-.4", 112 | 113 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 114 | "D3.8", "-.8", "-.8", "F3.8", "G3.8", "-.8", "B3.8", "-.8", "G3.8", "-.8", "G3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 115 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 116 | "G3.8", "-.8", "-.8", "G3.8", "G3.4T3", "A3.4T3", "B3.4T3", "C4.8", "-.8", "G3.8", "-.8", "C3.8", "-.8", "-.4", 117 | 118 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 119 | "D3.8", "-.8", "-.8", "F3.8", "G3.8", "-.8", "B3.8", "-.8", "G3.8", "-.8", "G3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 120 | "C3.8", "-.8", "-.8", "F#3.8", "G3.8", "-.8", "C4.8", "-.8", "F3.8", "-.8", "F3.8", "-.8", "C4.8", "C4.8", "F3.8", "-.8", 121 | "G3.8", "-.8", "-.8", "G3.8", "G3.4T3", "A3.4T3", "B3.4T3", "C4.8", "-.8", "G3.8", "-.8", "C3.8", "-.8", "-.4" 122 | ] 123 | } 124 | ] 125 | } -------------------------------------------------------------------------------- /src/songs/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "title" : "Test song", 3 | "tempo" : 60, 4 | "time_signature" : "4/4", 5 | "score" : [ 6 | { 7 | "instrument" : "oscillator-sine", 8 | "volume" : 0.5, 9 | "sheet" : [ 10 | "C.8T3", "D.8T3", "E.8T3", ["C5.8T3", "E5.8T3"], ["C5.8T3", "E5.8T3"], ["C5.8T3", "E5.8T3"], 11 | "C.8T3", "D.8T3", "E.8T3", "C.8T6", "D.8T6", "E.8T6", "F.8T6", "G.8T6", "A.8T6", 12 | "C.4", "D.4", "E.4", "F.4", "G.4", "A.4", "B.4", "C5.4" 13 | ] 14 | }, 15 | { 16 | "instrument" : "oscillator-sine", 17 | "volume" : 0.5, 18 | "sheet" : [ 19 | "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", 20 | "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", 21 | "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", 22 | "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", 23 | "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8", "C3.8", "-.8" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /vendor/modernizr.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.6.2 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-flexbox-flexboxlegacy-hsla-multiplebgs-opacity-rgba-cssanimations-csscolumns-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-cssclasses-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-file_api-fullscreen_api-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function C(a){j.cssText=a}function D(a,b){return C(n.join(a+";")+(b||""))}function E(a,b){return typeof a===b}function F(a,b){return!!~(""+a).indexOf(b)}function G(a,b){for(var d in a){var e=a[d];if(!F(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function H(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:E(f,"function")?f.bind(d||b):f}return!1}function I(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return E(b,"string")||E(b,"undefined")?G(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),H(e,b,c))}function J(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=E(e[d],"function"),E(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),A={}.hasOwnProperty,B;!E(A,"undefined")&&!E(A.call,"undefined")?B=function(a,b){return A.call(a,b)}:B=function(a,b){return b in a&&E(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return I("flexWrap")},s.flexboxlegacy=function(){return I("boxDirection")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!E(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!I("indexedDB",a)},s.hashchange=function(){return z("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return C("background-color:rgba(150,255,150,.5)"),F(j.backgroundColor,"rgba")},s.hsla=function(){return C("background-color:hsla(120,40%,100%,.5)"),F(j.backgroundColor,"rgba")||F(j.backgroundColor,"hsla")},s.multiplebgs=function(){return C("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return I("backgroundSize")},s.borderimage=function(){return I("borderImage")},s.opacity=function(){return D("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return I("animationName")},s.csscolumns=function(){return I("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return C((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),F(j.backgroundImage,"gradient")},s.cssreflections=function(){return I("boxReflect")},s.csstransforms=function(){return!!I("transform")},s.csstransforms3d=function(){var a=!!I("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return I("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var K in s)B(s,K)&&(x=K.toLowerCase(),e[x]=s[K](),v.push((e[x]?"":"no-")+x));return e.input||J(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)B(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},C(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.hasEvent=z,e.testProp=function(a){return G([a])},e.testAllProps=I,e.testStyles=y,g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f 0) 13 | ? ((typeof curScope[curPart] !== "undefined") ? curScope[curPart] : {}) 14 | : obj; 15 | 16 | curScope[curPart] = curObj; 17 | 18 | curScope = curScope[curPart]; 19 | } 20 | 21 | return curScope; 22 | }; 23 | })(this); 24 | -------------------------------------------------------------------------------- /vendor/sushi/extend.js: -------------------------------------------------------------------------------- 1 | Namespace("SH.extend", function (obj) { 2 | Array.prototype.slice.call(arguments, 1).forEach(function (el) { 3 | if (el) { 4 | for (var prop in el) { 5 | obj[prop] = el[prop]; 6 | } 7 | } 8 | }); 9 | 10 | return obj; 11 | }); 12 | --------------------------------------------------------------------------------