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 |

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 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(/>/,"ø")).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){0 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(/, "ù");
362 | 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 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=this._silenceTimeout*this._correctSampleRate&&0a",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
","
"],tr:[2,"","
"],col:[2,"
"],td:[3,"
"],_default:k.htmlSerialize?[0,"",""]:[1,"X"," "!==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>$2>");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("")).appendTo(b.documentElement),b=(Cb[0].contentWindow||Cb[0].contentDocument).document,b.write(),b.close(),c=Eb(a,b),Cb.detach()),Db[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return 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="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Gb=/^margin/,Hb=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ib,Jb,Kb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ib=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Hb.test(g)&&Gb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ib=function(a){return a.currentStyle},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Hb.test(g)&&!Kb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Lb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML="
a",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;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),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight)),b.innerHTML="
",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Mb=/alpha\([^)]*\)/i,Nb=/opacity\s*=\s*([^)]*)/,Ob=/^(none|table(?!-c[ea]).+)/,Pb=new RegExp("^("+S+")(.*)$","i"),Qb=new RegExp("^([+-])=("+S+")","i"),Rb={position:"absolute",visibility:"hidden",display:"block"},Sb={letterSpacing:"0",fontWeight:"400"},Tb=["Webkit","O","Moz","ms"];function Ub(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Tb.length;while(e--)if(b=Tb[e]+c,b in a)return b;return d}function Vb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fb(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wb(a,b,c){var d=Pb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Yb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ib(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Jb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Hb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xb(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Jb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ub(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ub(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Jb(a,b,d)),"normal"===f&&b in Sb&&(f=Sb[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Ob.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Rb,function(){return Yb(a,b,d)}):Yb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ib(a);return Wb(a,c,d?Xb(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Nb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Mb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Mb.test(f)?f.replace(Mb,e):f+" "+e)}}),m.cssHooks.marginRight=Lb(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Jb,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Gb.test(a)||(m.cssHooks[a+b].set=Wb)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ib(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Vb(this,!0)},hide:function(){return Vb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Zb(a,b,c,d,e){return new Zb.prototype.init(a,b,c,d,e)}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")
4 | },cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(a,b){b?dc.unshift(a):dc.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kc(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gc(b,!0),a,d,e)}}),m.each({slideDown:gc("show"),slideUp:gc("hide"),slideToggle:gc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($b=m.now();ct a",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lc=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lc,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mc,nc,oc=m.expr.attrHandle,pc=/^(?:checked|selected)$/i,qc=k.getSetAttribute,rc=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nc:mc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rc&&qc||!pc.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qc?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nc={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rc&&qc||!pc.test(c)?a.setAttribute(!qc&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=oc[b]||m.find.attr;oc[b]=rc&&qc||!pc.test(b)?function(a,b,d){var e,f;return d||(f=oc[b],oc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,oc[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rc&&qc||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mc&&mc.set(a,b,c)}}),qc||(mc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},oc.id=oc.name=oc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mc.set},m.attrHooks.contenteditable={set:function(a,b,c){mc.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sc=/^(?:input|select|textarea|button|object)$/i,tc=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sc.test(a.nodeName)||tc.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var uc=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(uc," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vc=m.now(),wc=/\?/,xc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yc,zc,Ac=/#.*$/,Bc=/([?&])_=[^&]*/,Cc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Dc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ec=/^(?:GET|HEAD)$/,Fc=/^\/\//,Gc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hc={},Ic={},Jc="*/".concat("*");try{zc=location.href}catch(Kc){zc=y.createElement("a"),zc.href="",zc=zc.href}yc=Gc.exec(zc.toLowerCase())||[];function Lc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mc(a,b,c,d){var e={},f=a===Ic;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nc(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Oc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zc,type:"GET",isLocal:Dc.test(yc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nc(Nc(a,m.ajaxSettings),b):Nc(m.ajaxSettings,a)},ajaxPrefilter:Lc(Hc),ajaxTransport:Lc(Ic),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zc)+"").replace(Ac,"").replace(Fc,yc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yc[1]&&c[2]===yc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yc[3]||("http:"===yc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mc(Hc,k,b,v),2===t)return v;h=k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Ec.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bc.test(e)?e.replace(Bc,"$1_="+vc++):e+(wc.test(e)?"&":"?")+"_="+vc++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mc(Ic,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Oc(k,v,c)),u=Pc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qc=/%20/g,Rc=/\[\]$/,Sc=/\r?\n/g,Tc=/^(?:submit|button|image|reset|file)$/i,Uc=/^(?:input|select|textarea|keygen)/i;function Vc(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rc.test(a)?d(a,e):Vc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vc(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vc(c,a[c],b,e);return d.join("&").replace(Qc,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Uc.test(this.nodeName)&&!Tc.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sc,"\r\n")}}):{name:b.name,value:c.replace(Sc,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zc()||$c()}:Zc;var Wc=0,Xc={},Yc=m.ajaxSettings.xhr();a.ActiveXObject&&m(a).on("unload",function(){for(var a in Xc)Xc[a](void 0,!0)}),k.cors=!!Yc&&"withCredentials"in Yc,Yc=k.ajax=!!Yc,Yc&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xc[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zc(){try{return new a.XMLHttpRequest}catch(b){}}function $c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _c=[],ad=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_c.pop()||m.expando+"_"+vc++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ad.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ad.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ad,"$1"+e):b.jsonp!==!1&&(b.url+=(wc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_c.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bd=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bd)return bd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("