├── .gitignore ├── Gruntfile.js ├── MIT-LICENSE.txt ├── README.md ├── bower.json ├── dist ├── sf2-parser-all.js └── sf2-parser-all.min.js ├── package.json ├── src └── sf2-parser.js └── tests ├── html └── soundfont-parser-tests.html ├── js └── sf2-parser-tests.js └── third-party ├── jquery └── js │ └── jquery.js └── qunit ├── addons └── composite │ ├── README.md │ ├── qunit-composite.css │ └── qunit-composite.js ├── css └── qunit.css └── js └── qunit.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | tests/sf2 4 | 5 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module*/ 2 | 3 | module.exports = function(grunt) { 4 | "use strict"; 5 | 6 | // Project configuration. 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON("package.json"), 9 | 10 | jshint: { 11 | all: [ 12 | "src/**/*.js", 13 | "tests/**/*js", 14 | "!**/third-party/**" 15 | ], 16 | options: { 17 | jshintrc: true 18 | } 19 | }, 20 | 21 | concat: { 22 | options: { 23 | separator: ";" 24 | }, 25 | 26 | all: { 27 | src: ["src/sf2-parser.js"], 28 | dest: "dist/<%= pkg.name %>-all.js" 29 | } 30 | }, 31 | 32 | uglify: { 33 | options: { 34 | beautify: { 35 | ascii_only: true 36 | } 37 | }, 38 | all: { 39 | files: [ 40 | { 41 | expand: true, 42 | cwd: "dist/", 43 | src: ["*.js"], 44 | dest: "dist/", 45 | ext: ".min.js", 46 | } 47 | ] 48 | } 49 | }, 50 | 51 | clean: { 52 | all: { 53 | src: ["dist/"] 54 | } 55 | } 56 | }); 57 | 58 | // Load relevant Grunt plugins. 59 | grunt.loadNpmTasks("grunt-contrib-concat"); 60 | grunt.loadNpmTasks("grunt-contrib-uglify"); 61 | grunt.loadNpmTasks("grunt-contrib-clean"); 62 | grunt.loadNpmTasks("grunt-contrib-jshint"); 63 | 64 | grunt.registerTask("default", ["clean", "jshint", "concat", "uglify"]); 65 | }; 66 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | This library is based on the SoundFont 2 parser from: 2 | sf2synth.js 3 | SoundFont Synthesizer for WebMidiLink 4 | https://github.com/gree/sf2synth.js 5 | 6 | The MIT License 7 | Copyright (c) 2013 imaya / GREE Inc. 8 | Modifications to the original are Copyright (c) 2015 Colin Clark 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SoundFont 2 (and 3) Parser 2 | ================== 3 | 4 | A library for parsing SoundFont (SF2, SF3) files in JavaScript. 5 | 6 | License and Credits 7 | ------------------- 8 | 9 | This library is based on the SoundFont 2 parser from [sf2synth.js, a SoundFont Synthesizer for WebMidiLink](https://github.com/gree/sf2synth.js). It was written by imaya / GREE Inc and licensed under the MIT license. Modifications to the original are by Colin Clark. 10 | 11 | How to Use It 12 | ------------- 13 | 14 | 1. Load your SoundFont file using XHR's arraybuffer responseType and wrap it as a Uint8Array 15 | 2. Instantiate a new parser instance 16 | 3. Call the parse() method to parse the SoundFont file 17 | 4. Use the getPresets() and getInstruments() methods to access preset and instrument data 18 | 5. Sample data is stored in the parser's sample member variable 19 | 20 | ### Example Code ### 21 | 22 | // Utility function to load a SoundFont file from a URL using XMLHttpRequest. 23 | // The same origin policy will apply, as for all XHR requests. 24 | function loadSoundFont(url, success, error) { 25 | var xhr = new XMLHttpRequest(); 26 | xhr.open("GET", url, true); 27 | xhr.responseType = "arraybuffer"; 28 | xhr.onreadystatechange = function () { 29 | if (xhr.readyState === 4) { 30 | if (xhr.status === 200) { 31 | success(new Uint8Array(xhr.response)); 32 | } else { 33 | if (options.error) { 34 | options.error(xhr.statusText); 35 | } 36 | } 37 | } 38 | }; 39 | xhr.send(); 40 | } 41 | 42 | // Load and parse a SoundFont file. 43 | loadSoundFont("sf_GMbank.sf2", function (sfData) { 44 | var parser = new sf2.Parser(sf2Data); 45 | parser.parse(); 46 | 47 | // Do something with the parsed SoundFont data. 48 | }); 49 | 50 | Summary of Changes from the Original 51 | ------------------------------------ 52 | 53 | * Separated out the SoundFont parsing library from the rest of the sf2synth synthesizer 54 | * Removed the dependence on Google Closure 55 | * Renamed the namespace to sf2 for brevity 56 | * Added boilerplate to support most JS module loaders, including CommonJS, AMD, Node.js and browser globals 57 | * Added a Grunt-based build process 58 | * Created Bower and npm packages 59 | 60 | To Do 61 | ----- 62 | 63 | * Improve the API of the parser by making it stateless 64 | * Add support for parsing in a Web Worker so that the main browser thread doesn't block 65 | * Massively extend the unit test coverage 66 | * Add support for running the unit tests in Node.js 67 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sf2-parser", 3 | "version": "1.0.0", 4 | "main": "dist/sf2-parser.min.js", 5 | "ignore": [ 6 | "tests", 7 | "Gruntfile.js", 8 | "MIT-LICENSE.txt", 9 | "package.json" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /dist/sf2-parser-all.js: -------------------------------------------------------------------------------- 1 | /*! JavaScript SoundFont 2 Parser. Copyright 2013-2015 imaya/GREE Inc and Colin Clark. Licensed under the MIT License. */ 2 | 3 | /* 4 | * JavaScript SoundFont 2 Parser 5 | * 6 | * Copyright 2013 imaya/GREE Inc 7 | * Copyright 2015 Colin Clark 8 | * 9 | * Based on code from the "SoundFont Synthesizer for WebMidiLink" 10 | * https://github.com/gree/sf2synth.js 11 | * 12 | * Licensed under the MIT License. 13 | */ 14 | 15 | /*global require*/ 16 | 17 | (function (root, factory) { 18 | if (typeof exports === "object") { 19 | // We're in a CommonJS-style loader. 20 | root.sf2 = exports; 21 | factory(exports); 22 | } else if (typeof define === "function" && define.amd) { 23 | // We're in an AMD-style loader. 24 | define(["exports"], function (exports) { 25 | root.sf2 = exports; 26 | return (root.sf2, factory(exports)); 27 | }); 28 | } else { 29 | // Plain old browser. 30 | root.sf2 = {}; 31 | factory(root.sf2); 32 | } 33 | }(this, function (exports) { 34 | "use strict"; 35 | 36 | var sf2 = exports; 37 | 38 | sf2.Parser = function (input, options) { 39 | options = options || {}; 40 | /** @type {ByteArray} */ 41 | this.input = input; 42 | /** @type {(Object|undefined)} */ 43 | this.parserOptions = options.parserOptions; 44 | 45 | /** @type {Array.} */ 46 | // this.presetHeader; 47 | /** @type {Array.} */ 48 | // this.presetZone; 49 | /** @type {Array.} */ 50 | // this.presetZoneModulator; 51 | /** @type {Array.} */ 52 | // this.presetZoneGenerator; 53 | /** @type {Array.} */ 54 | // this.instrument; 55 | /** @type {Array.} */ 56 | // this.instrumentZone; 57 | /** @type {Array.} */ 58 | // this.instrumentZoneModulator; 59 | /** @type {Array.} */ 60 | // this.instrumentZoneGenerator; 61 | /** @type {Array.} */ 62 | //this.sampleHeader; 63 | }; 64 | 65 | sf2.Parser.prototype.parse = function () { 66 | /** @type {sf2.Riff.Parser} */ 67 | var parser = new sf2.Riff.Parser(this.input, this.parserOptions); 68 | /** @type {?sf2.Riff.Chunk} */ 69 | var chunk; 70 | 71 | // parse RIFF chunk 72 | parser.parse(); 73 | if (parser.chunkList.length !== 1) { 74 | throw new Error('wrong chunk length'); 75 | } 76 | 77 | chunk = parser.getChunk(0); 78 | if (chunk === null) { 79 | throw new Error('chunk not found'); 80 | } 81 | 82 | this.parseRiffChunk(chunk); 83 | 84 | // TODO: Presumably this is here to reduce memory, 85 | // but does it really matter? Shouldn't we always be 86 | // referencing the underlying ArrayBuffer and thus 87 | // it will persist, in which case why delete it? 88 | this.input = null; 89 | }; 90 | 91 | /** 92 | * @param {sf2.Riff.Chunk} chunk 93 | */ 94 | sf2.Parser.prototype.parseRiffChunk = function (chunk) { 95 | /** @type {sf2.Riff.Parser} */ 96 | var parser; 97 | /** @type {ByteArray} */ 98 | var data = this.input; 99 | /** @type {number} */ 100 | var ip = chunk.offset; 101 | /** @type {string} */ 102 | var signature; 103 | 104 | // check parse target 105 | if (chunk.type !== 'RIFF') { 106 | throw new Error('invalid chunk type:' + chunk.type); 107 | } 108 | 109 | // check signature 110 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 111 | if (signature !== 'sfbk') { 112 | throw new Error('invalid signature:' + signature); 113 | } 114 | 115 | // read structure 116 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 117 | parser.parse(); 118 | if (parser.getNumberOfChunks() !== 3) { 119 | throw new Error('invalid sfbk structure'); 120 | } 121 | 122 | // INFO-list 123 | this.parseInfoList(/** @type {!sf2.Riff.Chunk} */(parser.getChunk(0))); 124 | 125 | // sdta-list 126 | this.parseSdtaList(/** @type {!sf2.Riff.Chunk} */(parser.getChunk(1))); 127 | 128 | // pdta-list 129 | this.parsePdtaList(/** @type {!sf2.Riff.Chunk} */(parser.getChunk(2))); 130 | }; 131 | 132 | /** 133 | * @param {sf2.Riff.Chunk} chunk 134 | */ 135 | sf2.Parser.prototype.parseInfoList = function (chunk) { 136 | /** @type {sf2.Riff.Parser} */ 137 | var parser; 138 | /** @type {ByteArray} */ 139 | var data = this.input; 140 | /** @type {number} */ 141 | var ip = chunk.offset; 142 | /** @type {string} */ 143 | var signature; 144 | 145 | // check parse target 146 | if (chunk.type !== 'LIST') { 147 | throw new Error('invalid chunk type:' + chunk.type); 148 | } 149 | 150 | // check signature 151 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 152 | if (signature !== 'INFO') { 153 | throw new Error('invalid signature:' + signature); 154 | } 155 | 156 | // read structure 157 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 158 | parser.parse(); 159 | }; 160 | 161 | /** 162 | * @param {sf2.Riff.Chunk} chunk 163 | */ 164 | sf2.Parser.prototype.parseSdtaList = function (chunk) { 165 | /** @type {sf2.Riff.Parser} */ 166 | var parser; 167 | /** @type {ByteArray} */ 168 | var data = this.input; 169 | /** @type {number} */ 170 | var ip = chunk.offset; 171 | /** @type {string} */ 172 | var signature; 173 | 174 | // check parse target 175 | if (chunk.type !== 'LIST') { 176 | throw new Error('invalid chunk type:' + chunk.type); 177 | } 178 | 179 | // check signature 180 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 181 | if (signature !== 'sdta') { 182 | throw new Error('invalid signature:' + signature); 183 | } 184 | 185 | // read structure 186 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 187 | parser.parse(); 188 | if (parser.chunkList.length !== 1) { 189 | throw new Error('TODO'); 190 | } 191 | this.samplingData = 192 | /** @type {{type: string, size: number, offset: number}} */ 193 | (parser.getChunk(0)); 194 | }; 195 | 196 | /** 197 | * @param {sf2.Riff.Chunk} chunk 198 | */ 199 | sf2.Parser.prototype.parsePdtaList = function (chunk) { 200 | /** @type {sf2.Riff.Parser} */ 201 | var parser; 202 | /** @type {ByteArray} */ 203 | var data = this.input; 204 | /** @type {number} */ 205 | var ip = chunk.offset; 206 | /** @type {string} */ 207 | var signature; 208 | 209 | // check parse target 210 | if (chunk.type !== 'LIST') { 211 | throw new Error('invalid chunk type:' + chunk.type); 212 | } 213 | 214 | // check signature 215 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 216 | if (signature !== 'pdta') { 217 | throw new Error('invalid signature:' + signature); 218 | } 219 | 220 | // read structure 221 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 222 | parser.parse(); 223 | 224 | // check number of chunks 225 | if (parser.getNumberOfChunks() !== 9) { 226 | throw new Error('invalid pdta chunk'); 227 | } 228 | 229 | this.parsePhdr(/** @type {sf2.Riff.Chunk} */(parser.getChunk(0))); 230 | this.parsePbag(/** @type {sf2.Riff.Chunk} */(parser.getChunk(1))); 231 | this.parsePmod(/** @type {sf2.Riff.Chunk} */(parser.getChunk(2))); 232 | this.parsePgen(/** @type {sf2.Riff.Chunk} */(parser.getChunk(3))); 233 | this.parseInst(/** @type {sf2.Riff.Chunk} */(parser.getChunk(4))); 234 | this.parseIbag(/** @type {sf2.Riff.Chunk} */(parser.getChunk(5))); 235 | this.parseImod(/** @type {sf2.Riff.Chunk} */(parser.getChunk(6))); 236 | this.parseIgen(/** @type {sf2.Riff.Chunk} */(parser.getChunk(7))); 237 | this.parseShdr(/** @type {sf2.Riff.Chunk} */(parser.getChunk(8))); 238 | }; 239 | 240 | /** 241 | * @param {sf2.Riff.Chunk} chunk 242 | */ 243 | sf2.Parser.prototype.parsePhdr = function (chunk) { 244 | /** @type {ByteArray} */ 245 | var data = this.input; 246 | /** @type {number} */ 247 | var ip = chunk.offset; 248 | /** @type {Array.} */ 249 | var presetHeader = this.presetHeader = []; 250 | /** @type {number} */ 251 | var size = chunk.offset + chunk.size; 252 | 253 | // check parse target 254 | if (chunk.type !== 'phdr') { 255 | throw new Error('invalid chunk type:' + chunk.type); 256 | } 257 | 258 | while (ip < size) { 259 | presetHeader.push({ 260 | presetName: String.fromCharCode.apply(null, data.subarray(ip, ip += 20)), 261 | preset: data[ip++] | (data[ip++] << 8), 262 | bank: data[ip++] | (data[ip++] << 8), 263 | presetBagIndex: data[ip++] | (data[ip++] << 8), 264 | library: (data[ip++] | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24)) >>> 0, 265 | genre: (data[ip++] | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24)) >>> 0, 266 | morphology: (data[ip++] | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24)) >>> 0 267 | }); 268 | } 269 | }; 270 | 271 | /** 272 | * @param {sf2.Riff.Chunk} chunk 273 | */ 274 | sf2.Parser.prototype.parsePbag = function (chunk) { 275 | /** @type {ByteArray} */ 276 | var data = this.input; 277 | /** @type {number} */ 278 | var ip = chunk.offset; 279 | /** @type {Array.} */ 280 | var presetZone = this.presetZone = []; 281 | /** @type {number} */ 282 | var size = chunk.offset + chunk.size; 283 | 284 | // check parse target 285 | if (chunk.type !== 'pbag') { 286 | throw new Error('invalid chunk type:' + chunk.type); 287 | } 288 | 289 | while (ip < size) { 290 | presetZone.push({ 291 | presetGeneratorIndex: data[ip++] | (data[ip++] << 8), 292 | presetModulatorIndex: data[ip++] | (data[ip++] << 8) 293 | }); 294 | } 295 | }; 296 | 297 | /** 298 | * @param {sf2.Riff.Chunk} chunk 299 | */ 300 | sf2.Parser.prototype.parsePmod = function (chunk) { 301 | // check parse target 302 | if (chunk.type !== 'pmod') { 303 | throw new Error('invalid chunk type:' + chunk.type); 304 | } 305 | 306 | this.presetZoneModulator = this.parseModulator(chunk); 307 | }; 308 | 309 | /** 310 | * @param {sf2.Riff.Chunk} chunk 311 | */ 312 | sf2.Parser.prototype.parsePgen = function (chunk) { 313 | // check parse target 314 | if (chunk.type !== 'pgen') { 315 | throw new Error('invalid chunk type:' + chunk.type); 316 | } 317 | this.presetZoneGenerator = this.parseGenerator(chunk); 318 | }; 319 | 320 | /** 321 | * @param {sf2.Riff.Chunk} chunk 322 | */ 323 | sf2.Parser.prototype.parseInst = function (chunk) { 324 | /** @type {ByteArray} */ 325 | var data = this.input; 326 | /** @type {number} */ 327 | var ip = chunk.offset; 328 | /** @type {Array.} */ 329 | var instrument = this.instrument = []; 330 | /** @type {number} */ 331 | var size = chunk.offset + chunk.size; 332 | 333 | // check parse target 334 | if (chunk.type !== 'inst') { 335 | throw new Error('invalid chunk type:' + chunk.type); 336 | } 337 | 338 | while (ip < size) { 339 | instrument.push({ 340 | instrumentName: String.fromCharCode.apply(null, data.subarray(ip, ip += 20)), 341 | instrumentBagIndex: data[ip++] | (data[ip++] << 8) 342 | }); 343 | } 344 | }; 345 | 346 | /** 347 | * @param {sf2.Riff.Chunk} chunk 348 | */ 349 | sf2.Parser.prototype.parseIbag = function (chunk) { 350 | /** @type {ByteArray} */ 351 | var data = this.input; 352 | /** @type {number} */ 353 | var ip = chunk.offset; 354 | /** @type {Array.} */ 355 | var instrumentZone = this.instrumentZone = []; 356 | /** @type {number} */ 357 | var size = chunk.offset + chunk.size; 358 | 359 | // check parse target 360 | if (chunk.type !== 'ibag') { 361 | throw new Error('invalid chunk type:' + chunk.type); 362 | } 363 | 364 | 365 | while (ip < size) { 366 | instrumentZone.push({ 367 | instrumentGeneratorIndex: data[ip++] | (data[ip++] << 8), 368 | instrumentModulatorIndex: data[ip++] | (data[ip++] << 8) 369 | }); 370 | } 371 | }; 372 | 373 | /** 374 | * @param {sf2.Riff.Chunk} chunk 375 | */ 376 | sf2.Parser.prototype.parseImod = function (chunk) { 377 | // check parse target 378 | if (chunk.type !== 'imod') { 379 | throw new Error('invalid chunk type:' + chunk.type); 380 | } 381 | 382 | this.instrumentZoneModulator = this.parseModulator(chunk); 383 | }; 384 | 385 | 386 | /** 387 | * @param {sf2.Riff.Chunk} chunk 388 | */ 389 | sf2.Parser.prototype.parseIgen = function (chunk) { 390 | // check parse target 391 | if (chunk.type !== 'igen') { 392 | throw new Error('invalid chunk type:' + chunk.type); 393 | } 394 | 395 | this.instrumentZoneGenerator = this.parseGenerator(chunk); 396 | }; 397 | 398 | /** 399 | * @param {sf2.Riff.Chunk} chunk 400 | */ 401 | sf2.Parser.prototype.parseShdr = function (chunk) { 402 | /** @type {ByteArray} */ 403 | var data = this.input; 404 | /** @type {number} */ 405 | var ip = chunk.offset; 406 | /** @type {Array.} */ 407 | var samples = this.sample = []; 408 | /** @type {Array.} */ 409 | var sampleHeader = this.sampleHeader = []; 410 | /** @type {number} */ 411 | var size = chunk.offset + chunk.size; 412 | /** @type {string} */ 413 | var sampleName; 414 | /** @type {number} */ 415 | var start; 416 | /** @type {number} */ 417 | var end; 418 | /** @type {number} */ 419 | var startLoop; 420 | /** @type {number} */ 421 | var endLoop; 422 | /** @type {number} */ 423 | var sampleRate; 424 | /** @type {number} */ 425 | var originalPitch; 426 | /** @type {number} */ 427 | var pitchCorrection; 428 | /** @type {number} */ 429 | var sampleLink; 430 | /** @type {number} */ 431 | var sampleType; 432 | 433 | // check parse target 434 | if (chunk.type !== 'shdr') { 435 | throw new Error('invalid chunk type:' + chunk.type); 436 | } 437 | 438 | while (ip < size) { 439 | sampleName = String.fromCharCode.apply(null, data.subarray(ip, ip += 20)); 440 | start = ( 441 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 442 | ) >>> 0; 443 | end = ( 444 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 445 | ) >>> 0; 446 | startLoop = ( 447 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 448 | ) >>> 0; 449 | endLoop = ( 450 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 451 | ) >>> 0; 452 | sampleRate = ( 453 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 454 | ) >>> 0; 455 | originalPitch = data[ip++]; 456 | pitchCorrection = (data[ip++] << 24) >> 24; 457 | sampleLink = data[ip++] | (data[ip++] << 8); 458 | sampleType = data[ip++] | (data[ip++] << 8); 459 | 460 | var sample = new Int16Array(new Uint8Array(data.subarray( 461 | this.samplingData.offset + start * 2, 462 | this.samplingData.offset + end * 2 463 | )).buffer); 464 | 465 | startLoop -= start; 466 | endLoop -= start; 467 | 468 | if (sampleRate > 0) { 469 | var adjust = this.adjustSampleData(sample, sampleRate); 470 | sample = adjust.sample; 471 | sampleRate *= adjust.multiply; 472 | startLoop *= adjust.multiply; 473 | endLoop *= adjust.multiply; 474 | } 475 | 476 | samples.push(sample); 477 | 478 | sampleHeader.push({ 479 | sampleName: sampleName, 480 | /* 481 | start: start, 482 | end: end, 483 | */ 484 | startLoop: startLoop, 485 | endLoop: endLoop, 486 | sampleRate: sampleRate, 487 | originalPitch: originalPitch, 488 | pitchCorrection: pitchCorrection, 489 | sampleLink: sampleLink, 490 | sampleType: sampleType 491 | }); 492 | } 493 | }; 494 | 495 | // TODO: This function is highly suspect, and should be removed 496 | // it does no interpolation of the sample data, and always forces 497 | // a sample rate of 22050 or higher. Why? 498 | sf2.Parser.prototype.adjustSampleData = function (sample, sampleRate) { 499 | /** @type {Int16Array} */ 500 | var newSample; 501 | /** @type {number} */ 502 | var i; 503 | /** @type {number} */ 504 | var il; 505 | /** @type {number} */ 506 | var j; 507 | /** @type {number} */ 508 | var multiply = 1; 509 | 510 | // buffer 511 | while (sampleRate < 22050) { 512 | newSample = new Int16Array(sample.length * 2); 513 | for (i = j = 0, il = sample.length; i < il; ++i) { 514 | newSample[j++] = sample[i]; 515 | newSample[j++] = sample[i]; 516 | } 517 | sample = newSample; 518 | multiply *= 2; 519 | sampleRate *= 2; 520 | } 521 | 522 | return { 523 | sample: sample, 524 | multiply: multiply 525 | }; 526 | }; 527 | 528 | /** 529 | * @param {sf2.Riff.Chunk} chunk 530 | * @return {Array.} 531 | */ 532 | sf2.Parser.prototype.parseModulator = function (chunk) { 533 | /** @type {ByteArray} */ 534 | var data = this.input; 535 | /** @type {number} */ 536 | var ip = chunk.offset; 537 | /** @type {number} */ 538 | var size = chunk.offset + chunk.size; 539 | /** @type {number} */ 540 | var code; 541 | /** @type {string} */ 542 | var key; 543 | /** @type {Array.} */ 544 | var output = []; 545 | 546 | while (ip < size) { 547 | // Src Oper 548 | // TODO 549 | ip += 2; 550 | 551 | // Dest Oper 552 | code = data[ip++] | (data[ip++] << 8); 553 | key = sf2.Parser.GeneratorEnumeratorTable[code]; 554 | if (key === undefined) { 555 | // Amount 556 | output.push({ 557 | type: key, 558 | value: { 559 | code: code, 560 | amount: data[ip] | (data[ip+1] << 8) << 16 >> 16, 561 | lo: data[ip++], 562 | hi: data[ip++] 563 | } 564 | }); 565 | } else { 566 | // Amount 567 | switch (key) { 568 | case 'keyRange': /* FALLTHROUGH */ 569 | case 'velRange': /* FALLTHROUGH */ 570 | case 'keynum': /* FALLTHROUGH */ 571 | case 'velocity': 572 | output.push({ 573 | type: key, 574 | value: { 575 | lo: data[ip++], 576 | hi: data[ip++] 577 | } 578 | }); 579 | break; 580 | default: 581 | output.push({ 582 | type: key, 583 | value: { 584 | amount: data[ip++] | (data[ip++] << 8) << 16 >> 16 585 | } 586 | }); 587 | break; 588 | } 589 | } 590 | 591 | // AmtSrcOper 592 | // TODO 593 | ip += 2; 594 | 595 | // Trans Oper 596 | // TODO 597 | ip += 2; 598 | } 599 | 600 | return output; 601 | }; 602 | 603 | /** 604 | * @param {sf2.Riff.Chunk} chunk 605 | * @return {Array.} 606 | */ 607 | sf2.Parser.prototype.parseGenerator = function (chunk) { 608 | /** @type {ByteArray} */ 609 | var data = this.input; 610 | /** @type {number} */ 611 | var ip = chunk.offset; 612 | /** @type {number} */ 613 | var size = chunk.offset + chunk.size; 614 | /** @type {number} */ 615 | var code; 616 | /** @type {string} */ 617 | var key; 618 | /** @type {Array.} */ 619 | var output = []; 620 | 621 | while (ip < size) { 622 | code = data[ip++] | (data[ip++] << 8); 623 | key = sf2.Parser.GeneratorEnumeratorTable[code]; 624 | if (key === undefined) { 625 | output.push({ 626 | type: key, 627 | value: { 628 | code: code, 629 | amount: data[ip] | (data[ip+1] << 8) << 16 >> 16, 630 | lo: data[ip++], 631 | hi: data[ip++] 632 | } 633 | }); 634 | continue; 635 | } 636 | 637 | switch (key) { 638 | case 'keynum': /* FALLTHROUGH */ 639 | case 'keyRange': /* FALLTHROUGH */ 640 | case 'velRange': /* FALLTHROUGH */ 641 | case 'velocity': 642 | output.push({ 643 | type: key, 644 | value: { 645 | lo: data[ip++], 646 | hi: data[ip++] 647 | } 648 | }); 649 | break; 650 | default: 651 | output.push({ 652 | type: key, 653 | value: { 654 | amount: data[ip++] | (data[ip++] << 8) << 16 >> 16 655 | } 656 | }); 657 | break; 658 | } 659 | } 660 | 661 | return output; 662 | }; 663 | 664 | sf2.Parser.prototype.getInstruments = function () { 665 | /** @type {Array.} */ 666 | var instrument = this.instrument; 667 | /** @type {Array.} */ 668 | var zone = this.instrumentZone; 669 | /** @type {Array.} */ 670 | var output = []; 671 | /** @type {number} */ 672 | var bagIndex; 673 | /** @type {number} */ 674 | var bagIndexEnd; 675 | /** @type {Array.} */ 676 | var zoneInfo; 677 | /** @type {{generator: Object, generatorInfo: Array.}} */ 678 | var instrumentGenerator; 679 | /** @type {{modulator: Object, modulatorInfo: Array.}} */ 680 | var instrumentModulator; 681 | /** @type {number} */ 682 | var i; 683 | /** @type {number} */ 684 | var il; 685 | /** @type {number} */ 686 | var j; 687 | /** @type {number} */ 688 | var jl; 689 | 690 | // instrument -> instrument bag -> generator / modulator 691 | for (i = 0, il = instrument.length; i < il; ++i) { 692 | bagIndex = instrument[i].instrumentBagIndex; 693 | bagIndexEnd = instrument[i+1] ? instrument[i+1].instrumentBagIndex : zone.length; 694 | zoneInfo = []; 695 | 696 | // instrument bag 697 | for (j = bagIndex, jl = bagIndexEnd; j < jl; ++j) { 698 | instrumentGenerator = this.createInstrumentGenerator_(zone, j); 699 | instrumentModulator = this.createInstrumentModulator_(zone, j); 700 | 701 | zoneInfo.push({ 702 | generator: instrumentGenerator.generator, 703 | generatorSequence: instrumentGenerator.generatorInfo, 704 | modulator: instrumentModulator.modulator, 705 | modulatorSequence: instrumentModulator.modulatorInfo 706 | }); 707 | } 708 | 709 | output.push({ 710 | name: instrument[i].instrumentName, 711 | info: zoneInfo 712 | }); 713 | } 714 | 715 | return output; 716 | }; 717 | 718 | sf2.Parser.prototype.getPresets = function () { 719 | /** @type {Array.} */ 720 | var preset = this.presetHeader; 721 | /** @type {Array.} */ 722 | var zone = this.presetZone; 723 | /** @type {Array.} */ 724 | var output = []; 725 | /** @type {number} */ 726 | var bagIndex; 727 | /** @type {number} */ 728 | var bagIndexEnd; 729 | /** @type {Array.} */ 730 | var zoneInfo; 731 | /** @type {number} */ 732 | var instrument; 733 | /** @type {{generator: Object, generatorInfo: Array.}} */ 734 | var presetGenerator; 735 | /** @type {{modulator: Object, modulatorInfo: Array.}} */ 736 | var presetModulator; 737 | /** @type {number} */ 738 | var i; 739 | /** @type {number} */ 740 | var il; 741 | /** @type {number} */ 742 | var j; 743 | /** @type {number} */ 744 | var jl; 745 | 746 | // preset -> preset bag -> generator / modulator 747 | for (i = 0, il = preset.length; i < il; ++i) { 748 | bagIndex = preset[i].presetBagIndex; 749 | bagIndexEnd = preset[i+1] ? preset[i+1].presetBagIndex : zone.length; 750 | zoneInfo = []; 751 | 752 | // preset bag 753 | for (j = bagIndex, jl = bagIndexEnd; j < jl; ++j) { 754 | presetGenerator = this.createPresetGenerator_(zone, j); 755 | presetModulator = this.createPresetModulator_(zone, j); 756 | 757 | zoneInfo.push({ 758 | generator: presetGenerator.generator, 759 | generatorSequence: presetGenerator.generatorInfo, 760 | modulator: presetModulator.modulator, 761 | modulatorSequence: presetModulator.modulatorInfo 762 | }); 763 | 764 | instrument = 765 | presetGenerator.generator.instrument !== undefined ? 766 | presetGenerator.generator.instrument.amount : 767 | presetModulator.modulator.instrument !== undefined ? 768 | presetModulator.modulator.instrument.amount : 769 | null; 770 | } 771 | 772 | output.push({ 773 | name: preset[i].presetName, 774 | info: zoneInfo, 775 | header: preset[i], 776 | instrument: instrument 777 | }); 778 | } 779 | 780 | return output; 781 | }; 782 | 783 | /** 784 | * @param {Array.} zone 785 | * @param {number} index 786 | * @returns {{generator: Object, generatorInfo: Array.}} 787 | * @private 788 | */ 789 | sf2.Parser.prototype.createInstrumentGenerator_ = function (zone, index) { 790 | var modgen = this.createBagModGen_( 791 | zone, 792 | zone[index].instrumentGeneratorIndex, 793 | zone[index+1] ? zone[index+1].instrumentGeneratorIndex: this.instrumentZoneGenerator.length, 794 | this.instrumentZoneGenerator 795 | ); 796 | 797 | return { 798 | generator: modgen.modgen, 799 | generatorInfo: modgen.modgenInfo 800 | }; 801 | }; 802 | 803 | /** 804 | * @param {Array.} zone 805 | * @param {number} index 806 | * @returns {{modulator: Object, modulatorInfo: Array.}} 807 | * @private 808 | */ 809 | sf2.Parser.prototype.createInstrumentModulator_ = function (zone, index) { 810 | var modgen = this.createBagModGen_( 811 | zone, 812 | zone[index].presetModulatorIndex, 813 | zone[index+1] ? zone[index+1].instrumentModulatorIndex: this.instrumentZoneModulator.length, 814 | this.instrumentZoneModulator 815 | ); 816 | 817 | return { 818 | modulator: modgen.modgen, 819 | modulatorInfo: modgen.modgenInfo 820 | }; 821 | }; 822 | 823 | /** 824 | * @param {Array.} zone 825 | * @param {number} index 826 | * @returns {{generator: Object, generatorInfo: Array.}} 827 | * @private 828 | */ 829 | sf2.Parser.prototype.createPresetGenerator_ = function (zone, index) { 830 | var modgen = this.createBagModGen_( 831 | zone, 832 | zone[index].presetGeneratorIndex, 833 | zone[index+1] ? zone[index+1].presetGeneratorIndex : this.presetZoneGenerator.length, 834 | this.presetZoneGenerator 835 | ); 836 | 837 | return { 838 | generator: modgen.modgen, 839 | generatorInfo: modgen.modgenInfo 840 | }; 841 | }; 842 | 843 | /** 844 | * @param {Array.} zone 845 | * @param {number} index 846 | * @returns {{modulator: Object, modulatorInfo: Array.}} 847 | * @private 848 | */ 849 | sf2.Parser.prototype.createPresetModulator_ = function (zone, index) { 850 | /** @type {{modgen: Object, modgenInfo: Array.}} */ 851 | var modgen = this.createBagModGen_( 852 | zone, 853 | zone[index].presetModulatorIndex, 854 | zone[index+1] ? zone[index+1].presetModulatorIndex : this.presetZoneModulator.length, 855 | this.presetZoneModulator 856 | ); 857 | 858 | return { 859 | modulator: modgen.modgen, 860 | modulatorInfo: modgen.modgenInfo 861 | }; 862 | }; 863 | 864 | /** 865 | * @param {Array.} zone 866 | * @param {number} indexStart 867 | * @param {number} indexEnd 868 | * @param zoneModGen 869 | * @returns {{modgen: Object, modgenInfo: Array.}} 870 | * @private 871 | */ 872 | sf2.Parser.prototype.createBagModGen_ = function (zone, indexStart, indexEnd, zoneModGen) { 873 | /** @type {Array.} */ 874 | var modgenInfo = []; 875 | /** @type {Object} */ 876 | var modgen = { 877 | unknown: [], 878 | 'keyRange': { 879 | hi: 127, 880 | lo: 0 881 | } 882 | }; // TODO 883 | /** @type {Object} */ 884 | var info; 885 | /** @type {number} */ 886 | var i; 887 | /** @type {number} */ 888 | var il; 889 | 890 | for (i = indexStart, il = indexEnd; i < il; ++i) { 891 | info = zoneModGen[i]; 892 | modgenInfo.push(info); 893 | 894 | if (info.type === 'unknown') { 895 | modgen.unknown.push(info.value); 896 | } else { 897 | modgen[info.type] = info.value; 898 | } 899 | } 900 | 901 | return { 902 | modgen: modgen, 903 | modgenInfo: modgenInfo 904 | }; 905 | }; 906 | 907 | 908 | /** 909 | * @type {Array.} 910 | * @const 911 | */ 912 | sf2.Parser.GeneratorEnumeratorTable = [ 913 | 'startAddrsOffset', 914 | 'endAddrsOffset', 915 | 'startloopAddrsOffset', 916 | 'endloopAddrsOffset', 917 | 'startAddrsCoarseOffset', 918 | 'modLfoToPitch', 919 | 'vibLfoToPitch', 920 | 'modEnvToPitch', 921 | 'initialFilterFc', 922 | 'initialFilterQ', 923 | 'modLfoToFilterFc', 924 | 'modEnvToFilterFc', 925 | 'endAddrsCoarseOffset', 926 | 'modLfoToVolume', 927 | undefined, // 14 928 | 'chorusEffectsSend', 929 | 'reverbEffectsSend', 930 | 'pan', 931 | undefined, 932 | undefined, 933 | undefined, // 18,19,20 934 | 'delayModLFO', 935 | 'freqModLFO', 936 | 'delayVibLFO', 937 | 'freqVibLFO', 938 | 'delayModEnv', 939 | 'attackModEnv', 940 | 'holdModEnv', 941 | 'decayModEnv', 942 | 'sustainModEnv', 943 | 'releaseModEnv', 944 | 'keynumToModEnvHold', 945 | 'keynumToModEnvDecay', 946 | 'delayVolEnv', 947 | 'attackVolEnv', 948 | 'holdVolEnv', 949 | 'decayVolEnv', 950 | 'sustainVolEnv', 951 | 'releaseVolEnv', 952 | 'keynumToVolEnvHold', 953 | 'keynumToVolEnvDecay', 954 | 'instrument', 955 | undefined, // 42 956 | 'keyRange', 957 | 'velRange', 958 | 'startloopAddrsCoarseOffset', 959 | 'keynum', 960 | 'velocity', 961 | 'initialAttenuation', 962 | undefined, // 49 963 | 'endloopAddrsCoarseOffset', 964 | 'coarseTune', 965 | 'fineTune', 966 | 'sampleID', 967 | 'sampleModes', 968 | undefined, // 55 969 | 'scaleTuning', 970 | 'exclusiveClass', 971 | 'overridingRootKey' 972 | ]; 973 | 974 | 975 | sf2.Riff = {}; 976 | 977 | sf2.Riff.Parser = function (input, options) { 978 | options = options || {}; 979 | /** @type {ByteArray} */ 980 | this.input = input; 981 | /** @type {number} */ 982 | this.ip = options.index || 0; 983 | /** @type {number} */ 984 | this.length = options.length || input.length - this.ip; 985 | /** @type {Array.} */ 986 | // this.chunkList; 987 | /** @type {number} */ 988 | this.offset = this.ip; 989 | /** @type {boolean} */ 990 | this.padding = options.padding !== undefined ? options.padding : true; 991 | /** @type {boolean} */ 992 | this.bigEndian = options.bigEndian !== undefined ? options.bigEndian : false; 993 | }; 994 | 995 | /** 996 | * @param {string} type 997 | * @param {number} size 998 | * @param {number} offset 999 | * @constructor 1000 | */ 1001 | sf2.Riff.Chunk = function (type, size, offset) { 1002 | /** @type {string} */ 1003 | this.type = type; 1004 | /** @type {number} */ 1005 | this.size = size; 1006 | /** @type {number} */ 1007 | this.offset = offset; 1008 | }; 1009 | 1010 | sf2.Riff.Parser.prototype.parse = function () { 1011 | /** @type {number} */ 1012 | var length = this.length + this.offset; 1013 | 1014 | this.chunkList = []; 1015 | 1016 | while (this.ip < length) { 1017 | this.parseChunk(); 1018 | } 1019 | }; 1020 | 1021 | sf2.Riff.Parser.prototype.parseChunk = function () { 1022 | /** @type {ByteArray} */ 1023 | var input = this.input; 1024 | /** @type {number} */ 1025 | var ip = this.ip; 1026 | /** @type {number} */ 1027 | var size; 1028 | 1029 | this.chunkList.push(new sf2.Riff.Chunk( 1030 | String.fromCharCode(input[ip++], input[ip++], input[ip++], input[ip++]), 1031 | (size = this.bigEndian ? 1032 | ((input[ip++] << 24) | (input[ip++] << 16) | 1033 | (input[ip++] << 8) | (input[ip++] )) >>> 0 : 1034 | ((input[ip++] ) | (input[ip++] << 8) | 1035 | (input[ip++] << 16) | (input[ip++] << 24)) >>> 0 1036 | ), 1037 | ip 1038 | )); 1039 | 1040 | ip += size; 1041 | 1042 | // padding 1043 | if (this.padding && ((ip - this.offset) & 1) === 1) { 1044 | ip++; 1045 | } 1046 | 1047 | this.ip = ip; 1048 | }; 1049 | 1050 | /** 1051 | * @param {number} index chunk index. 1052 | * @return {?sf2.Riff.Chunk} 1053 | */ 1054 | sf2.Riff.Parser.prototype.getChunk = function (index) { 1055 | /** @type {sf2.Riff.Chunk} */ 1056 | var chunk = this.chunkList[index]; 1057 | 1058 | if (chunk === undefined) { 1059 | return null; 1060 | } 1061 | 1062 | return chunk; 1063 | }; 1064 | 1065 | /** 1066 | * @return {number} 1067 | */ 1068 | sf2.Riff.Parser.prototype.getNumberOfChunks = function () { 1069 | return this.chunkList.length; 1070 | }; 1071 | 1072 | 1073 | return sf2; 1074 | })); 1075 | -------------------------------------------------------------------------------- /dist/sf2-parser-all.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"object"==typeof exports?(a.sf2=exports,b(exports)):"function"==typeof define&&define.amd?define(["exports"],function(c){return a.sf2=c,a.sf2,b(c)}):(a.sf2={},b(a.sf2))}(this,function(a){"use strict";var b=a;return b.Parser=function(a,b){b=b||{},this.input=a,this.parserOptions=b.parserOptions},b.Parser.prototype.parse=function(){var a,c=new b.Riff.Parser(this.input,this.parserOptions);if(c.parse(),1!==c.chunkList.length)throw new Error("wrong chunk length");if(a=c.getChunk(0),null===a)throw new Error("chunk not found");this.parseRiffChunk(a),this.input=null},b.Parser.prototype.parseRiffChunk=function(a){var c,d,e=this.input,f=a.offset;if("RIFF"!==a.type)throw new Error("invalid chunk type:"+a.type);if(d=String.fromCharCode(e[f++],e[f++],e[f++],e[f++]),"sfbk"!==d)throw new Error("invalid signature:"+d);if(c=new b.Riff.Parser(e,{index:f,length:a.size-4}),c.parse(),3!==c.getNumberOfChunks())throw new Error("invalid sfbk structure");this.parseInfoList(c.getChunk(0)),this.parseSdtaList(c.getChunk(1)),this.parsePdtaList(c.getChunk(2))},b.Parser.prototype.parseInfoList=function(a){var c,d,e=this.input,f=a.offset;if("LIST"!==a.type)throw new Error("invalid chunk type:"+a.type);if(d=String.fromCharCode(e[f++],e[f++],e[f++],e[f++]),"INFO"!==d)throw new Error("invalid signature:"+d);c=new b.Riff.Parser(e,{index:f,length:a.size-4}),c.parse()},b.Parser.prototype.parseSdtaList=function(a){var c,d,e=this.input,f=a.offset;if("LIST"!==a.type)throw new Error("invalid chunk type:"+a.type);if(d=String.fromCharCode(e[f++],e[f++],e[f++],e[f++]),"sdta"!==d)throw new Error("invalid signature:"+d);if(c=new b.Riff.Parser(e,{index:f,length:a.size-4}),c.parse(),1!==c.chunkList.length)throw new Error("TODO");this.samplingData=c.getChunk(0)},b.Parser.prototype.parsePdtaList=function(a){var c,d,e=this.input,f=a.offset;if("LIST"!==a.type)throw new Error("invalid chunk type:"+a.type);if(d=String.fromCharCode(e[f++],e[f++],e[f++],e[f++]),"pdta"!==d)throw new Error("invalid signature:"+d);if(c=new b.Riff.Parser(e,{index:f,length:a.size-4}),c.parse(),9!==c.getNumberOfChunks())throw new Error("invalid pdta chunk");this.parsePhdr(c.getChunk(0)),this.parsePbag(c.getChunk(1)),this.parsePmod(c.getChunk(2)),this.parsePgen(c.getChunk(3)),this.parseInst(c.getChunk(4)),this.parseIbag(c.getChunk(5)),this.parseImod(c.getChunk(6)),this.parseIgen(c.getChunk(7)),this.parseShdr(c.getChunk(8))},b.Parser.prototype.parsePhdr=function(a){var b=this.input,c=a.offset,d=this.presetHeader=[],e=a.offset+a.size;if("phdr"!==a.type)throw new Error("invalid chunk type:"+a.type);for(;e>c;)d.push({presetName:String.fromCharCode.apply(null,b.subarray(c,c+=20)),preset:b[c++]|b[c++]<<8,bank:b[c++]|b[c++]<<8,presetBagIndex:b[c++]|b[c++]<<8,library:(b[c++]|b[c++]<<8|b[c++]<<16|b[c++]<<24)>>>0,genre:(b[c++]|b[c++]<<8|b[c++]<<16|b[c++]<<24)>>>0,morphology:(b[c++]|b[c++]<<8|b[c++]<<16|b[c++]<<24)>>>0})},b.Parser.prototype.parsePbag=function(a){var b=this.input,c=a.offset,d=this.presetZone=[],e=a.offset+a.size;if("pbag"!==a.type)throw new Error("invalid chunk type:"+a.type);for(;e>c;)d.push({presetGeneratorIndex:b[c++]|b[c++]<<8,presetModulatorIndex:b[c++]|b[c++]<<8})},b.Parser.prototype.parsePmod=function(a){if("pmod"!==a.type)throw new Error("invalid chunk type:"+a.type);this.presetZoneModulator=this.parseModulator(a)},b.Parser.prototype.parsePgen=function(a){if("pgen"!==a.type)throw new Error("invalid chunk type:"+a.type);this.presetZoneGenerator=this.parseGenerator(a)},b.Parser.prototype.parseInst=function(a){var b=this.input,c=a.offset,d=this.instrument=[],e=a.offset+a.size;if("inst"!==a.type)throw new Error("invalid chunk type:"+a.type);for(;e>c;)d.push({instrumentName:String.fromCharCode.apply(null,b.subarray(c,c+=20)),instrumentBagIndex:b[c++]|b[c++]<<8})},b.Parser.prototype.parseIbag=function(a){var b=this.input,c=a.offset,d=this.instrumentZone=[],e=a.offset+a.size;if("ibag"!==a.type)throw new Error("invalid chunk type:"+a.type);for(;e>c;)d.push({instrumentGeneratorIndex:b[c++]|b[c++]<<8,instrumentModulatorIndex:b[c++]|b[c++]<<8})},b.Parser.prototype.parseImod=function(a){if("imod"!==a.type)throw new Error("invalid chunk type:"+a.type);this.instrumentZoneModulator=this.parseModulator(a)},b.Parser.prototype.parseIgen=function(a){if("igen"!==a.type)throw new Error("invalid chunk type:"+a.type);this.instrumentZoneGenerator=this.parseGenerator(a)},b.Parser.prototype.parseShdr=function(a){var b,c,d,e,f,g,h,i,j,k,l=this.input,m=a.offset,n=this.sample=[],o=this.sampleHeader=[],p=a.offset+a.size;if("shdr"!==a.type)throw new Error("invalid chunk type:"+a.type);for(;p>m;){b=String.fromCharCode.apply(null,l.subarray(m,m+=20)),c=(l[m++]<<0|l[m++]<<8|l[m++]<<16|l[m++]<<24)>>>0,d=(l[m++]<<0|l[m++]<<8|l[m++]<<16|l[m++]<<24)>>>0,e=(l[m++]<<0|l[m++]<<8|l[m++]<<16|l[m++]<<24)>>>0,f=(l[m++]<<0|l[m++]<<8|l[m++]<<16|l[m++]<<24)>>>0,g=(l[m++]<<0|l[m++]<<8|l[m++]<<16|l[m++]<<24)>>>0,h=l[m++],i=l[m++]<<24>>24,j=l[m++]|l[m++]<<8,k=l[m++]|l[m++]<<8;var q=new Int16Array(new Uint8Array(l.subarray(this.samplingData.offset+2*c,this.samplingData.offset+2*d)).buffer);if(e-=c,f-=c,g>0){var r=this.adjustSampleData(q,g);q=r.sample,g*=r.multiply,e*=r.multiply,f*=r.multiply}n.push(q),o.push({sampleName:b,startLoop:e,endLoop:f,sampleRate:g,originalPitch:h,pitchCorrection:i,sampleLink:j,sampleType:k})}},b.Parser.prototype.adjustSampleData=function(a,b){for(var c,d,e,f,g=1;22050>b;){for(c=new Int16Array(2*a.length),d=f=0,e=a.length;e>d;++d)c[f++]=a[d],c[f++]=a[d];a=c,g*=2,b*=2}return{sample:a,multiply:g}},b.Parser.prototype.parseModulator=function(a){for(var c,d,e=this.input,f=a.offset,g=a.offset+a.size,h=[];g>f;){if(f+=2,c=e[f++]|e[f++]<<8,d=b.Parser.GeneratorEnumeratorTable[c],void 0===d)h.push({type:d,value:{code:c,amount:e[f]|e[f+1]<<8<<16>>16,lo:e[f++],hi:e[f++]}});else switch(d){case"keyRange":case"velRange":case"keynum":case"velocity":h.push({type:d,value:{lo:e[f++],hi:e[f++]}});break;default:h.push({type:d,value:{amount:e[f++]|e[f++]<<8<<16>>16}})}f+=2,f+=2}return h},b.Parser.prototype.parseGenerator=function(a){for(var c,d,e=this.input,f=a.offset,g=a.offset+a.size,h=[];g>f;)if(c=e[f++]|e[f++]<<8,d=b.Parser.GeneratorEnumeratorTable[c],void 0!==d)switch(d){case"keynum":case"keyRange":case"velRange":case"velocity":h.push({type:d,value:{lo:e[f++],hi:e[f++]}});break;default:h.push({type:d,value:{amount:e[f++]|e[f++]<<8<<16>>16}})}else h.push({type:d,value:{code:c,amount:e[f]|e[f+1]<<8<<16>>16,lo:e[f++],hi:e[f++]}});return h},b.Parser.prototype.getInstruments=function(){var a,b,c,d,e,f,g,h,i,j=this.instrument,k=this.instrumentZone,l=[];for(f=0,g=j.length;g>f;++f){for(a=j[f].instrumentBagIndex,b=j[f+1]?j[f+1].instrumentBagIndex:k.length,c=[],h=a,i=b;i>h;++h)d=this.createInstrumentGenerator_(k,h),e=this.createInstrumentModulator_(k,h),c.push({generator:d.generator,generatorSequence:d.generatorInfo,modulator:e.modulator,modulatorSequence:e.modulatorInfo});l.push({name:j[f].instrumentName,info:c})}return l},b.Parser.prototype.getPresets=function(){var a,b,c,d,e,f,g,h,i,j,k=this.presetHeader,l=this.presetZone,m=[];for(g=0,h=k.length;h>g;++g){for(a=k[g].presetBagIndex,b=k[g+1]?k[g+1].presetBagIndex:l.length,c=[],i=a,j=b;j>i;++i)e=this.createPresetGenerator_(l,i),f=this.createPresetModulator_(l,i),c.push({generator:e.generator,generatorSequence:e.generatorInfo,modulator:f.modulator,modulatorSequence:f.modulatorInfo}),d=void 0!==e.generator.instrument?e.generator.instrument.amount:void 0!==f.modulator.instrument?f.modulator.instrument.amount:null;m.push({name:k[g].presetName,info:c,header:k[g],instrument:d})}return m},b.Parser.prototype.createInstrumentGenerator_=function(a,b){var c=this.createBagModGen_(a,a[b].instrumentGeneratorIndex,a[b+1]?a[b+1].instrumentGeneratorIndex:this.instrumentZoneGenerator.length,this.instrumentZoneGenerator);return{generator:c.modgen,generatorInfo:c.modgenInfo}},b.Parser.prototype.createInstrumentModulator_=function(a,b){var c=this.createBagModGen_(a,a[b].presetModulatorIndex,a[b+1]?a[b+1].instrumentModulatorIndex:this.instrumentZoneModulator.length,this.instrumentZoneModulator);return{modulator:c.modgen,modulatorInfo:c.modgenInfo}},b.Parser.prototype.createPresetGenerator_=function(a,b){var c=this.createBagModGen_(a,a[b].presetGeneratorIndex,a[b+1]?a[b+1].presetGeneratorIndex:this.presetZoneGenerator.length,this.presetZoneGenerator);return{generator:c.modgen,generatorInfo:c.modgenInfo}},b.Parser.prototype.createPresetModulator_=function(a,b){var c=this.createBagModGen_(a,a[b].presetModulatorIndex,a[b+1]?a[b+1].presetModulatorIndex:this.presetZoneModulator.length,this.presetZoneModulator);return{modulator:c.modgen,modulatorInfo:c.modgenInfo}},b.Parser.prototype.createBagModGen_=function(a,b,c,d){var e,f,g,h=[],i={unknown:[],keyRange:{hi:127,lo:0}};for(f=b,g=c;g>f;++f)e=d[f],h.push(e),"unknown"===e.type?i.unknown.push(e.value):i[e.type]=e.value;return{modgen:i,modgenInfo:h}},b.Parser.GeneratorEnumeratorTable=["startAddrsOffset","endAddrsOffset","startloopAddrsOffset","endloopAddrsOffset","startAddrsCoarseOffset","modLfoToPitch","vibLfoToPitch","modEnvToPitch","initialFilterFc","initialFilterQ","modLfoToFilterFc","modEnvToFilterFc","endAddrsCoarseOffset","modLfoToVolume",void 0,"chorusEffectsSend","reverbEffectsSend","pan",void 0,void 0,void 0,"delayModLFO","freqModLFO","delayVibLFO","freqVibLFO","delayModEnv","attackModEnv","holdModEnv","decayModEnv","sustainModEnv","releaseModEnv","keynumToModEnvHold","keynumToModEnvDecay","delayVolEnv","attackVolEnv","holdVolEnv","decayVolEnv","sustainVolEnv","releaseVolEnv","keynumToVolEnvHold","keynumToVolEnvDecay","instrument",void 0,"keyRange","velRange","startloopAddrsCoarseOffset","keynum","velocity","initialAttenuation",void 0,"endloopAddrsCoarseOffset","coarseTune","fineTune","sampleID","sampleModes",void 0,"scaleTuning","exclusiveClass","overridingRootKey"],b.Riff={},b.Riff.Parser=function(a,b){b=b||{},this.input=a,this.ip=b.index||0,this.length=b.length||a.length-this.ip,this.offset=this.ip,this.padding=void 0!==b.padding?b.padding:!0,this.bigEndian=void 0!==b.bigEndian?b.bigEndian:!1},b.Riff.Chunk=function(a,b,c){this.type=a,this.size=b,this.offset=c},b.Riff.Parser.prototype.parse=function(){var a=this.length+this.offset;for(this.chunkList=[];this.ip>>0:(c[d++]|c[d++]<<8|c[d++]<<16|c[d++]<<24)>>>0,d)),d+=a,this.padding&&1===(d-this.offset&1)&&d++,this.ip=d},b.Riff.Parser.prototype.getChunk=function(a){var b=this.chunkList[a];return void 0===b?null:b},b.Riff.Parser.prototype.getNumberOfChunks=function(){return this.chunkList.length},b}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sf2-parser", 3 | "version": "1.0.0", 4 | "description": "A parser for SoundFont (SF2) files.", 5 | "author": [ 6 | "imaya/GREE Inc", 7 | "Colin Clark" 8 | ], 9 | "homepage": "http://github.com/colinbdclark/sf2-parser", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/colinbdclark/sf2-parser.git" 13 | }, 14 | "bugs": "http://github.com/colinbdclark/sf2-parser/issues", 15 | "licenses": [ 16 | { 17 | "type": "MIT", 18 | "url": "http://opensource.org/licenses/MIT" 19 | } 20 | ], 21 | "keywords": [ 22 | "MIDI", 23 | "SoundFont", 24 | "SF2", 25 | "music" 26 | ], 27 | "readmeFilename": "README.md", 28 | "devDependencies": { 29 | "grunt": "^0.4.5", 30 | "grunt-contrib-clean": "~0.4.1", 31 | "grunt-contrib-concat": "~0.3.0", 32 | "grunt-contrib-jshint": "~0.8.0", 33 | "grunt-contrib-uglify": "~0.3.2" 34 | }, 35 | "dependencies": {}, 36 | "engines": { 37 | "node": ">=0.10.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/sf2-parser.js: -------------------------------------------------------------------------------- 1 | /*! JavaScript SoundFont 2 Parser. Copyright 2013-2015 imaya/GREE Inc and Colin Clark. Licensed under the MIT License. */ 2 | 3 | /* 4 | * JavaScript SoundFont 2 Parser 5 | * 6 | * Copyright 2013 imaya/GREE Inc 7 | * Copyright 2015 Colin Clark 8 | * 9 | * Based on code from the "SoundFont Synthesizer for WebMidiLink" 10 | * https://github.com/gree/sf2synth.js 11 | * 12 | * Licensed under the MIT License. 13 | */ 14 | 15 | /*global require*/ 16 | 17 | (function (root, factory) { 18 | if (typeof exports === "object") { 19 | // We're in a CommonJS-style loader. 20 | root.sf2 = exports; 21 | factory(exports); 22 | } else if (typeof define === "function" && define.amd) { 23 | // We're in an AMD-style loader. 24 | define(["exports"], function (exports) { 25 | root.sf2 = exports; 26 | return (root.sf2, factory(exports)); 27 | }); 28 | } else { 29 | // Plain old browser. 30 | root.sf2 = {}; 31 | factory(root.sf2); 32 | } 33 | }(this, function (exports) { 34 | "use strict"; 35 | 36 | var sf2 = exports; 37 | 38 | sf2.Parser = function (input, options) { 39 | options = options || {}; 40 | /** @type {ByteArray} */ 41 | this.input = input; 42 | /** @type {(Object|undefined)} */ 43 | this.parserOptions = options.parserOptions; 44 | 45 | /** @type {Array.} */ 46 | // this.presetHeader; 47 | /** @type {Array.} */ 48 | // this.presetZone; 49 | /** @type {Array.} */ 50 | // this.presetZoneModulator; 51 | /** @type {Array.} */ 52 | // this.presetZoneGenerator; 53 | /** @type {Array.} */ 54 | // this.instrument; 55 | /** @type {Array.} */ 56 | // this.instrumentZone; 57 | /** @type {Array.} */ 58 | // this.instrumentZoneModulator; 59 | /** @type {Array.} */ 60 | // this.instrumentZoneGenerator; 61 | /** @type {Array.} */ 62 | //this.sampleHeader; 63 | }; 64 | 65 | sf2.Parser.prototype.parse = function () { 66 | /** @type {sf2.Riff.Parser} */ 67 | var parser = new sf2.Riff.Parser(this.input, this.parserOptions); 68 | /** @type {?sf2.Riff.Chunk} */ 69 | var chunk; 70 | 71 | // parse RIFF chunk 72 | parser.parse(); 73 | if (parser.chunkList.length !== 1) { 74 | throw new Error('wrong chunk length'); 75 | } 76 | 77 | chunk = parser.getChunk(0); 78 | if (chunk === null) { 79 | throw new Error('chunk not found'); 80 | } 81 | 82 | this.parseRiffChunk(chunk); 83 | 84 | // TODO: Presumably this is here to reduce memory, 85 | // but does it really matter? Shouldn't we always be 86 | // referencing the underlying ArrayBuffer and thus 87 | // it will persist, in which case why delete it? 88 | this.input = null; 89 | }; 90 | 91 | /** 92 | * @param {sf2.Riff.Chunk} chunk 93 | */ 94 | sf2.Parser.prototype.parseRiffChunk = function (chunk) { 95 | /** @type {sf2.Riff.Parser} */ 96 | var parser; 97 | /** @type {ByteArray} */ 98 | var data = this.input; 99 | /** @type {number} */ 100 | var ip = chunk.offset; 101 | /** @type {string} */ 102 | var signature; 103 | 104 | // check parse target 105 | if (chunk.type !== 'RIFF') { 106 | throw new Error('invalid chunk type:' + chunk.type); 107 | } 108 | 109 | // check signature 110 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 111 | if (signature !== 'sfbk') { 112 | throw new Error('invalid signature:' + signature); 113 | } 114 | 115 | // read structure 116 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 117 | parser.parse(); 118 | if (parser.getNumberOfChunks() !== 3) { 119 | throw new Error('invalid sfbk structure'); 120 | } 121 | 122 | // INFO-list 123 | this.parseInfoList(/** @type {!sf2.Riff.Chunk} */(parser.getChunk(0))); 124 | 125 | // sdta-list 126 | this.parseSdtaList(/** @type {!sf2.Riff.Chunk} */(parser.getChunk(1))); 127 | 128 | // pdta-list 129 | this.parsePdtaList(/** @type {!sf2.Riff.Chunk} */(parser.getChunk(2))); 130 | }; 131 | 132 | /** 133 | * @param {sf2.Riff.Chunk} chunk 134 | */ 135 | sf2.Parser.prototype.parseInfoList = function (chunk) { 136 | /** @type {sf2.Riff.Parser} */ 137 | var parser; 138 | /** @type {ByteArray} */ 139 | var data = this.input; 140 | /** @type {number} */ 141 | var ip = chunk.offset; 142 | /** @type {string} */ 143 | var signature; 144 | 145 | // check parse target 146 | if (chunk.type !== 'LIST') { 147 | throw new Error('invalid chunk type:' + chunk.type); 148 | } 149 | 150 | // check signature 151 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 152 | if (signature !== 'INFO') { 153 | throw new Error('invalid signature:' + signature); 154 | } 155 | 156 | // read structure 157 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 158 | parser.parse(); 159 | }; 160 | 161 | /** 162 | * @param {sf2.Riff.Chunk} chunk 163 | */ 164 | sf2.Parser.prototype.parseSdtaList = function (chunk) { 165 | /** @type {sf2.Riff.Parser} */ 166 | var parser; 167 | /** @type {ByteArray} */ 168 | var data = this.input; 169 | /** @type {number} */ 170 | var ip = chunk.offset; 171 | /** @type {string} */ 172 | var signature; 173 | 174 | // check parse target 175 | if (chunk.type !== 'LIST') { 176 | throw new Error('invalid chunk type:' + chunk.type); 177 | } 178 | 179 | // check signature 180 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 181 | if (signature !== 'sdta') { 182 | throw new Error('invalid signature:' + signature); 183 | } 184 | 185 | // read structure 186 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 187 | parser.parse(); 188 | if (parser.chunkList.length !== 1) { 189 | throw new Error('TODO'); 190 | } 191 | this.samplingData = 192 | /** @type {{type: string, size: number, offset: number}} */ 193 | (parser.getChunk(0)); 194 | }; 195 | 196 | /** 197 | * @param {sf2.Riff.Chunk} chunk 198 | */ 199 | sf2.Parser.prototype.parsePdtaList = function (chunk) { 200 | /** @type {sf2.Riff.Parser} */ 201 | var parser; 202 | /** @type {ByteArray} */ 203 | var data = this.input; 204 | /** @type {number} */ 205 | var ip = chunk.offset; 206 | /** @type {string} */ 207 | var signature; 208 | 209 | // check parse target 210 | if (chunk.type !== 'LIST') { 211 | throw new Error('invalid chunk type:' + chunk.type); 212 | } 213 | 214 | // check signature 215 | signature = String.fromCharCode(data[ip++], data[ip++], data[ip++], data[ip++]); 216 | if (signature !== 'pdta') { 217 | throw new Error('invalid signature:' + signature); 218 | } 219 | 220 | // read structure 221 | parser = new sf2.Riff.Parser(data, {'index': ip, 'length': chunk.size - 4}); 222 | parser.parse(); 223 | 224 | // check number of chunks 225 | if (parser.getNumberOfChunks() !== 9) { 226 | throw new Error('invalid pdta chunk'); 227 | } 228 | 229 | this.parsePhdr(/** @type {sf2.Riff.Chunk} */(parser.getChunk(0))); 230 | this.parsePbag(/** @type {sf2.Riff.Chunk} */(parser.getChunk(1))); 231 | this.parsePmod(/** @type {sf2.Riff.Chunk} */(parser.getChunk(2))); 232 | this.parsePgen(/** @type {sf2.Riff.Chunk} */(parser.getChunk(3))); 233 | this.parseInst(/** @type {sf2.Riff.Chunk} */(parser.getChunk(4))); 234 | this.parseIbag(/** @type {sf2.Riff.Chunk} */(parser.getChunk(5))); 235 | this.parseImod(/** @type {sf2.Riff.Chunk} */(parser.getChunk(6))); 236 | this.parseIgen(/** @type {sf2.Riff.Chunk} */(parser.getChunk(7))); 237 | this.parseShdr(/** @type {sf2.Riff.Chunk} */(parser.getChunk(8))); 238 | }; 239 | 240 | /** 241 | * @param {sf2.Riff.Chunk} chunk 242 | */ 243 | sf2.Parser.prototype.parsePhdr = function (chunk) { 244 | /** @type {ByteArray} */ 245 | var data = this.input; 246 | /** @type {number} */ 247 | var ip = chunk.offset; 248 | /** @type {Array.} */ 249 | var presetHeader = this.presetHeader = []; 250 | /** @type {number} */ 251 | var size = chunk.offset + chunk.size; 252 | 253 | // check parse target 254 | if (chunk.type !== 'phdr') { 255 | throw new Error('invalid chunk type:' + chunk.type); 256 | } 257 | 258 | while (ip < size) { 259 | presetHeader.push({ 260 | presetName: String.fromCharCode.apply(null, data.subarray(ip, ip += 20)), 261 | preset: data[ip++] | (data[ip++] << 8), 262 | bank: data[ip++] | (data[ip++] << 8), 263 | presetBagIndex: data[ip++] | (data[ip++] << 8), 264 | library: (data[ip++] | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24)) >>> 0, 265 | genre: (data[ip++] | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24)) >>> 0, 266 | morphology: (data[ip++] | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24)) >>> 0 267 | }); 268 | } 269 | }; 270 | 271 | /** 272 | * @param {sf2.Riff.Chunk} chunk 273 | */ 274 | sf2.Parser.prototype.parsePbag = function (chunk) { 275 | /** @type {ByteArray} */ 276 | var data = this.input; 277 | /** @type {number} */ 278 | var ip = chunk.offset; 279 | /** @type {Array.} */ 280 | var presetZone = this.presetZone = []; 281 | /** @type {number} */ 282 | var size = chunk.offset + chunk.size; 283 | 284 | // check parse target 285 | if (chunk.type !== 'pbag') { 286 | throw new Error('invalid chunk type:' + chunk.type); 287 | } 288 | 289 | while (ip < size) { 290 | presetZone.push({ 291 | presetGeneratorIndex: data[ip++] | (data[ip++] << 8), 292 | presetModulatorIndex: data[ip++] | (data[ip++] << 8) 293 | }); 294 | } 295 | }; 296 | 297 | /** 298 | * @param {sf2.Riff.Chunk} chunk 299 | */ 300 | sf2.Parser.prototype.parsePmod = function (chunk) { 301 | // check parse target 302 | if (chunk.type !== 'pmod') { 303 | throw new Error('invalid chunk type:' + chunk.type); 304 | } 305 | 306 | this.presetZoneModulator = this.parseModulator(chunk); 307 | }; 308 | 309 | /** 310 | * @param {sf2.Riff.Chunk} chunk 311 | */ 312 | sf2.Parser.prototype.parsePgen = function (chunk) { 313 | // check parse target 314 | if (chunk.type !== 'pgen') { 315 | throw new Error('invalid chunk type:' + chunk.type); 316 | } 317 | this.presetZoneGenerator = this.parseGenerator(chunk); 318 | }; 319 | 320 | /** 321 | * @param {sf2.Riff.Chunk} chunk 322 | */ 323 | sf2.Parser.prototype.parseInst = function (chunk) { 324 | /** @type {ByteArray} */ 325 | var data = this.input; 326 | /** @type {number} */ 327 | var ip = chunk.offset; 328 | /** @type {Array.} */ 329 | var instrument = this.instrument = []; 330 | /** @type {number} */ 331 | var size = chunk.offset + chunk.size; 332 | 333 | // check parse target 334 | if (chunk.type !== 'inst') { 335 | throw new Error('invalid chunk type:' + chunk.type); 336 | } 337 | 338 | while (ip < size) { 339 | instrument.push({ 340 | instrumentName: String.fromCharCode.apply(null, data.subarray(ip, ip += 20)), 341 | instrumentBagIndex: data[ip++] | (data[ip++] << 8) 342 | }); 343 | } 344 | }; 345 | 346 | /** 347 | * @param {sf2.Riff.Chunk} chunk 348 | */ 349 | sf2.Parser.prototype.parseIbag = function (chunk) { 350 | /** @type {ByteArray} */ 351 | var data = this.input; 352 | /** @type {number} */ 353 | var ip = chunk.offset; 354 | /** @type {Array.} */ 355 | var instrumentZone = this.instrumentZone = []; 356 | /** @type {number} */ 357 | var size = chunk.offset + chunk.size; 358 | 359 | // check parse target 360 | if (chunk.type !== 'ibag') { 361 | throw new Error('invalid chunk type:' + chunk.type); 362 | } 363 | 364 | 365 | while (ip < size) { 366 | instrumentZone.push({ 367 | instrumentGeneratorIndex: data[ip++] | (data[ip++] << 8), 368 | instrumentModulatorIndex: data[ip++] | (data[ip++] << 8) 369 | }); 370 | } 371 | }; 372 | 373 | /** 374 | * @param {sf2.Riff.Chunk} chunk 375 | */ 376 | sf2.Parser.prototype.parseImod = function (chunk) { 377 | // check parse target 378 | if (chunk.type !== 'imod') { 379 | throw new Error('invalid chunk type:' + chunk.type); 380 | } 381 | 382 | this.instrumentZoneModulator = this.parseModulator(chunk); 383 | }; 384 | 385 | 386 | /** 387 | * @param {sf2.Riff.Chunk} chunk 388 | */ 389 | sf2.Parser.prototype.parseIgen = function (chunk) { 390 | // check parse target 391 | if (chunk.type !== 'igen') { 392 | throw new Error('invalid chunk type:' + chunk.type); 393 | } 394 | 395 | this.instrumentZoneGenerator = this.parseGenerator(chunk); 396 | }; 397 | 398 | /** 399 | * @param {sf2.Riff.Chunk} chunk 400 | */ 401 | sf2.Parser.prototype.parseShdr = function (chunk) { 402 | /** @type {ByteArray} */ 403 | var data = this.input; 404 | /** @type {number} */ 405 | var ip = chunk.offset; 406 | /** @type {Array.} */ 407 | var samples = this.sample = []; 408 | /** @type {Array.} */ 409 | var sampleHeader = this.sampleHeader = []; 410 | /** @type {number} */ 411 | var size = chunk.offset + chunk.size; 412 | /** @type {string} */ 413 | var sampleName; 414 | /** @type {number} */ 415 | var start; 416 | /** @type {number} */ 417 | var end; 418 | /** @type {number} */ 419 | var startLoop; 420 | /** @type {number} */ 421 | var endLoop; 422 | /** @type {number} */ 423 | var sampleRate; 424 | /** @type {number} */ 425 | var originalPitch; 426 | /** @type {number} */ 427 | var pitchCorrection; 428 | /** @type {number} */ 429 | var sampleLink; 430 | /** @type {number} */ 431 | var sampleType; 432 | 433 | // check parse target 434 | if (chunk.type !== 'shdr') { 435 | throw new Error('invalid chunk type:' + chunk.type); 436 | } 437 | 438 | while (ip < size) { 439 | sampleName = String.fromCharCode.apply(null, data.subarray(ip, ip += 20)); 440 | start = ( 441 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 442 | ) >>> 0; 443 | end = ( 444 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 445 | ) >>> 0; 446 | startLoop = ( 447 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 448 | ) >>> 0; 449 | endLoop = ( 450 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 451 | ) >>> 0; 452 | sampleRate = ( 453 | (data[ip++] << 0) | (data[ip++] << 8) | (data[ip++] << 16) | (data[ip++] << 24) 454 | ) >>> 0; 455 | originalPitch = data[ip++]; 456 | pitchCorrection = (data[ip++] << 24) >> 24; 457 | sampleLink = data[ip++] | (data[ip++] << 8); 458 | sampleType = data[ip++] | (data[ip++] << 8); 459 | 460 | if (!this.parserOptions.isSf3) { 461 | // .sf3 (ogg) uses relative positions for loop 462 | // points, whereas .sf2 (wav) uses absolute 463 | startLoop -= start; 464 | endLoop -= start; 465 | 466 | // wav offset is specified in it's 2-byte samples 467 | // whereas ogg offset is specified just in bytes 468 | start *= 2; 469 | end *= 2; 470 | } 471 | var sample = new Int8Array(data.subarray( 472 | this.samplingData.offset + start, 473 | this.samplingData.offset + end 474 | )).buffer; 475 | 476 | if (!this.parserOptions.isSf3) { 477 | // only possible with .sf2 (wav) since .sf3 478 | // (ogg) is not just raw sample values 479 | if (sampleRate > 0) { 480 | var adjust = this.adjustSampleData(sample, sampleRate); 481 | sample = adjust.sample; 482 | sampleRate *= adjust.multiply; 483 | startLoop *= adjust.multiply; 484 | endLoop *= adjust.multiply; 485 | } 486 | } 487 | 488 | samples.push(sample); 489 | 490 | sampleHeader.push({ 491 | sampleName: sampleName, 492 | /* 493 | start: start, 494 | end: end, 495 | */ 496 | startLoop: startLoop, 497 | endLoop: endLoop, 498 | sampleRate: sampleRate, 499 | originalPitch: originalPitch, 500 | pitchCorrection: pitchCorrection, 501 | sampleLink: sampleLink, 502 | sampleType: sampleType 503 | }); 504 | } 505 | }; 506 | 507 | // TODO: This function is questionable; 508 | // it doesn't interpolate the sample data 509 | // and always forces a sample rate of 22050 or higher. Why? 510 | sf2.Parser.prototype.adjustSampleData = function (sample, sampleRate) { 511 | /** @type {Int16Array} */ 512 | var newSample; 513 | /** @type {number} */ 514 | var i; 515 | /** @type {number} */ 516 | var il; 517 | /** @type {number} */ 518 | var j; 519 | /** @type {number} */ 520 | var multiply = 1; 521 | 522 | // buffer 523 | while (sampleRate < 22050) { 524 | newSample = new Int16Array(sample.length * 2); 525 | for (i = j = 0, il = sample.length; i < il; ++i) { 526 | newSample[j++] = sample[i]; 527 | newSample[j++] = sample[i]; 528 | } 529 | sample = newSample; 530 | multiply *= 2; 531 | sampleRate *= 2; 532 | } 533 | 534 | return { 535 | sample: sample, 536 | multiply: multiply 537 | }; 538 | }; 539 | 540 | /** 541 | * @param {sf2.Riff.Chunk} chunk 542 | * @return {Array.} 543 | */ 544 | sf2.Parser.prototype.parseModulator = function (chunk) { 545 | /** @type {ByteArray} */ 546 | var data = this.input; 547 | /** @type {number} */ 548 | var ip = chunk.offset; 549 | /** @type {number} */ 550 | var size = chunk.offset + chunk.size; 551 | /** @type {number} */ 552 | var code; 553 | /** @type {string} */ 554 | var key; 555 | /** @type {Array.} */ 556 | var output = []; 557 | 558 | while (ip < size) { 559 | // Src Oper 560 | // TODO 561 | ip += 2; 562 | 563 | // Dest Oper 564 | code = data[ip++] | (data[ip++] << 8); 565 | key = sf2.Parser.GeneratorEnumeratorTable[code]; 566 | if (key === undefined) { 567 | // Amount 568 | output.push({ 569 | type: key, 570 | value: { 571 | code: code, 572 | amount: data[ip] | (data[ip+1] << 8) << 16 >> 16, 573 | lo: data[ip++], 574 | hi: data[ip++] 575 | } 576 | }); 577 | } else { 578 | // Amount 579 | switch (key) { 580 | case 'keyRange': /* FALLTHROUGH */ 581 | case 'velRange': /* FALLTHROUGH */ 582 | case 'keynum': /* FALLTHROUGH */ 583 | case 'velocity': 584 | output.push({ 585 | type: key, 586 | value: { 587 | lo: data[ip++], 588 | hi: data[ip++] 589 | } 590 | }); 591 | break; 592 | default: 593 | output.push({ 594 | type: key, 595 | value: { 596 | amount: data[ip++] | (data[ip++] << 8) << 16 >> 16 597 | } 598 | }); 599 | break; 600 | } 601 | } 602 | 603 | // AmtSrcOper 604 | // TODO 605 | ip += 2; 606 | 607 | // Trans Oper 608 | // TODO 609 | ip += 2; 610 | } 611 | 612 | return output; 613 | }; 614 | 615 | /** 616 | * @param {sf2.Riff.Chunk} chunk 617 | * @return {Array.} 618 | */ 619 | sf2.Parser.prototype.parseGenerator = function (chunk) { 620 | /** @type {ByteArray} */ 621 | var data = this.input; 622 | /** @type {number} */ 623 | var ip = chunk.offset; 624 | /** @type {number} */ 625 | var size = chunk.offset + chunk.size; 626 | /** @type {number} */ 627 | var code; 628 | /** @type {string} */ 629 | var key; 630 | /** @type {Array.} */ 631 | var output = []; 632 | 633 | while (ip < size) { 634 | code = data[ip++] | (data[ip++] << 8); 635 | key = sf2.Parser.GeneratorEnumeratorTable[code]; 636 | if (key === undefined) { 637 | output.push({ 638 | type: key, 639 | value: { 640 | code: code, 641 | amount: data[ip] | (data[ip+1] << 8) << 16 >> 16, 642 | lo: data[ip++], 643 | hi: data[ip++] 644 | } 645 | }); 646 | continue; 647 | } 648 | 649 | switch (key) { 650 | case 'keynum': /* FALLTHROUGH */ 651 | case 'keyRange': /* FALLTHROUGH */ 652 | case 'velRange': /* FALLTHROUGH */ 653 | case 'velocity': 654 | output.push({ 655 | type: key, 656 | value: { 657 | lo: data[ip++], 658 | hi: data[ip++] 659 | } 660 | }); 661 | break; 662 | default: 663 | output.push({ 664 | type: key, 665 | value: { 666 | amount: data[ip++] | (data[ip++] << 8) << 16 >> 16 667 | } 668 | }); 669 | break; 670 | } 671 | } 672 | 673 | return output; 674 | }; 675 | 676 | sf2.Parser.prototype.getInstruments = function () { 677 | /** @type {Array.} */ 678 | var instrument = this.instrument; 679 | /** @type {Array.} */ 680 | var zone = this.instrumentZone; 681 | /** @type {Array.} */ 682 | var output = []; 683 | /** @type {number} */ 684 | var bagIndex; 685 | /** @type {number} */ 686 | var bagIndexEnd; 687 | /** @type {Array.} */ 688 | var zoneInfo; 689 | /** @type {{generator: Object, generatorInfo: Array.}} */ 690 | var instrumentGenerator; 691 | /** @type {{modulator: Object, modulatorInfo: Array.}} */ 692 | var instrumentModulator; 693 | /** @type {number} */ 694 | var i; 695 | /** @type {number} */ 696 | var il; 697 | /** @type {number} */ 698 | var j; 699 | /** @type {number} */ 700 | var jl; 701 | 702 | // instrument -> instrument bag -> generator / modulator 703 | for (i = 0, il = instrument.length; i < il; ++i) { 704 | bagIndex = instrument[i].instrumentBagIndex; 705 | bagIndexEnd = instrument[i+1] ? instrument[i+1].instrumentBagIndex : zone.length; 706 | zoneInfo = []; 707 | 708 | // instrument bag 709 | for (j = bagIndex, jl = bagIndexEnd; j < jl; ++j) { 710 | instrumentGenerator = this.createInstrumentGenerator_(zone, j); 711 | instrumentModulator = this.createInstrumentModulator_(zone, j); 712 | 713 | zoneInfo.push({ 714 | generator: instrumentGenerator.generator, 715 | generatorSequence: instrumentGenerator.generatorInfo, 716 | modulator: instrumentModulator.modulator, 717 | modulatorSequence: instrumentModulator.modulatorInfo 718 | }); 719 | } 720 | 721 | output.push({ 722 | name: instrument[i].instrumentName, 723 | info: zoneInfo 724 | }); 725 | } 726 | 727 | return output; 728 | }; 729 | 730 | sf2.Parser.prototype.getPresets = function () { 731 | /** @type {Array.} */ 732 | var preset = this.presetHeader; 733 | /** @type {Array.} */ 734 | var zone = this.presetZone; 735 | /** @type {Array.} */ 736 | var output = []; 737 | /** @type {number} */ 738 | var bagIndex; 739 | /** @type {number} */ 740 | var bagIndexEnd; 741 | /** @type {Array.} */ 742 | var zoneInfo; 743 | /** @type {number} */ 744 | var instrument; 745 | /** @type {{generator: Object, generatorInfo: Array.}} */ 746 | var presetGenerator; 747 | /** @type {{modulator: Object, modulatorInfo: Array.}} */ 748 | var presetModulator; 749 | /** @type {number} */ 750 | var i; 751 | /** @type {number} */ 752 | var il; 753 | /** @type {number} */ 754 | var j; 755 | /** @type {number} */ 756 | var jl; 757 | 758 | // preset -> preset bag -> generator / modulator 759 | for (i = 0, il = preset.length; i < il; ++i) { 760 | bagIndex = preset[i].presetBagIndex; 761 | bagIndexEnd = preset[i+1] ? preset[i+1].presetBagIndex : zone.length; 762 | zoneInfo = []; 763 | 764 | // preset bag 765 | for (j = bagIndex, jl = bagIndexEnd; j < jl; ++j) { 766 | presetGenerator = this.createPresetGenerator_(zone, j); 767 | presetModulator = this.createPresetModulator_(zone, j); 768 | 769 | zoneInfo.push({ 770 | generator: presetGenerator.generator, 771 | generatorSequence: presetGenerator.generatorInfo, 772 | modulator: presetModulator.modulator, 773 | modulatorSequence: presetModulator.modulatorInfo 774 | }); 775 | 776 | instrument = 777 | presetGenerator.generator.instrument !== undefined ? 778 | presetGenerator.generator.instrument.amount : 779 | presetModulator.modulator.instrument !== undefined ? 780 | presetModulator.modulator.instrument.amount : 781 | null; 782 | } 783 | 784 | output.push({ 785 | name: preset[i].presetName, 786 | info: zoneInfo, 787 | header: preset[i], 788 | instrument: instrument 789 | }); 790 | } 791 | 792 | return output; 793 | }; 794 | 795 | /** 796 | * @param {Array.} zone 797 | * @param {number} index 798 | * @returns {{generator: Object, generatorInfo: Array.}} 799 | * @private 800 | */ 801 | sf2.Parser.prototype.createInstrumentGenerator_ = function (zone, index) { 802 | var modgen = this.createBagModGen_( 803 | zone, 804 | zone[index].instrumentGeneratorIndex, 805 | zone[index+1] ? zone[index+1].instrumentGeneratorIndex: this.instrumentZoneGenerator.length, 806 | this.instrumentZoneGenerator 807 | ); 808 | 809 | return { 810 | generator: modgen.modgen, 811 | generatorInfo: modgen.modgenInfo 812 | }; 813 | }; 814 | 815 | /** 816 | * @param {Array.} zone 817 | * @param {number} index 818 | * @returns {{modulator: Object, modulatorInfo: Array.}} 819 | * @private 820 | */ 821 | sf2.Parser.prototype.createInstrumentModulator_ = function (zone, index) { 822 | var modgen = this.createBagModGen_( 823 | zone, 824 | zone[index].presetModulatorIndex, 825 | zone[index+1] ? zone[index+1].instrumentModulatorIndex: this.instrumentZoneModulator.length, 826 | this.instrumentZoneModulator 827 | ); 828 | 829 | return { 830 | modulator: modgen.modgen, 831 | modulatorInfo: modgen.modgenInfo 832 | }; 833 | }; 834 | 835 | /** 836 | * @param {Array.} zone 837 | * @param {number} index 838 | * @returns {{generator: Object, generatorInfo: Array.}} 839 | * @private 840 | */ 841 | sf2.Parser.prototype.createPresetGenerator_ = function (zone, index) { 842 | var modgen = this.createBagModGen_( 843 | zone, 844 | zone[index].presetGeneratorIndex, 845 | zone[index+1] ? zone[index+1].presetGeneratorIndex : this.presetZoneGenerator.length, 846 | this.presetZoneGenerator 847 | ); 848 | 849 | return { 850 | generator: modgen.modgen, 851 | generatorInfo: modgen.modgenInfo 852 | }; 853 | }; 854 | 855 | /** 856 | * @param {Array.} zone 857 | * @param {number} index 858 | * @returns {{modulator: Object, modulatorInfo: Array.}} 859 | * @private 860 | */ 861 | sf2.Parser.prototype.createPresetModulator_ = function (zone, index) { 862 | /** @type {{modgen: Object, modgenInfo: Array.}} */ 863 | var modgen = this.createBagModGen_( 864 | zone, 865 | zone[index].presetModulatorIndex, 866 | zone[index+1] ? zone[index+1].presetModulatorIndex : this.presetZoneModulator.length, 867 | this.presetZoneModulator 868 | ); 869 | 870 | return { 871 | modulator: modgen.modgen, 872 | modulatorInfo: modgen.modgenInfo 873 | }; 874 | }; 875 | 876 | /** 877 | * @param {Array.} zone 878 | * @param {number} indexStart 879 | * @param {number} indexEnd 880 | * @param zoneModGen 881 | * @returns {{modgen: Object, modgenInfo: Array.}} 882 | * @private 883 | */ 884 | sf2.Parser.prototype.createBagModGen_ = function (zone, indexStart, indexEnd, zoneModGen) { 885 | /** @type {Array.} */ 886 | var modgenInfo = []; 887 | /** @type {Object} */ 888 | var modgen = { 889 | unknown: [], 890 | 'keyRange': { 891 | hi: 127, 892 | lo: 0 893 | } 894 | }; // TODO 895 | /** @type {Object} */ 896 | var info; 897 | /** @type {number} */ 898 | var i; 899 | /** @type {number} */ 900 | var il; 901 | 902 | for (i = indexStart, il = indexEnd; i < il; ++i) { 903 | info = zoneModGen[i]; 904 | modgenInfo.push(info); 905 | 906 | if (info.type === 'unknown') { 907 | modgen.unknown.push(info.value); 908 | } else { 909 | modgen[info.type] = info.value; 910 | } 911 | } 912 | 913 | return { 914 | modgen: modgen, 915 | modgenInfo: modgenInfo 916 | }; 917 | }; 918 | 919 | 920 | /** 921 | * @type {Array.} 922 | * @const 923 | */ 924 | sf2.Parser.GeneratorEnumeratorTable = [ 925 | 'startAddrsOffset', 926 | 'endAddrsOffset', 927 | 'startloopAddrsOffset', 928 | 'endloopAddrsOffset', 929 | 'startAddrsCoarseOffset', 930 | 'modLfoToPitch', 931 | 'vibLfoToPitch', 932 | 'modEnvToPitch', 933 | 'initialFilterFc', 934 | 'initialFilterQ', 935 | 'modLfoToFilterFc', 936 | 'modEnvToFilterFc', 937 | 'endAddrsCoarseOffset', 938 | 'modLfoToVolume', 939 | undefined, // 14 940 | 'chorusEffectsSend', 941 | 'reverbEffectsSend', 942 | 'pan', 943 | undefined, 944 | undefined, 945 | undefined, // 18,19,20 946 | 'delayModLFO', 947 | 'freqModLFO', 948 | 'delayVibLFO', 949 | 'freqVibLFO', 950 | 'delayModEnv', 951 | 'attackModEnv', 952 | 'holdModEnv', 953 | 'decayModEnv', 954 | 'sustainModEnv', 955 | 'releaseModEnv', 956 | 'keynumToModEnvHold', 957 | 'keynumToModEnvDecay', 958 | 'delayVolEnv', 959 | 'attackVolEnv', 960 | 'holdVolEnv', 961 | 'decayVolEnv', 962 | 'sustainVolEnv', 963 | 'releaseVolEnv', 964 | 'keynumToVolEnvHold', 965 | 'keynumToVolEnvDecay', 966 | 'instrument', 967 | undefined, // 42 968 | 'keyRange', 969 | 'velRange', 970 | 'startloopAddrsCoarseOffset', 971 | 'keynum', 972 | 'velocity', 973 | 'initialAttenuation', 974 | undefined, // 49 975 | 'endloopAddrsCoarseOffset', 976 | 'coarseTune', 977 | 'fineTune', 978 | 'sampleID', 979 | 'sampleModes', 980 | undefined, // 55 981 | 'scaleTuning', 982 | 'exclusiveClass', 983 | 'overridingRootKey' 984 | ]; 985 | 986 | 987 | sf2.Riff = {}; 988 | 989 | sf2.Riff.Parser = function (input, options) { 990 | options = options || {}; 991 | /** @type {ByteArray} */ 992 | this.input = input; 993 | /** @type {number} */ 994 | this.ip = options.index || 0; 995 | /** @type {number} */ 996 | this.length = options.length || input.length - this.ip; 997 | /** @type {Array.} */ 998 | // this.chunkList; 999 | /** @type {number} */ 1000 | this.offset = this.ip; 1001 | /** @type {boolean} */ 1002 | this.isSf3 = options.isSf3 !== undefined ? options.isSf3 : true; 1003 | /** @type {boolean} */ 1004 | this.padding = options.padding !== undefined ? options.padding : true; 1005 | /** @type {boolean} */ 1006 | this.bigEndian = options.bigEndian !== undefined ? options.bigEndian : false; 1007 | }; 1008 | 1009 | /** 1010 | * @param {string} type 1011 | * @param {number} size 1012 | * @param {number} offset 1013 | * @constructor 1014 | */ 1015 | sf2.Riff.Chunk = function (type, size, offset) { 1016 | /** @type {string} */ 1017 | this.type = type; 1018 | /** @type {number} */ 1019 | this.size = size; 1020 | /** @type {number} */ 1021 | this.offset = offset; 1022 | }; 1023 | 1024 | sf2.Riff.Parser.prototype.parse = function () { 1025 | /** @type {number} */ 1026 | var length = this.length + this.offset; 1027 | 1028 | this.chunkList = []; 1029 | 1030 | while (this.ip < length) { 1031 | this.parseChunk(); 1032 | } 1033 | }; 1034 | 1035 | sf2.Riff.Parser.prototype.parseChunk = function () { 1036 | /** @type {ByteArray} */ 1037 | var input = this.input; 1038 | /** @type {number} */ 1039 | var ip = this.ip; 1040 | /** @type {number} */ 1041 | var size; 1042 | 1043 | this.chunkList.push(new sf2.Riff.Chunk( 1044 | String.fromCharCode(input[ip++], input[ip++], input[ip++], input[ip++]), 1045 | (size = this.bigEndian ? 1046 | ((input[ip++] << 24) | (input[ip++] << 16) | 1047 | (input[ip++] << 8) | (input[ip++] )) >>> 0 : 1048 | ((input[ip++] ) | (input[ip++] << 8) | 1049 | (input[ip++] << 16) | (input[ip++] << 24)) >>> 0 1050 | ), 1051 | ip 1052 | )); 1053 | 1054 | ip += size; 1055 | 1056 | // padding 1057 | if (!this.isSf3 && this.padding && ((ip - this.offset) & 1) === 1) { 1058 | ip++; 1059 | } 1060 | 1061 | this.ip = ip; 1062 | }; 1063 | 1064 | /** 1065 | * @param {number} index chunk index. 1066 | * @return {?sf2.Riff.Chunk} 1067 | */ 1068 | sf2.Riff.Parser.prototype.getChunk = function (index) { 1069 | /** @type {sf2.Riff.Chunk} */ 1070 | var chunk = this.chunkList[index]; 1071 | 1072 | if (chunk === undefined) { 1073 | return null; 1074 | } 1075 | 1076 | return chunk; 1077 | }; 1078 | 1079 | /** 1080 | * @return {number} 1081 | */ 1082 | sf2.Riff.Parser.prototype.getNumberOfChunks = function () { 1083 | return this.chunkList.length; 1084 | }; 1085 | 1086 | 1087 | return sf2; 1088 | })); 1089 | -------------------------------------------------------------------------------- /tests/html/soundfont-parser-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SoundFont 2 Parser Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

SoundFont 2 Parser Tests

20 |

21 |
22 |

23 |
    24 | 25 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/js/sf2-parser-tests.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | QUnit.module("Parser Tests"); 5 | 6 | function loadSoundFont (url, success, error) { 7 | var xhr = new XMLHttpRequest(); 8 | xhr.open("GET", url, true); 9 | xhr.responseType = "arraybuffer"; 10 | xhr.onreadystatechange = function () { 11 | if (xhr.readyState === 4) { 12 | if (xhr.status === 200) { 13 | success(new Uint8Array(xhr.response)); 14 | } else { 15 | if (options.error) { 16 | options.error(xhr.statusText); 17 | } 18 | } 19 | } 20 | }; 21 | xhr.send(); 22 | } 23 | 24 | QUnit.test("Instantiating a parser", function () { 25 | var parser = new sf2.Parser(); 26 | QUnit.ok(parser instanceof sf2.Parser); 27 | }); 28 | 29 | QUnit.asyncTest("Parsing a SoundFont file", function () { 30 | function success (data) { 31 | var parser = new sf2.Parser(data); 32 | parser.parse(); 33 | var presets = parser.getPresets(); 34 | var instruments = parser.getInstruments(); 35 | 36 | ok(instruments); 37 | start(); 38 | } 39 | 40 | function error (err) { 41 | ok(false, "There was an error while parsing the test SoundFont file: " + err); 42 | } 43 | 44 | loadSoundFont("../sf2/sf_GMbank.sf2", success, error); 45 | }); 46 | }()); 47 | -------------------------------------------------------------------------------- /tests/third-party/qunit/addons/composite/README.md: -------------------------------------------------------------------------------- 1 | # QUnit Composite [![Build Status](https://travis-ci.org/jquery/qunit-composite.png)](https://travis-ci.org/jquery/qunit-composite) [![NPM version](https://badge.fury.io/js/qunit-composite.png)](http://badge.fury.io/js/qunit-composite) 2 | 3 | Composite is a QUnit plugin that, when handed an array of files, will 4 | open each of those files inside of an iframe, run the tests, and 5 | display the results as a single suite of QUnit tests. 6 | 7 | The "Rerun" link next to each suite allows you to quickly rerun that suite, 8 | outside the composite runner. 9 | 10 | If you want to see what assertion failed in a long list of assertions, 11 | just use the regular "Hide passed tests" checkbox. 12 | 13 | ## Usage 14 | 15 | Load QUnit itself as usual _plus_ `qunit-composite.css` and `qunit-composite.js`, 16 | then specify the test suites to load using `QUnit.testSuites`: 17 | 18 | ```js 19 | QUnit.testSuites([ 20 | "example-test-1.html", 21 | "example-test-2.html", 22 | // optionally provide a name and path 23 | { name: "Example Test 3", path: "example-test-3.html" } 24 | ]); 25 | ``` 26 | 27 | Optionally, give the composed module a name (defaults to "Composition #1"): 28 | 29 | ``` 30 | QUnit.testSuites( "Example tests", [ 31 | "example-test-1.html", 32 | "example-test-2.html" 33 | ]); 34 | ``` 35 | 36 | ## Notes 37 | - Although it is possible to do so, we do not recommend mixing QUnit Composite suites (`QUnit.testSuites`) on the same page 38 | as regular tests and modules (`QUnit.test`/`test`, `QUnit.module`/`module`). 39 | - The QUnit Composite plugin can be used for testing suites on the "file://" protocol **unless** any of the referenced suites 40 | are outside of the test page's directory (e.g. `../otherTest.html`) due to web security restrictions. You can work around this 41 | restriction by running them in Google Chrome or [PhantomJS](http://phantomjs.org), _with web security disabled_ — or, 42 | of course, by not referencing suites outside of the current test page's directory. 43 | -------------------------------------------------------------------------------- /tests/third-party/qunit/addons/composite/qunit-composite.css: -------------------------------------------------------------------------------- 1 | .qunit-composite-suite { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | 6 | margin: 0; 7 | padding: 0; 8 | border-width: 1px 0 0; 9 | height: 45%; 10 | width: 100%; 11 | 12 | background: #fff; 13 | } 14 | -------------------------------------------------------------------------------- /tests/third-party/qunit/addons/composite/qunit-composite.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JUnit reporter for QUnit v1.0.1 3 | * 4 | * https://github.com/jquery/qunit-composite 5 | * 6 | * Copyright 2013 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * https://jquery.org/license/ 9 | */ 10 | (function( QUnit ) { 11 | var iframe, hasBound, addClass, 12 | modules = 1, 13 | executingComposite = false; 14 | 15 | // TODO: Kill this fallback method once QUnit 1.12 is released 16 | addClass = typeof QUnit.addClass === "function" ? 17 | QUnit.addClass : 18 | (function() { 19 | var hasClass = typeof QUnit.hasClass === "function" ? 20 | QUnit.hasClass : 21 | function hasClass( elem, name ) { 22 | return ( " " + elem.className + " " ).indexOf( " " + name + " " ) > -1; 23 | }; 24 | return function addClass( elem, name ) { 25 | if ( !hasClass( elem, name ) ) { 26 | elem.className += ( elem.className ? " " : "" ) + name; 27 | } 28 | }; 29 | })(); 30 | 31 | function runSuite( suite ) { 32 | var path; 33 | 34 | if ( QUnit.is( "object", suite ) ) { 35 | path = suite.path; 36 | suite = suite.name; 37 | } else { 38 | path = suite; 39 | } 40 | 41 | QUnit.asyncTest( suite, function() { 42 | iframe.setAttribute( "src", path ); 43 | // QUnit.start is called from the child iframe's QUnit.done hook. 44 | }); 45 | } 46 | 47 | function initIframe() { 48 | var iframeWin, 49 | body = document.body; 50 | 51 | function onIframeLoad() { 52 | var moduleName, testName, 53 | count = 0; 54 | 55 | if ( !iframe.src ) { 56 | return; 57 | } 58 | 59 | iframeWin.QUnit.moduleStart(function( data ) { 60 | // Capture module name for messages 61 | moduleName = data.name; 62 | }); 63 | 64 | iframeWin.QUnit.testStart(function( data ) { 65 | // AMB: Hoisted this up here so it is possible to run tests from link URL in the case they fail 66 | var current = QUnit.id( QUnit.config.current.id ); 67 | // Update Rerun link to point to the standalone test suite page 68 | current.getElementsByTagName( "a" )[ 0 ].href = iframe.src; 69 | 70 | // Capture test name for messages 71 | testName = data.name; 72 | }); 73 | iframeWin.QUnit.testDone(function() { 74 | testName = undefined; 75 | }); 76 | 77 | iframeWin.QUnit.log(function( data ) { 78 | if (testName === undefined) { 79 | return; 80 | } 81 | // Pass all test details through to the main page 82 | var message = ( moduleName ? moduleName + ": " : "" ) + testName + ": " + ( data.message || ( data.result ? "okay" : "failed" ) ); 83 | expect( ++count ); 84 | QUnit.push( data.result, data.actual, data.expected, message ); 85 | }); 86 | 87 | // Continue the outer test when the iframe's test is done 88 | iframeWin.QUnit.done( QUnit.start ); 89 | } 90 | 91 | iframe = document.createElement( "iframe" ); 92 | iframe.className = "qunit-composite-suite"; 93 | body.appendChild( iframe ); 94 | 95 | QUnit.addEvent( iframe, "load", onIframeLoad ); 96 | 97 | iframeWin = iframe.contentWindow; 98 | } 99 | 100 | /** 101 | * @param {string} [name] Module name to group these test suites. 102 | * @param {Array} suites List of suites where each suite 103 | * may either be a string (path to the html test page), 104 | * or an object with a path and name property. 105 | */ 106 | QUnit.testSuites = function( name, suites ) { 107 | var i, suitesLen; 108 | 109 | if ( arguments.length === 1 ) { 110 | suites = name; 111 | name = "Composition #" + modules++; 112 | } 113 | suitesLen = suites.length; 114 | 115 | if ( !hasBound ) { 116 | hasBound = true; 117 | QUnit.begin( initIframe ); 118 | 119 | // TODO: Would be better to use something like QUnit.once( 'moduleDone' ) 120 | // after the last test suite. 121 | QUnit.moduleDone( function () { 122 | executingComposite = false; 123 | } ); 124 | 125 | QUnit.done(function() { 126 | iframe.style.display = "none"; 127 | }); 128 | } 129 | 130 | QUnit.module( name, { 131 | setup: function () { 132 | executingComposite = true; 133 | } 134 | }); 135 | 136 | for ( i = 0; i < suitesLen; i++ ) { 137 | runSuite( suites[ i ] ); 138 | } 139 | }; 140 | 141 | QUnit.testStart(function() { 142 | 143 | }); 144 | 145 | QUnit.testDone(function() { 146 | if ( !executingComposite ) { 147 | return; 148 | } 149 | 150 | var i, len, 151 | current = QUnit.id( this.config.current.id ), 152 | children = current.children, 153 | src = iframe.src; 154 | 155 | QUnit.addEvent( current, "dblclick", function( e ) { 156 | var target = e && e.target ? e.target : window.event.srcElement; 157 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 158 | target = target.parentNode; 159 | } 160 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 161 | window.location = src; 162 | } 163 | }); 164 | 165 | // Undo QUnit's auto-expansion for bad tests 166 | for ( i = 0, len = children.length; i < len; i++ ) { 167 | if ( children[ i ].nodeName.toLowerCase() === "ol" ) { 168 | addClass( children[ i ], "qunit-collapsed" ); 169 | } 170 | } 171 | 172 | }); 173 | 174 | })( QUnit ); 175 | -------------------------------------------------------------------------------- /tests/third-party/qunit/css/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /tests/third-party/qunit/js/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2013 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * https://jquery.org/license/ 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | assert, 15 | config, 16 | onErrorFnPrev, 17 | testId = 0, 18 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 19 | toString = Object.prototype.toString, 20 | hasOwn = Object.prototype.hasOwnProperty, 21 | // Keep a local reference to Date (GH-283) 22 | Date = window.Date, 23 | setTimeout = window.setTimeout, 24 | defined = { 25 | setTimeout: typeof window.setTimeout !== "undefined", 26 | sessionStorage: (function() { 27 | var x = "qunit-test-string"; 28 | try { 29 | sessionStorage.setItem( x, x ); 30 | sessionStorage.removeItem( x ); 31 | return true; 32 | } catch( e ) { 33 | return false; 34 | } 35 | }()) 36 | }, 37 | /** 38 | * Provides a normalized error string, correcting an issue 39 | * with IE 7 (and prior) where Error.prototype.toString is 40 | * not properly implemented 41 | * 42 | * Based on http://es5.github.com/#x15.11.4.4 43 | * 44 | * @param {String|Error} error 45 | * @return {String} error message 46 | */ 47 | errorString = function( error ) { 48 | var name, message, 49 | errorString = error.toString(); 50 | if ( errorString.substring( 0, 7 ) === "[object" ) { 51 | name = error.name ? error.name.toString() : "Error"; 52 | message = error.message ? error.message.toString() : ""; 53 | if ( name && message ) { 54 | return name + ": " + message; 55 | } else if ( name ) { 56 | return name; 57 | } else if ( message ) { 58 | return message; 59 | } else { 60 | return "Error"; 61 | } 62 | } else { 63 | return errorString; 64 | } 65 | }, 66 | /** 67 | * Makes a clone of an object using only Array or Object as base, 68 | * and copies over the own enumerable properties. 69 | * 70 | * @param {Object} obj 71 | * @return {Object} New object with only the own properties (recursively). 72 | */ 73 | objectValues = function( obj ) { 74 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 75 | /*jshint newcap: false */ 76 | var key, val, 77 | vals = QUnit.is( "array", obj ) ? [] : {}; 78 | for ( key in obj ) { 79 | if ( hasOwn.call( obj, key ) ) { 80 | val = obj[key]; 81 | vals[key] = val === Object(val) ? objectValues(val) : val; 82 | } 83 | } 84 | return vals; 85 | }; 86 | 87 | function Test( settings ) { 88 | extend( this, settings ); 89 | this.assertions = []; 90 | this.testNumber = ++Test.count; 91 | } 92 | 93 | Test.count = 0; 94 | 95 | Test.prototype = { 96 | init: function() { 97 | var a, b, li, 98 | tests = id( "qunit-tests" ); 99 | 100 | if ( tests ) { 101 | b = document.createElement( "strong" ); 102 | b.innerHTML = this.nameHtml; 103 | 104 | // `a` initialized at top of scope 105 | a = document.createElement( "a" ); 106 | a.innerHTML = "Rerun"; 107 | a.href = QUnit.url({ testNumber: this.testNumber }); 108 | 109 | li = document.createElement( "li" ); 110 | li.appendChild( b ); 111 | li.appendChild( a ); 112 | li.className = "running"; 113 | li.id = this.id = "qunit-test-output" + testId++; 114 | 115 | tests.appendChild( li ); 116 | } 117 | }, 118 | setup: function() { 119 | if ( 120 | // Emit moduleStart when we're switching from one module to another 121 | this.module !== config.previousModule || 122 | // They could be equal (both undefined) but if the previousModule property doesn't 123 | // yet exist it means this is the first test in a suite that isn't wrapped in a 124 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 125 | // Without this, reporters can get testStart before moduleStart which is a problem. 126 | !hasOwn.call( config, "previousModule" ) 127 | ) { 128 | if ( hasOwn.call( config, "previousModule" ) ) { 129 | runLoggingCallbacks( "moduleDone", QUnit, { 130 | name: config.previousModule, 131 | failed: config.moduleStats.bad, 132 | passed: config.moduleStats.all - config.moduleStats.bad, 133 | total: config.moduleStats.all 134 | }); 135 | } 136 | config.previousModule = this.module; 137 | config.moduleStats = { all: 0, bad: 0 }; 138 | runLoggingCallbacks( "moduleStart", QUnit, { 139 | name: this.module 140 | }); 141 | } 142 | 143 | config.current = this; 144 | 145 | this.testEnvironment = extend({ 146 | setup: function() {}, 147 | teardown: function() {} 148 | }, this.moduleTestEnvironment ); 149 | 150 | this.started = +new Date(); 151 | runLoggingCallbacks( "testStart", QUnit, { 152 | name: this.testName, 153 | module: this.module 154 | }); 155 | 156 | /*jshint camelcase:false */ 157 | 158 | 159 | /** 160 | * Expose the current test environment. 161 | * 162 | * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. 163 | */ 164 | QUnit.current_testEnvironment = this.testEnvironment; 165 | 166 | /*jshint camelcase:true */ 167 | 168 | if ( !config.pollution ) { 169 | saveGlobal(); 170 | } 171 | if ( config.notrycatch ) { 172 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 173 | return; 174 | } 175 | try { 176 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 177 | } catch( e ) { 178 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 179 | } 180 | }, 181 | run: function() { 182 | config.current = this; 183 | 184 | var running = id( "qunit-testresult" ); 185 | 186 | if ( running ) { 187 | running.innerHTML = "Running:
    " + this.nameHtml; 188 | } 189 | 190 | if ( this.async ) { 191 | QUnit.stop(); 192 | } 193 | 194 | this.callbackStarted = +new Date(); 195 | 196 | if ( config.notrycatch ) { 197 | this.callback.call( this.testEnvironment, QUnit.assert ); 198 | this.callbackRuntime = +new Date() - this.callbackStarted; 199 | return; 200 | } 201 | 202 | try { 203 | this.callback.call( this.testEnvironment, QUnit.assert ); 204 | this.callbackRuntime = +new Date() - this.callbackStarted; 205 | } catch( e ) { 206 | this.callbackRuntime = +new Date() - this.callbackStarted; 207 | 208 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 209 | // else next test will carry the responsibility 210 | saveGlobal(); 211 | 212 | // Restart the tests if they're blocking 213 | if ( config.blocking ) { 214 | QUnit.start(); 215 | } 216 | } 217 | }, 218 | teardown: function() { 219 | config.current = this; 220 | if ( config.notrycatch ) { 221 | if ( typeof this.callbackRuntime === "undefined" ) { 222 | this.callbackRuntime = +new Date() - this.callbackStarted; 223 | } 224 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 225 | return; 226 | } else { 227 | try { 228 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 229 | } catch( e ) { 230 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 231 | } 232 | } 233 | checkPollution(); 234 | }, 235 | finish: function() { 236 | config.current = this; 237 | if ( config.requireExpects && this.expected === null ) { 238 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 239 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 240 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 241 | } else if ( this.expected === null && !this.assertions.length ) { 242 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 243 | } 244 | 245 | var i, assertion, a, b, time, li, ol, 246 | test = this, 247 | good = 0, 248 | bad = 0, 249 | tests = id( "qunit-tests" ); 250 | 251 | this.runtime = +new Date() - this.started; 252 | config.stats.all += this.assertions.length; 253 | config.moduleStats.all += this.assertions.length; 254 | 255 | if ( tests ) { 256 | ol = document.createElement( "ol" ); 257 | ol.className = "qunit-assert-list"; 258 | 259 | for ( i = 0; i < this.assertions.length; i++ ) { 260 | assertion = this.assertions[i]; 261 | 262 | li = document.createElement( "li" ); 263 | li.className = assertion.result ? "pass" : "fail"; 264 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 265 | ol.appendChild( li ); 266 | 267 | if ( assertion.result ) { 268 | good++; 269 | } else { 270 | bad++; 271 | config.stats.bad++; 272 | config.moduleStats.bad++; 273 | } 274 | } 275 | 276 | // store result when possible 277 | if ( QUnit.config.reorder && defined.sessionStorage ) { 278 | if ( bad ) { 279 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 280 | } else { 281 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 282 | } 283 | } 284 | 285 | if ( bad === 0 ) { 286 | addClass( ol, "qunit-collapsed" ); 287 | } 288 | 289 | // `b` initialized at top of scope 290 | b = document.createElement( "strong" ); 291 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 292 | 293 | addEvent(b, "click", function() { 294 | var next = b.parentNode.lastChild, 295 | collapsed = hasClass( next, "qunit-collapsed" ); 296 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 297 | }); 298 | 299 | addEvent(b, "dblclick", function( e ) { 300 | var target = e && e.target ? e.target : window.event.srcElement; 301 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 302 | target = target.parentNode; 303 | } 304 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 305 | window.location = QUnit.url({ testNumber: test.testNumber }); 306 | } 307 | }); 308 | 309 | // `time` initialized at top of scope 310 | time = document.createElement( "span" ); 311 | time.className = "runtime"; 312 | time.innerHTML = this.runtime + " ms"; 313 | 314 | // `li` initialized at top of scope 315 | li = id( this.id ); 316 | li.className = bad ? "fail" : "pass"; 317 | li.removeChild( li.firstChild ); 318 | a = li.firstChild; 319 | li.appendChild( b ); 320 | li.appendChild( a ); 321 | li.appendChild( time ); 322 | li.appendChild( ol ); 323 | 324 | } else { 325 | for ( i = 0; i < this.assertions.length; i++ ) { 326 | if ( !this.assertions[i].result ) { 327 | bad++; 328 | config.stats.bad++; 329 | config.moduleStats.bad++; 330 | } 331 | } 332 | } 333 | 334 | runLoggingCallbacks( "testDone", QUnit, { 335 | name: this.testName, 336 | module: this.module, 337 | failed: bad, 338 | passed: this.assertions.length - bad, 339 | total: this.assertions.length, 340 | duration: this.runtime 341 | }); 342 | 343 | QUnit.reset(); 344 | 345 | config.current = undefined; 346 | }, 347 | 348 | queue: function() { 349 | var bad, 350 | test = this; 351 | 352 | synchronize(function() { 353 | test.init(); 354 | }); 355 | function run() { 356 | // each of these can by async 357 | synchronize(function() { 358 | test.setup(); 359 | }); 360 | synchronize(function() { 361 | test.run(); 362 | }); 363 | synchronize(function() { 364 | test.teardown(); 365 | }); 366 | synchronize(function() { 367 | test.finish(); 368 | }); 369 | } 370 | 371 | // `bad` initialized at top of scope 372 | // defer when previous test run passed, if storage is available 373 | bad = QUnit.config.reorder && defined.sessionStorage && 374 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 375 | 376 | if ( bad ) { 377 | run(); 378 | } else { 379 | synchronize( run, true ); 380 | } 381 | } 382 | }; 383 | 384 | // Root QUnit object. 385 | // `QUnit` initialized at top of scope 386 | QUnit = { 387 | 388 | // call on start of module test to prepend name to all tests 389 | module: function( name, testEnvironment ) { 390 | config.currentModule = name; 391 | config.currentModuleTestEnvironment = testEnvironment; 392 | config.modules[name] = true; 393 | }, 394 | 395 | asyncTest: function( testName, expected, callback ) { 396 | if ( arguments.length === 2 ) { 397 | callback = expected; 398 | expected = null; 399 | } 400 | 401 | QUnit.test( testName, expected, callback, true ); 402 | }, 403 | 404 | test: function( testName, expected, callback, async ) { 405 | var test, 406 | nameHtml = "" + escapeText( testName ) + ""; 407 | 408 | if ( arguments.length === 2 ) { 409 | callback = expected; 410 | expected = null; 411 | } 412 | 413 | if ( config.currentModule ) { 414 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 415 | } 416 | 417 | test = new Test({ 418 | nameHtml: nameHtml, 419 | testName: testName, 420 | expected: expected, 421 | async: async, 422 | callback: callback, 423 | module: config.currentModule, 424 | moduleTestEnvironment: config.currentModuleTestEnvironment, 425 | stack: sourceFromStacktrace( 2 ) 426 | }); 427 | 428 | if ( !validTest( test ) ) { 429 | return; 430 | } 431 | 432 | test.queue(); 433 | }, 434 | 435 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 436 | expect: function( asserts ) { 437 | if (arguments.length === 1) { 438 | config.current.expected = asserts; 439 | } else { 440 | return config.current.expected; 441 | } 442 | }, 443 | 444 | start: function( count ) { 445 | // QUnit hasn't been initialized yet. 446 | // Note: RequireJS (et al) may delay onLoad 447 | if ( config.semaphore === undefined ) { 448 | QUnit.begin(function() { 449 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 450 | setTimeout(function() { 451 | QUnit.start( count ); 452 | }); 453 | }); 454 | return; 455 | } 456 | 457 | config.semaphore -= count || 1; 458 | // don't start until equal number of stop-calls 459 | if ( config.semaphore > 0 ) { 460 | return; 461 | } 462 | // ignore if start is called more often then stop 463 | if ( config.semaphore < 0 ) { 464 | config.semaphore = 0; 465 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 466 | return; 467 | } 468 | // A slight delay, to avoid any current callbacks 469 | if ( defined.setTimeout ) { 470 | setTimeout(function() { 471 | if ( config.semaphore > 0 ) { 472 | return; 473 | } 474 | if ( config.timeout ) { 475 | clearTimeout( config.timeout ); 476 | } 477 | 478 | config.blocking = false; 479 | process( true ); 480 | }, 13); 481 | } else { 482 | config.blocking = false; 483 | process( true ); 484 | } 485 | }, 486 | 487 | stop: function( count ) { 488 | config.semaphore += count || 1; 489 | config.blocking = true; 490 | 491 | if ( config.testTimeout && defined.setTimeout ) { 492 | clearTimeout( config.timeout ); 493 | config.timeout = setTimeout(function() { 494 | QUnit.ok( false, "Test timed out" ); 495 | config.semaphore = 1; 496 | QUnit.start(); 497 | }, config.testTimeout ); 498 | } 499 | } 500 | }; 501 | 502 | // `assert` initialized at top of scope 503 | // Assert helpers 504 | // All of these must either call QUnit.push() or manually do: 505 | // - runLoggingCallbacks( "log", .. ); 506 | // - config.current.assertions.push({ .. }); 507 | // We attach it to the QUnit object *after* we expose the public API, 508 | // otherwise `assert` will become a global variable in browsers (#341). 509 | assert = { 510 | /** 511 | * Asserts rough true-ish result. 512 | * @name ok 513 | * @function 514 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 515 | */ 516 | ok: function( result, msg ) { 517 | if ( !config.current ) { 518 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 519 | } 520 | result = !!result; 521 | msg = msg || (result ? "okay" : "failed" ); 522 | 523 | var source, 524 | details = { 525 | module: config.current.module, 526 | name: config.current.testName, 527 | result: result, 528 | message: msg 529 | }; 530 | 531 | msg = "" + escapeText( msg ) + ""; 532 | 533 | if ( !result ) { 534 | source = sourceFromStacktrace( 2 ); 535 | if ( source ) { 536 | details.source = source; 537 | msg += "
    Source:
    " + escapeText( source ) + "
    "; 538 | } 539 | } 540 | runLoggingCallbacks( "log", QUnit, details ); 541 | config.current.assertions.push({ 542 | result: result, 543 | message: msg 544 | }); 545 | }, 546 | 547 | /** 548 | * Assert that the first two arguments are equal, with an optional message. 549 | * Prints out both actual and expected values. 550 | * @name equal 551 | * @function 552 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 553 | */ 554 | equal: function( actual, expected, message ) { 555 | /*jshint eqeqeq:false */ 556 | QUnit.push( expected == actual, actual, expected, message ); 557 | }, 558 | 559 | /** 560 | * @name notEqual 561 | * @function 562 | */ 563 | notEqual: function( actual, expected, message ) { 564 | /*jshint eqeqeq:false */ 565 | QUnit.push( expected != actual, actual, expected, message ); 566 | }, 567 | 568 | /** 569 | * @name propEqual 570 | * @function 571 | */ 572 | propEqual: function( actual, expected, message ) { 573 | actual = objectValues(actual); 574 | expected = objectValues(expected); 575 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 576 | }, 577 | 578 | /** 579 | * @name notPropEqual 580 | * @function 581 | */ 582 | notPropEqual: function( actual, expected, message ) { 583 | actual = objectValues(actual); 584 | expected = objectValues(expected); 585 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 586 | }, 587 | 588 | /** 589 | * @name deepEqual 590 | * @function 591 | */ 592 | deepEqual: function( actual, expected, message ) { 593 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 594 | }, 595 | 596 | /** 597 | * @name notDeepEqual 598 | * @function 599 | */ 600 | notDeepEqual: function( actual, expected, message ) { 601 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 602 | }, 603 | 604 | /** 605 | * @name strictEqual 606 | * @function 607 | */ 608 | strictEqual: function( actual, expected, message ) { 609 | QUnit.push( expected === actual, actual, expected, message ); 610 | }, 611 | 612 | /** 613 | * @name notStrictEqual 614 | * @function 615 | */ 616 | notStrictEqual: function( actual, expected, message ) { 617 | QUnit.push( expected !== actual, actual, expected, message ); 618 | }, 619 | 620 | "throws": function( block, expected, message ) { 621 | var actual, 622 | expectedOutput = expected, 623 | ok = false; 624 | 625 | // 'expected' is optional 626 | if ( typeof expected === "string" ) { 627 | message = expected; 628 | expected = null; 629 | } 630 | 631 | config.current.ignoreGlobalErrors = true; 632 | try { 633 | block.call( config.current.testEnvironment ); 634 | } catch (e) { 635 | actual = e; 636 | } 637 | config.current.ignoreGlobalErrors = false; 638 | 639 | if ( actual ) { 640 | // we don't want to validate thrown error 641 | if ( !expected ) { 642 | ok = true; 643 | expectedOutput = null; 644 | // expected is a regexp 645 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 646 | ok = expected.test( errorString( actual ) ); 647 | // expected is a constructor 648 | } else if ( actual instanceof expected ) { 649 | ok = true; 650 | // expected is a validation function which returns true is validation passed 651 | } else if ( expected.call( {}, actual ) === true ) { 652 | expectedOutput = null; 653 | ok = true; 654 | } 655 | 656 | QUnit.push( ok, actual, expectedOutput, message ); 657 | } else { 658 | QUnit.pushFailure( message, null, "No exception was thrown." ); 659 | } 660 | } 661 | }; 662 | 663 | /** 664 | * @deprecated since 1.8.0 665 | * Kept assertion helpers in root for backwards compatibility. 666 | */ 667 | extend( QUnit, assert ); 668 | 669 | /** 670 | * @deprecated since 1.9.0 671 | * Kept root "raises()" for backwards compatibility. 672 | * (Note that we don't introduce assert.raises). 673 | */ 674 | QUnit.raises = assert[ "throws" ]; 675 | 676 | /** 677 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 678 | * Kept to avoid TypeErrors for undefined methods. 679 | */ 680 | QUnit.equals = function() { 681 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 682 | }; 683 | QUnit.same = function() { 684 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 685 | }; 686 | 687 | // We want access to the constructor's prototype 688 | (function() { 689 | function F() {} 690 | F.prototype = QUnit; 691 | QUnit = new F(); 692 | // Make F QUnit's constructor so that we can add to the prototype later 693 | QUnit.constructor = F; 694 | }()); 695 | 696 | /** 697 | * Config object: Maintain internal state 698 | * Later exposed as QUnit.config 699 | * `config` initialized at top of scope 700 | */ 701 | config = { 702 | // The queue of tests to run 703 | queue: [], 704 | 705 | // block until document ready 706 | blocking: true, 707 | 708 | // when enabled, show only failing tests 709 | // gets persisted through sessionStorage and can be changed in UI via checkbox 710 | hidepassed: false, 711 | 712 | // by default, run previously failed tests first 713 | // very useful in combination with "Hide passed tests" checked 714 | reorder: true, 715 | 716 | // by default, modify document.title when suite is done 717 | altertitle: true, 718 | 719 | // when enabled, all tests must call expect() 720 | requireExpects: false, 721 | 722 | // add checkboxes that are persisted in the query-string 723 | // when enabled, the id is set to `true` as a `QUnit.config` property 724 | urlConfig: [ 725 | { 726 | id: "noglobals", 727 | label: "Check for Globals", 728 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 729 | }, 730 | { 731 | id: "notrycatch", 732 | label: "No try-catch", 733 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 734 | } 735 | ], 736 | 737 | // Set of all modules. 738 | modules: {}, 739 | 740 | // logging callback queues 741 | begin: [], 742 | done: [], 743 | log: [], 744 | testStart: [], 745 | testDone: [], 746 | moduleStart: [], 747 | moduleDone: [] 748 | }; 749 | 750 | // Export global variables, unless an 'exports' object exists, 751 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 752 | if ( typeof exports === "undefined" ) { 753 | extend( window, QUnit.constructor.prototype ); 754 | 755 | // Expose QUnit object 756 | window.QUnit = QUnit; 757 | } 758 | 759 | // Initialize more QUnit.config and QUnit.urlParams 760 | (function() { 761 | var i, 762 | location = window.location || { search: "", protocol: "file:" }, 763 | params = location.search.slice( 1 ).split( "&" ), 764 | length = params.length, 765 | urlParams = {}, 766 | current; 767 | 768 | if ( params[ 0 ] ) { 769 | for ( i = 0; i < length; i++ ) { 770 | current = params[ i ].split( "=" ); 771 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 772 | // allow just a key to turn on a flag, e.g., test.html?noglobals 773 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 774 | urlParams[ current[ 0 ] ] = current[ 1 ]; 775 | } 776 | } 777 | 778 | QUnit.urlParams = urlParams; 779 | 780 | // String search anywhere in moduleName+testName 781 | config.filter = urlParams.filter; 782 | 783 | // Exact match of the module name 784 | config.module = urlParams.module; 785 | 786 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 787 | 788 | // Figure out if we're running the tests from a server or not 789 | QUnit.isLocal = location.protocol === "file:"; 790 | }()); 791 | 792 | // Extend QUnit object, 793 | // these after set here because they should not be exposed as global functions 794 | extend( QUnit, { 795 | assert: assert, 796 | 797 | config: config, 798 | 799 | // Initialize the configuration options 800 | init: function() { 801 | extend( config, { 802 | stats: { all: 0, bad: 0 }, 803 | moduleStats: { all: 0, bad: 0 }, 804 | started: +new Date(), 805 | updateRate: 1000, 806 | blocking: false, 807 | autostart: true, 808 | autorun: false, 809 | filter: "", 810 | queue: [], 811 | semaphore: 1 812 | }); 813 | 814 | var tests, banner, result, 815 | qunit = id( "qunit" ); 816 | 817 | if ( qunit ) { 818 | qunit.innerHTML = 819 | "

    " + escapeText( document.title ) + "

    " + 820 | "

    " + 821 | "
    " + 822 | "

    " + 823 | "
      "; 824 | } 825 | 826 | tests = id( "qunit-tests" ); 827 | banner = id( "qunit-banner" ); 828 | result = id( "qunit-testresult" ); 829 | 830 | if ( tests ) { 831 | tests.innerHTML = ""; 832 | } 833 | 834 | if ( banner ) { 835 | banner.className = ""; 836 | } 837 | 838 | if ( result ) { 839 | result.parentNode.removeChild( result ); 840 | } 841 | 842 | if ( tests ) { 843 | result = document.createElement( "p" ); 844 | result.id = "qunit-testresult"; 845 | result.className = "result"; 846 | tests.parentNode.insertBefore( result, tests ); 847 | result.innerHTML = "Running...
       "; 848 | } 849 | }, 850 | 851 | // Resets the test setup. Useful for tests that modify the DOM. 852 | /* 853 | DEPRECATED: Use multiple tests instead of resetting inside a test. 854 | Use testStart or testDone for custom cleanup. 855 | This method will throw an error in 2.0, and will be removed in 2.1 856 | */ 857 | reset: function() { 858 | var fixture = id( "qunit-fixture" ); 859 | if ( fixture ) { 860 | fixture.innerHTML = config.fixture; 861 | } 862 | }, 863 | 864 | // Trigger an event on an element. 865 | // @example triggerEvent( document.body, "click" ); 866 | triggerEvent: function( elem, type, event ) { 867 | if ( document.createEvent ) { 868 | event = document.createEvent( "MouseEvents" ); 869 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 870 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 871 | 872 | elem.dispatchEvent( event ); 873 | } else if ( elem.fireEvent ) { 874 | elem.fireEvent( "on" + type ); 875 | } 876 | }, 877 | 878 | // Safe object type checking 879 | is: function( type, obj ) { 880 | return QUnit.objectType( obj ) === type; 881 | }, 882 | 883 | objectType: function( obj ) { 884 | if ( typeof obj === "undefined" ) { 885 | return "undefined"; 886 | // consider: typeof null === object 887 | } 888 | if ( obj === null ) { 889 | return "null"; 890 | } 891 | 892 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 893 | type = match && match[1] || ""; 894 | 895 | switch ( type ) { 896 | case "Number": 897 | if ( isNaN(obj) ) { 898 | return "nan"; 899 | } 900 | return "number"; 901 | case "String": 902 | case "Boolean": 903 | case "Array": 904 | case "Date": 905 | case "RegExp": 906 | case "Function": 907 | return type.toLowerCase(); 908 | } 909 | if ( typeof obj === "object" ) { 910 | return "object"; 911 | } 912 | return undefined; 913 | }, 914 | 915 | push: function( result, actual, expected, message ) { 916 | if ( !config.current ) { 917 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 918 | } 919 | 920 | var output, source, 921 | details = { 922 | module: config.current.module, 923 | name: config.current.testName, 924 | result: result, 925 | message: message, 926 | actual: actual, 927 | expected: expected 928 | }; 929 | 930 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 931 | message = "" + message + ""; 932 | output = message; 933 | 934 | if ( !result ) { 935 | expected = escapeText( QUnit.jsDump.parse(expected) ); 936 | actual = escapeText( QUnit.jsDump.parse(actual) ); 937 | output += ""; 938 | 939 | if ( actual !== expected ) { 940 | output += ""; 941 | output += ""; 942 | } 943 | 944 | source = sourceFromStacktrace(); 945 | 946 | if ( source ) { 947 | details.source = source; 948 | output += ""; 949 | } 950 | 951 | output += "
      Expected:
      " + expected + "
      Result:
      " + actual + "
      Diff:
      " + QUnit.diff( expected, actual ) + "
      Source:
      " + escapeText( source ) + "
      "; 952 | } 953 | 954 | runLoggingCallbacks( "log", QUnit, details ); 955 | 956 | config.current.assertions.push({ 957 | result: !!result, 958 | message: output 959 | }); 960 | }, 961 | 962 | pushFailure: function( message, source, actual ) { 963 | if ( !config.current ) { 964 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 965 | } 966 | 967 | var output, 968 | details = { 969 | module: config.current.module, 970 | name: config.current.testName, 971 | result: false, 972 | message: message 973 | }; 974 | 975 | message = escapeText( message ) || "error"; 976 | message = "" + message + ""; 977 | output = message; 978 | 979 | output += ""; 980 | 981 | if ( actual ) { 982 | output += ""; 983 | } 984 | 985 | if ( source ) { 986 | details.source = source; 987 | output += ""; 988 | } 989 | 990 | output += "
      Result:
      " + escapeText( actual ) + "
      Source:
      " + escapeText( source ) + "
      "; 991 | 992 | runLoggingCallbacks( "log", QUnit, details ); 993 | 994 | config.current.assertions.push({ 995 | result: false, 996 | message: output 997 | }); 998 | }, 999 | 1000 | url: function( params ) { 1001 | params = extend( extend( {}, QUnit.urlParams ), params ); 1002 | var key, 1003 | querystring = "?"; 1004 | 1005 | for ( key in params ) { 1006 | if ( hasOwn.call( params, key ) ) { 1007 | querystring += encodeURIComponent( key ) + "=" + 1008 | encodeURIComponent( params[ key ] ) + "&"; 1009 | } 1010 | } 1011 | return window.location.protocol + "//" + window.location.host + 1012 | window.location.pathname + querystring.slice( 0, -1 ); 1013 | }, 1014 | 1015 | extend: extend, 1016 | id: id, 1017 | addEvent: addEvent, 1018 | addClass: addClass, 1019 | hasClass: hasClass, 1020 | removeClass: removeClass 1021 | // load, equiv, jsDump, diff: Attached later 1022 | }); 1023 | 1024 | /** 1025 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 1026 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 1027 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 1028 | * Doing this allows us to tell if the following methods have been overwritten on the actual 1029 | * QUnit object. 1030 | */ 1031 | extend( QUnit.constructor.prototype, { 1032 | 1033 | // Logging callbacks; all receive a single argument with the listed properties 1034 | // run test/logs.html for any related changes 1035 | begin: registerLoggingCallback( "begin" ), 1036 | 1037 | // done: { failed, passed, total, runtime } 1038 | done: registerLoggingCallback( "done" ), 1039 | 1040 | // log: { result, actual, expected, message } 1041 | log: registerLoggingCallback( "log" ), 1042 | 1043 | // testStart: { name } 1044 | testStart: registerLoggingCallback( "testStart" ), 1045 | 1046 | // testDone: { name, failed, passed, total, duration } 1047 | testDone: registerLoggingCallback( "testDone" ), 1048 | 1049 | // moduleStart: { name } 1050 | moduleStart: registerLoggingCallback( "moduleStart" ), 1051 | 1052 | // moduleDone: { name, failed, passed, total } 1053 | moduleDone: registerLoggingCallback( "moduleDone" ) 1054 | }); 1055 | 1056 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 1057 | config.autorun = true; 1058 | } 1059 | 1060 | QUnit.load = function() { 1061 | runLoggingCallbacks( "begin", QUnit, {} ); 1062 | 1063 | // Initialize the config, saving the execution queue 1064 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, 1065 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, 1066 | numModules = 0, 1067 | moduleNames = [], 1068 | moduleFilterHtml = "", 1069 | urlConfigHtml = "", 1070 | oldconfig = extend( {}, config ); 1071 | 1072 | QUnit.init(); 1073 | extend(config, oldconfig); 1074 | 1075 | config.blocking = false; 1076 | 1077 | len = config.urlConfig.length; 1078 | 1079 | for ( i = 0; i < len; i++ ) { 1080 | val = config.urlConfig[i]; 1081 | if ( typeof val === "string" ) { 1082 | val = { 1083 | id: val, 1084 | label: val, 1085 | tooltip: "[no tooltip available]" 1086 | }; 1087 | } 1088 | config[ val.id ] = QUnit.urlParams[ val.id ]; 1089 | urlConfigHtml += ""; 1095 | } 1096 | for ( i in config.modules ) { 1097 | if ( config.modules.hasOwnProperty( i ) ) { 1098 | moduleNames.push(i); 1099 | } 1100 | } 1101 | numModules = moduleNames.length; 1102 | moduleNames.sort( function( a, b ) { 1103 | return a.localeCompare( b ); 1104 | }); 1105 | moduleFilterHtml += ""; 1116 | 1117 | // `userAgent` initialized at top of scope 1118 | userAgent = id( "qunit-userAgent" ); 1119 | if ( userAgent ) { 1120 | userAgent.innerHTML = navigator.userAgent; 1121 | } 1122 | 1123 | // `banner` initialized at top of scope 1124 | banner = id( "qunit-header" ); 1125 | if ( banner ) { 1126 | banner.innerHTML = "" + banner.innerHTML + " "; 1127 | } 1128 | 1129 | // `toolbar` initialized at top of scope 1130 | toolbar = id( "qunit-testrunner-toolbar" ); 1131 | if ( toolbar ) { 1132 | // `filter` initialized at top of scope 1133 | filter = document.createElement( "input" ); 1134 | filter.type = "checkbox"; 1135 | filter.id = "qunit-filter-pass"; 1136 | 1137 | addEvent( filter, "click", function() { 1138 | var tmp, 1139 | ol = document.getElementById( "qunit-tests" ); 1140 | 1141 | if ( filter.checked ) { 1142 | ol.className = ol.className + " hidepass"; 1143 | } else { 1144 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 1145 | ol.className = tmp.replace( / hidepass /, " " ); 1146 | } 1147 | if ( defined.sessionStorage ) { 1148 | if (filter.checked) { 1149 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 1150 | } else { 1151 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 1152 | } 1153 | } 1154 | }); 1155 | 1156 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1157 | filter.checked = true; 1158 | // `ol` initialized at top of scope 1159 | ol = document.getElementById( "qunit-tests" ); 1160 | ol.className = ol.className + " hidepass"; 1161 | } 1162 | toolbar.appendChild( filter ); 1163 | 1164 | // `label` initialized at top of scope 1165 | label = document.createElement( "label" ); 1166 | label.setAttribute( "for", "qunit-filter-pass" ); 1167 | label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); 1168 | label.innerHTML = "Hide passed tests"; 1169 | toolbar.appendChild( label ); 1170 | 1171 | urlConfigCheckboxesContainer = document.createElement("span"); 1172 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; 1173 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); 1174 | // For oldIE support: 1175 | // * Add handlers to the individual elements instead of the container 1176 | // * Use "click" instead of "change" 1177 | // * Fallback from event.target to event.srcElement 1178 | addEvents( urlConfigCheckboxes, "click", function( event ) { 1179 | var params = {}, 1180 | target = event.target || event.srcElement; 1181 | params[ target.name ] = target.checked ? true : undefined; 1182 | window.location = QUnit.url( params ); 1183 | }); 1184 | toolbar.appendChild( urlConfigCheckboxesContainer ); 1185 | 1186 | if (numModules > 1) { 1187 | moduleFilter = document.createElement( "span" ); 1188 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 1189 | moduleFilter.innerHTML = moduleFilterHtml; 1190 | addEvent( moduleFilter.lastChild, "change", function() { 1191 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1192 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1193 | 1194 | window.location = QUnit.url({ 1195 | module: ( selectedModule === "" ) ? undefined : selectedModule, 1196 | // Remove any existing filters 1197 | filter: undefined, 1198 | testNumber: undefined 1199 | }); 1200 | }); 1201 | toolbar.appendChild(moduleFilter); 1202 | } 1203 | } 1204 | 1205 | // `main` initialized at top of scope 1206 | main = id( "qunit-fixture" ); 1207 | if ( main ) { 1208 | config.fixture = main.innerHTML; 1209 | } 1210 | 1211 | if ( config.autostart ) { 1212 | QUnit.start(); 1213 | } 1214 | }; 1215 | 1216 | addEvent( window, "load", QUnit.load ); 1217 | 1218 | // `onErrorFnPrev` initialized at top of scope 1219 | // Preserve other handlers 1220 | onErrorFnPrev = window.onerror; 1221 | 1222 | // Cover uncaught exceptions 1223 | // Returning true will suppress the default browser handler, 1224 | // returning false will let it run. 1225 | window.onerror = function ( error, filePath, linerNr ) { 1226 | var ret = false; 1227 | if ( onErrorFnPrev ) { 1228 | ret = onErrorFnPrev( error, filePath, linerNr ); 1229 | } 1230 | 1231 | // Treat return value as window.onerror itself does, 1232 | // Only do our handling if not suppressed. 1233 | if ( ret !== true ) { 1234 | if ( QUnit.config.current ) { 1235 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1236 | return true; 1237 | } 1238 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1239 | } else { 1240 | QUnit.test( "global failure", extend( function() { 1241 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1242 | }, { validTest: validTest } ) ); 1243 | } 1244 | return false; 1245 | } 1246 | 1247 | return ret; 1248 | }; 1249 | 1250 | function done() { 1251 | config.autorun = true; 1252 | 1253 | // Log the last module results 1254 | if ( config.currentModule ) { 1255 | runLoggingCallbacks( "moduleDone", QUnit, { 1256 | name: config.currentModule, 1257 | failed: config.moduleStats.bad, 1258 | passed: config.moduleStats.all - config.moduleStats.bad, 1259 | total: config.moduleStats.all 1260 | }); 1261 | } 1262 | delete config.previousModule; 1263 | 1264 | var i, key, 1265 | banner = id( "qunit-banner" ), 1266 | tests = id( "qunit-tests" ), 1267 | runtime = +new Date() - config.started, 1268 | passed = config.stats.all - config.stats.bad, 1269 | html = [ 1270 | "Tests completed in ", 1271 | runtime, 1272 | " milliseconds.
      ", 1273 | "", 1274 | passed, 1275 | " assertions of ", 1276 | config.stats.all, 1277 | " passed, ", 1278 | config.stats.bad, 1279 | " failed." 1280 | ].join( "" ); 1281 | 1282 | if ( banner ) { 1283 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1284 | } 1285 | 1286 | if ( tests ) { 1287 | id( "qunit-testresult" ).innerHTML = html; 1288 | } 1289 | 1290 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1291 | // show ✖ for good, ✔ for bad suite result in title 1292 | // use escape sequences in case file gets loaded with non-utf-8-charset 1293 | document.title = [ 1294 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1295 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1296 | ].join( " " ); 1297 | } 1298 | 1299 | // clear own sessionStorage items if all tests passed 1300 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1301 | // `key` & `i` initialized at top of scope 1302 | for ( i = 0; i < sessionStorage.length; i++ ) { 1303 | key = sessionStorage.key( i++ ); 1304 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1305 | sessionStorage.removeItem( key ); 1306 | } 1307 | } 1308 | } 1309 | 1310 | // scroll back to top to show results 1311 | if ( window.scrollTo ) { 1312 | window.scrollTo(0, 0); 1313 | } 1314 | 1315 | runLoggingCallbacks( "done", QUnit, { 1316 | failed: config.stats.bad, 1317 | passed: passed, 1318 | total: config.stats.all, 1319 | runtime: runtime 1320 | }); 1321 | } 1322 | 1323 | /** @return Boolean: true if this test should be ran */ 1324 | function validTest( test ) { 1325 | var include, 1326 | filter = config.filter && config.filter.toLowerCase(), 1327 | module = config.module && config.module.toLowerCase(), 1328 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1329 | 1330 | // Internally-generated tests are always valid 1331 | if ( test.callback && test.callback.validTest === validTest ) { 1332 | delete test.callback.validTest; 1333 | return true; 1334 | } 1335 | 1336 | if ( config.testNumber ) { 1337 | return test.testNumber === config.testNumber; 1338 | } 1339 | 1340 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1341 | return false; 1342 | } 1343 | 1344 | if ( !filter ) { 1345 | return true; 1346 | } 1347 | 1348 | include = filter.charAt( 0 ) !== "!"; 1349 | if ( !include ) { 1350 | filter = filter.slice( 1 ); 1351 | } 1352 | 1353 | // If the filter matches, we need to honour include 1354 | if ( fullName.indexOf( filter ) !== -1 ) { 1355 | return include; 1356 | } 1357 | 1358 | // Otherwise, do the opposite 1359 | return !include; 1360 | } 1361 | 1362 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1363 | // Later Safari and IE10 are supposed to support error.stack as well 1364 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1365 | function extractStacktrace( e, offset ) { 1366 | offset = offset === undefined ? 3 : offset; 1367 | 1368 | var stack, include, i; 1369 | 1370 | if ( e.stacktrace ) { 1371 | // Opera 1372 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1373 | } else if ( e.stack ) { 1374 | // Firefox, Chrome 1375 | stack = e.stack.split( "\n" ); 1376 | if (/^error$/i.test( stack[0] ) ) { 1377 | stack.shift(); 1378 | } 1379 | if ( fileName ) { 1380 | include = []; 1381 | for ( i = offset; i < stack.length; i++ ) { 1382 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 1383 | break; 1384 | } 1385 | include.push( stack[ i ] ); 1386 | } 1387 | if ( include.length ) { 1388 | return include.join( "\n" ); 1389 | } 1390 | } 1391 | return stack[ offset ]; 1392 | } else if ( e.sourceURL ) { 1393 | // Safari, PhantomJS 1394 | // hopefully one day Safari provides actual stacktraces 1395 | // exclude useless self-reference for generated Error objects 1396 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1397 | return; 1398 | } 1399 | // for actual exceptions, this is useful 1400 | return e.sourceURL + ":" + e.line; 1401 | } 1402 | } 1403 | function sourceFromStacktrace( offset ) { 1404 | try { 1405 | throw new Error(); 1406 | } catch ( e ) { 1407 | return extractStacktrace( e, offset ); 1408 | } 1409 | } 1410 | 1411 | /** 1412 | * Escape text for attribute or text content. 1413 | */ 1414 | function escapeText( s ) { 1415 | if ( !s ) { 1416 | return ""; 1417 | } 1418 | s = s + ""; 1419 | // Both single quotes and double quotes (for attributes) 1420 | return s.replace( /['"<>&]/g, function( s ) { 1421 | switch( s ) { 1422 | case "'": 1423 | return "'"; 1424 | case "\"": 1425 | return """; 1426 | case "<": 1427 | return "<"; 1428 | case ">": 1429 | return ">"; 1430 | case "&": 1431 | return "&"; 1432 | } 1433 | }); 1434 | } 1435 | 1436 | function synchronize( callback, last ) { 1437 | config.queue.push( callback ); 1438 | 1439 | if ( config.autorun && !config.blocking ) { 1440 | process( last ); 1441 | } 1442 | } 1443 | 1444 | function process( last ) { 1445 | function next() { 1446 | process( last ); 1447 | } 1448 | var start = new Date().getTime(); 1449 | config.depth = config.depth ? config.depth + 1 : 1; 1450 | 1451 | while ( config.queue.length && !config.blocking ) { 1452 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1453 | config.queue.shift()(); 1454 | } else { 1455 | setTimeout( next, 13 ); 1456 | break; 1457 | } 1458 | } 1459 | config.depth--; 1460 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1461 | done(); 1462 | } 1463 | } 1464 | 1465 | function saveGlobal() { 1466 | config.pollution = []; 1467 | 1468 | if ( config.noglobals ) { 1469 | for ( var key in window ) { 1470 | if ( hasOwn.call( window, key ) ) { 1471 | // in Opera sometimes DOM element ids show up here, ignore them 1472 | if ( /^qunit-test-output/.test( key ) ) { 1473 | continue; 1474 | } 1475 | config.pollution.push( key ); 1476 | } 1477 | } 1478 | } 1479 | } 1480 | 1481 | function checkPollution() { 1482 | var newGlobals, 1483 | deletedGlobals, 1484 | old = config.pollution; 1485 | 1486 | saveGlobal(); 1487 | 1488 | newGlobals = diff( config.pollution, old ); 1489 | if ( newGlobals.length > 0 ) { 1490 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1491 | } 1492 | 1493 | deletedGlobals = diff( old, config.pollution ); 1494 | if ( deletedGlobals.length > 0 ) { 1495 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1496 | } 1497 | } 1498 | 1499 | // returns a new Array with the elements that are in a but not in b 1500 | function diff( a, b ) { 1501 | var i, j, 1502 | result = a.slice(); 1503 | 1504 | for ( i = 0; i < result.length; i++ ) { 1505 | for ( j = 0; j < b.length; j++ ) { 1506 | if ( result[i] === b[j] ) { 1507 | result.splice( i, 1 ); 1508 | i--; 1509 | break; 1510 | } 1511 | } 1512 | } 1513 | return result; 1514 | } 1515 | 1516 | function extend( a, b ) { 1517 | for ( var prop in b ) { 1518 | if ( hasOwn.call( b, prop ) ) { 1519 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 1520 | if ( !( prop === "constructor" && a === window ) ) { 1521 | if ( b[ prop ] === undefined ) { 1522 | delete a[ prop ]; 1523 | } else { 1524 | a[ prop ] = b[ prop ]; 1525 | } 1526 | } 1527 | } 1528 | } 1529 | 1530 | return a; 1531 | } 1532 | 1533 | /** 1534 | * @param {HTMLElement} elem 1535 | * @param {string} type 1536 | * @param {Function} fn 1537 | */ 1538 | function addEvent( elem, type, fn ) { 1539 | // Standards-based browsers 1540 | if ( elem.addEventListener ) { 1541 | elem.addEventListener( type, fn, false ); 1542 | // IE 1543 | } else { 1544 | elem.attachEvent( "on" + type, fn ); 1545 | } 1546 | } 1547 | 1548 | /** 1549 | * @param {Array|NodeList} elems 1550 | * @param {string} type 1551 | * @param {Function} fn 1552 | */ 1553 | function addEvents( elems, type, fn ) { 1554 | var i = elems.length; 1555 | while ( i-- ) { 1556 | addEvent( elems[i], type, fn ); 1557 | } 1558 | } 1559 | 1560 | function hasClass( elem, name ) { 1561 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1562 | } 1563 | 1564 | function addClass( elem, name ) { 1565 | if ( !hasClass( elem, name ) ) { 1566 | elem.className += (elem.className ? " " : "") + name; 1567 | } 1568 | } 1569 | 1570 | function removeClass( elem, name ) { 1571 | var set = " " + elem.className + " "; 1572 | // Class name may appear multiple times 1573 | while ( set.indexOf(" " + name + " ") > -1 ) { 1574 | set = set.replace(" " + name + " " , " "); 1575 | } 1576 | // If possible, trim it for prettiness, but not necessarily 1577 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); 1578 | } 1579 | 1580 | function id( name ) { 1581 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1582 | document.getElementById( name ); 1583 | } 1584 | 1585 | function registerLoggingCallback( key ) { 1586 | return function( callback ) { 1587 | config[key].push( callback ); 1588 | }; 1589 | } 1590 | 1591 | // Supports deprecated method of completely overwriting logging callbacks 1592 | function runLoggingCallbacks( key, scope, args ) { 1593 | var i, callbacks; 1594 | if ( QUnit.hasOwnProperty( key ) ) { 1595 | QUnit[ key ].call(scope, args ); 1596 | } else { 1597 | callbacks = config[ key ]; 1598 | for ( i = 0; i < callbacks.length; i++ ) { 1599 | callbacks[ i ].call( scope, args ); 1600 | } 1601 | } 1602 | } 1603 | 1604 | // Test for equality any JavaScript type. 1605 | // Author: Philippe Rathé 1606 | QUnit.equiv = (function() { 1607 | 1608 | // Call the o related callback with the given arguments. 1609 | function bindCallbacks( o, callbacks, args ) { 1610 | var prop = QUnit.objectType( o ); 1611 | if ( prop ) { 1612 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1613 | return callbacks[ prop ].apply( callbacks, args ); 1614 | } else { 1615 | return callbacks[ prop ]; // or undefined 1616 | } 1617 | } 1618 | } 1619 | 1620 | // the real equiv function 1621 | var innerEquiv, 1622 | // stack to decide between skip/abort functions 1623 | callers = [], 1624 | // stack to avoiding loops from circular referencing 1625 | parents = [], 1626 | parentsB = [], 1627 | 1628 | getProto = Object.getPrototypeOf || function ( obj ) { 1629 | /*jshint camelcase:false */ 1630 | return obj.__proto__; 1631 | }, 1632 | callbacks = (function () { 1633 | 1634 | // for string, boolean, number and null 1635 | function useStrictEquality( b, a ) { 1636 | /*jshint eqeqeq:false */ 1637 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1638 | // to catch short annotation VS 'new' annotation of a 1639 | // declaration 1640 | // e.g. var i = 1; 1641 | // var j = new Number(1); 1642 | return a == b; 1643 | } else { 1644 | return a === b; 1645 | } 1646 | } 1647 | 1648 | return { 1649 | "string": useStrictEquality, 1650 | "boolean": useStrictEquality, 1651 | "number": useStrictEquality, 1652 | "null": useStrictEquality, 1653 | "undefined": useStrictEquality, 1654 | 1655 | "nan": function( b ) { 1656 | return isNaN( b ); 1657 | }, 1658 | 1659 | "date": function( b, a ) { 1660 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1661 | }, 1662 | 1663 | "regexp": function( b, a ) { 1664 | return QUnit.objectType( b ) === "regexp" && 1665 | // the regex itself 1666 | a.source === b.source && 1667 | // and its modifiers 1668 | a.global === b.global && 1669 | // (gmi) ... 1670 | a.ignoreCase === b.ignoreCase && 1671 | a.multiline === b.multiline && 1672 | a.sticky === b.sticky; 1673 | }, 1674 | 1675 | // - skip when the property is a method of an instance (OOP) 1676 | // - abort otherwise, 1677 | // initial === would have catch identical references anyway 1678 | "function": function() { 1679 | var caller = callers[callers.length - 1]; 1680 | return caller !== Object && typeof caller !== "undefined"; 1681 | }, 1682 | 1683 | "array": function( b, a ) { 1684 | var i, j, len, loop, aCircular, bCircular; 1685 | 1686 | // b could be an object literal here 1687 | if ( QUnit.objectType( b ) !== "array" ) { 1688 | return false; 1689 | } 1690 | 1691 | len = a.length; 1692 | if ( len !== b.length ) { 1693 | // safe and faster 1694 | return false; 1695 | } 1696 | 1697 | // track reference to avoid circular references 1698 | parents.push( a ); 1699 | parentsB.push( b ); 1700 | for ( i = 0; i < len; i++ ) { 1701 | loop = false; 1702 | for ( j = 0; j < parents.length; j++ ) { 1703 | aCircular = parents[j] === a[i]; 1704 | bCircular = parentsB[j] === b[i]; 1705 | if ( aCircular || bCircular ) { 1706 | if ( a[i] === b[i] || aCircular && bCircular ) { 1707 | loop = true; 1708 | } else { 1709 | parents.pop(); 1710 | parentsB.pop(); 1711 | return false; 1712 | } 1713 | } 1714 | } 1715 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1716 | parents.pop(); 1717 | parentsB.pop(); 1718 | return false; 1719 | } 1720 | } 1721 | parents.pop(); 1722 | parentsB.pop(); 1723 | return true; 1724 | }, 1725 | 1726 | "object": function( b, a ) { 1727 | /*jshint forin:false */ 1728 | var i, j, loop, aCircular, bCircular, 1729 | // Default to true 1730 | eq = true, 1731 | aProperties = [], 1732 | bProperties = []; 1733 | 1734 | // comparing constructors is more strict than using 1735 | // instanceof 1736 | if ( a.constructor !== b.constructor ) { 1737 | // Allow objects with no prototype to be equivalent to 1738 | // objects with Object as their constructor. 1739 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1740 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1741 | return false; 1742 | } 1743 | } 1744 | 1745 | // stack constructor before traversing properties 1746 | callers.push( a.constructor ); 1747 | 1748 | // track reference to avoid circular references 1749 | parents.push( a ); 1750 | parentsB.push( b ); 1751 | 1752 | // be strict: don't ensure hasOwnProperty and go deep 1753 | for ( i in a ) { 1754 | loop = false; 1755 | for ( j = 0; j < parents.length; j++ ) { 1756 | aCircular = parents[j] === a[i]; 1757 | bCircular = parentsB[j] === b[i]; 1758 | if ( aCircular || bCircular ) { 1759 | if ( a[i] === b[i] || aCircular && bCircular ) { 1760 | loop = true; 1761 | } else { 1762 | eq = false; 1763 | break; 1764 | } 1765 | } 1766 | } 1767 | aProperties.push(i); 1768 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1769 | eq = false; 1770 | break; 1771 | } 1772 | } 1773 | 1774 | parents.pop(); 1775 | parentsB.pop(); 1776 | callers.pop(); // unstack, we are done 1777 | 1778 | for ( i in b ) { 1779 | bProperties.push( i ); // collect b's properties 1780 | } 1781 | 1782 | // Ensures identical properties name 1783 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1784 | } 1785 | }; 1786 | }()); 1787 | 1788 | innerEquiv = function() { // can take multiple arguments 1789 | var args = [].slice.apply( arguments ); 1790 | if ( args.length < 2 ) { 1791 | return true; // end transition 1792 | } 1793 | 1794 | return (function( a, b ) { 1795 | if ( a === b ) { 1796 | return true; // catch the most you can 1797 | } else if ( a === null || b === null || typeof a === "undefined" || 1798 | typeof b === "undefined" || 1799 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1800 | return false; // don't lose time with error prone cases 1801 | } else { 1802 | return bindCallbacks(a, callbacks, [ b, a ]); 1803 | } 1804 | 1805 | // apply transition with (1..n) arguments 1806 | }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); 1807 | }; 1808 | 1809 | return innerEquiv; 1810 | }()); 1811 | 1812 | /** 1813 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1814 | * http://flesler.blogspot.com Licensed under BSD 1815 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1816 | * 1817 | * @projectDescription Advanced and extensible data dumping for Javascript. 1818 | * @version 1.0.0 1819 | * @author Ariel Flesler 1820 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1821 | */ 1822 | QUnit.jsDump = (function() { 1823 | function quote( str ) { 1824 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; 1825 | } 1826 | function literal( o ) { 1827 | return o + ""; 1828 | } 1829 | function join( pre, arr, post ) { 1830 | var s = jsDump.separator(), 1831 | base = jsDump.indent(), 1832 | inner = jsDump.indent(1); 1833 | if ( arr.join ) { 1834 | arr = arr.join( "," + s + inner ); 1835 | } 1836 | if ( !arr ) { 1837 | return pre + post; 1838 | } 1839 | return [ pre, inner + arr, base + post ].join(s); 1840 | } 1841 | function array( arr, stack ) { 1842 | var i = arr.length, ret = new Array(i); 1843 | this.up(); 1844 | while ( i-- ) { 1845 | ret[i] = this.parse( arr[i] , undefined , stack); 1846 | } 1847 | this.down(); 1848 | return join( "[", ret, "]" ); 1849 | } 1850 | 1851 | var reName = /^function (\w+)/, 1852 | jsDump = { 1853 | // type is used mostly internally, you can fix a (custom)type in advance 1854 | parse: function( obj, type, stack ) { 1855 | stack = stack || [ ]; 1856 | var inStack, res, 1857 | parser = this.parsers[ type || this.typeOf(obj) ]; 1858 | 1859 | type = typeof parser; 1860 | inStack = inArray( obj, stack ); 1861 | 1862 | if ( inStack !== -1 ) { 1863 | return "recursion(" + (inStack - stack.length) + ")"; 1864 | } 1865 | if ( type === "function" ) { 1866 | stack.push( obj ); 1867 | res = parser.call( this, obj, stack ); 1868 | stack.pop(); 1869 | return res; 1870 | } 1871 | return ( type === "string" ) ? parser : this.parsers.error; 1872 | }, 1873 | typeOf: function( obj ) { 1874 | var type; 1875 | if ( obj === null ) { 1876 | type = "null"; 1877 | } else if ( typeof obj === "undefined" ) { 1878 | type = "undefined"; 1879 | } else if ( QUnit.is( "regexp", obj) ) { 1880 | type = "regexp"; 1881 | } else if ( QUnit.is( "date", obj) ) { 1882 | type = "date"; 1883 | } else if ( QUnit.is( "function", obj) ) { 1884 | type = "function"; 1885 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1886 | type = "window"; 1887 | } else if ( obj.nodeType === 9 ) { 1888 | type = "document"; 1889 | } else if ( obj.nodeType ) { 1890 | type = "node"; 1891 | } else if ( 1892 | // native arrays 1893 | toString.call( obj ) === "[object Array]" || 1894 | // NodeList objects 1895 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1896 | ) { 1897 | type = "array"; 1898 | } else if ( obj.constructor === Error.prototype.constructor ) { 1899 | type = "error"; 1900 | } else { 1901 | type = typeof obj; 1902 | } 1903 | return type; 1904 | }, 1905 | separator: function() { 1906 | return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; 1907 | }, 1908 | // extra can be a number, shortcut for increasing-calling-decreasing 1909 | indent: function( extra ) { 1910 | if ( !this.multiline ) { 1911 | return ""; 1912 | } 1913 | var chr = this.indentChar; 1914 | if ( this.HTML ) { 1915 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1916 | } 1917 | return new Array( this.depth + ( extra || 0 ) ).join(chr); 1918 | }, 1919 | up: function( a ) { 1920 | this.depth += a || 1; 1921 | }, 1922 | down: function( a ) { 1923 | this.depth -= a || 1; 1924 | }, 1925 | setParser: function( name, parser ) { 1926 | this.parsers[name] = parser; 1927 | }, 1928 | // The next 3 are exposed so you can use them 1929 | quote: quote, 1930 | literal: literal, 1931 | join: join, 1932 | // 1933 | depth: 1, 1934 | // This is the list of parsers, to modify them, use jsDump.setParser 1935 | parsers: { 1936 | window: "[Window]", 1937 | document: "[Document]", 1938 | error: function(error) { 1939 | return "Error(\"" + error.message + "\")"; 1940 | }, 1941 | unknown: "[Unknown]", 1942 | "null": "null", 1943 | "undefined": "undefined", 1944 | "function": function( fn ) { 1945 | var ret = "function", 1946 | // functions never have name in IE 1947 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 1948 | 1949 | if ( name ) { 1950 | ret += " " + name; 1951 | } 1952 | ret += "( "; 1953 | 1954 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1955 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1956 | }, 1957 | array: array, 1958 | nodelist: array, 1959 | "arguments": array, 1960 | object: function( map, stack ) { 1961 | /*jshint forin:false */ 1962 | var ret = [ ], keys, key, val, i; 1963 | QUnit.jsDump.up(); 1964 | keys = []; 1965 | for ( key in map ) { 1966 | keys.push( key ); 1967 | } 1968 | keys.sort(); 1969 | for ( i = 0; i < keys.length; i++ ) { 1970 | key = keys[ i ]; 1971 | val = map[ key ]; 1972 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1973 | } 1974 | QUnit.jsDump.down(); 1975 | return join( "{", ret, "}" ); 1976 | }, 1977 | node: function( node ) { 1978 | var len, i, val, 1979 | open = QUnit.jsDump.HTML ? "<" : "<", 1980 | close = QUnit.jsDump.HTML ? ">" : ">", 1981 | tag = node.nodeName.toLowerCase(), 1982 | ret = open + tag, 1983 | attrs = node.attributes; 1984 | 1985 | if ( attrs ) { 1986 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1987 | val = attrs[i].nodeValue; 1988 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1989 | // Those have values like undefined, null, 0, false, "" or "inherit". 1990 | if ( val && val !== "inherit" ) { 1991 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 1992 | } 1993 | } 1994 | } 1995 | ret += close; 1996 | 1997 | // Show content of TextNode or CDATASection 1998 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1999 | ret += node.nodeValue; 2000 | } 2001 | 2002 | return ret + open + "/" + tag + close; 2003 | }, 2004 | // function calls it internally, it's the arguments part of the function 2005 | functionArgs: function( fn ) { 2006 | var args, 2007 | l = fn.length; 2008 | 2009 | if ( !l ) { 2010 | return ""; 2011 | } 2012 | 2013 | args = new Array(l); 2014 | while ( l-- ) { 2015 | // 97 is 'a' 2016 | args[l] = String.fromCharCode(97+l); 2017 | } 2018 | return " " + args.join( ", " ) + " "; 2019 | }, 2020 | // object calls it internally, the key part of an item in a map 2021 | key: quote, 2022 | // function calls it internally, it's the content of the function 2023 | functionCode: "[code]", 2024 | // node calls it internally, it's an html attribute value 2025 | attribute: quote, 2026 | string: quote, 2027 | date: quote, 2028 | regexp: literal, 2029 | number: literal, 2030 | "boolean": literal 2031 | }, 2032 | // if true, entities are escaped ( <, >, \t, space and \n ) 2033 | HTML: false, 2034 | // indentation unit 2035 | indentChar: " ", 2036 | // if true, items in a collection, are separated by a \n, else just a space. 2037 | multiline: true 2038 | }; 2039 | 2040 | return jsDump; 2041 | }()); 2042 | 2043 | // from jquery.js 2044 | function inArray( elem, array ) { 2045 | if ( array.indexOf ) { 2046 | return array.indexOf( elem ); 2047 | } 2048 | 2049 | for ( var i = 0, length = array.length; i < length; i++ ) { 2050 | if ( array[ i ] === elem ) { 2051 | return i; 2052 | } 2053 | } 2054 | 2055 | return -1; 2056 | } 2057 | 2058 | /* 2059 | * Javascript Diff Algorithm 2060 | * By John Resig (http://ejohn.org/) 2061 | * Modified by Chu Alan "sprite" 2062 | * 2063 | * Released under the MIT license. 2064 | * 2065 | * More Info: 2066 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2067 | * 2068 | * Usage: QUnit.diff(expected, actual) 2069 | * 2070 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2071 | */ 2072 | QUnit.diff = (function() { 2073 | /*jshint eqeqeq:false, eqnull:true */ 2074 | function diff( o, n ) { 2075 | var i, 2076 | ns = {}, 2077 | os = {}; 2078 | 2079 | for ( i = 0; i < n.length; i++ ) { 2080 | if ( !hasOwn.call( ns, n[i] ) ) { 2081 | ns[ n[i] ] = { 2082 | rows: [], 2083 | o: null 2084 | }; 2085 | } 2086 | ns[ n[i] ].rows.push( i ); 2087 | } 2088 | 2089 | for ( i = 0; i < o.length; i++ ) { 2090 | if ( !hasOwn.call( os, o[i] ) ) { 2091 | os[ o[i] ] = { 2092 | rows: [], 2093 | n: null 2094 | }; 2095 | } 2096 | os[ o[i] ].rows.push( i ); 2097 | } 2098 | 2099 | for ( i in ns ) { 2100 | if ( hasOwn.call( ns, i ) ) { 2101 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { 2102 | n[ ns[i].rows[0] ] = { 2103 | text: n[ ns[i].rows[0] ], 2104 | row: os[i].rows[0] 2105 | }; 2106 | o[ os[i].rows[0] ] = { 2107 | text: o[ os[i].rows[0] ], 2108 | row: ns[i].rows[0] 2109 | }; 2110 | } 2111 | } 2112 | } 2113 | 2114 | for ( i = 0; i < n.length - 1; i++ ) { 2115 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 2116 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 2117 | 2118 | n[ i + 1 ] = { 2119 | text: n[ i + 1 ], 2120 | row: n[i].row + 1 2121 | }; 2122 | o[ n[i].row + 1 ] = { 2123 | text: o[ n[i].row + 1 ], 2124 | row: i + 1 2125 | }; 2126 | } 2127 | } 2128 | 2129 | for ( i = n.length - 1; i > 0; i-- ) { 2130 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 2131 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 2132 | 2133 | n[ i - 1 ] = { 2134 | text: n[ i - 1 ], 2135 | row: n[i].row - 1 2136 | }; 2137 | o[ n[i].row - 1 ] = { 2138 | text: o[ n[i].row - 1 ], 2139 | row: i - 1 2140 | }; 2141 | } 2142 | } 2143 | 2144 | return { 2145 | o: o, 2146 | n: n 2147 | }; 2148 | } 2149 | 2150 | return function( o, n ) { 2151 | o = o.replace( /\s+$/, "" ); 2152 | n = n.replace( /\s+$/, "" ); 2153 | 2154 | var i, pre, 2155 | str = "", 2156 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 2157 | oSpace = o.match(/\s+/g), 2158 | nSpace = n.match(/\s+/g); 2159 | 2160 | if ( oSpace == null ) { 2161 | oSpace = [ " " ]; 2162 | } 2163 | else { 2164 | oSpace.push( " " ); 2165 | } 2166 | 2167 | if ( nSpace == null ) { 2168 | nSpace = [ " " ]; 2169 | } 2170 | else { 2171 | nSpace.push( " " ); 2172 | } 2173 | 2174 | if ( out.n.length === 0 ) { 2175 | for ( i = 0; i < out.o.length; i++ ) { 2176 | str += "" + out.o[i] + oSpace[i] + ""; 2177 | } 2178 | } 2179 | else { 2180 | if ( out.n[0].text == null ) { 2181 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 2182 | str += "" + out.o[n] + oSpace[n] + ""; 2183 | } 2184 | } 2185 | 2186 | for ( i = 0; i < out.n.length; i++ ) { 2187 | if (out.n[i].text == null) { 2188 | str += "" + out.n[i] + nSpace[i] + ""; 2189 | } 2190 | else { 2191 | // `pre` initialized at top of scope 2192 | pre = ""; 2193 | 2194 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 2195 | pre += "" + out.o[n] + oSpace[n] + ""; 2196 | } 2197 | str += " " + out.n[i].text + nSpace[i] + pre; 2198 | } 2199 | } 2200 | } 2201 | 2202 | return str; 2203 | }; 2204 | }()); 2205 | 2206 | // for CommonJS environments, export everything 2207 | if ( typeof exports !== "undefined" ) { 2208 | extend( exports, QUnit.constructor.prototype ); 2209 | } 2210 | 2211 | // get at whatever the global object is, like window in browsers 2212 | }( (function() {return this;}.call()) )); 2213 | --------------------------------------------------------------------------------