├── htdocs_example ├── favicon.ico ├── screenshot.gif ├── stdlib │ ├── font.woff │ ├── pixel.gif │ ├── btn_donate_LG.gif │ ├── font.css │ ├── analytics.js │ ├── jquery.details.min.js │ ├── common.css │ ├── mini_controls.js │ ├── mini_display.js │ ├── scriptprocessor_player.min.js │ └── jquery1.11.min.js ├── floppy_blinking2.gif ├── music │ └── LMan - Vortex.sid ├── add_on.css └── index.html ├── README.md └── scriptprocessor_player.js /htdocs_example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wothke/webaudio-player/HEAD/htdocs_example/favicon.ico -------------------------------------------------------------------------------- /htdocs_example/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wothke/webaudio-player/HEAD/htdocs_example/screenshot.gif -------------------------------------------------------------------------------- /htdocs_example/stdlib/font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wothke/webaudio-player/HEAD/htdocs_example/stdlib/font.woff -------------------------------------------------------------------------------- /htdocs_example/stdlib/pixel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wothke/webaudio-player/HEAD/htdocs_example/stdlib/pixel.gif -------------------------------------------------------------------------------- /htdocs_example/floppy_blinking2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wothke/webaudio-player/HEAD/htdocs_example/floppy_blinking2.gif -------------------------------------------------------------------------------- /htdocs_example/music/LMan - Vortex.sid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wothke/webaudio-player/HEAD/htdocs_example/music/LMan - Vortex.sid -------------------------------------------------------------------------------- /htdocs_example/stdlib/btn_donate_LG.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wothke/webaudio-player/HEAD/htdocs_example/stdlib/btn_donate_LG.gif -------------------------------------------------------------------------------- /htdocs_example/stdlib/font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Open Sans Light'), local('OpenSans-Light'), url(font.woff) format('woff'); 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generic ScriptProcessor based WebAudio player 2 | 3 | I've had enough of these Microsoft morons and I am no longer maintaining 4 | my repositories on github. 5 | 6 | This project can now be found here: 7 | 8 | 9 | https://bitbucket.org/wothke/webaudio-player -------------------------------------------------------------------------------- /htdocs_example/stdlib/analytics.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 5 | 6 | ga('create', 'UA-60370798-1', 'auto'); 7 | ga('send', 'pageview'); 8 | -------------------------------------------------------------------------------- /htdocs_example/stdlib/jquery.details.min.js: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/details v0.1.0 by @mathias | includes http://mths.be/noselect v1.0.3 */ 2 | ;(function(a,f){var e=f.fn,d,c=Object.prototype.toString.call(window.opera)=='[object Opera]',g=(function(l){var j=l.createElement('details'),i,h,k;if(!('open' in j)){return false}h=l.body||(function(){var m=l.documentElement;i=true;return m.insertBefore(l.createElement('body'),m.firstElementChild||m.firstChild)}());j.innerHTML='ab';j.style.display='block';h.appendChild(j);k=j.offsetHeight;j.open=true;k=k!=j.offsetHeight;h.removeChild(j);if(i){h.parentNode.removeChild(h)}return k}(a)),b=function(i,l,k,h){var j=i.prop('open'),m=j&&h||!j&&!h;if(m){i.removeClass('open').prop('open',false).triggerHandler('close.details');l.attr('aria-expanded',false);k.hide()}else{i.addClass('open').prop('open',true).triggerHandler('open.details');l.attr('aria-expanded',true);k.show()}};e.noSelect=function(){var h='none';return this.bind('selectstart dragstart mousedown',function(){return false}).css({MozUserSelect:h,msUserSelect:h,webkitUserSelect:h,userSelect:h})};if(g){d=e.details=function(){return this.each(function(){var i=f(this),h=f('summary',i).first();h.attr({role:'button','aria-expanded':i.prop('open')}).on('click',function(){var j=i.prop('open');h.attr('aria-expanded',!j);i.triggerHandler((j?'close':'open')+'.details')})})};d.support=g}else{d=e.details=function(){return this.each(function(){var h=f(this),j=f('summary',h).first(),i=h.children(':not(summary)'),k=h.contents(':not(summary)');if(!j.length){j=f('').text('Details').prependTo(h)}if(i.length!=k.length){k.filter(function(){return this.nodeType==3&&/[^ \t\n\f\r]/.test(this.data)}).wrap('');i=h.children(':not(summary)')}h.prop('open',typeof h.attr('open')=='string');b(h,j,i);j.attr('role','button').noSelect().prop('tabIndex',0).on('click',function(){j.focus();b(h,j,i,true)}).keyup(function(l){if(32==l.keyCode||(13==l.keyCode&&!c)){l.preventDefault();j.click()}})})};d.support=g}}(document,jQuery)); -------------------------------------------------------------------------------- /htdocs_example/add_on.css: -------------------------------------------------------------------------------- 1 | /* player specific positioning of the displayed 'frequency spectrum', etc */ 2 | .logo { 3 | position: relative; 4 | top: 40px; 5 | left: 800px; 6 | z-index:2; 7 | 8 | -webkit-box-reflect: below 10px -webkit-linear-gradient(top, transparent 10%, transparent 40%, rgba(255,255,255,0.9)); 9 | } 10 | #logo { 11 | -webkit-transform: translateX(-310px) translateY(-10px) translateZ(-440px) rotateX(-10deg) rotateY(-60deg) rotateZ(1deg); 12 | -moz-transform: translateX(-310px) translateY(-70px) translateZ(-440px) rotateX(-28deg) rotateY(-60deg) rotateZ(-6deg); 13 | } 14 | #moz-reflect-logo:after { 15 | content: ""; 16 | display: none; 17 | } 18 | 19 | #moz-reflect-logo.enableMozReflection:after { 20 | display: block; 21 | 22 | background: -moz-linear-gradient(top, white, white 30%, rgba(255,255,255,0.6) 65%, rgba(255,255,255,0.3)) -60px, 23 | -moz-element(#moz-reflect-logo) 0px -77px no-repeat; 24 | position:relative; 25 | width: auto; 26 | height: 200px; 27 | margin-bottom: 0px; 28 | -moz-transform: scaleY(-1); 29 | } 30 | .spectrum { 31 | position: relative; 32 | top: -20px; 33 | left: 265px; 34 | z-index:1; 35 | 36 | -webkit-box-reflect: below 5px -webkit-linear-gradient(top, transparent, transparent 50%, rgba(255,255,255,0.4)); 37 | } 38 | #spectrum { 39 | -webkit-transform: translateX(-150px) translateY(-250px) translateZ(-500px) rotateX(-30deg) rotateY(45deg); 40 | -moz-transform: translateX(-150px) translateY(-510px) translateZ(-440px) rotateX(-40deg) rotateY(45deg); 41 | } 42 | 43 | #moz-reflect-spectrum:after { 44 | content: ""; 45 | display: none; 46 | } 47 | 48 | #moz-reflect-spectrum.enableMozReflection:after { 49 | display: block; 50 | background: -moz-linear-gradient(top, white, white 30%, rgba(255,255,255,0.6) 65%, rgba(255,255,255,0.3)) -60px, 51 | -moz-element(#moz-reflect-spectrum) 0px -27px no-repeat; 52 | position:relative; 53 | top: 0px; 54 | width: auto; 55 | height: 155px; 56 | margin-bottom: 0px; 57 | -moz-transform: scaleY(-1); 58 | } 59 | 60 | .drop { 61 | position: relative; 62 | width:300px; 63 | height:300px; 64 | } 65 | #drop { 66 | -webkit-transform: translateX(180px) translateY(-130px) translateZ(0px) rotateX(10deg) rotateY(-5deg) rotateZ(8deg); 67 | -moz-transform: translateX(180px) translateY(-400px) translateZ(0px) rotateX(10deg) rotateY(-5deg) rotateZ(8deg); 68 | } 69 | -------------------------------------------------------------------------------- /htdocs_example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 30 | 31 | TinyJSid - the first HTML5/JavaScript C64 music player 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 122 | 123 | 124 | 125 | 126 |
127 | 128 |
129 | What's this? 130 |
131 |

Experimental JavaScript/HTML5 version of Tiny'R'Sid.

132 | 133 |

2013 by Juergen Wothke (The source code can be found here.)

134 | 135 |

This page does not use any plugins but is based exclusively on the draft version WebAudio API. You'll need Chrome or Firefox to make it play the music. The visual effects work best in Chrome. (If Firefox passes out - press 'reload'... it's experimental.)

136 | 137 |

Contrarry to most other HTML5 based pages out there, the music here is NOT based on OscillatorNode based waveforms or the playback of some sampledata file. Instead the samples here are completely calculated within JavaScript by running the Tiny'R'Sid emulator logic.

138 | 139 |

Please use the below controls to navigate between the songs that you have dropped on the player: 140 | 141 |

142 |
143 | 144 |
145 | 148 |
149 |
150 |
151 |
152 |
153 | 154 | 155 | -------------------------------------------------------------------------------- /htdocs_example/stdlib/common.css: -------------------------------------------------------------------------------- 1 | /* general definitions for background,
, etc*/ 2 | ::selection { 3 | color: #eee; 4 | background: darkred; 5 | } 6 | body ::-webkit-scrollbar { 7 | height: 16px; 8 | overflow: visible; 9 | width: 16px; 10 | } 11 | body ::-webkit-scrollbar-thumb { 12 | background-color: rgba(0, 0, 0, .1); 13 | background-clip: padding-box; 14 | border: solid transparent; 15 | min-height: 28px; 16 | padding: 100px 0 0; 17 | box-shadow: inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07); 18 | border-width: 1px 1px 1px 6px; 19 | } 20 | body ::-webkit-scrollbar-thumb:hover { 21 | background-color: rgba(0, 0, 0, 0.5); 22 | } 23 | body ::-webkit-scrollbar-button { 24 | height: 0; 25 | width: 0; 26 | } 27 | ::-webkit-scrollbar-track { 28 | background-clip: padding-box; 29 | border: solid transparent; 30 | border-width: 0 0 0 4px; 31 | } 32 | body ::-webkit-scrollbar-corner { 33 | background: transparent; 34 | } 35 | body { 36 | color: #222; 37 | font-family: 'Open Sans', arial, sans-serif; 38 | font-weight: 300; 39 | -webkit-font-smoothing: antialiased; 40 | padding: 2em; 41 | background: -webkit-gradient(radial, center center, 500, center center, 1400, from(rgba(0,0,0,0)), to(rgba(0,0,0,0.6))) #fff; 42 | background: -moz-radial-gradient(farthest-side, rgba(0,0,0,0) 90%, rgba(0,0,0,0.2) 150%) #fff; 43 | background: -webkit-radial-gradient(farthest-side, rgba(0,0,0,0) 90%, rgba(0,0,0,0.2) 150%) #fff; 44 | background: -ms-radial-gradient(farthest-side, rgba(0,0,0,0) 90%, rgba(0,0,0,0.2) 150%) #fff; 45 | background: -o-radial-gradient(farthest-side, rgba(0,0,0,0) 90%, rgba(0,0,0,0.2) 150%) #fff; 46 | background: radial-gradient(farthest-side, rgba(0,0,0,0) 90%, rgba(0,0,0,0.2) 150%) #fff; 47 | box-sizing: border-box; 48 | } 49 | a { 50 | color: navy; 51 | } 52 | details { 53 | position: absolute; 54 | top: 0; 55 | left: 1em; 56 | margin: 1em 0; 57 | padding: 10px; 58 | background: #fff; 59 | background: rgba(155,155,155,0.1); 60 | border: 1px solid rgba(0,0,0,0.3); 61 | border-radius: 5px; 62 | max-width: 600px; 63 | font-size: 10pt; 64 | z-index: 100; 65 | background-color:rgba(247,247,247,0.8); 66 | } 67 | details > div { 68 | margin: 10px 0; 69 | } 70 | details > summary { 71 | cursor: pointer; 72 | white-space: nowrap; 73 | } 74 | /* Firefox workaround */ 75 | .no-details details > summary:before { float: left; width: 15px; content: '\25B6'; } 76 | .no-details details.open > summary:before { content: '\25BC'; } 77 | 78 | 79 | button { 80 | display: inline-block; 81 | background: -webkit-gradient(linear, 0% 40%, 0% 70%, from(#F9F9F9), to(#E3E3E3)); 82 | background: -webkit-linear-gradient(#F9F9F9 40%, #E3E3E3 70%); 83 | background: -moz-linear-gradient(#F9F9F9 40%, #E3E3E3 70%); 84 | background: -ms-linear-gradient(#F9F9F9 40%, #E3E3E3 70%); 85 | background: -o-linear-gradient(#F9F9F9 40%, #E3E3E3 70%); 86 | background: linear-gradient(#F9F9F9 40%, #E3E3E3 70%); 87 | border: 1px solid #999; 88 | -webkit-border-radius: 3px; 89 | border-radius: 3px; 90 | padding: 5px 8px; 91 | outline: none; 92 | white-space: nowrap; 93 | -webkit-user-select: none; 94 | -moz-user-select: none; 95 | user-select: none; 96 | cursor: pointer; 97 | text-shadow: 1px 1px #fff; 98 | font-weight: 700; 99 | font-size: 10pt; 100 | } 101 | button:not(:disabled):hover, 102 | button:not(:disabled).active { 103 | border-color: black; 104 | } 105 | button:not(:disabled):active, 106 | button:not(:disabled).active { 107 | background: -webkit-gradient(linear, 0% 40%, 0% 70%, from(#E3E3E3), to(#F9F9F9)); 108 | background: -webkit-linear-gradient(#E3E3E3 40%, #F9F9F9 70%); 109 | background: -moz-linear-gradient(#E3E3E3 40%, #F9F9F9 70%); 110 | background: -ms-linear-gradient(#E3E3E3 40%, #F9F9F9 70%); 111 | background: -o-linear-gradient(#E3E3E3 40%, #F9F9F9 70%); 112 | background: linear-gradient(#E3E3E3 40%, #F9F9F9 70%); 113 | } 114 | 115 | 116 | input[type="range"]{ 117 | -webkit-appearance: none !important; 118 | margin: -5px 0; /*align with center line of buttons in FF*/ 119 | } 120 | 121 | input[type=range]::-webkit-slider-runnable-track { 122 | background-color: #555; 123 | box-shadow: 0 -1px 1px rgba(255,255,255,0.5) inset; 124 | border-radius: 10px; 125 | } 126 | 127 | input[type="range"]::-moz-range-track { 128 | border: 0px; 129 | background: #555; 130 | height: 7px; 131 | border-radius: 10px; 132 | top: 45px; 133 | } 134 | 135 | input[type="range"]::-moz-range-thumb { 136 | width: 20px; 137 | height: 7px; 138 | box-shadow: 1px 1px 5px rgba(0,0,0,1); 139 | border-radius: 2px; 140 | cursor: pointer; 141 | border-top: 1px solid #fff; 142 | background: #ccc -moz-linear-gradient(top, rgb(240, 240, 240), rgb(210, 210, 210)); 143 | } 144 | 145 | input[type="range"]::-webkit-slider-thumb { 146 | -webkit-appearance: none !important; 147 | width: 20px; 148 | height: 8px; 149 | box-shadow: 1px 1px 5px rgba(0,0,0,1); 150 | cursor: pointer; 151 | border-top: 1px solid #fff; 152 | background: #ccc -webkit-linear-gradient(top, rgb(240, 240, 240), rgb(210, 210, 210)); 153 | } 154 | h2 { 155 | margin: 0; 156 | font-weight: 300; 157 | } 158 | 159 | html, body { 160 | overflow: hidden; 161 | margin: 0; 162 | padding: 0; 163 | } 164 | body > section { 165 | display: -webkit-flex; 166 | display: flex; 167 | 168 | -webkit-flex-direction: column; 169 | flex-direction: column; 170 | -webkit-justify-content: center; 171 | justify-content: center; 172 | -webkit-align-content: center; 173 | align-content: center; 174 | -webkit-align-items: center; 175 | align-items: center; 176 | box-sizing: border-box; 177 | height: 100%; 178 | -webkit-perspective: 900; 179 | -moz-perspective: 900px; 180 | perspective: 900px; 181 | -webkit-transform-style: preserve-3d; 182 | -moz-transform-style: preserve-3d; 183 | 184 | } 185 | section > * { 186 | display: -webkit-flex; 187 | -webkit-align-items: center; 188 | -moz-align-items: center; 189 | } 190 | 191 | #pal { 192 | float: right; 193 | margin: 0; 194 | padding: 0; 195 | } 196 | .tooltip 197 | { 198 | float: right; 199 | display: inline; 200 | position: relative; 201 | text-decoration: none; 202 | top: 15px; 203 | left: -10px; 204 | padding: 5px 15px; 205 | z-index: 101; 206 | 207 | } 208 | .tooltip:hover 209 | { 210 | float: right; 211 | display: inline; 212 | position: relative; 213 | text-decoration: none; 214 | top: 3px; 215 | left: -10px; 216 | padding: 5px 15px; 217 | 218 | } 219 | .tooltip:hover:after 220 | { 221 | float: right; 222 | display: inline; 223 | background: #333; 224 | background: rgba(254,233,192,.9); 225 | border: solid; 226 | border-width: 1px; 227 | border-radius: 5px; 228 | top: 5px; 229 | color: #000; 230 | content: attr(alt); 231 | left: -400px; 232 | padding: 5px 15px; 233 | position: absolute; 234 | z-index: 102; 235 | width: 350px; 236 | } 237 | .tooltip:hover:before 238 | { 239 | float: right; 240 | display: inline; 241 | border: solid; 242 | border-color: transparent black; 243 | border-width: 6px 0px 6px 6px; 244 | top: 20px; 245 | content: ""; 246 | left: -120px; 247 | position: relative; 248 | z-index: 102; 249 | } 250 | 251 | aside { 252 | position: absolute; 253 | left: 1em; 254 | top: 3em; 255 | z-index: 10; 256 | } 257 | label { 258 | cursor: pointer; 259 | } 260 | 261 | -------------------------------------------------------------------------------- /htdocs_example/stdlib/mini_controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minimal controls for ScriptNodePlayer. 3 | * 4 | *

Features an initial playlist, drag&drop of additional music files, and controls for "play", "pause", 5 | * "next song", "previous song", "seek" (optional), "volume". 6 | * 7 | *

This crude UI is not meant to be reusable but to show how the ScriptNodePlayer is used. () 8 | */ 9 | BasicPlayerControls = function(songs, enableSeek, enableSpeedTweak, doParseUrl, doOnDropFile, current) { 10 | this._doOnDropFile= doOnDropFile; 11 | this._current= (typeof current != 'undefined')?current:-1; 12 | if(Object.prototype.toString.call( songs ) === '[object Array]') { 13 | this._someSongs= songs; 14 | } else { 15 | console.log("warning: no valid song list supplied.. starting empty"); 16 | this._someSongs= []; 17 | } 18 | this._enableSeek= enableSeek; 19 | this._enableSpeedTweak= enableSpeedTweak; 20 | this._doParseUrl = doParseUrl; 21 | 22 | this.initDomElements(); 23 | }; 24 | 25 | BasicPlayerControls.prototype = { 26 | // facade for player functionality so that BasicPlayerControls user does not also need to know the player 27 | pause: function() { ScriptNodePlayer.getInstance().pause(); }, 28 | resume: function() { ScriptNodePlayer.getInstance().resume(); }, 29 | setVolume: function(value) { ScriptNodePlayer.getInstance().setVolume(value); }, 30 | getSongInfo: function () { return ScriptNodePlayer.getInstance().getSongInfo(); }, 31 | 32 | addSong: function(filename) { 33 | this._someSongs.push(filename); 34 | }, 35 | seekPos: function(relPos) { 36 | var p= ScriptNodePlayer.getInstance(); 37 | p.seekPlaybackPosition(Math.round(p.getMaxPlaybackPosition()*relPos)); 38 | }, 39 | // some playlist handling 40 | removeFromPlaylist: function(songname) { 41 | if (this._someSongs[this._current] == songname) { 42 | this._someSongs.splice(this._current, 1); 43 | if (this._current + 1 == this._someSongs.length) this._current= 0; 44 | } 45 | }, 46 | playNextSong: function() { 47 | var ready= ScriptNodePlayer.getInstance().isReady(); 48 | if (ready && this._someSongs.length) { 49 | this._current= (++this._current >=this._someSongs.length) ? 0 : this._current; 50 | var someSong= this._someSongs[this._current]; 51 | this.playSong(someSong); 52 | } 53 | }, 54 | playPreviousSong: function() { 55 | if (ScriptNodePlayer.getInstance().isReady() && this._someSongs.length) { 56 | this._current= (--this._current<0) ? this._current+this._someSongs.length : this._current; 57 | var someSong= this._someSongs[this._current]; 58 | this.playSong(someSong); 59 | } 60 | }, 61 | 62 | playSongWithBackand: function (options, onSuccess) { 63 | // backend adapter to be used has been explicitly specified 64 | var o= options.backendAdapter; 65 | ScriptNodePlayer.createInstance(o.adapter, o.basePath, o.preload, o.enableSpectrum, 66 | onSuccess, o.doOnTrackReadyToPlay, o.doOnTrackEnd); 67 | }, 68 | playSong: function(someSong) { 69 | var arr= this._doParseUrl(someSong); 70 | var options= arr[1]; 71 | if (typeof options.backendAdapter != 'undefined') { 72 | var name= arr[0]; 73 | var o= options.backendAdapter; 74 | this.playSongWithBackand(options, (function(){ 75 | var p= ScriptNodePlayer.getInstance(); 76 | 77 | p.loadMusicFromURL(name, options, 78 | (function(filename){ 79 | }), 80 | (function(){ 81 | this.removeFromPlaylist(someSong); /* no point trying to play this again */ }.bind(this)), 82 | (function(total, loaded){})); 83 | 84 | o.doOnPlayerReady(); 85 | }.bind(this))); 86 | } else { 87 | var p= ScriptNodePlayer.getInstance(); 88 | if (p.isReady()) { 89 | p.loadMusicFromURL(arr[0], options, 90 | (function(filename){}), 91 | (function(){ this.removeFromPlaylist(someSong); /* no point trying to play this again */ }.bind(this)), 92 | (function(total, loaded){})); 93 | } 94 | } 95 | }, 96 | animate: function() { 97 | // animate playback position slider 98 | var slider = document.getElementById("seekPos"); 99 | if(slider && !slider.blockUpdates) { 100 | var p= ScriptNodePlayer.getInstance(); 101 | slider.value = Math.round(255*p.getPlaybackPosition()/p.getMaxPlaybackPosition()); 102 | } 103 | }, 104 | 105 | // --------------------- drag&drop feature ----------------------------------- 106 | dropFile: function(checkReady, ev, funcName, options, onCompletion) { 107 | ev.preventDefault(); 108 | var data = ev.dataTransfer.getData("Text"); 109 | var file = ev.dataTransfer.files[0]; 110 | var p= ScriptNodePlayer.getInstance(); 111 | 112 | if ((!checkReady || ScriptNodePlayer.getInstance().isReady()) && file instanceof File) { 113 | if (this._doOnDropFile) { 114 | var options= this._doOnDropFile(file.name); // get suitable backend, etc 115 | var o= options.backendAdapter; 116 | 117 | this.pause(); // don't play while reconfiguring.. 118 | 119 | this.playSongWithBackand(options, (function(){ 120 | var p= ScriptNodePlayer.getInstance(); 121 | var f= p[funcName].bind(p); 122 | 123 | f(file, options, 124 | onCompletion, 125 | (function(){ /* fail */ 126 | this.removeFromPlaylist(file.name); /* no point trying to play this again */ 127 | }.bind(this)), 128 | (function(total, loaded){}) /* progress */ 129 | ); 130 | 131 | o.doOnPlayerReady(); 132 | }.bind(this))); 133 | 134 | } else { 135 | var p= ScriptNodePlayer.getInstance(); 136 | var f= p[funcName].bind(p); 137 | f(file, options, onCompletion, (function(){console.log("fatal error: tmp file could not be stored");}), (function(total, loaded){})); 138 | } 139 | } 140 | }, 141 | drop: function(ev) { 142 | var options= {}; 143 | this.dropFile(true, ev, 'loadMusicFromTmpFile', options, (function(filename){ 144 | this.addSong(filename); 145 | }).bind(this)); 146 | }, 147 | 148 | initExtensions: function() {}, // to be overridden in subclass 149 | 150 | allowDrop: function(ev) { 151 | ev.preventDefault(); 152 | ev.dataTransfer.dropEffect = 'move'; // needed for FF 153 | }, 154 | initTooltip: function() { 155 | var tooltipDiv= document.getElementById("tooltip"); 156 | 157 | var f = document.createElement("form"); 158 | f.setAttribute('method',"post"); 159 | f.setAttribute('action',"https://www.paypal.com/cgi-bin/webscr"); 160 | f.setAttribute('target',"_blank"); 161 | 162 | var i1 = document.createElement("input"); 163 | i1.type = "hidden"; 164 | i1.value = "_s-xclick"; 165 | i1.name = "cmd"; 166 | f.appendChild(i1); 167 | 168 | var i2 = document.createElement("input"); 169 | i2.type = "hidden"; 170 | i2.value = "E7ACAHA7W5FYC"; 171 | i2.name = "hosted_button_id"; 172 | f.appendChild(i2); 173 | 174 | var i3 = document.createElement("input"); 175 | i3.type = "image"; 176 | i3.src= "stdlib/btn_donate_LG.gif"; 177 | i3.border= "0"; 178 | i3.name="submit"; 179 | i3.alt="PayPal - The safer, easier way to pay online!"; 180 | f.appendChild(i3); 181 | 182 | var i4 = document.createElement("img"); 183 | i4.alt = ""; 184 | i4.border = "0"; 185 | i4.src = "stdlib/pixel.gif"; 186 | i4.width = "1"; 187 | i4.height = "1"; 188 | f.appendChild(i4); 189 | 190 | tooltipDiv.appendChild(f); 191 | }, 192 | initDrop: function() { 193 | // the 'window' level handlers are needed to show a useful mouse cursor in Firefox 194 | window.addEventListener("dragover",function(e){ 195 | e = e || event; 196 | e.preventDefault(); 197 | e.dataTransfer.dropEffect = 'none'; 198 | },true); 199 | window.addEventListener("drop",function(e){ 200 | e = e || event; 201 | e.preventDefault(); 202 | },true); 203 | 204 | var dropDiv= document.getElementById("drop"); 205 | dropDiv.ondrop = this.drop.bind(this); 206 | dropDiv.ondragover = this.allowDrop.bind(this); 207 | }, 208 | appendControlElement: function(elmt) { 209 | var controls= document.getElementById("controls"); 210 | controls.appendChild(elmt); 211 | controls.appendChild(document.createTextNode(" ")); // spacer 212 | }, 213 | initDomElements: function() { 214 | var play = document.createElement("BUTTON"); 215 | play.id = "play"; 216 | play.innerHTML= " >"; 217 | play.onclick = function(e){ this.resume(); }.bind(this); 218 | this.appendControlElement(play); 219 | 220 | var pause = document.createElement("BUTTON"); 221 | pause.id = "pause"; 222 | pause.innerHTML= " ||"; 223 | pause.onclick = function(e){ this.pause(); }.bind(this); 224 | this.appendControlElement(pause); 225 | 226 | var previous = document.createElement("BUTTON"); 227 | previous.id = "previous"; 228 | previous.innerHTML= " |<<"; 229 | previous.onclick = this.playPreviousSong.bind(this); 230 | this.appendControlElement(previous); 231 | 232 | var next = document.createElement("BUTTON"); 233 | next.id = "next"; 234 | next.innerHTML= " >>|"; 235 | next.onclick = this.playNextSong.bind(this); 236 | this.appendControlElement(next); 237 | 238 | var gain = document.createElement("input"); 239 | gain.id = "gain"; 240 | gain.name = "gain"; 241 | gain.type = "range"; 242 | gain.min = 0; 243 | gain.max = 255; 244 | gain.value = 255; 245 | gain.onchange = function(e){ this.setVolume(gain.value/255); }.bind(this); 246 | this.appendControlElement(gain); 247 | 248 | if (this._enableSeek) { 249 | var seek = document.createElement("input"); 250 | seek.type = "range"; 251 | seek.min = 0; 252 | seek.max = 255; 253 | seek.value = 0; 254 | seek.id = "seekPos"; 255 | seek.name = "seekPos"; 256 | // FF: 'onchange' triggers once the final value is selected; 257 | // Chrome: already triggers while dragging; 'oninput' does not exist in IE 258 | // but supposedly has the same functionality in Chrome & FF 259 | seek.oninput = function(e){ 260 | if (window.chrome) 261 | seek.blockUpdates= true; 262 | }; 263 | seek.onchange = function(e){ 264 | if (!window.chrome) 265 | seek.onmouseup(e); 266 | }; 267 | seek.onmouseup = function(e){ 268 | var p= ScriptNodePlayer.getInstance(); 269 | this.seekPos(seek.value/255); 270 | seek.blockUpdates= false; 271 | }.bind(this); 272 | this.appendControlElement(seek); 273 | } 274 | if (this._enableSpeedTweak) { 275 | var speed = document.createElement("input"); 276 | speed.type = "range"; 277 | speed.min = 0; 278 | speed.max = 100; 279 | speed.value = 50; 280 | speed.id = "speed"; 281 | speed.name = "speed"; 282 | speed.onchange = function(e){ 283 | if (!window.chrome) 284 | speed.onmouseup(e); 285 | }; 286 | 287 | speed.onmouseup = function(e){ 288 | var p= ScriptNodePlayer.getInstance(); 289 | 290 | var tweak= 0.2; // allow 20% speed correction 291 | var f= (speed.value/50)-1; // -1..1 292 | 293 | var s= p.getDefaultSampleRate(); 294 | s= Math.round(s*(1+(tweak*f))); 295 | p.resetSampleRate(s); 296 | }.bind(this); 297 | this.appendControlElement(speed); 298 | } 299 | 300 | this.initDrop(); 301 | this.initTooltip(); 302 | 303 | this.initExtensions(); 304 | } 305 | }; 306 | -------------------------------------------------------------------------------- /htdocs_example/stdlib/mini_display.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple music visualization for ScriptNodePlayer. 3 | * 4 | *

Use SongDisplay to render animated frequency spectrum and basic meta info about the current song. 5 | * 6 | *

This file also handles HTML5

and their simulation in Firefox: set "window.openDetails" 7 | * (before loading this file) to control if "details" are initially open/closed 8 | */ 9 | 10 | 11 | /** 12 | * Accessor must be subclassed to define attribute access for a specific backend. 13 | */ 14 | DisplayAccessor = function (doGetSongInfo) { 15 | this.doGetSongInfo= doGetSongInfo; 16 | }; 17 | 18 | DisplayAccessor.prototype = { 19 | getDisplayTitle: function() {}, 20 | getDisplaySubtitle: function() {}, 21 | getDisplayLine1: function() {}, 22 | getDisplayLine2: function() {}, 23 | getDisplayLine3: function() {}, 24 | 25 | // ---------------- utilities ----------------- 26 | getSongInfo: function() { 27 | return this.doGetSongInfo(); 28 | }, 29 | }; 30 | 31 | /* 32 | * This render class drives the "requestAnimationFrame" cycle. 33 | * 34 | * external dependencies: 35 | * ScriptNodePlayer.getInstance() 36 | * document canvas elements: "spectrumCanvas", "logoCanvas", 37 | * document div elements: "moz-reflect-spectrum", "moz-reflect-logo" 38 | * 39 | * @param displayAccessor subclass of DisplayAccessor 40 | * @param colors either the URL to an image or an array containing color strings 41 | * @param barType 0: "growing bars", 1: "jumping bars" 42 | * @param cpuLimit percentage of CPU that can be used for graphics: 0= 0%, 1=100%.. or anything inbetween 43 | * @param doAnimate some function that will be added to the built-in rendering 44 | */ 45 | SongDisplay = function(displayAccessor, colors, barType, cpuLimit, doAnimate) { 46 | this.displayAccessor= displayAccessor; 47 | this.hexChars= "0123456789ABCDEF"; 48 | 49 | if (typeof colors == 'string' || colors instanceof String) { 50 | // URL of image to be used 51 | this.colors; 52 | this.backgroundImgUrl= colors; 53 | this.backgroundImg= 0; 54 | } else if( Object.prototype.toString.call( colors ) === '[object Array]' ) { 55 | // array containing colors 56 | this.colors= colors; 57 | } else { 58 | console.log("fatal error: invalid 'colors' argument"); 59 | } 60 | 61 | this.doAnimate= doAnimate; 62 | this.barType= barType; 63 | 64 | this.WIDTH= 800; 65 | this.HEIGHT= 100; 66 | 67 | this.barWidth = 5; 68 | this.barHeigth= 30; 69 | this.barSpacing= 10; 70 | this.colorOffset=0; 71 | 72 | this.refreshCounter=0; 73 | this.lastRenderTime=0; 74 | this.worstRenderTime=0; 75 | 76 | this.timeLimit= 1000/60*cpuLimit; // renderiung should take no longer than 10% of the available time (e.g. 3-4 millis per frame) 77 | 78 | this.canvasSpectrum = document.getElementById('spectrumCanvas'); 79 | this.ctxSpectrum = this.canvasSpectrum.getContext('2d'); 80 | // this.canvasSpectrum.width = this.WIDTH; 81 | 82 | this.mozReflectSpectrum = document.getElementById('moz-reflect-spectrum'); 83 | this.mozReflectLogo = document.getElementById('moz-reflect-logo'); 84 | 85 | this.canvasLegend = document.getElementById('logoCanvas'); 86 | this.ctxLegend = this.canvasLegend.getContext('2d'); 87 | }; 88 | 89 | SongDisplay.prototype = { 90 | reqAnimationFrame: function() { 91 | window.requestAnimationFrame(this.redraw.bind(this)); 92 | }, 93 | updateImage: function(src) { 94 | this.backgroundImg= 0; 95 | var imgObj = new Image(); 96 | 97 | imgObj.onload = function () { 98 | this.backgroundImg= imgObj; 99 | }.bind(this); 100 | imgObj.src=src; 101 | }, 102 | redraw: function() { 103 | if (!this.colors && !this.backgroundImg) this.updateImage(this.backgroundImgUrl); 104 | 105 | if(this.doAnimate) this.doAnimate(); 106 | this.redrawSpectrum(); 107 | 108 | this.reqAnimationFrame(); 109 | }, 110 | setBarDimensions: function(w, h, s) { 111 | this.barWidth = w; 112 | this.barHeigth= h; 113 | this.barSpacing= s; 114 | }, 115 | redrawSpectrum: function() { 116 | // with low enough load the browser should be able to render 60fps: the load from the music generation 117 | // cannot be changed but the load from the rendering can.. 118 | this.worstRenderTime= Math.max(this.worstRenderTime, this.lastRenderTime); 119 | 120 | this.lastRenderTime= new Date().getTime(); // start new measurement 121 | 122 | var slowdownFactor= this.timeLimit?Math.max(1, this.worstRenderTime/this.timeLimit):1; 123 | this.refreshCounter++; 124 | 125 | if (this.refreshCounter >= slowdownFactor) { 126 | this.refreshCounter= 0; 127 | 128 | var freqByteData= ScriptNodePlayer.getInstance().getFreqByteData(); 129 | 130 | var OFFSET = 100; 131 | 132 | var numBars = Math.round(this.WIDTH / this.barSpacing); 133 | 134 | if(typeof this.caps === 'undefined') { 135 | this.caps= new Array(numBars); 136 | this.decayRate= 0.99; 137 | for (var i= 0; i>16) & 0xff; 277 | var g1= (from >>8) & 0xff; 278 | var b1= from & 0xff; 279 | 280 | var r= Math.round(r1+(((to >>16) & 0xff)-r1)*s); 281 | var g= Math.round(g1+(((to >>8) & 0xff)-g1)*s); 282 | var b= Math.round(b1+((to & 0xff)-b1)*s); 283 | 284 | return "#" +this.hex(r>>4) +this.hex(r) +this.hex(g>>4) +this.hex(g) +this.hex(b>>4) +this.hex(b); 285 | }, 286 | hex: function(n) { 287 | return this.hexChars.charAt(n & 0xf); 288 | } 289 | }; 290 | 291 | window.console || (window.console = { 'log': alert }); 292 | $(function() { 293 | $('html').addClass($.fn.details.support ? 'details' : 'no-details'); 294 | $('details').details(); 295 | 296 | // initially expand details 297 | if (!(window.openDetails === 'undefined') && window.openDetails) { 298 | if ($.fn.details.support) { 299 | $('details').attr('open', ''); // Chrome 300 | } else { 301 | var $details= $('details'); 302 | var $detailsSummary = $('summary', $details).first(); 303 | var $detailsNotSummary = $details.children(':not(summary)'); 304 | 305 | $details.addClass('open').prop('open', true).triggerHandler('open.details'); 306 | $detailsNotSummary.show(); 307 | } 308 | } 309 | if (!window.chrome) { 310 | // hack to get rid of Firefox specific pseudo elements used to sim webkit-box-reflect 311 | var e = document.getElementById("moz-reflect-logo"); 312 | e.className += " enableMozReflection"; 313 | var e2 = document.getElementById("moz-reflect-spectrum"); 314 | e2.className += " enableMozReflection"; 315 | } 316 | }); 317 | 318 | 319 | -------------------------------------------------------------------------------- /htdocs_example/stdlib/scriptprocessor_player.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic ScriptProcessor based WebAudio player. 3 | *

AudioBackendAdapterBase: an abstract base class for specific backend (i.e. 'sample data producer') integration. 4 | * 5 | * version 1.1.2 (with WASM support, cached filename translation & track switch bugfix, "internal filename" 6 | * mapping, getVolume, setPanning, AudioContext get/resume, AbstractTicker revisited, bugfix for 7 | * duplicate events, improved "play after user gesture" support + doubled sample buffer size), 8 | * support for use of "alias" names for same file (see modland), added EmsHEAPF32BackendAdapter, 9 | * added silence detection, extended copyTickerData signature, added JCH's "choppy ticker" fix, 10 | * added setSilenceTimeout() 11 | * 12 | * Copyright (C) 2019 Juergen Wothke 13 | * 14 | * Terms of Use: This software is licensed under a CC BY-NC-SA 15 | * (http://creativecommons.org/licenses/by-nc-sa/4.0/). 16 | */ 17 | var fetchSamples=function(e){window.player.genSamples.bind(window.player)(e)},calcTick=function(e){window.player.tick.bind(window.player)(e)},setGlobalWebAudioCtx=function(){if(void 0===window._gPlayerAudioCtx){var t="Web Audio API is not supported in this browser";try{"AudioContext"in window?window._gPlayerAudioCtx=new AudioContext:"webkitAudioContext"in window?window._gPlayerAudioCtx=new webkitAudioContext:alert(t+e)}catch(e){alert(t+e)}}try{"suspended"===window._gPlayerAudioCtx.state&&"ontouchstart"in window&&window._gPlayerAudioCtx.resume()}catch(e){}};function surrogateCtor(){}function extend(e,t,i){for(var a in surrogateCtor.prototype=e.prototype,t.prototype=new surrogateCtor,(t.prototype.constructor=t).base=e,i)t.prototype[a]=i[a];return t}AbstractTicker=function(){},AbstractTicker.prototype={init:function(e,t){},start:function(){},computeAudioSamplesNotify:function(){},resampleData:function(e,t,i,a){},copyTickerData:function(e,t,i){},calcTickData:function(e,t){}};var SAMPLES_PER_BUFFER=16384;AudioBackendAdapterBase=function(e,t){this._resampleBuffer=new Float32Array,this._channels=e,this._bytesPerSample=t,this._sampleRate=44100,this._inputSampleRate=44100,this._observer,this._manualSetupComplete=!0},AudioBackendAdapterBase.prototype={computeAudioSamples:function(){this.error("computeAudioSamples")},loadMusicData:function(e,t,i,a,r){this.error("loadMusicData")},evalTrackOptions:function(e){this.error("evalTrackOptions")},updateSongInfo:function(e,t){this.error("updateSongInfo")},getSongInfoMeta:function(){this.error("getSongInfoMeta")},getAudioBuffer:function(){this.error("getAudioBuffer")},getAudioBufferLength:function(){this.error("getAudioBufferLength")},readFloatSample:function(e,t){this.error("readFloatSample")},applyPanning:function(e,t,i){this.error("applyPanning")},getBytesPerSample:function(){return this._bytesPerSample},getChannels:function(){return this._channels},isAdapterReady:function(){return!0},mapInternalFilename:function(e,t,i){return(e||t)+i},mapUrl:function(e){return e},uploadFile:function(e,t){return 0},isManualSetupComplete:function(){return this._manualSetupComplete},teardown:function(){this.error("teardown")},getMaxPlaybackPosition:function(){return 0},getPlaybackPosition:function(){return 0},seekPlaybackPosition:function(e){return-1},getPathAndFilename:function(e){this.error("getPathAndFilename")},registerFileData:function(e,t){this.error("registerFileData")},mapBackendFilename:function(e){return e},mapCacheFileName:function(e){return e},handleBackendSongAttributes:function(e,t){this.error("handleBackendSongAttributes")},mapUri2Fs:function(e){var t=e.replace(/\/\//,"ýý");return t=(t=(t=(t=(t=(t=(t=t.replace(/\?/,"ÿ")).replace(/:/,"þ")).replace(/\*/,"ü")).replace(/"/,"û")).replace(//,"ø")).replace(/\|/,"÷")},mapFs2Uri:function(e){var t=e.replace(/ýý/,"//");return t=(t=(t=(t=(t=(t=(t=t.replace(/ÿ/,"?")).replace(/þ/,":")).replace(/ü/,"*")).replace(/û/,'"')).replace(/ù/,"<")).replace(/ø/,">")).replace(/÷/,"|")},setObserver:function(e){this._observer=e},notifyAdapterReady:function(){void 0!==this._observer&&this._observer.notify()},error:function(e){alert("fatal error: abstract method '"+e+"' must be defined")},resetSampleRate:function(e,t){0this._resampleBuffer.length&&(this._resampleBuffer=this.allocResampleBuffer(i))},allocResampleBuffer:function(e){return new Float32Array(e)},getCopiedAudio:function(e,t,i,a){var r;for(r=0;rthis._resampleBuffer.length&&(this._resampleBuffer=this.allocResampleBuffer(n)),this.resampleToFloat(this._channels,0,e,t,this.readFloatSample.bind(this),this._resampleBuffer,r),2==this._channels&&this.resampleToFloat(this._channels,1,e,t,this.readFloatSample.bind(this),this._resampleBuffer,r)}return r},resampleToFloat:function(e,t,i,a,r,n,s){for(var o,d=0,c=0,l=s-0,h=a-0,u=Math.abs(l-d),f=d>8,d=(n<<8)-s>>8;this.Module.HEAP16[e+a]=o,this.Module.HEAP16[e+a+1]=d}}}),i}(),EmsHEAPF32BackendAdapter=function(){var i=function(e,t){i.base.call(this,e,t),this._bytesPerSample=4};return extend(EmsHEAP16BackendAdapter,i,{readFloatSample:function(e,t){return this.Module.HEAPF32[e+t]},applyPanning:function(e,t,i){var a,r,n,s;for(i=256*i/2,a=0;a<2*t;a+=2){var o=(256*(r=this.Module.HEAPF32[e+a])+(s=((n=this.Module.HEAPF32[e+a+1])-r)*i))/256,d=(256*n-s)/256;this.Module.HEAPF32[e+a]=o,this.Module.HEAPF32[e+a+1]=d}}}),i}(),FileCache=function(){this._binaryFileMap={},this._pendingFileMap={},this._isWaitingForFile=!1},FileCache.prototype={getFileMap:function(){return this._binaryFileMap},getPendingMap:function(){return this._pendingFileMap},setWaitingForFile:function(e){this._isWaitingForFile=e},isWaitingForFile:function(){return this._isWaitingForFile},getFile:function(e){var t;return e in this._binaryFileMap&&(t=this._binaryFileMap[e]),t},setFile:function(e,t){this._binaryFileMap[e]=t,this._isWaitingForFile=!1}};var ScriptNodePlayer=(PlayerImpl=function(e,t,i,a,r,n,s,o,d,c){void 0===e&&alert("fatal error: backendAdapter not specified"),void 0===r&&alert("fatal error: onPlayerReady not specified"),void 0===n&&alert("fatal error: onTrackReadyToPlay not specified"),void 0===s&&alert("fatal error: onTrackEnd not specified"),void 0!==c&&(window.SAMPLES_PER_BUFFER=c),2 FS name: "+t),this.preloadFile(t,function(){this.initIfNeeded(this.lastUsedFilename,this.lastUsedData,this.lastUsedOptions)}.bind(this),!1)},fileSizeRequestCallback:function(e){var t=this._backendAdapter.mapBackendFilename(e),i=this._backendAdapter.mapCacheFileName(t);return this.getCache().getFile(i).length},songUpdateCallback:function(e){this._backendAdapter.handleBackendSongAttributes(e,this._songInfo),this._onUpdate&&this._onUpdate()},preload:function(e,t,i){if(0===t)i();else{t--;var a=function(){this.preload(e,t,i)}.bind(this);this.preloadFile(e[t],a,!0)}},preloadFile:function(r,n,e){var s=this._backendAdapter.mapCacheFileName(r),t=this.getCache().getFile(s);if(void 0!==t){var i=0;if(0==t)i=1,this.trace("error: preloadFile could not get cached: "+r);else{this.trace("preloadFile found cached file using name: "+s);var a=this._backendAdapter.getPathAndFilename(r);this._backendAdapter.registerFileData(a,t)}return e&&n(),i}if(this.trace("preloadFile FAILED to find cached file using name: "+s),this._isPaused=!0,this.setWaitingForFile(!0),this._isSongReady=!1,!(s in this.getCache().getPendingMap())){this.getCache().getPendingMap()[s]=1;var o=new XMLHttpRequest;o.open("GET",this._backendAdapter.mapUrl(r),!0),o.responseType="arraybuffer",o.onload=function(e){var t=o.response;if(t){this.trace("preloadFile successfully loaded: "+r);var i=this._backendAdapter.getPathAndFilename(r),a=new Uint8Array(t);this._backendAdapter.registerFileData(i,a),this.trace("preloadFile cached file using name: "+s),this.getCache().setFile(s,a)}delete this.getCache().getPendingMap()[s]||this.trace("remove file from pending failed: "+s),n()}.bind(this),o.onreadystatuschange=function(e){4==o.readyState&&404==o.status&&(this.trace("preloadFile failed to load: "+r),this.getCache().setFile(s,0))}.bind(this),o.onerror=function(e){this.getCache().setFile(s,0)}.bind(this),o.send(null)}return-1},tick:function(e){this._isPaused||this._currentTick++},genSamples:function(e){var t,i=this.isStereo()&&1this._currentTimeout?(this.trace("'song end' forced after "+this._currentTimeout/this._correctSampleRate+" secs"),s=1):(s=this._backendAdapter.computeAudioSamples(),void 0!==this._externalTicker&&this._externalTicker.computeAudioSamplesNotify()),0!==s)return this.fillEmpty(n,a,t),s<0?(this._isPaused=!0,this._isSongReady=!1,void this.setWaitingForFile(!0)):this.isWaitingForFile()?void 0:(1=this._silenceTimeout*this._correctSampleRate&&0a){var c=a-this._numberOfSamplesRendered;for(r=0;r>1,this._backendAdapter),s=e[this._sourceBufferIdx++],o=e[this._sourceBufferIdx++],t[r+this._numberOfSamplesRendered]=s,i[r+this._numberOfSamplesRendered]=o,n+=d(s)+d(o);this._numberOfSamplesToRender-=c,this._numberOfSamplesRendered=a}else{for(r=0;r>1,this._backendAdapter),s=e[this._sourceBufferIdx++],o=e[this._sourceBufferIdx++],t[r+this._numberOfSamplesRendered]=s,i[r+this._numberOfSamplesRendered]=o,n+=d(s)+d(o);this._numberOfSamplesRendered+=this._numberOfSamplesToRender,this._numberOfSamplesToRender=0}this.detectSilence(n)},copySamplesMono:function(e,t,i){var a,r=0,n=0,s=Math.abs;if(this._numberOfSamplesRendered+this._numberOfSamplesToRender>i){var o=i-this._numberOfSamplesRendered;for(a=0;aSamplePlayer: The generic player which must be parameterized with a specific AudioBackendAdapterBase 7 | * subclass (which is not contained in this lib) 8 | * 9 | *

AudioBackendAdapterBase: an abstract base class for specific backend (i.e. 'sample data producer') integration. 10 | * 11 | * version 1.1.2 (with WASM support, cached filename translation & track switch bugfix, "internal filename" 12 | * mapping, getVolume, setPanning, AudioContext get/resume, AbstractTicker revisited, bugfix for 13 | * duplicate events, improved "play after user gesture" support + doubled sample buffer size), 14 | * support for use of "alias" names for same file (see modland), added EmsHEAPF32BackendAdapter, 15 | * added silence detection, extended copyTickerData signature, added JCH's "choppy ticker" fix, 16 | * added setSilenceTimeout() 17 | * 18 | * Copyright (C) 2019 Juergen Wothke 19 | * 20 | * Terms of Use: This software is licensed under a CC BY-NC-SA 21 | * (http://creativecommons.org/licenses/by-nc-sa/4.0/). 22 | */ 23 | 24 | var fetchSamples= function (e) { 25 | // it seems that it is necessary to keep this explicit reference to the event-handler 26 | // in order to pervent the dumbshit Chrome GC from detroying it eventually 27 | 28 | var f= window.player['genSamples'].bind(window.player); // need to re-bind the instance.. after all this 29 | // joke language has no real concept of OO 30 | f(e); 31 | }; 32 | 33 | var calcTick= function (e) { 34 | var f= window.player['tick'].bind(window.player); 35 | f(e); 36 | }; 37 | 38 | var setGlobalWebAudioCtx= function() { 39 | if (typeof window._gPlayerAudioCtx == 'undefined') { // cannot be instantiated 2x (so make it global) 40 | var errText= 'Web Audio API is not supported in this browser'; 41 | try { 42 | if('AudioContext' in window) { 43 | window._gPlayerAudioCtx = new AudioContext(); 44 | } else if('webkitAudioContext' in window) { 45 | window._gPlayerAudioCtx = new webkitAudioContext(); // legacy stuff 46 | } else { 47 | alert(errText + e); 48 | } 49 | } catch(e) { 50 | alert(errText + e); 51 | } 52 | } 53 | try { 54 | if (window._gPlayerAudioCtx.state === 'suspended' && 'ontouchstart' in window) { //iOS shit 55 | window._gPlayerAudioCtx.resume(); 56 | } 57 | } catch(ignore) {} 58 | } 59 | 60 | /* 61 | Poor man's JavaScript inheritance: 'extend' must be used to subclass AudioBackendAdapterBase to create backend specific adapters. 62 | 63 | usage: 64 | 65 | SomeBackendAdapter = (function(){ var $this = function () { $this.base.call(this, channels, bytesPerSample);}; 66 | extend(AudioBackendAdapterBase, $this, { 67 | getAudioBuffer: function() { 68 | ... 69 | }, 70 | getAudioBufferLength: function() { 71 | ... 72 | }, 73 | ... 74 | }); return $this; })(); 75 | */ 76 | function surrogateCtor() {} 77 | function extend(base, sub, methods) { 78 | surrogateCtor.prototype = base.prototype; 79 | sub.prototype = new surrogateCtor(); 80 | sub.prototype.constructor = sub; 81 | sub.base = base; 82 | for (var name in methods) { 83 | sub.prototype[name] = methods[name]; 84 | } 85 | return sub; 86 | } 87 | 88 | /* 89 | * Subclass this class in order to sync/associate stuff with the audio playback. 90 | * 91 | * The basic problem: WebAudio will request additional audio data whenever *it feels like* requesting it. The provider of that data has 92 | * no way of knowing when exactly the delivered data will actually be used. WebAudio usually requests it *before* its current supply runs 93 | * out. Supposing WebAudio requests chunks of 8192 samples at a time (which is the default used here). Depending on the user's screen refresh 94 | * rate (e.g. 50Hz) and the browser's playback rate (e.g. 44100Hz) a different number of samples will correspond to one typical animation frame, 95 | * i.e. screen redraw (e.g. 882 samples). The sample "supply" delivered in one batch may then last for roughly 1/5 of a second (obviously much less 96 | * when higher playback speeds are used). 97 | * The size of the sample data batches delivered by the underlying emulator may then not directly match the chunks requested by WebAudio, i.e. 98 | * there may be more or also less data than what is needed for one WebAudio request. And as a further complication the sample rate used by the 99 | * backend may differ from the one used by WebAudio, i.e. the raw data relivered by the emulator backend may be subject to a resampling. 100 | * With regards to the actual audio playback this isn't a problem. But the problems start if there is additional data accociated with the 101 | * audio data (maybe some raw data that was used to create the respective audio data) and the GUI needs to handle that add-on data *IN SYNC* 102 | * with the actual playback, e.g. visualize the audio that is played back. 103 | * 104 | * It is the purpose of this AbstractTicker API to deal with that problem and provide the GUI with some API that allows to access 105 | * add-on data in-sync with the playback. 106 | * 107 | * If a respective subclass is specified upon instanciation of the ScriptNodePlayer, then the player will track 108 | * playback progress as 'ticks' (one 'tick' typically measuring 256 audio samples). "Ticks" are measured within the 109 | * context of the current playback buffer and whenever a new buffer is played the counting restarts from 0. 110 | * 111 | * During playback (e.g. from some "animation frame" handler) the current playback position can be queried using 112 | * ScriptNodePlayer.getInstance().getCurrentTick(). 113 | * 114 | * The idea is for the AbstractTicker to provide additional "tick resolution" data that can be queried using the 115 | * "current tick". During playback the original audio buffers are fed to the AbstractTicker before they are played 116 | * (see 'calcTickData'). This allows the AbstractTicker to build/update its "tick resolution" data. 117 | */ 118 | AbstractTicker = function() {} 119 | AbstractTicker.prototype = { 120 | /* 121 | * Constructor that allows the AbstractTicker to setup its own data structures (the 122 | * number of 'tick' events associated with each sample buffer is: samplesPerBuffer/tickerStepWidth). 123 | * @samplesPerBuffer number of audio samples in the original playback buffers - that the AbstractTicker can use to 124 | * derive its additional data streams from 125 | * @tickerStepWidth number of audio samples that are played between "tick events" 126 | */ 127 | init: function(samplesPerBuffer, tickerStepWidth) {}, 128 | /* 129 | * Gets called at the start of each audio buffer generation. 130 | */ 131 | start: function() {}, 132 | /* 133 | * Gets called each time the computeAudioSamples() has been invoked. 134 | * @deprecated Legacy API used in early VU meter experiments 135 | */ 136 | computeAudioSamplesNotify: function() {}, 137 | /* 138 | * Hook allows to resample the add-on data in-sync with the underlying audio data. 139 | */ 140 | resampleData: function(sampleRate, inputSampleRate, origLen, backendAdapter) {}, 141 | /* 142 | * Copies data from the resampled input buffers to the "WebAudio audio buffer" sized output. 143 | */ 144 | copyTickerData: function(outBufferIdx, inBufferIdx, backendAdapter) {}, 145 | /* 146 | * Invoked after audio buffer content has been generated. 147 | * @deprecated Legacy API used in early VU meter experiments 148 | */ 149 | calcTickData: function(output1, output2) {} 150 | }; 151 | 152 | 153 | var SAMPLES_PER_BUFFER = 16384; // allowed: buffer sizes: 256, 512, 1024, 2048, 4096, 8192, 16384 154 | 155 | 156 | /* 157 | * Abstract 'audio backend adapter'. 158 | * 159 | * Not for "end users"! Base infrastructure for the integration of new backends: 160 | * 161 | * Must be subclassed for the integration of a specific backend: It adapts the APIs provided by a 162 | * specific backend to the ones required by the player (e.g. access to raw sample data.) It 163 | * provides hooks that can be used to pass loaded files to the backend. The adapter also has 164 | * built-in resampling logic so that exactly the sampleRate required by the player is provided). 165 | * 166 | * Most backends are pretty straight forward: A music file is input and the backend plays it. Things are 167 | * more complicated if the backend code relies on additional files - maybe depending on the input - 168 | * that must be loaded in order to play the music. The problem arises because in the traditional runtime 169 | * environment files are handled synchronously: the code waits until the file is loaded and then uses it. 170 | * 171 | * "Unfortunately" there is no blocking file-load available to JavaScript on a web page. So unless some 172 | * virtual filesystem is built-up beforehand (containing every file that the backend could possibly ever 173 | * try to load) the backend code is stuck with an asynchronous file loading scheme, and the original 174 | * backend code must be changed to a model that deals with browser's "file is not yet ready" response. 175 | * 176 | * The player offers a trial & error approach to deal with asynchronous file-loading. The backend code 177 | * is expected (i.e. it must be adapted accordingly) to attempt a file-load call (which is handled by 178 | * an async web request linked to some sort of result cache). If the requested data isn't cached yet, 179 | * then the backend code is expected to fail but return a corresponding error status back to the 180 | * player (i.e. the player then knows that the code failed because some file wasn't available yet - and 181 | * as soon as the file-load is completed it retries the whole initialization sequence). 182 | * (see "fileRequestCallback()" for more info) 183 | */ 184 | AudioBackendAdapterBase = function (channels, bytesPerSample) { 185 | this._resampleBuffer= new Float32Array(); 186 | this._channels= channels; 187 | this._bytesPerSample= bytesPerSample; 188 | this._sampleRate= 44100; 189 | this._inputSampleRate= 44100; 190 | this._observer; 191 | this._manualSetupComplete= true; // override if necessary 192 | }; 193 | 194 | AudioBackendAdapterBase.prototype = { 195 | 196 | // ************* core functions that must be defined by a subclass 197 | 198 | /** 199 | * Fills the audio buffer with the next batch of samples 200 | * Return 0: OK, -1: temp issue - waiting for file, 1: end, 2: error 201 | */ 202 | computeAudioSamples: function() {this.error("computeAudioSamples");}, 203 | 204 | /** 205 | * Load the song's binary data into the backend as a first step towards playback. 206 | * The subclass can either use the 'data' directly or us the 'filename' to retrieve it indirectly 207 | * (e.g. when regular file I/O APIs are used). 208 | */ 209 | loadMusicData: function(sampleRate, path, filename, data, options) {this.error("loadMusicData");}, 210 | 211 | /** 212 | * Second step towards playback: Select specific sub-song from the loaded song file. 213 | * Allows to select a specific sub-song and/or apply additional song setting.. 214 | */ 215 | evalTrackOptions: function(options) {this.error("evalTrackOptions");}, 216 | 217 | /** 218 | * Get info about currently selected music file and track. Respective info very much depends on 219 | * the specific backend - use getSongInfoMeta() to check for available attributes. 220 | */ 221 | updateSongInfo: function(filename, result) {this.error("updateSongInfo");}, 222 | 223 | /** 224 | * Advertises the song attributes that can be provided by this backend. 225 | */ 226 | getSongInfoMeta: function() {this.error("getSongInfoMeta");}, 227 | 228 | 229 | // ************* sample buffer and format related 230 | 231 | /** 232 | * Return: pointer to memory buffer that contains the sample data 233 | */ 234 | getAudioBuffer: function() {this.error("getAudioBuffer");}, 235 | 236 | /** 237 | * Return: length of the audio buffer in 'ticks' (e.g. mono buffer with 1 8-bit 238 | * sample= 1; stereo buffer with 1 32-bit * sample for each channel also= 1) 239 | */ 240 | getAudioBufferLength: function() {this.error("getAudioBufferLength");}, 241 | 242 | /** 243 | * Reads one audio sample from the specified position. 244 | * Return sample value in range: -1..1 245 | */ 246 | readFloatSample: function(buffer, idx) {this.error("readFloatSample");}, 247 | 248 | /** 249 | * @param pan 0..2 (1 creates mono) 250 | */ 251 | applyPanning: function(buffer, len, pan) {this.error("applyPanning");}, 252 | 253 | /** 254 | * Return size one sample in bytes 255 | */ 256 | getBytesPerSample: function() { 257 | return this._bytesPerSample; 258 | }, 259 | 260 | /** 261 | * Number of channels, i.e. 1= mono, 2= stereo 262 | */ 263 | getChannels: function() { 264 | return this._channels; 265 | }, 266 | 267 | // ************* optional: setup related 268 | /* 269 | * Implement if subclass needs additional setup logic. 270 | */ 271 | isAdapterReady: function() { 272 | return true; 273 | }, 274 | 275 | /* 276 | * Creates the URL used to retrieve the song file. 277 | */ 278 | mapInternalFilename: function(overridePath, defaultPath, uri) { 279 | return ((overridePath)?overridePath:defaultPath) + uri; // this._basePath ever needed? 280 | }, 281 | /* 282 | * Allows to map the filenames used in the emulation to external URLs. 283 | */ 284 | mapUrl: function(filename) { 285 | return filename; 286 | }, 287 | 288 | /* 289 | * Allows to perform some file input based manual setup sequence (e.g. setting some BIOS). 290 | * return 0: step successful & init completed, -1: error, 1: step successful 291 | */ 292 | uploadFile: function(filename, options) { 293 | return 0; 294 | }, 295 | 296 | /* 297 | * Check if this AudioBackendAdapterBase still needs manually performed 298 | * setup steps (see uploadFile()) 299 | */ 300 | isManualSetupComplete: function() { 301 | return this._manualSetupComplete; 302 | }, 303 | 304 | /** 305 | * Cleanup backend before playing next music file 306 | */ 307 | teardown: function() {this.error("teardown");}, 308 | 309 | // ************* optional: song "position seek" functionality (only available in backend) 310 | 311 | /** 312 | * Return: default 0 = seeking not supported 313 | */ 314 | getMaxPlaybackPosition: function() { return 0;}, 315 | 316 | /** 317 | * Return: default 0 318 | */ 319 | getPlaybackPosition: function() { return 0;}, 320 | 321 | /** 322 | * Move playback to 'pos': must be between 0 and getMaxPlaybackPosition() 323 | * Return: 0 if successful 324 | */ 325 | seekPlaybackPosition: function(pos) { return -1;}, 326 | 327 | // ************* optional: async file-loading related (only if needed) 328 | 329 | /** 330 | * Transform input filename into path/filename expected by the backend 331 | * Return array with 2 elements: 0: basePath (backend specific - most don't need one), 332 | * 1: filename (incl. the remainder of the path) 333 | */ 334 | getPathAndFilename: function(filename) {this.error("getPathAndFilename");}, 335 | 336 | /** 337 | * Let backend store a loaded file in such a way that it can later deal with it. 338 | * Return a filehandle meaningful to the used backend 339 | */ 340 | registerFileData: function(pathFilenameArray, data) {this.error("registerFileData");}, 341 | 342 | // if filename/path used by backend does not match the one used by the browser 343 | mapBackendFilename: function(name) { return name;}, 344 | 345 | // introduced for backward-compatibility.. 346 | mapCacheFileName: function(name) { return name;}, 347 | /* 348 | * Backend may "push" update of song attributes (like author, copyright, etc) 349 | */ 350 | handleBackendSongAttributes: function(backendAttr, target) {this.error("handleBackendSongAttributes");}, 351 | 352 | 353 | // ************* built-in utility functions 354 | mapUri2Fs: function(uri) { // use extended ASCII that most likely isn't used in filenames 355 | // replace chars that cannot be used in file/foldernames 356 | var out= uri.replace(/\/\//, "ýý"); 357 | out = out.replace(/\?/, "ÿ"); 358 | out = out.replace(/:/, "þ"); 359 | out = out.replace(/\*/, "ü"); 360 | out = out.replace(/"/, "û"); 361 | out = out.replace(//, "ø"); 363 | out = out.replace(/\|/, "÷"); 364 | return out; 365 | }, 366 | mapFs2Uri: function(fs) { 367 | var out= fs.replace(/ýý/, "//"); 368 | out = out.replace(/ÿ/, "?"); 369 | out = out.replace(/þ/, ":"); 370 | out = out.replace(/ü/, "*"); 371 | out = out.replace(/û/, "\""); 372 | out = out.replace(/ù/, "<"); 373 | out = out.replace(/ø/, ">"); 374 | out = out.replace(/÷/, "|"); 375 | return out; 376 | }, 377 | 378 | // used for interaction with player 379 | setObserver: function(o) { 380 | this._observer= o; 381 | }, 382 | notifyAdapterReady: function() { 383 | if (typeof this._observer !== "undefined" ) this._observer.notify(); 384 | }, 385 | error: function(name) { 386 | alert("fatal error: abstract method '"+name+"' must be defined"); 387 | }, 388 | resetSampleRate: function(sampleRate, inputSampleRate) { 389 | if (sampleRate > 0) { this._sampleRate= sampleRate; } 390 | if (inputSampleRate > 0) { this._inputSampleRate= inputSampleRate; } 391 | 392 | var s= Math.round(SAMPLES_PER_BUFFER *this._sampleRate/this._inputSampleRate) *this.getChannels(); 393 | 394 | if (s > this._resampleBuffer.length) { 395 | this._resampleBuffer= this.allocResampleBuffer(s); 396 | } 397 | }, 398 | allocResampleBuffer: function(s) { 399 | return new Float32Array(s); 400 | }, 401 | getCopiedAudio: function(input, len, funcReadFloat, resampleOutput) { 402 | var i; 403 | // just copy the rescaled values so there is no need for special handling in playback loop 404 | for(i= 0; i this._resampleBuffer.length) { this._resampleBuffer= this.allocResampleBuffer(bufSize); } 424 | 425 | // only mono and interleaved stereo data is currently implemented.. 426 | this.resampleToFloat(this._channels, 0, input, len, this.readFloatSample.bind(this), this._resampleBuffer, resampleLen); 427 | if (this._channels == 2) { 428 | this.resampleToFloat(this._channels, 1, input, len, this.readFloatSample.bind(this), this._resampleBuffer, resampleLen); 429 | } 430 | } 431 | return resampleLen; 432 | }, 433 | 434 | // utility 435 | resampleToFloat: function(channels, channelId, inputPtr, len, funcReadFloat, resampleOutput, resampleLen) { 436 | // Bresenham (line drawing) algorithm based resampling 437 | var x0= 0; 438 | var y0= 0; 439 | var x1= resampleLen - 0; 440 | var y1= len - 0; 441 | 442 | var dx = Math.abs(x1-x0), sx = x0=x1 && y0>=y1) { break; } 452 | e2 = 2*err; 453 | if (e2 > dy) { err += dy; x0 += sx; } 454 | if (e2 < dx) { err += dx; y0 += sy; } 455 | } 456 | }, 457 | getResampleBuffer: function() { 458 | return this._resampleBuffer; 459 | } 460 | }; 461 | 462 | /* 463 | * Emscripten based backends that produce 16-bit sample data. 464 | * 465 | * NOTE: This impl adds handling for asynchronously initialized 'backends', i.e. 466 | * the 'backend' that is passed in, may not yet be usable (see WebAssebly based impls: 467 | * here a respective "onRuntimeInitialized" event will eventually originate from the 'backend'). 468 | * The 'backend' allows to register a "adapterCallback" hook to propagate the event - which is 469 | * used here. The player typically observes the backend-adapter and when the adapter state changes, a 470 | * "notifyAdapterReady" is triggered so that the player is notified of the change. 471 | */ 472 | EmsHEAP16BackendAdapter = (function(){ var $this = function (backend, channels) { 473 | $this.base.call(this, channels, 2); 474 | this.Module= backend; 475 | 476 | // required if WASM (asynchronously loaded) is used in the backend impl 477 | this.Module["adapterCallback"] = function() { // when Module is ready 478 | this.doOnAdapterReady(); // hook allows to perform additional initialization 479 | this.notifyAdapterReady(); // propagate to change to player 480 | }.bind(this); 481 | 482 | if (!window.Math.fround) { window.Math.fround = window.Math.round; } // < Chrome 38 hack 483 | }; 484 | extend(AudioBackendAdapterBase, $this, { 485 | doOnAdapterReady: function() { }, // noop, to be overridden in subclasses 486 | 487 | /* async emscripten init means that adapter may not immediately be ready - see async WASM compilation */ 488 | isAdapterReady: function() { 489 | if (typeof this.Module.notReady === "undefined") return true; // default for backward compatibility 490 | return !this.Module.notReady; 491 | }, 492 | registerEmscriptenFileData: function(pathFilenameArray, data) { 493 | // create a virtual emscripten FS for all the songs that are touched.. so the compiled code will 494 | // always find what it is looking for.. some players will look to additional resource files in the same folder.. 495 | 496 | // Unfortunately the FS.findObject() API is not exported.. so it's exception catching time 497 | try { 498 | this.Module.FS_createPath("/", pathFilenameArray[0], true, true); 499 | } catch(e) { 500 | } 501 | var f; 502 | try { 503 | if (typeof this.Module.FS_createDataFile == 'undefined') { 504 | f= true; // backend without FS (ignore for drag&drop files) 505 | } else { 506 | f= this.Module.FS_createDataFile(pathFilenameArray[0], pathFilenameArray[1], data, true, true); 507 | 508 | var p= ScriptNodePlayer.getInstance().trace("registerEmscriptenFileData: [" + 509 | pathFilenameArray[0]+ "][" +pathFilenameArray[1]+ "] size: "+ data.length); 510 | } 511 | } catch(err) { 512 | // file may already exist, e.g. drag/dropped again.. just keep entry 513 | 514 | } 515 | return f; 516 | }, 517 | readFloatSample: function(buffer, idx) { 518 | return (this.Module.HEAP16[buffer+idx])/0x8000; 519 | }, 520 | // certain songs use an unfavorable L/R separation - e.g. bass on one channel - that is 521 | // not nice to listen to. This "panning" impl allows to "mono"-ify those songs.. (this._pan=1 522 | // creates mono) 523 | applyPanning: function(buffer, len, pan) { 524 | pan= pan * 256.0 / 2.0; 525 | 526 | var i, l, r, m; 527 | for (i = 0; i < len*2; i+=2) { 528 | l = this.Module.HEAP16[buffer+i]; 529 | r = this.Module.HEAP16[buffer+i+1]; 530 | m = (r - l) * pan; 531 | 532 | var nl= ((l << 8) + m) >> 8; 533 | var nr= ((r << 8) - m) >> 8; 534 | this.Module.HEAP16[buffer+i] = nl; 535 | this.Module.HEAP16[buffer+i+1] = nr; 536 | /* 537 | if ((this.Module.HEAP16[buffer+i] != nl) || (this.Module.HEAP16[buffer+i+1] == nr)) { 538 | console.log("X"); 539 | }*/ 540 | } 541 | } 542 | }); return $this; })(); 543 | 544 | /* 545 | * Emscripten based backends that produce 32-bit float sample data. 546 | * 547 | * NOTE: This impl adds handling for asynchronously initialized 'backends', i.e. 548 | * the 'backend' that is passed in, may not yet be usable (see WebAssebly based impls: 549 | * here a respective "onRuntimeInitialized" event will eventually originate from the 'backend'). 550 | * The 'backend' allows to register a "adapterCallback" hook to propagate the event - which is 551 | * used here. The player typically observes the backend-adapter and when the adapter state changes, a 552 | * "notifyAdapterReady" is triggered so that the player is notified of the change. 553 | */ 554 | EmsHEAPF32BackendAdapter = (function(){ var $this = function (backend, channels) { 555 | $this.base.call(this, backend, channels); 556 | 557 | this._bytesPerSample= 4; 558 | }; 559 | extend(EmsHEAP16BackendAdapter, $this, { 560 | readFloatSample: function(buffer, idx) { 561 | return (this.Module.HEAPF32[buffer+idx]); 562 | }, 563 | // certain songs use an unfavorable L/R separation - e.g. bass on one channel - that is 564 | // not nice to listen to. This "panning" impl allows to "mono"-ify those songs.. (this._pan=1 565 | // creates mono) 566 | applyPanning: function(buffer, len, pan) { 567 | pan= pan * 256.0 / 2.0; 568 | var i, l, r, m; 569 | for (i = 0; i < len*2; i+=2) { 570 | l = this.Module.HEAPF32[buffer+i]; 571 | r = this.Module.HEAPF32[buffer+i+1]; 572 | m = (r - l) * pan; 573 | 574 | var nl= ((l *256) + m) /256; 575 | var nr= ((r *256) - m) /256; 576 | this.Module.HEAPF32[buffer+i] = nl; 577 | this.Module.HEAPF32[buffer+i+1] = nr; 578 | } 579 | } 580 | }); return $this; })(); 581 | 582 | // cache all loaded files in global cache. 583 | FileCache = function() { 584 | this._binaryFileMap= {}; // cache for loaded "file" binaries 585 | this._pendingFileMap= {}; 586 | 587 | this._isWaitingForFile= false; // signals that some file loading is still in progress 588 | }; 589 | 590 | FileCache.prototype = { 591 | getFileMap: function () { 592 | return this._binaryFileMap; 593 | }, 594 | getPendingMap: function () { 595 | return this._pendingFileMap; 596 | }, 597 | setWaitingForFile: function (val) { 598 | this._isWaitingForFile= val; 599 | }, 600 | isWaitingForFile: function () { 601 | return this._isWaitingForFile; 602 | }, 603 | getFile: function (filename) { 604 | var data; 605 | if (filename in this._binaryFileMap) { 606 | data= this._binaryFileMap[filename]; 607 | } 608 | return data; 609 | }, 610 | 611 | // FIXME the unlimited caching of files should probably be restricted: 612 | // currently all loaded song data stays in memory as long as the page is opened 613 | // maybe just add some manual "reset"? 614 | setFile: function(filename, data) { 615 | this._binaryFileMap[filename]= data; 616 | this._isWaitingForFile= false; 617 | } 618 | }; 619 | 620 | 621 | /** 622 | * Generic ScriptProcessor based WebAudio music player (end user API). 623 | * 624 | *

Deals with the WebAudio node pipeline, feeds the sample data chunks delivered by 625 | * the backend into the WebAudio input buffers, provides basic file input facilities. 626 | * 627 | * This player is used as a singleton (i.e. instanciation of a player destroys the previous one). 628 | * 629 | * GUI can use the player via: 630 | * ScriptNodePlayer.createInstance(...); and 631 | * ScriptNodePlayer.getInstance(); 632 | */ 633 | var ScriptNodePlayer = (function () { 634 | /* 635 | * @param externalTicker must be a subclass of AbstractTicker 636 | */ 637 | PlayerImpl = function(backendAdapter, basePath, requiredFiles, spectrumEnabled, onPlayerReady, onTrackReadyToPlay, onTrackEnd, onUpdate, externalTicker, bufferSize) { 638 | if(typeof backendAdapter === 'undefined') { alert("fatal error: backendAdapter not specified"); } 639 | if(typeof onPlayerReady === 'undefined') { alert("fatal error: onPlayerReady not specified"); } 640 | if(typeof onTrackReadyToPlay === 'undefined') { alert("fatal error: onTrackReadyToPlay not specified"); } 641 | if(typeof onTrackEnd === 'undefined') { alert("fatal error: onTrackEnd not specified"); } 642 | if(typeof bufferSize !== 'undefined') { window.SAMPLES_PER_BUFFER= bufferSize; } 643 | 644 | if (backendAdapter.getChannels() >2) { alert("fatal error: only 1 or 2 output channels supported"); } 645 | this._backendAdapter= backendAdapter; 646 | this._backendAdapter.setObserver(this); 647 | 648 | this._basePath= basePath; 649 | this._traceSwitch= false; 650 | 651 | this._spectrumEnabled= spectrumEnabled; 652 | 653 | // container for song infos like: name, author, etc 654 | this._songInfo = {}; 655 | 656 | // hooks that allow to react to specific events 657 | this._onTrackReadyToPlay= onTrackReadyToPlay; 658 | this._onTrackEnd= onTrackEnd; 659 | this._onPlayerReady= onPlayerReady; 660 | this._onUpdate= onUpdate; // optional 661 | 662 | 663 | // "external ticker" allows to sync separately maintained data with the actual audio playback 664 | this._tickerStepWidth= 256; // shortest available (i.e. tick every 256 samples) 665 | if(typeof externalTicker !== 'undefined') { 666 | externalTicker.init(SAMPLES_PER_BUFFER, this._tickerStepWidth); 667 | } 668 | this._externalTicker = externalTicker; 669 | this._currentTick= 0; 670 | 671 | this._silenceStarttime= -1; 672 | this._silenceTimeout= 5; // by default 5 secs of silence will end a song 673 | 674 | // audio buffer handling 675 | this._sourceBuffer; 676 | this._sourceBufferLen; 677 | this._numberOfSamplesRendered= 0; 678 | this._numberOfSamplesToRender= 0; 679 | this._sourceBufferIdx=0; 680 | 681 | // // additional timeout based "song end" handling 682 | this._currentPlaytime= 0; 683 | this._currentTimeout= -1; 684 | 685 | if (!this.isAutoPlayCripple()) { 686 | // original impl 687 | setGlobalWebAudioCtx(); 688 | 689 | this._sampleRate = window._gPlayerAudioCtx.sampleRate; 690 | this._correctSampleRate= this._sampleRate; 691 | this._backendAdapter.resetSampleRate(this._sampleRate, -1); 692 | } 693 | // general WebAudio stuff 694 | this._bufferSource; 695 | this._gainNode; 696 | this._analyzerNode; 697 | this._scriptNode; 698 | this._freqByteData = 0; 699 | 700 | this._pan= null; // default: inactive 701 | 702 | // the below entry points are published globally they can be 703 | // easily referenced from the outside.. 704 | 705 | window.fileRequestCallback= this.fileRequestCallback.bind(this); 706 | window.fileSizeRequestCallback= this.fileSizeRequestCallback.bind(this); 707 | window.songUpdateCallback= this.songUpdateCallback.bind(this); 708 | 709 | // --------------- player status stuff ---------- 710 | 711 | this._isPaused= false; // 'end' of a song also triggers this state 712 | 713 | // setup asyc completion of initialization 714 | this._isPlayerReady= false; // this state means that the player is initialized and can be used now 715 | this._isSongReady= false; // initialized (including file-loads that might have been necessary) 716 | this._initInProgress= false; 717 | 718 | this._preLoadReady= false; 719 | 720 | window.player= this; 721 | 722 | var f= window.player['preloadFiles'].bind(window.player); 723 | f(requiredFiles, function() { 724 | this._preLoadReady= true; 725 | if (this._preLoadReady && this._backendAdapter.isAdapterReady() && this._backendAdapter.isManualSetupComplete()) { 726 | this._isPlayerReady= true; 727 | this._onPlayerReady(); 728 | } 729 | }.bind(this)); 730 | }; 731 | 732 | 733 | PlayerImpl.prototype = { 734 | 735 | // ******* general 736 | notify: function() { // used to handle asynchronously initialized backend impls 737 | if ((typeof this.deferredPreload !== "undefined") && this._backendAdapter.isAdapterReady()) { 738 | // now that the runtime is ready the "preload" can be started 739 | var files= this.deferredPreload[0]; 740 | var onCompletionHandler= this.deferredPreload[1]; 741 | delete this.deferredPreload; 742 | 743 | this.preload(files, files.length, onCompletionHandler); 744 | } 745 | 746 | if (!this._isPlayerReady && this._preLoadReady && this._backendAdapter.isAdapterReady() && this._backendAdapter.isManualSetupComplete()) { 747 | this._isPlayerReady= true; 748 | this._onPlayerReady(); 749 | } 750 | }, 751 | handleBackendEvent: function() { this.notify(); }, // deprecated, use notify()! 752 | 753 | /** 754 | * Is the player ready for use? (i.e. initialization completed) 755 | */ 756 | isReady: function() { 757 | return this._isPlayerReady; 758 | }, 759 | 760 | /** 761 | * Change the default 5sec timeout (0 means no timeout). 762 | */ 763 | setSilenceTimeout: function(silenceTimeout) { 764 | // usecase: user may temporarrily turn off output (see DeepSID) and player should not end song 765 | this._silenceTimeout= silenceTimeout; 766 | }, 767 | 768 | /** 769 | * Turn on debug output to JavaScript console. 770 | */ 771 | setTraceMode: function (on) { 772 | this._traceSwitch= on; 773 | }, 774 | 775 | // ******* basic playback features 776 | 777 | /* 778 | * start audio playback 779 | */ 780 | play: function() { 781 | this._isPaused= false; 782 | 783 | // this function isn't invoked directly from some "user gesture" (but 784 | // indirectly from "onload" handler) so it might not work on braindead iOS shit 785 | try { this._bufferSource.start(0); } catch(ignore) {} 786 | }, 787 | /* 788 | * pause audio playback 789 | */ 790 | pause: function() { 791 | if ((!this.isWaitingForFile()) && (!this._initInProgress) && this._isSongReady) { 792 | this._isPaused= true; 793 | } 794 | }, 795 | isPaused: function() { 796 | return this._isPaused; 797 | }, 798 | 799 | /* 800 | * resume audio playback 801 | */ 802 | resume: function() { 803 | if ((!this.isWaitingForFile()) && (!this._initInProgress) && this._isSongReady) { 804 | this.play(); 805 | } 806 | }, 807 | 808 | /* 809 | * gets the index of the 'tick' that is currently playing. 810 | * allows to sync separately stored data with the audio playback. 811 | */ 812 | getCurrentTick: function() { 813 | var idx= Math.ceil(SAMPLES_PER_BUFFER/this._tickerStepWidth)-1; 814 | idx= Math.min(idx, this._currentTick) 815 | return idx; 816 | }, 817 | 818 | /* 819 | * set the playback volume (input between 0 and 1) 820 | */ 821 | setVolume: function(value) { 822 | if (typeof this._gainNode != 'undefined') { 823 | this._gainNode.gain.value= value; 824 | } 825 | }, 826 | 827 | getVolume: function() { 828 | if (typeof this._gainNode != 'undefined') { 829 | return this._gainNode.gain.value; 830 | } 831 | return -1; 832 | }, 833 | /** 834 | * @value null=inactive; or range; -1 to 1 (-1 is original stereo, 0 creates "mono", 1 is inverted stereo) 835 | */ 836 | setPanning: function(value) { 837 | this._pan= value; 838 | }, 839 | 840 | /* 841 | * is playback in stereo? 842 | */ 843 | isStereo: function() { 844 | return this._backendAdapter.getChannels() == 2; 845 | }, 846 | 847 | /** 848 | * Get backend specific song infos like 'author', 'name', etc. 849 | */ 850 | getSongInfo: function () { 851 | return this._songInfo; 852 | }, 853 | 854 | /** 855 | * Get meta info about backend specific song infos, e.g. what attributes are available and what type are they. 856 | */ 857 | getSongInfoMeta: function() { 858 | return this._backendAdapter.getSongInfoMeta(); 859 | }, 860 | 861 | /* 862 | * Manually defined playback time to use until 'end' of a track (only affects the 863 | * currently selected track). 864 | * @param t time in millis 865 | */ 866 | setPlaybackTimeout: function(t) { 867 | this._currentPlaytime= 0; 868 | if (t<0) { 869 | this._currentTimeout= -1; 870 | } else { 871 | this._currentTimeout= t/1000*this._correctSampleRate; 872 | } 873 | }, 874 | /* 875 | * Timeout in seconds. 876 | */ 877 | getPlaybackTimeout: function() { 878 | if (this._currentTimeout < 0) { 879 | return -1; 880 | } else { 881 | return Math.round(this._currentTimeout/this._correctSampleRate); 882 | } 883 | }, 884 | 885 | getCurrentPlaytime: function() { 886 | // return Math.round(this._currentPlaytime/this._correctSampleRate); 887 | return this._currentPlaytime/this._correctSampleRate; // let user do the rounding in needed 888 | }, 889 | 890 | // ******* access to frequency spectrum data (if enabled upon construction) 891 | 892 | getFreqByteData: function () { 893 | if (this._analyzerNode) { 894 | if (this._freqByteData === 0) { 895 | this._freqByteData = new Uint8Array(this._analyzerNode.frequencyBinCount); 896 | } 897 | this._analyzerNode.getByteFrequencyData(this._freqByteData); 898 | } 899 | return this._freqByteData; 900 | }, 901 | 902 | // ******* song "position seek" related (if available with used backend) 903 | 904 | /** 905 | * Return: default 0 seeking not supported 906 | */ 907 | getMaxPlaybackPosition: function() { return this._backendAdapter.getMaxPlaybackPosition();}, 908 | 909 | /** 910 | * Return: default 0 911 | */ 912 | getPlaybackPosition: function() { return this._backendAdapter.getPlaybackPosition();}, 913 | 914 | /** 915 | * Move playback to 'pos': must be between 0 and getMaxSeekPosition() 916 | * Return: 0 if successful 917 | */ 918 | seekPlaybackPosition: function(pos) { return this._backendAdapter.seekPlaybackPosition(pos);}, 919 | 920 | // ******* (music) file input related 921 | 922 | /** 923 | * Loads from a JavaScript File object - e.g. used for 'drag & drop'. 924 | */ 925 | loadMusicFromTmpFile: function (file, options, onCompletion, onFail, onProgress) { 926 | this.initByUserGesture(); // cannot be done from the callbacks below.. see iOS shit 927 | 928 | var filename= file.name; // format detection may depend on prefixes and postfixes.. 929 | 930 | this._fileReadyNotify= ""; 931 | 932 | var fullFilename= ((options.basePath)?options.basePath:this._basePath) + filename; // this._basePath ever needed? 933 | if (this.loadMusicDataFromCache(fullFilename, options, onFail)) { return; } 934 | 935 | var reader = new FileReader(); 936 | reader.onload = function() { 937 | 938 | var pfn= this._backendAdapter.getPathAndFilename(filename); 939 | var data= new Uint8Array(reader.result); 940 | var fileHandle= this._backendAdapter.registerFileData(pfn, data); 941 | if (typeof fileHandle === 'undefined' ) { 942 | onFail(); 943 | return; 944 | } else { 945 | var cacheFilename= this._backendAdapter.mapCacheFileName(fullFilename); 946 | this.getCache().setFile(cacheFilename, data); 947 | } 948 | this.prepareTrackForPlayback(fullFilename, reader.result, options); 949 | onCompletion(filename); 950 | }.bind(this); 951 | reader.onprogress = function (oEvent) { 952 | if (onProgress) { 953 | onProgress(oEvent.total, oEvent.loaded); 954 | } 955 | }.bind(this); 956 | 957 | reader.readAsArrayBuffer(file); 958 | }, 959 | isAppleShit: function() { 960 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); 961 | }, 962 | isAutoPlayCripple: function() { 963 | return window.chrome || this.isAppleShit(); 964 | }, 965 | initByUserGesture: function() { 966 | // try to setup as much as possible while it is "directly triggered" 967 | // by "user gesture" (i.e. here).. seems POS iOS does not correctly 968 | // recognize any async-indirections started from here.. bloody Apple idiots 969 | if (typeof this._sampleRate == 'undefined') { 970 | setGlobalWebAudioCtx(); 971 | 972 | this._sampleRate = window._gPlayerAudioCtx.sampleRate; 973 | this._correctSampleRate= this._sampleRate; 974 | this._backendAdapter.resetSampleRate(this._sampleRate, -1); 975 | } else { 976 | // just in case: handle Chrome's new bullshit "autoplay policy" 977 | if (window._gPlayerAudioCtx.state == "suspended") { 978 | try {window._gPlayerAudioCtx.resume();} catch(e) {} 979 | } 980 | } 981 | 982 | if (typeof this._bufferSource != 'undefined') { 983 | try { 984 | this._bufferSource.stop(0); 985 | } catch(err) {} // ignore for the benefit of Safari(OS X) 986 | } else { 987 | var ctx= window._gPlayerAudioCtx; 988 | 989 | if (this.isAppleShit()) this.iOSHack(ctx); 990 | 991 | this._analyzerNode = ctx.createAnalyser(); 992 | this._scriptNode= this.createScriptProcessor(ctx); 993 | this._gainNode = ctx.createGain(); 994 | 995 | this._scriptNode.connect(this._gainNode); 996 | 997 | // optional add-on 998 | if (typeof this._externalTicker !== 'undefined') { 999 | var tickerScriptNode= this.createTickerScriptProcessor(ctx); 1000 | tickerScriptNode.connect(this._gainNode); 1001 | } 1002 | 1003 | // note: "panning" experiments using StereoPanner, ChannelSplitter / ChannelMerger 1004 | // led to bloody useless results: rather implement respective "panning" 1005 | // logic directly to get the exact effect that is needed here.. 1006 | 1007 | if (this._spectrumEnabled) { 1008 | this._gainNode.connect(this._analyzerNode); 1009 | this._analyzerNode.connect(ctx.destination); 1010 | } else { 1011 | this._gainNode.connect(ctx.destination); 1012 | 1013 | } 1014 | this._bufferSource = ctx.createBufferSource(); 1015 | if (!this._bufferSource.start) { 1016 | this._bufferSource.start = this._bufferSource.noteOn; 1017 | this._bufferSource.stop = this._bufferSource.noteOff; 1018 | } 1019 | } 1020 | }, 1021 | /** 1022 | * Loads from an URL. 1023 | */ 1024 | loadMusicFromURL: function(url, options, onCompletion, onFail, onProgress) { 1025 | this.initByUserGesture(); // cannot be done from the callbacks below.. see iOS shit 1026 | 1027 | var fullFilename= this._backendAdapter.mapInternalFilename(options.basePath, this._basePath, url); 1028 | 1029 | this._fileReadyNotify= ""; 1030 | 1031 | if (this.loadMusicDataFromCache(fullFilename, options, onFail)) { return; } 1032 | 1033 | var xhr = new XMLHttpRequest(); 1034 | xhr.open("GET", this._backendAdapter.mapUrl(fullFilename), true); 1035 | xhr.responseType = "arraybuffer"; 1036 | 1037 | xhr.onload = function (oEvent) { 1038 | this.trace("loadMusicFromURL successfully loaded: "+ fullFilename); 1039 | 1040 | if(!this.prepareTrackForPlayback(fullFilename, xhr.response, options)) { 1041 | if (!this.isWaitingForFile()) { 1042 | onFail(); 1043 | } 1044 | } else { 1045 | onCompletion(fullFilename); 1046 | } 1047 | /*else { // playback should be started from _onTrackReadyToPlay() 1048 | this.play(); 1049 | }*/ 1050 | }.bind(this); 1051 | xhr.onprogress = function (oEvent) { 1052 | if(onProgress) { 1053 | onProgress(oEvent.total, oEvent.loaded); 1054 | } 1055 | }.bind(this); 1056 | xhr.onreadystatuschange = function (oEvent) { 1057 | if (oReq.readyState==4 && oReq.status==404) { 1058 | this.trace("loadMusicFromURL failed to load: "+ fullFilename); 1059 | } 1060 | }.bind(this); 1061 | 1062 | xhr.send(null); 1063 | }, 1064 | 1065 | /* 1066 | * Manually perform some file input based initialization sequence - 1067 | * as/if required by the backend. (only needed for special cases) 1068 | */ 1069 | uploadFile: function (file, options, onCompletion, onFail, onProgress) { 1070 | var reader = new FileReader(); 1071 | reader.onload = function() { 1072 | var pfn= this._backendAdapter.getPathAndFilename(file.name); 1073 | var data= new Uint8Array(reader.result); 1074 | var fileHandle= this._backendAdapter.registerFileData(pfn, data); 1075 | if (typeof fileHandle === 'undefined' ) { 1076 | onFail(); 1077 | return; 1078 | } 1079 | var status = this._backendAdapter.uploadFile(file.name, options); 1080 | if (status === 0) { 1081 | onCompletion(file.name); 1082 | this._onPlayerReady(); 1083 | } else if (status == 1) { 1084 | onCompletion(file.name); 1085 | } 1086 | }.bind(this); 1087 | reader.onprogress = function (oEvent) { 1088 | if (onProgress) { 1089 | onProgress(oEvent.total, oEvent.loaded); 1090 | } 1091 | }.bind(this); 1092 | 1093 | reader.readAsArrayBuffer(file); 1094 | }, 1095 | 1096 | // ******** internal utils (MUST NOT be ued outside of the player or respective backendAdapters -------------- 1097 | 1098 | /** 1099 | * Load a music data and prepare to play a specific track. 1100 | */ 1101 | prepareTrackForPlayback: function (fullFilename, data, options) { 1102 | this._isPaused= true; 1103 | 1104 | // hack: so we get back at the options during retry attempts 1105 | this.lastUsedFilename= fullFilename; 1106 | this.lastUsedData= data; 1107 | this.lastUsedOptions= options; 1108 | 1109 | this._isSongReady= false; 1110 | this.setWaitingForFile(false); 1111 | 1112 | return this.initIfNeeded(fullFilename, data, options); 1113 | }, 1114 | trace: function(str) { 1115 | if (this._traceSwitch) { console.log(str); } 1116 | }, 1117 | setWait: function(isWaiting) { 1118 | this.setWaitingForFile(isWaiting); 1119 | }, 1120 | getDefaultSampleRate: function() { 1121 | return this._correctSampleRate; 1122 | }, 1123 | initIfNeeded: function (fullFilename, data, options) { 1124 | var status= this.loadMusicData(fullFilename, data, options); 1125 | if (status <0) { 1126 | this._isSongReady= false; 1127 | this.setWaitingForFile(true); 1128 | this._initInProgress= false; 1129 | 1130 | } else if (status === 0) { 1131 | // this._isPaused= false; 1132 | this.setWaitingForFile(false); 1133 | this._isSongReady= true; 1134 | this._currentPlaytime= 0; 1135 | this._initInProgress= false; 1136 | 1137 | this.trace("successfully completed init"); 1138 | 1139 | // in scenarios where a synchronous file-load is involved this first call will typically fail 1140 | // but trigger the file load 1141 | var ret= this._backendAdapter.evalTrackOptions(options); 1142 | if (ret !== 0) { 1143 | this.trace("error preparing track options"); 1144 | return false; 1145 | } 1146 | this.updateSongInfo(fullFilename); 1147 | 1148 | if ((this.lastUsedFilename == fullFilename)) { 1149 | if (this._fileReadyNotify == fullFilename) { 1150 | // duplicate we already notified about.. probably some retry due to missing load-on-demand files 1151 | this.play(); // user had already expressed his wish to play 1152 | } else { 1153 | this._silenceStarttime= -1; // reset silence detection 1154 | 1155 | this._onTrackReadyToPlay(); 1156 | } 1157 | this._fileReadyNotify= fullFilename; 1158 | } 1159 | this._isPaused= false; 1160 | return true; 1161 | 1162 | } else { 1163 | this._initInProgress= false; 1164 | // error that cannot be resolved.. (e.g. file not exists) 1165 | this.trace("initIfNeeded - fatal error"); 1166 | } 1167 | return false; 1168 | }, 1169 | loadMusicDataFromCache: function(fullFilename, options, onFail) { 1170 | // reset timeout handling (of previous song.. which still might be playing) 1171 | this._currentTimeout= -1; 1172 | this._currentPlaytime= 0; 1173 | this._isPaused= true; 1174 | 1175 | var cacheFilename= this._backendAdapter.mapCacheFileName(fullFilename); 1176 | var data= this.getCache().getFile(cacheFilename); 1177 | 1178 | if (typeof data != 'undefined') { 1179 | 1180 | this.trace("loadMusicDataFromCache found cached file using name: "+ cacheFilename); 1181 | 1182 | if(!this.prepareTrackForPlayback(fullFilename, data, options)) { 1183 | if (!this.isWaitingForFile()) { 1184 | onFail(); 1185 | } else { 1186 | } 1187 | } 1188 | return true; 1189 | } else { 1190 | this.trace("loadMusicDataFromCache FAILED to find cached file using name: "+ cacheFilename); 1191 | } 1192 | return false; 1193 | }, 1194 | getAudioContext: function() { 1195 | this.initByUserGesture(); // for backward compatibility 1196 | return window._gPlayerAudioCtx; // exposed due to Chrome's new bullshit "autoplay policy" 1197 | }, 1198 | iOSHack: function(ctx) { 1199 | try { 1200 | var source = window._gPlayerAudioCtx.createBufferSource(); 1201 | if (!source.start) { 1202 | source.start = source.noteOn; 1203 | source.stop = source.noteOff; 1204 | } 1205 | 1206 | source.buffer = window._gPlayerAudioCtx.createBuffer(1, 1, 22050); // empty buffer 1207 | source.connect(window._gPlayerAudioCtx.destination); 1208 | 1209 | source.start(0); 1210 | 1211 | } catch (ignore) {} 1212 | }, 1213 | updateSongInfo: function (fullFilename) { 1214 | this._songInfo= {}; 1215 | this._backendAdapter.updateSongInfo(fullFilename, this._songInfo); 1216 | }, 1217 | loadMusicData: function(fullFilename, arrayBuffer, options) { 1218 | this._backendAdapter.teardown(); 1219 | 1220 | if (arrayBuffer) { 1221 | var pfn= this._backendAdapter.getPathAndFilename(fullFilename); 1222 | 1223 | var data= new Uint8Array(arrayBuffer); 1224 | this._backendAdapter.registerFileData(pfn, data); // in case the backend "needs" to retrieve the file by name 1225 | 1226 | var cacheFilename= this._backendAdapter.mapCacheFileName(fullFilename); 1227 | this.getCache().setFile(cacheFilename, data); 1228 | 1229 | var ret= this._backendAdapter.loadMusicData(this._sampleRate, pfn[0], pfn[1], data, options); 1230 | 1231 | if (ret === 0) { 1232 | this.resetBuffer(); 1233 | } 1234 | return ret; 1235 | } 1236 | }, 1237 | resetBuffer: function () { 1238 | this._numberOfSamplesRendered= 0; 1239 | this._numberOfSamplesToRender= 0; 1240 | this._sourceBufferIdx=0; 1241 | }, 1242 | resetSampleRate: function(sampleRate) { 1243 | // override the default (correct) sample rate to make playback faster/slower 1244 | this._backendAdapter.resetSampleRate(sampleRate, -1); 1245 | 1246 | if (sampleRate > 0) { this._sampleRate= sampleRate; } 1247 | 1248 | this.resetBuffer(); 1249 | }, 1250 | createScriptProcessor: function(audioCtx) { 1251 | // use the number of channels that the backend wants 1252 | var scriptNode = audioCtx.createScriptProcessor(SAMPLES_PER_BUFFER, 0, this._backendAdapter.getChannels()); 1253 | scriptNode.onaudioprocess = fetchSamples; 1254 | // scriptNode.onaudioprocess = window.player.genSamples.bind(window.player); // doesn't work with dumbshit Chrome GC 1255 | return scriptNode; 1256 | }, 1257 | createTickerScriptProcessor: function(audioCtx) { 1258 | var scriptNode; 1259 | // "ticker" uses shortest buffer length available so that onaudioprocess 1260 | // is invoked more frequently than the above scriptProcessor.. it is the purpose 1261 | // of the "ticker" to supply data that is used for an "animation frame" (e.g. to display a VU meter), 1262 | // i.e. accuracy is not a big issue since we are talking about 60fps.. (at 48000kHz the 256 sample 1263 | // buffer would work up to 187.5 fps.. only people using unusually high playback rates might touch the limit..) 1264 | 1265 | // this script processor does not actually produce any audible output.. it just provides a callback 1266 | // that is synchronized with the actual music playback.. (the alternative would be to manually try and 1267 | // keep track of the playback progress..) 1268 | scriptNode = audioCtx.createScriptProcessor(256, 0, 1); 1269 | scriptNode.onaudioprocess = calcTick; 1270 | return scriptNode; 1271 | }, 1272 | fillEmpty: function(outSize, output1, output2) { 1273 | var availableSpace = outSize-this._numberOfSamplesRendered; 1274 | 1275 | for (i= 0; i FS name: "+fullFilename ); 1298 | 1299 | return this.preloadFile(fullFilename, function() { 1300 | this.initIfNeeded(this.lastUsedFilename, this.lastUsedData, this.lastUsedOptions); 1301 | }.bind(this), false); 1302 | }, 1303 | // convenience API which lets backend directly query the file size 1304 | fileSizeRequestCallback: function (name) { 1305 | var filename= this._backendAdapter.mapBackendFilename(name); 1306 | var cacheFilename= this._backendAdapter.mapCacheFileName(filename); 1307 | var f= this.getCache().getFile(cacheFilename); // this API is only called after the file has actually loaded 1308 | return f.length; 1309 | }, 1310 | 1311 | // may be invoked by backend to "push" updated song attributes (some backends only "learn" about infos 1312 | // like songname, author, etc while the song is actually played..) 1313 | songUpdateCallback:function(attr) { 1314 | // notification that emu has updated infos regarding the currently played song.. 1315 | this._backendAdapter.handleBackendSongAttributes(attr, this._songInfo); 1316 | 1317 | if(this._onUpdate) { 1318 | this._onUpdate(); 1319 | } 1320 | }, 1321 | 1322 | // ------------------------------------------------------------------------------------------------------- 1323 | 1324 | preload: function(files, id, onCompletionHandler) { 1325 | if (id === 0) { 1326 | // we are done preloading 1327 | onCompletionHandler(); 1328 | } else { 1329 | id--; 1330 | var funcCompleted= function() {this.preload(files, id, onCompletionHandler);}.bind(this); // trigger next load 1331 | this.preloadFile(files[id], funcCompleted, true); 1332 | } 1333 | }, 1334 | preloadFile: function (fullFilename, onLoadedHandler, notifyOnCached) { 1335 | // note: function is used for "preload" and for "backend callback" loading... return values 1336 | // are only used for the later 1337 | 1338 | var cacheFilename= this._backendAdapter.mapCacheFileName(fullFilename); 1339 | var data= this.getCache().getFile(cacheFilename); 1340 | if (typeof data != 'undefined') { 1341 | var retVal= 0; 1342 | // the respective file has already been setup 1343 | if (data == 0) { 1344 | retVal= 1; 1345 | this.trace("error: preloadFile could not get cached: "+ fullFilename); 1346 | } else { 1347 | this.trace("preloadFile found cached file using name: "+ cacheFilename); 1348 | 1349 | // but in cases were alias names as used for the same file (see modland shit) 1350 | // the file may NOT yet have been registered in the FS 1351 | // setup data in our virtual FS (the next access should then be OK) 1352 | var pfn= this._backendAdapter.getPathAndFilename(fullFilename); 1353 | var f= this._backendAdapter.registerFileData(pfn, data); 1354 | } 1355 | if(notifyOnCached) 1356 | onLoadedHandler(); // trigger next in chain needed for preload / but hurts "backend callback" 1357 | return retVal; 1358 | } else { 1359 | this.trace("preloadFile FAILED to find cached file using name: "+ cacheFilename); 1360 | } 1361 | 1362 | // backend will be stuck without this file and we better make 1363 | // sure to not use it before it has been properly reinitialized 1364 | this._isPaused= true; 1365 | this.setWaitingForFile(true); 1366 | this._isSongReady= false; 1367 | 1368 | // requested data not available.. we better load it for next time 1369 | if (!(cacheFilename in this.getCache().getPendingMap())) { // avoid duplicate loading 1370 | this.getCache().getPendingMap()[cacheFilename] = 1; 1371 | 1372 | var oReq = new XMLHttpRequest(); 1373 | oReq.open("GET", this._backendAdapter.mapUrl(fullFilename), true); 1374 | oReq.responseType = "arraybuffer"; 1375 | 1376 | oReq.onload = function (oEvent) { 1377 | var arrayBuffer = oReq.response; 1378 | if (arrayBuffer) { 1379 | this.trace("preloadFile successfully loaded: "+ fullFilename); 1380 | 1381 | // setup data in our virtual FS (the next access should then be OK) 1382 | var pfn= this._backendAdapter.getPathAndFilename(fullFilename); 1383 | var data= new Uint8Array(arrayBuffer); 1384 | var f= this._backendAdapter.registerFileData(pfn, data); 1385 | 1386 | this.trace("preloadFile cached file using name: "+ cacheFilename); 1387 | 1388 | this.getCache().setFile(cacheFilename, data); 1389 | } 1390 | if(!delete this.getCache().getPendingMap()[cacheFilename]) { 1391 | this.trace("remove file from pending failed: "+cacheFilename); 1392 | } 1393 | onLoadedHandler(); 1394 | }.bind(this); 1395 | oReq.onreadystatuschange = function (oEvent) { 1396 | if (oReq.readyState==4 && oReq.status==404) { 1397 | this.trace("preloadFile failed to load: "+ fullFilename); 1398 | 1399 | this.getCache().setFile(cacheFilename, 0); 1400 | } 1401 | }.bind(this); 1402 | oReq.onerror = function (oEvent) { 1403 | 1404 | this.getCache().setFile(cacheFilename, 0); 1405 | }.bind(this); 1406 | 1407 | oReq.send(null); 1408 | } 1409 | return -1; 1410 | }, 1411 | tick: function(event) { 1412 | if (!this._isPaused) 1413 | this._currentTick++; 1414 | }, 1415 | // called for 'onaudioprocess' to feed new batch of sample data 1416 | genSamples: function(event) { 1417 | var genStereo= this.isStereo() && event.outputBuffer.numberOfChannels>1; 1418 | 1419 | var output1 = event.outputBuffer.getChannelData(0); 1420 | var output2; 1421 | if (genStereo) { 1422 | output2 = event.outputBuffer.getChannelData(1); 1423 | } 1424 | if ((!this._isSongReady) || this.isWaitingForFile() || this._isPaused) { 1425 | var i; 1426 | for (i= 0; i0) && (this._currentPlaytime > this._currentTimeout)) { 1445 | this.trace("'song end' forced after "+ this._currentTimeout/this._correctSampleRate +" secs"); 1446 | status= 1; 1447 | } else { 1448 | status = this._backendAdapter.computeAudioSamples(); 1449 | if (typeof this._externalTicker !== 'undefined') { 1450 | this._externalTicker.computeAudioSamplesNotify(); 1451 | } 1452 | } 1453 | 1454 | if (status !== 0) { 1455 | // no frame left 1456 | this.fillEmpty(outSize, output1, output2); 1457 | 1458 | if (status <0) { 1459 | // file-load: emu just discovered that we need to load another file 1460 | this._isPaused= true; 1461 | this._isSongReady= false; // previous init is invalid 1462 | this.setWaitingForFile(true); 1463 | return; // complete init sequence must be repeated 1464 | } 1465 | if (this.isWaitingForFile()) { 1466 | // this state may just have been set by the backend.. try again later 1467 | return; 1468 | } else { 1469 | if (status > 1) { 1470 | this.trace("playback aborted with an error"); 1471 | } 1472 | 1473 | this._isPaused= true; // stop playback (or this will retrigger again and again before new song is started) 1474 | if (this._onTrackEnd) { 1475 | this._onTrackEnd(); 1476 | } 1477 | return; 1478 | } 1479 | } 1480 | // refresh just in case they are not using one fixed buffer.. 1481 | this._sourceBuffer= this._backendAdapter.getAudioBuffer(); 1482 | this._sourceBufferLen= this._backendAdapter.getAudioBufferLength(); 1483 | 1484 | if (this._pan != null) 1485 | this._backendAdapter.applyPanning(this._sourceBuffer, this._sourceBufferLen, this._pan+1.0); 1486 | 1487 | this._numberOfSamplesToRender = this._backendAdapter.getResampledAudio(this._sourceBuffer, this._sourceBufferLen); 1488 | 1489 | if (typeof this._externalTicker !== 'undefined') { 1490 | this._backendAdapter.resampleTickerData(this._externalTicker, this._sourceBufferLen); 1491 | } 1492 | this._sourceBufferIdx=0; 1493 | } 1494 | 1495 | var resampleBuffer= this._backendAdapter.getResampleBuffer(); 1496 | if (genStereo) { 1497 | this.copySamplesStereo(resampleBuffer, output1, output2, outSize); 1498 | } else { 1499 | this.copySamplesMono(resampleBuffer, output1, outSize); 1500 | } 1501 | } 1502 | // keep track how long we are playing: just filled one WebAudio buffer which will be played at 1503 | this._currentPlaytime+= outSize * this._correctSampleRate/this._sampleRate; 1504 | 1505 | // silence detection at end of song 1506 | if ((this._silenceStarttime > 0) && ((this._currentPlaytime - this._silenceStarttime) >= this._silenceTimeout*this._correctSampleRate ) && (this._silenceTimeout >0)) { 1507 | this._isPaused= true; // stop playback (or this will retrigger again and again before new song is started) 1508 | if (this._onTrackEnd) { 1509 | this._onTrackEnd(); 1510 | } 1511 | } 1512 | } 1513 | if (typeof this._externalTicker !== 'undefined') { 1514 | this._externalTicker.calcTickData(output1, output2); 1515 | this._currentTick= 0; 1516 | } 1517 | }, 1518 | detectSilence: function(s) { 1519 | if (this._silenceStarttime == 0) { // i.e. song has been playing 1520 | if (s == 0) { // silence detected 1521 | this._silenceStarttime= this._currentPlaytime; 1522 | } 1523 | } else if (s > 0) { // i.e. false alarm or very start of playback 1524 | this._silenceStarttime= 0; 1525 | } 1526 | }, 1527 | copySamplesStereo: function(resampleBuffer, output1, output2, outSize) { 1528 | var i; 1529 | var s= 0, l= 0, r= 0; 1530 | var abs= Math.abs; 1531 | if (this._numberOfSamplesRendered + this._numberOfSamplesToRender > outSize) { 1532 | var availableSpace = outSize-this._numberOfSamplesRendered; 1533 | 1534 | for (i= 0; i>1), this._backendAdapter); 1537 | } 1538 | l= resampleBuffer[this._sourceBufferIdx++]; 1539 | r= resampleBuffer[this._sourceBufferIdx++]; 1540 | 1541 | output1[i+this._numberOfSamplesRendered]= l; 1542 | output2[i+this._numberOfSamplesRendered]= r; 1543 | 1544 | s+= abs(l) + abs(r); 1545 | } 1546 | 1547 | this._numberOfSamplesToRender -= availableSpace; 1548 | this._numberOfSamplesRendered = outSize; 1549 | } else { 1550 | for (i= 0; i>1), this._backendAdapter); 1553 | } 1554 | l= resampleBuffer[this._sourceBufferIdx++]; 1555 | r= resampleBuffer[this._sourceBufferIdx++]; 1556 | 1557 | output1[i+this._numberOfSamplesRendered]= l; 1558 | output2[i+this._numberOfSamplesRendered]= r; 1559 | 1560 | s+= abs(l) + abs(r); 1561 | } 1562 | this._numberOfSamplesRendered += this._numberOfSamplesToRender; 1563 | this._numberOfSamplesToRender = 0; 1564 | } 1565 | this.detectSilence(s); 1566 | }, 1567 | copySamplesMono: function(resampleBuffer, output1, outSize) { 1568 | var i; 1569 | var s= 0, o= 0; 1570 | var abs= Math.abs; 1571 | if (this._numberOfSamplesRendered + this._numberOfSamplesToRender > outSize) { 1572 | var availableSpace = outSize-this._numberOfSamplesRendered; 1573 | 1574 | for (i= 0; ia?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="

",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; 3 | if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("